परिवर्तन लॉग के आधार पर स्टॉक मात्रा की गणना


10

कल्पना करें कि आपके पास निम्न तालिका संरचना है:

LogId | ProductId | FromPositionId | ToPositionId | Date                 | Quantity
-----------------------------------------------------------------------------------
1     | 123       | 0              | 10002        | 2018-01-01 08:10:22  | 5
2     | 123       | 0              | 10003        | 2018-01-03 15:15:10  | 9
3     | 123       | 10002          | 10004        | 2018-01-07 21:08:56  | 3
4     | 123       | 10004          | 0            | 2018-02-09 10:03:23  | 1

FromPositionIdऔर ToPositionIdस्टॉक पोजीशन हैं। कुछ स्थिति आईडी: उदाहरण के लिए विशेष अर्थ है 0। एक घटना से या 0इसका मतलब है कि स्टॉक बनाया या हटा दिया गया था। से 0और एक वितरण से शेयर किया जा सकता है के लिए 0एक भेज दिया आदेश हो सकता है।

इस तालिका में वर्तमान में लगभग 5.5 मिलियन पंक्तियाँ हैं। हम प्रत्येक उत्पाद के लिए स्टॉक मूल्य की गणना करते हैं और एक कैश टेबल में एक स्थिति पर एक क्वेरी का उपयोग करते हैं जो कुछ इस तरह दिखता है:

WITH t AS
(
    SELECT ToPositionId AS PositionId, SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY ToPositionId, ProductId
    UNION
    SELECT FromPositionId AS PositionId, -SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY FromPositionId, ProductId
)

SELECT t.ProductId, t.PositionId, SUM(t.Quantity) AS Quantity
FROM t
WHERE NOT t.PositionId = 0
GROUP BY t.ProductId, t.PositionId
HAVING SUM(t.Quantity) > 0

भले ही यह उचित समय (लगभग 20 सेकंड) में पूरा हो जाता है, मुझे लगता है कि यह शेयर मूल्यों की गणना का एक बहुत ही अक्षम तरीका है। हम INSERTइस तालिका में शायद ही कभी कुछ करते हैं : लेकिन हम कभी-कभी अंदर जाते हैं और मात्रा को समायोजित करते हैं या इन पंक्तियों को बनाने वाले लोगों द्वारा गलतियों के कारण मैन्युअल रूप से एक पंक्ति को हटाते हैं।

मेरे पास एक अलग तालिका में "चौकियों" बनाने का विचार था, समय में एक विशिष्ट बिंदु तक मूल्य की गणना करता है और हमारे स्टॉक मात्रा कैश तालिका बनाते समय एक शुरुआती मूल्य के रूप में उपयोग करता है:

ProductId | PositionId | Date                | Quantity
-------------------------------------------------------
123       | 10002      | 2018-01-07 21:08:56 | 2

तथ्य यह है कि हम कभी-कभी पंक्तियों को बदलते हैं, इससे समस्या पैदा होती है, उस स्थिति में हमें लॉग पंक्ति को बदलने के बाद बनाई गई किसी भी चेकपॉइंट को हटाने के लिए भी याद रखना चाहिए। यह अब तक चौकियों की गणना नहीं करके हल किया जा सकता है, लेकिन अब और पिछले चेकपॉइंट के बीच एक महीने का समय छोड़ दें (हम बहुत कम ही बदलाव करते हैं जो बहुत दूर हैं)।

तथ्य यह है कि हमें कभी-कभी पंक्तियों को बदलने की आवश्यकता होती है, बचने के लिए कठिन होते हैं और मैं अभी भी ऐसा करने में सक्षम होना चाहता हूं, यह इस संरचना में नहीं दिखाया गया है, लेकिन लॉग इवेंट कभी-कभी अन्य तालिकाओं में अन्य रिकॉर्ड से बंधे होते हैं, और दूसरी लॉग पंक्ति जोड़ते हैं सही मात्रा प्राप्त करने के लिए कभी-कभी संभव नहीं है।

लॉग टेबल है, जैसा कि आप कल्पना कर सकते हैं, बहुत तेजी से बढ़ रहा है और गणना करने का समय केवल समय के साथ बढ़ेगा।

तो मेरे प्रश्न के लिए, आप इसे कैसे हल करेंगे? क्या वर्तमान स्टॉक मूल्य की गणना करने का एक अधिक कुशल तरीका है? क्या चौकियों का मेरा विचार अच्छा है?

