การคำนวณปริมาณสต็อคตามบันทึกการเปลี่ยนแปลง


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เป็นตำแหน่งหุ้น บางตำแหน่ง ID: s 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: s ในตารางนี้ แต่บางครั้งเราเข้าไปข้างในและปรับปริมาณหรือลบแถวด้วยตนเองเนื่องจากความผิดพลาดของผู้ที่สร้างแถวเหล่านี้

ฉันมีความคิดในการสร้าง "จุดตรวจสอบ" ในตารางที่แยกต่างหากคำนวณค่าจนถึงจุดเฉพาะเวลาและใช้เป็นค่าเริ่มต้นเมื่อสร้างตารางแคชปริมาณสต็อกของเรา:

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

ความจริงที่ว่าบางครั้งเราเปลี่ยนแถวทำให้เกิดปัญหาในกรณีนี้เราต้องจำไว้ว่าให้ลบจุดตรวจสอบใด ๆ ที่สร้างขึ้นหลังจากแถวบันทึกที่เราเปลี่ยนไป สิ่งนี้สามารถแก้ไขได้โดยไม่คำนวณจุดตรวจจนถึงตอนนี้ แต่ให้เหลือหนึ่งเดือนระหว่างตอนนี้กับจุดตรวจสุดท้าย (เราไม่ค่อยทำการเปลี่ยนแปลงที่ไกลมาก)

ความจริงที่ว่าบางครั้งเราจำเป็นต้องเปลี่ยนแถวนั้นยากที่จะหลีกเลี่ยงและฉันต้องการที่จะยังคงสามารถทำสิ่งนี้ได้มันไม่ได้แสดงในโครงสร้างนี้ แต่บางครั้งเหตุการณ์บันทึกจะเชื่อมโยงกับระเบียนอื่นในตารางอื่นและเพิ่มแถวบันทึกอื่น เพื่อให้ได้ปริมาณที่เหมาะสมบางครั้งก็เป็นไปไม่ได้

ตารางบันทึกคือคุณสามารถจินตนาการได้ว่าการเติบโตค่อนข้างเร็วและเวลาในการคำนวณจะเพิ่มขึ้นตามเวลา

ดังนั้นสำหรับคำถามของฉันคุณจะแก้ปัญหานี้อย่างไร มีวิธีที่มีประสิทธิภาพมากกว่าในการคำนวณมูลค่าหุ้นปัจจุบันหรือไม่? ความคิดของฉันเกี่ยวกับจุดตรวจที่ดีหรือไม่?

เรากำลังเรียกใช้ SQL Server 2014 Web (12.0.5511)

แผนการดำเนินการ: https://www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

ที่จริงฉันให้เวลาดำเนินการผิดข้างต้น 20s เป็นเวลาที่การปรับปรุงแคชที่สมบูรณ์ใช้ แบบสอบถามนี้ใช้เวลาประมาณ 6-10 วินาทีในการเรียกใช้ (8 วินาทีเมื่อฉันสร้างแผนแบบสอบถามนี้) นอกจากนี้ยังมีการเข้าร่วมในแบบสอบถามนี้ที่ไม่ได้อยู่ในคำถามเดิม

คำตอบ:


6

บางครั้งคุณสามารถปรับปรุงประสิทธิภาพการสืบค้นเพียงแค่ทำการปรับแต่งเล็กน้อยแทนที่จะเปลี่ยนการสืบค้นทั้งหมด ฉันสังเกตเห็นในแผนแบบสอบถามจริงของคุณว่าแบบสอบถามของคุณรั่วไหลไปยัง tempdb ในสามแห่ง นี่คือตัวอย่างหนึ่ง:

การรั่วไหลของ tempdb

การแก้ไขการรั่วไหลของ tempdb เหล่านั้นอาจช่วยปรับปรุงประสิทธิภาพได้ หากQuantityไม่เป็นลบเสมอคุณสามารถแทนที่UNIONด้วยUNION ALLซึ่งจะเปลี่ยนผู้ดำเนินการแฮชสหภาพเป็นอย่างอื่นที่ไม่ต้องการสิทธิ์หน่วยความจำ การรั่วไหลของ tempdb อื่น ๆ ของคุณมีสาเหตุมาจากปัญหาเกี่ยวกับการประเมิน cardinality คุณอยู่บน SQL Server 2014 และใช้ CE ใหม่ดังนั้นจึงอาจเป็นเรื่องยากที่จะปรับปรุงการประมาณค่า cardinality เนื่องจากเครื่องมือเพิ่มประสิทธิภาพการสืบค้นจะไม่ใช้สถิติหลายคอลัมน์ เป็นการแก้ไขด่วนพิจารณาใช้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);

หากคุณต้องการปรับปรุงประสิทธิภาพเพิ่มเติมฉันขอแนะนำให้ลองดูการจัดทำดัชนีแทนการสร้างและบำรุงรักษาตารางจุดตรวจของคุณเอง มุมมองที่มีการจัดทำดัชนีจะง่ายกว่าการใช้โซลูชันที่กำหนดเองซึ่งเกี่ยวข้องกับตาราง material หรือทริกเกอร์ของคุณเอง พวกเขาจะเพิ่มค่าใช้จ่ายเล็กน้อยในการดำเนินการ DML ทั้งหมด แต่อาจช่วยให้คุณลบดัชนีที่ไม่ได้ทำคลัสเตอร์บางส่วนที่คุณมีอยู่ในปัจจุบัน มุมมองที่จัดทำดัชนีดูเหมือนจะได้รับการสนับสนุนในรุ่นเว็บของผลิตภัณฑ์

มีข้อ จำกัด บางประการเกี่ยวกับมุมมองที่จัดทำดัชนีดังนั้นคุณจะต้องสร้างมุมมองเหล่านั้น ด้านล่างนี้เป็นตัวอย่างการนำไปใช้งานพร้อมกับข้อมูลปลอมที่ฉันใช้สำหรับการทดสอบ:

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 ms บนเครื่องของฉัน:

ป้อนคำอธิบายรูปภาพที่นี่

ส่วนที่ดีที่สุดคือคุณไม่ต้องเปลี่ยนรหัสแอปพลิเคชันใด ๆ ที่โหลดข้อมูลลงในProductPositionLogตาราง คุณเพียงแค่ต้องตรวจสอบว่าค่าใช้จ่าย DML ของมุมมองที่จัดทำดัชนีเป็นที่ยอมรับ


2

ฉันไม่คิดว่าวิธีการปัจจุบันของคุณเป็นสิ่งที่ไม่มีประสิทธิภาพ ดูเหมือนว่าจะเป็นวิธีที่ตรงไปตรงมาสวยมาก อีกวิธีหนึ่งอาจใช้UNPIVOTclause แต่ฉันไม่แน่ใจว่าเป็นการปรับปรุงประสิทธิภาพ ฉันใช้ทั้งสองวิธีด้วยรหัสด้านล่าง (เพียง 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 วินาทีแล้ว ถึงกระนั้นก็เป็นอะไรที่มากกว่าการทดสอบของคุณฉันคิดว่ามันอาจเป็นเพราะการบล็อคหรืออะไรทำนองนั้น เหตุผลสำหรับระบบจุดตรวจสอบของฉันคือเพื่อลดการโหลดบนเซิร์ฟเวอร์และมันจะเป็นวิธีที่จะทำให้แน่ใจว่าประสิทธิภาพการทำงานยังคงดีเมื่อบันทึกเติบโตขึ้น ฉันส่งแผนแบบสอบถามด้านบนหากคุณต้องการดู ขอบคุณ
Henrik
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.