Tính toán số lượng chứng khoán dựa trên nhật ký thay đổi


10

Hãy tưởng tượng rằng bạn có cấu trúc bảng sau:

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

FromPositionIdToPositionIdcác vị trí cổ phiếu. Một số vị trí ID: s có ý nghĩa đặc biệt, ví dụ 0. Một sự kiện từ hoặc 0có nghĩa là cổ phiếu đã được tạo hoặc xóa. Từ 0có thể là chứng khoán từ một giao hàng và 0có thể là một đơn đặt hàng được vận chuyển.

Bảng này hiện đang giữ khoảng 5,5 triệu hàng. Chúng tôi tính toán giá trị chứng khoán cho từng sản phẩm và vị trí vào bảng bộ đệm theo lịch biểu bằng cách sử dụng truy vấn trông giống như sau:

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

Mặc dù điều này hoàn thành trong một khoảng thời gian hợp lý (khoảng 20 giây), tôi cảm thấy như đây là một cách tính không hiệu quả để tính các giá trị chứng khoán. Chúng tôi hiếm khi làm bất cứ điều gì ngoại trừ INSERT: trong bảng này, nhưng đôi khi chúng tôi đi vào và điều chỉnh số lượng hoặc xóa một hàng theo cách thủ công do những người tạo ra các hàng này.

Tôi đã có ý tưởng tạo "điểm kiểm tra" trong một bảng riêng biệt, tính toán giá trị đến một thời điểm cụ thể và sử dụng đó làm giá trị bắt đầu khi tạo bảng bộ đệm số lượng chứng khoán của chúng tôi:

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

Thực tế là đôi khi chúng ta thay đổi các hàng đặt ra một vấn đề cho vấn đề này, trong trường hợp đó chúng ta cũng phải nhớ xóa bất kỳ điểm kiểm tra nào được tạo sau khi hàng nhật ký chúng ta thay đổi. Điều này có thể được giải quyết bằng cách không tính toán các điểm kiểm tra cho đến bây giờ, nhưng để lại một tháng từ giờ đến điểm kiểm tra cuối cùng (chúng tôi rất hiếm khi thực hiện các thay đổi lùi xa).

Thực tế là đôi khi chúng ta cần thay đổi các hàng là điều khó tránh và tôi muốn vẫn có thể làm điều này, nó không được hiển thị trong cấu trúc này nhưng các sự kiện nhật ký đôi khi được gắn với các bản ghi khác trong các bảng khác và thêm một hàng nhật ký khác để có được số lượng đúng đôi khi không thể.

Bảng nhật ký là, như bạn có thể tưởng tượng, phát triển khá nhanh và thời gian để tính toán sẽ chỉ tăng theo thời gian.

Vì vậy, với câu hỏi của tôi, làm thế nào bạn sẽ giải quyết điều này? Có cách nào hiệu quả hơn để tính giá trị cổ phiếu hiện tại? Là ý tưởng của tôi về trạm kiểm soát là một tốt?

Chúng tôi đang chạy SQL Server 2014 Web (12.0.5511)

Kế hoạch thực hiện: https://www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

Tôi thực sự đã đưa ra thời gian thực hiện sai ở trên, 20s là thời gian mà bản cập nhật hoàn chỉnh của bộ đệm đã mất. Truy vấn này mất khoảng 6-10 giây để chạy (8 giây khi tôi tạo kế hoạch truy vấn này). Ngoài ra còn có một tham gia trong truy vấn này không có trong câu hỏi ban đầu.

Câu trả lời:


6

Đôi khi bạn có thể cải thiện hiệu suất truy vấn chỉ bằng cách thực hiện một chút điều chỉnh thay vì thay đổi toàn bộ truy vấn của mình. Tôi nhận thấy trong kế hoạch truy vấn thực tế của bạn rằng truy vấn của bạn tràn sang tempdb ở ba nơi. Đây là một ví dụ:

tràn tempdb

Giải quyết các sự cố tràn tempdb có thể cải thiện hiệu suất. Nếu Quantityluôn luôn không âm thì bạn có thể thay thế UNIONbằng UNION ALLtoán tử có khả năng thay đổi toán tử liên kết băm thành một thứ khác không yêu cầu cấp bộ nhớ. Sự cố tràn tempdb khác của bạn là do các vấn đề với ước tính cardinality. Bạn đang sử dụng SQL Server 2014 và đang sử dụng CE mới nên có thể khó cải thiện các ước tính về số lượng thẻ vì trình tối ưu hóa truy vấn sẽ không sử dụng thống kê nhiều cột. Để khắc phục nhanh, hãy xem xét sử dụng MIN_MEMORY_GRANTgợi ý truy vấn có sẵn trong SQL Server 2014 SP2. Cấp bộ nhớ cho truy vấn của bạn chỉ là 49104 KB và khoản trợ cấp tối đa có sẵn là 5054840 KB, vì vậy, hy vọng việc trả lại nó sẽ không ảnh hưởng quá nhiều đến đồng thời. 10% là dự đoán bắt đầu hợp lý nhưng bạn có thể cần điều chỉnh và thực hiện tùy thuộc vào phần cứng và dữ liệu của mình. Đặt tất cả lại với nhau, đây là những gì truy vấn của bạn có thể trông như sau:

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);