हम SQL सर्वर 2014 वेब (12.0.5511) चला रहे हैं

निष्पादन योजना: https://www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

मैंने वास्तव में ऊपर गलत निष्पादन समय दिया, 20 वह समय था जब कैश का पूरा अपडेट लिया गया था। इस क्वेरी को चलाने में लगभग 6-10 सेकंड लगते हैं (जब मैंने यह क्वेरी प्लान बनाया तो 8 सेकंड)। इस प्रश्न में एक जुड़ाव भी है जो मूल प्रश्न में नहीं था।

जवाबों:


6

कभी-कभी आप अपनी संपूर्ण क्वेरी को बदलने के बजाय थोड़ा सा ट्यूनिंग करके क्वेरी प्रदर्शन में सुधार कर सकते हैं। मैंने आपकी वास्तविक क्वेरी योजना में देखा कि आपकी क्वेरी तीन स्थानों पर tempdb तक फैलती है। यहाँ एक उदाहरण है:

tempdb फैल

उन tempdb spills को हल करने से प्रदर्शन में सुधार हो सकता है। तो Quantityहमेशा होता है गैर नकारात्मक तो आप जगह ले सकता है UNIONके साथ UNION ALLजो की संभावना कुछ और है कि एक स्मृति अनुदान की आवश्यकता नहीं है के लिए हैश संघ ऑपरेटर बदल जाएगा। आपके अन्य टेम्पर्ड बीजाणु कार्डिनैलिटी अनुमान के साथ मुद्दों के कारण होते हैं। आप SQL सर्वर 2014 पर हैं और नए CE का उपयोग कर रहे हैं, इसलिए कार्डिनिटी के अनुमानों को सुधारना मुश्किल हो सकता है क्योंकि क्वेरी ऑप्टिमाइज़र मल्टी-कॉलम आंकड़ों का उपयोग नहीं करेगा। एक त्वरित सुधार के रूप MIN_MEMORY_GRANTमें, SQL Server 2014 SP2 में उपलब्ध क्वेरी संकेत का उपयोग करने पर विचार करें। आपकी क्वेरी की मेमोरी ग्रांट केवल 49104 KB है और अधिकतम उपलब्ध अनुदान 5054840 KB है, इसलिए उम्मीद है कि इसे टक्कर देने से कंसिस्टेंसी बहुत अधिक प्रभावित नहीं होगी। 10% एक उचित शुरुआती अनुमान है, लेकिन आपको इसे अपने हार्डवेयर और डेटा के आधार पर समायोजित और पूरा करने की आवश्यकता हो सकती है। सभी को एक साथ रखते हुए, यह वही है जो आपकी क्वेरी की तरह लग सकता है:

WITH t AS
(
    SELECT ToPositionId AS PositionId, SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY ToPositionId, ProductId
    UNION ALL
    SELECT FromPositionId AS PositionId, -SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY FromPositionId, ProductId
)

SELECT t.ProductId, t.PositionId, SUM(t.Quantity) AS Quantity
FROM t
WHERE NOT t.PositionId = 0
GROUP BY t.ProductId, t.PositionId
HAVING SUM(t.Quantity) > 0
OPTION (MIN_GRANT_PERCENT = 10);

यदि आप आगे प्रदर्शन में सुधार करना चाहते हैं, तो मैं आपकी जांच की मेज बनाने और बनाए रखने के बजाय अनुक्रमित विचारों को आज़माने की सलाह देता हूं । अनुक्रमणित विचार, अपने स्वयं के भौतिक सारणी या ट्रिगर्स को शामिल करने वाले कस्टम समाधान की तुलना में सही होना बहुत आसान है। वे सभी डीएमएल संचालन में ओवरहेड की एक छोटी राशि जोड़ देंगे, लेकिन यह आपको कुछ गैर-अनुक्रमित अनुक्रमित को निकालने की अनुमति दे सकता है जो आपके पास वर्तमान में है। उत्पाद के वेब संस्करण में अनुक्रमित दृश्य समर्थित हैं

अनुक्रमित दृश्यों पर कुछ प्रतिबंध हैं इसलिए आपको उनकी एक जोड़ी बनाने की आवश्यकता होगी। नीचे एक उदाहरण कार्यान्वयन है, नकली डेटा के साथ जो मैंने परीक्षण के लिए उपयोग किया है:

