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


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:具有特殊含义,例如0。来自或到的事件0表示已创建或删除库存。从0可能是交货的库存,到0可能是发货的订单。

该表当前包含约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

即使完成了合理的时间(大约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

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

Answers:


6

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

tempdb溢出

解决这些tempdb溢出问题可能会提高性能。如果Quantity始终为非负数,则可以替换UNIONUNION ALL这很可能会将哈希联合运算符更改为不需要内存授予的其他内容。您的其他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);

如果您想进一步提高性能,建议您尝试索引视图,而不是构建和维护自己的检查点表。与包含您自己的物化表或触发器的自定义解决方案相比,索引视图明显更易于正确使用。它们将为所有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毫秒内完成:

在此处输入图片说明

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


2

我真的不认为您当前的方法效率不高。似乎是一种非常简单的方法。另一种方法可能是使用UNPIVOT子句,但是我不确定这会提高性能。我用下面的代码(刚好超过500万行)实现了这两种方法,并且每种方法在我的笔记本电脑上大约在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秒。尽管如此,它比您的测试还要多,我想这可能是由于阻塞或类似原因造成的。我的检查点系统的原因是为了最大程度地减少服务器上的负载,这将是确保性能随着日志增长而保持良好状态的一种方法。我想在上面提交一个查询计划。谢谢。
亨里克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.