Değişiklik günlüğüne göre stok miktarının hesaplanması


10

Aşağıdaki tablo yapısına sahip olduğunuzu düşünün:

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

FromPositionIdve ToPositionIdhisse senedi pozisyonlarıdır. Örneğin bazı pozisyon kimlikleri özel bir anlama sahiptir 0. Bir olaydan veya bir olay, 0hisse senedinin yaratıldığı veya kaldırıldığı anlamına gelir. Gönderen 0stoktan olabilir ve 0sevk edilen bir sipariş olabilir.

Bu tablo şu anda yaklaşık 5.5 milyon satır içermektedir. Her ürün için stok değerini hesaplıyoruz ve şuna benzer bir sorgu kullanarak bir çizelgede önbellek tablosuna yerleştiriyoruz:

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

Bu makul bir sürede (yaklaşık 20 saniye) tamamlansa da, bunun hisse senedi değerlerini hesaplamanın oldukça verimsiz bir yolu olduğunu hissediyorum. INSERTBu tabloda nadiren başka bir şey yapmıyoruz , ancak bazen bu satırları oluşturan kişilerin hatalarından dolayı içeri girip miktarı ayarlıyoruz veya bir satırı manuel olarak kaldırıyoruz.

Ayrı bir tabloda "kontrol noktaları" oluşturma, belirli bir zamana kadar değeri hesaplama ve hisse senedi miktarı önbellek tabloyu oluştururken başlangıç ​​değeri olarak kullanma fikri vardı:

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

Bazen satırları değiştirmemiz bunun için bir sorun teşkil eder, bu durumda değiştirdiğimiz günlük satırından sonra oluşturulan tüm kontrol noktalarını kaldırmayı da unutmamalıyız. Bu, şimdiye kadar kontrol noktalarını hesaplamamakla çözülebilir, ancak şimdiki ve son kontrol noktası arasında bir ay bırakın (çok nadiren değişiklikler yapıyoruz).

Bazen satırları değiştirmemiz gerektiğinden kaçınmak zordur ve bunu hala yapmak istiyorum, bu yapıda gösterilmiyor, ancak günlük olayları bazen diğer tablolardaki diğer kayıtlara bağlı ve başka bir günlük satırı ekleniyor doğru miktarı elde etmek bazen mümkün değildir.

Günlük tablosu, tahmin edebileceğiniz gibi, oldukça hızlı büyüyor ve hesaplama zamanı sadece zamanla artacak.

Soruma göre, bunu nasıl çözerdiniz? Mevcut hisse senedi değerini hesaplamanın daha verimli bir yolu var mı? Kontrol noktaları fikrim iyi mi?

SQL Server 2014 Web (12.0.5511) çalıştırıyoruz

Uygulama planı: https://www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

Aslında yukarıdaki yanlış yürütme süresini verdim, 20'ler önbelleğin tam güncellemesinin aldığı zamandı. Bu sorgunun çalışması yaklaşık 6-10 saniye sürer (bu sorgu planını oluşturduğumda 8 saniye). Bu sorguda orijinal soruda yer almayan bir de birleştirme var.

Yanıtlar:


6

Bazen, sorgunuzun tamamını değiştirmek yerine biraz ayar yaparak sorgu performansını artırabilirsiniz. Gerçek sorgu planınızda, sorgunuzun üç yerde tempdb'ye döküldüğünü fark ettim. İşte bir örnek:

tempdb dökülmeleri

Bu tempdb dökülmelerini gidermek performansı artırabilir. Her Quantityzaman negatif değilse , hash union operatörünü bellek yardımı gerektirmeyen başka bir şeyle değiştirecek olan UNIONile UNION ALLdeğiştirebilirsiniz. Diğer tempdb dökülmelerinize kardinalite tahmini ile ilgili sorunlar neden olur. SQL Server 2014 kullanıyorsunuz ve yeni CE kullanıyorsunuz, bu nedenle sorgu optimize edici çok sütunlu istatistik kullanmayacağı için kardinalite tahminlerini iyileştirmek zor olabilir. Hızlı bir düzeltme olarak, SQL Server 2014 SP2'deMIN_MEMORY_GRANT sunulan sorgu ipucunu kullanmayı düşünün. Sorgunuzun bellek hibesi sadece 49104 KB ve kullanılabilir maksimum hibe 5054840 KB olduğundan umarım onu ​​çarpmak eşzamanlılığı çok fazla etkilemez. % 10 makul bir başlangıç ​​tahminidir, ancak donanımınıza ve verilerinize bağlı olarak ayarlamanız ve yapmanız gerekebilir. Hepsini bir araya getirirsek, sorgunuz şöyle görünebilir:

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