CREATE TABLE dbo.ProductPositionLog (
    LogId BIGINT NOT NULL,
    ProductId BIGINT NOT NULL,
    FromPositionId BIGINT NOT NULL,
    ToPositionId BIGINT NOT NULL,
    Quantity INT NOT NULL,
    FILLER VARCHAR(20),
    PRIMARY KEY (LogId)
);

INSERT INTO dbo.ProductPositionLog WITH (TABLOCK)
SELECT RN, RN % 100, RN % 3999, 3998 - (RN % 3999), RN % 10, REPLICATE('Z', 20)
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q;

CREATE INDEX NCI1 ON dbo.ProductPositionLog (ToPositionId, ProductId) INCLUDE (Quantity);
CREATE INDEX NCI2 ON dbo.ProductPositionLog (FromPositionId, ProductId) INCLUDE (Quantity);

GO    

CREATE VIEW ProductPositionLog_1
WITH SCHEMABINDING  
AS  
   SELECT ToPositionId AS PositionId, SUM(Quantity) AS Quantity, ProductId, COUNT_BIG(*) CNT
    FROM dbo.ProductPositionLog
    WHERE ToPositionId <> 0
    GROUP BY ToPositionId, ProductId
GO  

CREATE UNIQUE CLUSTERED INDEX IDX_V1   
    ON ProductPositionLog_1 (PositionId, ProductId);  
GO  

CREATE VIEW ProductPositionLog_2
WITH SCHEMABINDING  
AS  
   SELECT FromPositionId AS PositionId, SUM(Quantity) AS Quantity, ProductId, COUNT_BIG(*) CNT
    FROM dbo.ProductPositionLog
    WHERE FromPositionId <> 0
    GROUP BY FromPositionId, ProductId
GO  

CREATE UNIQUE CLUSTERED INDEX IDX_V2   
    ON ProductPositionLog_2 (PositionId, ProductId);  
GO  

अनुक्रमित विचारों के बिना क्वेरी को मेरी मशीन पर समाप्त होने में लगभग 2.7 सेकंड लगते हैं। सीरियल में खदान चलाने के अलावा मुझे आपकी एक समान योजना मिलती है:

यहाँ छवि विवरण दर्ज करें

मुझे विश्वास है कि आपको NOEXPANDसंकेत के साथ अनुक्रमित विचारों को क्वेरी करने की आवश्यकता होगी क्योंकि आप एंटरप्राइज़ संस्करण पर नहीं हैं। यहाँ ऐसा करने का एक तरीका है:

WITH t AS
(
    SELECT PositionId, Quantity, ProductId 
    FROM ProductPositionLog_1 WITH (NOEXPAND)
    UNION ALL
    SELECT PositionId, Quantity, ProductId 
    FROM ProductPositionLog_2 WITH (NOEXPAND)
)
SELECT t.ProductId, t.PositionId, SUM(t.Quantity) AS Quantity
FROM t
GROUP BY t.ProductId, t.PositionId
HAVING SUM(t.Quantity) > 0;

इस क्वेरी में मेरी मशीन पर 400 एमएस से कम की सरल योजना और खत्म है:

यहाँ छवि विवरण दर्ज करें

सबसे अच्छी बात यह है कि आपको किसी भी एप्लिकेशन कोड को बदलना नहीं होगा जो डेटा को ProductPositionLogतालिका में लोड करता है । आपको बस यह सत्यापित करने की आवश्यकता है कि अनुक्रमित विचारों की जोड़ी का डीएमएल ओवरहेड स्वीकार्य है।


2

मुझे नहीं लगता कि वास्तव में आपका वर्तमान दृष्टिकोण अक्षम है। यह करने के लिए एक बहुत सीधा रास्ता की तरह लगता है। एक अन्य दृष्टिकोण एक UNPIVOTक्लॉज का उपयोग करना हो सकता है , लेकिन मुझे यकीन नहीं है कि यह प्रदर्शन में सुधार होगा। मैंने दोनों दृष्टिकोणों को नीचे दिए गए कोड (केवल 5 मिलियन से अधिक पंक्तियों) के साथ लागू किया, और प्रत्येक मेरे लैपटॉप पर लगभग 2 सेकंड में लौट आया, इसलिए मुझे यकीन नहीं है कि वास्तविक एक की तुलना में मेरे डेटा सेट के बारे में इतना अलग क्या है। मैंने कोई इंडेक्स (प्राथमिक कुंजी के अलावा अन्य LogId) भी नहीं जोड़ा ।

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[ProductPositionLog]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[ProductPositionLog] (
[LogId] int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
[ProductId] int NULL,
[FromPositionId] int NULL,
[ToPositionId] int NULL,
[Date] datetime NULL,
[Quantity] int NULL
)
END;
GO

