根据变更日志计算库存数量

Hen*_*rik 10 sql-server optimization sql-server-2014 running-totals

假设您有以下表结构:

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
Run Code Online (Sandbox Code Playgroud)

FromPositionId并且ToPositionId是股票头寸。某些位置 ID:s 具有特殊含义,例如0。事件 from 或 to0表示库存已创建或删除。From0可能是交货的库存,to0可能是发货的订单。

该表目前包含大约 550 万行。我们使用如下所示的查询计算每个产品的库存价值并按计划将其定位到缓存表中:

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
Run Code Online (Sandbox Code Playgroud)

尽管这在合理的时间内完成(大约 20 秒),但我觉得这是一种非常低效的计算股票价值的方法。除了INSERT这个表中的 :s 之外,我们很少做任何事情,但有时我们会因为生成这些行的人的错误而手动调整数量或删除一行。

我有一个在单独的表中创建“检查点”的想法,计算到特定时间点的值,并在创建我们的库存数量缓存表时将其用作起始值:

ProductId | PositionId | Date                | Quantity
-------------------------------------------------------
123       | 10002      | 2018-01-07 21:08:56 | 2
Run Code Online (Sandbox Code Playgroud)

我们有时更改行的事实对此造成了问题,在这种情况下,我们还必须记住删除在我们更改日志行之后创建的任何检查点。这可以通过直到现在不计算检查点来解决,而是在现在和最后一个检查点之间留一个月的时间(我们很少在那么远的时候进行更改)。

我们有时需要更改行的事实很难避免,我希望仍然能够这样做,它没有显示在此结构中,但日志事件有时会与其他表中的其他记录相关联,并添加另一个日志行获得正确的数量有时是不可能的。

可以想象,日志表增长得非常快,计算时间只会随着时间的推移而增加。

所以对于我的问题,你将如何解决这个问题?有没有更有效的方法来计算当前的股票价值?我对检查站的想法是好的吗?

我们正在运行 SQL Server 2014 Web (12.0.5511)

执行计划:https : //www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

上面我其实给出了错误的执行时间,20s是完全更新缓存所花费的时间。此查询大约需要 6-10 秒才能运行(我创建此查询计划时为 8 秒)。此查询中还有一个连接,它不在原始问题中。

Joe*_*ish 6

有时,您只需进行一点调整而不是更改整个查询,就可以提高查询性能。我注意到在您的实际查询计划中,您的查询在三个地方溢出到 tempdb。下面是一个例子:

tempdb 溢出

解决这些 tempdb 溢出可能会提高性能。如果Quantity始终为非负,则您可以替换UNIONUNION ALLwhich 可能会将哈希联合运算符更改为不需要内存授予的其他内容。您的其他 tempdb 溢出是由基数估计问题引起的。您使用的是 SQL Server 2014 并使用新的 CE,因此可能难以改进基数估计,因为查询优化器不会使用多列统计信息。作为快速修复,请考虑使用SQL Server 2014 SP2 中MIN_MEMORY_GRANT提供的查询提示. 您的查询的内存授权仅为 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);
Run Code Online (Sandbox Code Playgroud)

如果您希望进一步提高性能,我建议您尝试索引视图,而不是构建和维护您自己的检查点表。索引视图比涉及您自己的物化表或触发器的自定义解决方案更容易获得正确的结果。它们会为所有 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  
Run Code Online (Sandbox Code Playgroud)

如果没有索引视图,查询需要大约 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;
Run Code Online (Sandbox Code Playgroud)

这个查询有一个更简单的计划,在我的机器上在 400 毫秒内完成:

在此处输入图片说明

最好的部分是您不必更改将数据加载到ProductPositionLog表中的任何应用程序代码。您只需要验证这对索引视图的 DML 开销是否可接受。