Nếu bạn muốn cải thiện hiệu suất hơn nữa, tôi khuyên bạn nên thử các chế độ xem được lập chỉ mục thay vì xây dựng và duy trì bảng điểm kiểm tra của riêng bạn. Các khung nhìn được lập chỉ mục dễ dàng hơn để có được quyền hơn so với một giải pháp tùy chỉnh liên quan đến bảng được kích hoạt hoặc bảng cụ thể của riêng bạn. Họ sẽ thêm một lượng nhỏ chi phí cho tất cả các hoạt động DML nhưng nó có thể cho phép bạn loại bỏ một số chỉ mục không bao gồm mà bạn hiện có. Chế độ xem được lập chỉ mục dường như được hỗ trợ trong phiên bản web của sản phẩm.

Có một số hạn chế đối với các chế độ xem được lập chỉ mục, do đó bạn sẽ cần tạo một cặp trong số chúng. Dưới đây là một ví dụ triển khai, cùng với dữ liệu giả mà tôi đã sử dụng để kiểm tra:

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  

Nếu không có lượt xem được lập chỉ mục, truy vấn sẽ mất khoảng 2,7 giây để hoàn tất trên máy của tôi. Tôi nhận được một kế hoạch tương tự như của bạn ngoại trừ của tôi chạy nối tiếp:

nhập mô tả hình ảnh ở đây

Tôi tin rằng bạn sẽ cần truy vấn các lượt xem được lập chỉ mục với NOEXPANDgợi ý vì bạn không có phiên bản doanh nghiệp. Đây là một cách để làm điều đó:

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;

Truy vấn này có một kế hoạch đơn giản hơn và kết thúc dưới 400 ms trên máy của tôi:

nhập mô tả hình ảnh ở đây

Phần tốt nhất là bạn sẽ không phải thay đổi bất kỳ mã ứng dụng nào tải dữ liệu vào ProductPositionLogbảng. Bạn chỉ cần xác minh rằng chi phí DML của cặp khung nhìn được lập chỉ mục có thể chấp nhận được.


2

Tôi thực sự không nghĩ rằng cách tiếp cận hiện tại của bạn là không hiệu quả. Có vẻ như một cách khá đơn giản để làm điều đó. Một cách tiếp cận khác có thể là sử dụng một UNPIVOTmệnh đề, nhưng tôi không chắc đó sẽ là một cải tiến hiệu suất. Tôi đã thực hiện cả hai cách tiếp cận với mã dưới đây (chỉ hơn 5 triệu hàng) và mỗi lần trả về sau khoảng 2 giây trên máy tính xách tay của tôi, vì vậy tôi không chắc có gì khác biệt về bộ dữ liệu của mình so với dữ liệu thực. Tôi thậm chí không thêm bất kỳ chỉ mục nào (ngoài khóa chính 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

Theo như các trạm kiểm soát, nó có vẻ là một ý tưởng hợp lý với tôi. Vì bạn nói rằng các cập nhật và xóa thực sự không thường xuyên, tôi chỉ cần thêm một kích hoạt vào ProductPositionLogđó để cập nhật và xóa và điều chỉnh bảng điểm kiểm tra một cách thích hợp. Và để chắc chắn hơn, thỉnh thoảng tôi sẽ tính toán lại các điểm kiểm tra và bảng bộ đệm.


Cảm ơn bạn đã kiểm tra của bạn! Như tôi đã nhận xét về câu hỏi của mình ở trên, tôi đã viết sai thời gian thực hiện trong câu hỏi của mình (đối với truy vấn cụ thể này), nó gần đến 10 giây. Tuy nhiên, nó nhiều hơn một chút so với trong các thử nghiệm của bạn. Tôi đoán có thể là do chặn hoặc một cái gì đó tương tự. Lý do cho hệ thống điểm kiểm tra của tôi là để giảm thiểu tải trên máy chủ và đó sẽ là một cách để đảm bảo hiệu suất vẫn tốt khi nhật ký phát triển. Tôi đã gửi một kế hoạch truy vấn ở trên nếu bạn muốn có một cái nhìn. Cảm ơn.
Henrik
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.