SET IDENTITY_INSERT [ProductPositionLog] ON

INSERT INTO [ProductPositionLog] ([LogId], [ProductId], [FromPositionId], [ToPositionId], [Date], [Quantity])
VALUES (1, 123, 0, 1, '2018-01-01 08:10:22', 5)
INSERT INTO [ProductPositionLog] ([LogId], [ProductId], [FromPositionId], [ToPositionId], [Date], [Quantity])
VALUES (2, 123, 0, 2, '2018-01-03 15:15:10', 9)
INSERT INTO [ProductPositionLog] ([LogId], [ProductId], [FromPositionId], [ToPositionId], [Date], [Quantity])
VALUES (3, 123, 1, 3, '2018-01-07 21:08:56', 3)
INSERT INTO [ProductPositionLog] ([LogId], [ProductId], [FromPositionId], [ToPositionId], [Date], [Quantity])
VALUES (4, 123, 3, 0, '2018-02-09 10:03:23', 2)
INSERT INTO [ProductPositionLog] ([LogId], [ProductId], [FromPositionId], [ToPositionId], [Date], [Quantity])
VALUES (5, 123, 2, 3, '2018-02-09 10:03:23', 4)
SET IDENTITY_INSERT [ProductPositionLog] OFF

GO

INSERT INTO ProductPositionLog
SELECT ProductId + 1,
  FromPositionId + CASE WHEN FromPositionId = 0 THEN 0 ELSE 1 END,
  ToPositionId + CASE WHEN ToPositionId = 0 THEN 0 ELSE 1 END,
  [Date], Quantity
FROM ProductPositionLog
GO 20

-- Henrik's original solution.
WITH t AS
(
    SELECT ToPositionId AS PositionId, SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY ToPositionId, ProductId
    UNION
    SELECT FromPositionId AS PositionId, -SUM(Quantity) AS Quantity, ProductId 
    FROM ProductPositionLog
    GROUP BY FromPositionId, ProductId
)
SELECT t.ProductId, t.PositionId, SUM(t.Quantity) AS Quantity
FROM t
WHERE NOT t.PositionId = 0
GROUP BY t.ProductId, t.PositionId
HAVING SUM(t.Quantity) > 0
GO

-- Same results via unpivot
SELECT ProductId, PositionId,
  SUM(CAST(TransferType AS INT) * Quantity) AS Quantity
FROM   
   (SELECT ProductId, Quantity, FromPositionId AS [-1], ToPositionId AS [1]
   FROM ProductPositionLog) p  
  UNPIVOT  
     (PositionId FOR TransferType IN 
        ([-1], [1])
  ) AS unpvt
WHERE PositionId <> 0
GROUP BY ProductId, PositionId

जहां तक ​​चौकियों की बात है, यह मेरे लिए एक उचित विचार है। चूँकि आप कहते हैं कि ProductPositionLogअपडेट और डिलीट वास्तव में निराला हैं, मैं सिर्फ अपडेट और आग पर उस आग पर एक ट्रिगर जोड़ूंगा और हटा दूंगा और वह चेकपॉइंट टेबल को उचित रूप से समायोजित करता है। और बस अतिरिक्त रूप से सुनिश्चित होने के लिए, मैं कभी-कभी स्क्रैच से चेकपॉइंट और कैश टेबल को पुनर्गणना करूंगा।


आपके परीक्षणों के लिए धन्यवाद! जैसा कि मैंने अपने प्रश्न पर टिप्पणी की थी ऊपर मैंने अपने प्रश्न में गलत निष्पादन समय लिखा था (इस विशिष्ट प्रश्न के लिए), यह 10 सेकंड के करीब है। फिर भी, यह आपके परीक्षणों की तुलना में थोड़ा अधिक है। मुझे लगता है कि यह अवरुद्ध या ऐसा कुछ होने के कारण हो सकता है। मेरे चेकपॉइंट सिस्टम का कारण सर्वर पर लोड को कम करना होगा, और यह सुनिश्चित करने का एक तरीका होगा कि लॉग बढ़ने पर प्रदर्शन अच्छा बना रहे। यदि आप एक नज़र रखना चाहते हैं तो मैंने एक क्वेरी योजना प्रस्तुत की है। धन्यवाद।
हेनरिक
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.