변경 로그를 기준으로 재고 수량 계산


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

FromPositionIdToPositionId재고 위치입니다. 예를 들어 일부 위치 ID는 특별한 의미가 있습니다 0. 또는 이벤트는 0주식이 생성 또는 제거되었음을 나타냅니다. From 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 초). 이 질문에는 원래 질문에 포함되지 않은 조인도 있습니다.

답변:


6

때로는 전체 쿼리를 변경하는 대신 약간의 조정만으로 쿼리 성능을 향상시킬 수 있습니다. 실제 쿼리 계획에서 쿼리가 세 곳에서 tempdb로 유출되는 것으로 나타났습니다. 예를 들면 다음과 같습니다.

tempdb 유출

이러한 tempdb 유출을 해결하면 성능이 향상 될 수 있습니다. 경우 Quantity다음 대체 할 수있는 음이 아닌이 항상 UNION함께 UNION ALL있는 가능성이 메모리 부여를 필요로하지 않는 뭔가 다른 해시 조합 연산자를 변경합니다. 다른 tempdb 유출은 카디널리티 추정 문제로 인해 발생합니다. SQL Server 2014를 사용하고 있고 새로운 CE를 사용하고 있으므로 쿼리 최적화 프로그램이 다중 열 통계를 사용하지 않기 때문에 카디널리티 추정을 개선하기가 어려울 수 있습니다. 빠른 수정 MIN_MEMORY_GRANT으로 SQL Server 2014 SP2 에서 제공 되는 쿼리 힌트 사용을 고려하십시오.. 쿼리의 메모리 부여는 49104KB에 불과하고 사용 가능한 최대 부여는 5054840KB이므로이를 부딪 치면 동시성에 큰 영향을 미치지 않습니다. 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 초가 걸립니다. 나는 직렬로 달리는 것을 제외하고는 당신과 비슷한 계획을 얻습니다.

여기에 이미지 설명을 입력하십시오

NOEXPANDEnterprise 버전이 아니기 때문에 인덱싱 된 뷰를 힌트 로 쿼리해야한다고 생각합니다 . 이를 수행하는 한 가지 방법이 있습니다.

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;

이 쿼리는 더 간단한 계획을 가지고 있으며 내 컴퓨터에서 400ms 미만으로 완료됩니다.

여기에 이미지 설명을 입력하십시오

가장 좋은 점은 데이터를 ProductPositionLog테이블에 로드하는 응용 프로그램 코드를 변경할 필요가 없다는 것 입니다. 인덱싱 된 뷰 쌍의 DML 오버 헤드가 허용되는지 확인하기 만하면됩니다.


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 초에 가깝습니다. 그래도 테스트보다 약간 더 큽니다. 차단이나 그와 같은 것으로 생각됩니다. 내 체크 포인트 시스템의 이유는 서버의로드를 최소화하기위한 것이며 로그가 커짐에 따라 성능을 유지하는 방법입니다. 살펴보고 싶다면 위의 쿼리 계획을 제출했습니다. 감사.
Henrik
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.