Performansı daha da artırmak istiyorsanız , kendi kontrol noktası tablonuzu oluşturmak ve korumak yerine dizine alınmış görünümleri denemenizi öneririz . Dizine alınmış görünümleri elde etmek, kendi somutlaştırılmış tablonuzu veya tetikleyicilerinizi içeren özel bir çözümden önemli ölçüde daha kolaydır. Tüm DML işlemlerine küçük bir miktar ek yük eklerler, ancak şu anda sahip olduğunuz kümelenmemiş dizinlerin bazılarını kaldırmanıza izin verebilir. Dizine eklenen görünümler , ürünün web sürümünde destekleniyor gibi görünüyor .

Dizinlenmiş görünümlerde bazı kısıtlamalar vardır, bu nedenle bir çift oluşturmanız gerekir. Aşağıda, test için kullandığım sahte verilerle birlikte örnek bir uygulama yer almaktadır:

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  

Dizine alınmış görünümler olmadan, sorgunun makinemde tamamlanması yaklaşık 2.7 saniye sürer. Benimkine benzer bir plan alıyorum, benimki seri halinde çalışır:

resim açıklamasını buraya girin

NOEXPANDKurumsal sürümde olmadığınız için dizine alınan görünümleri ipucu ile sorgulamanız gerektiğine inanıyorum . İşte bunu yapmanın bir yolu:

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;

Bu sorgu daha basit bir plana sahip ve makinemde 400 ms'nin altında bitiyor:

resim açıklamasını buraya girin

En iyi yanı, ProductPositionLogtabloya veri yükleyen uygulama kodlarını değiştirmek zorunda kalmamanızdır . Dizine alınmış görünüm çiftinin DML ek yükünün kabul edilebilir olduğunu doğrulamanız yeterlidir.


2

Mevcut yaklaşımınızın bu kadar verimsiz olduğunu düşünmüyorum. Bunu yapmanın oldukça basit bir yolu gibi görünüyor. Başka bir yaklaşım bir UNPIVOTmadde kullanmak olabilir , ancak performans iyileştirmesi olacağından emin değilim. Her iki yaklaşımı da aşağıdaki kodla (5 milyondan fazla satır) uyguladım ve her biri dizüstü bilgisayarımda yaklaşık 2 saniye içinde geri döndü, bu yüzden veri setimde gerçek olana kıyasla neyin farklı olduğunu bilmiyorum. Hatta herhangi bir dizin eklemedim (birincil anahtar dışında 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

Kontrol noktalarına gelince, benim için makul bir fikir gibi görünüyor. Güncelleştirmelerin ve silme işlemlerinin gerçekten nadir olduğunu söylediğiniz için, yalnızca ProductPositionLoggüncelleme ve silme işlemine başlayan ve denetim noktası tablosunu uygun şekilde ayarlayan bir tetikleyici ekleyeceğim . Ve sadece ekstra emin olmak için, kontrol noktasını ve önbellek tablolarını zaman zaman sıfırdan yeniden hesaplayacağım.


Testleriniz için teşekkürler! Yukarıdaki soruma yorum yaptığım gibi, sorumdaki yanlış yürütme süresini yazdım (bu özel sorgu için), 10 saniyeye daha yakın. Yine de, testlerinizden biraz daha fazla.Bence engelleme veya bunun gibi bir şeyden kaynaklanıyor olabilir. Denetim noktası sistemimin nedeni, sunucudaki yükü en aza indirgemek ve günlük büyüdükçe performansın iyi kalmasını sağlamak için bir yol olacaktır. Bir göz atmak istiyorsanız yukarıdaki bir sorgu planı gönderdim. Teşekkürler.
Henrik
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.