Menghitung jumlah stok berdasarkan log perubahan


10

Bayangkan Anda memiliki struktur tabel berikut:

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

FromPositionIddan ToPositionIdposisi saham. Beberapa ID posisi: memiliki makna khusus, misalnya 0. Peristiwa dari atau ke 0berarti stok dibuat atau dihapus. Dari 0bisa menjadi stok dari pengiriman dan 0menjadi pesanan dikirim.

Tabel ini saat ini menampung sekitar 5,5 juta baris. Kami menghitung nilai stok untuk setiap produk dan posisi ke dalam tabel cache pada jadwal menggunakan kueri yang terlihat seperti ini:

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

Meskipun ini selesai dalam jumlah waktu yang wajar (sekitar 20 detik), saya merasa seperti ini adalah cara yang cukup tidak efisien untuk menghitung nilai persediaan. Kami jarang melakukan apa pun kecuali INSERT: di tabel ini, tetapi kadang-kadang kami masuk dan menyesuaikan kuantitas atau menghapus baris secara manual karena kesalahan oleh orang-orang yang menghasilkan baris ini.

Saya memiliki ide untuk membuat "pos pemeriksaan" di tabel terpisah, menghitung nilai hingga titik waktu tertentu dan menggunakannya sebagai nilai awal saat membuat tabel cache jumlah stok kami:

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

Fakta bahwa kadang-kadang kita mengubah baris menimbulkan masalah, dalam hal ini kita juga harus ingat untuk menghapus pos pemeriksaan yang dibuat setelah baris log yang kita ubah. Ini dapat diselesaikan dengan tidak menghitung pos pemeriksaan sampai sekarang, tetapi biarkan sebulan antara sekarang dan pos pemeriksaan terakhir (kami sangat jarang melakukan perubahan sejauh itu).

Fakta bahwa kita kadang-kadang perlu mengubah baris sulit untuk dihindari dan saya ingin tetap bisa melakukan ini, itu tidak ditampilkan dalam struktur ini tetapi peristiwa log kadang-kadang terikat dengan catatan lain di tabel lain, dan menambahkan baris log lain untuk mendapatkan jumlah yang tepat terkadang tidak mungkin.

Tabel log, seperti yang Anda bayangkan, tumbuh cukup cepat dan waktu untuk menghitung hanya akan meningkat seiring waktu.

Jadi untuk pertanyaan saya, bagaimana Anda menyelesaikan ini? Apakah ada cara yang lebih efisien untuk menghitung nilai stok saat ini? Apakah ide saya tentang pos pemeriksaan bagus?

Kami menjalankan SQL Server 2014 Web (12.0.5511)

Rencana eksekusi: https://www.brentozar.com/pastetheplan/?id=Bk8gyc68Q

Saya benar-benar memberikan waktu eksekusi yang salah di atas, 20-an adalah waktu yang dibutuhkan untuk menyelesaikan pembaruan cache. Kueri ini membutuhkan sekitar 6-10 detik untuk dijalankan (8 detik ketika saya membuat rencana kueri ini). Ada juga yang bergabung dalam kueri ini yang tidak ada di pertanyaan awal.

Jawaban:


6

Terkadang Anda dapat meningkatkan kinerja kueri hanya dengan melakukan sedikit penyetelan alih-alih mengubah seluruh kueri Anda. Saya perhatikan dalam rencana kueri Anda yang sebenarnya bahwa kueri Anda tumpah ke tempdb di tiga tempat. Ini salah satu contohnya:

tumpahan tempdb

Mengatasi tumpahan tempdb tersebut dapat meningkatkan kinerja. Jika Quantityselalu non-negatif maka Anda dapat mengganti UNIONdengan UNION ALLyang kemungkinan akan mengubah operator serikat hash menjadi sesuatu yang lain yang tidak memerlukan hibah memori. Tumpahan tempdb Anda yang lain disebabkan oleh masalah dengan estimasi kardinalitas. Anda menggunakan SQL Server 2014 dan menggunakan CE baru sehingga mungkin sulit untuk meningkatkan perkiraan kardinalitas karena pengoptimal kueri tidak akan menggunakan statistik multi-kolom. Sebagai perbaikan cepat, pertimbangkan untuk menggunakan MIN_MEMORY_GRANTpetunjuk kueri yang tersedia di SQL Server 2014 SP2. Hibah memori kueri Anda hanya 49104 KB dan maksimal hibah yang tersedia adalah 5054840 KB, jadi semoga saja tidak akan terlalu memengaruhi konkurensi. 10% adalah perkiraan awal yang masuk akal, tetapi Anda mungkin perlu menyesuaikannya dan melakukannya tergantung pada perangkat keras dan data Anda. Menyatukan semua itu, seperti inilah tampilan kueri Anda:

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

Jika Anda ingin meningkatkan kinerja lebih lanjut, saya sarankan untuk mencoba tampilan yang diindeks alih-alih membangun dan mempertahankan tabel pos pemeriksaan Anda sendiri. Tampilan yang diindeks secara signifikan lebih mudah dilakukan daripada solusi kustom yang melibatkan tabel atau pemicu terwujud Anda sendiri. Mereka akan menambahkan sejumlah kecil overhead ke semua operasi DML tetapi mungkin memungkinkan Anda untuk menghapus beberapa indeks nonclustered yang saat ini Anda miliki. Tampilan yang diindeks tampaknya didukung dalam edisi web produk.

Ada beberapa batasan pada tampilan yang diindeks sehingga Anda harus membuat sepasang dari mereka. Di bawah ini adalah contoh implementasi, bersama dengan data palsu yang saya gunakan untuk pengujian:

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  

Tanpa tampilan yang diindeks, kueri membutuhkan waktu sekitar 2,7 detik untuk selesai di mesin saya. Saya mendapatkan paket yang mirip dengan milik Anda kecuali seri saya berjalan dalam serial:

masukkan deskripsi gambar di sini

Saya percaya bahwa Anda perlu menanyakan tampilan yang diindeks dengan NOEXPANDpetunjuk karena Anda tidak pada edisi perusahaan. Inilah satu cara untuk melakukannya:

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;

Kueri ini memiliki paket yang lebih sederhana dan selesai di bawah 400 ms pada mesin saya:

masukkan deskripsi gambar di sini

Bagian terbaiknya adalah Anda tidak perlu mengubah kode aplikasi apa pun yang memuat data ke dalam ProductPositionLogtabel. Anda hanya perlu memverifikasi bahwa overhead DML dari sepasang tampilan yang diindeks dapat diterima.


2

Saya tidak berpikir pendekatan Anda saat ini tidak efisien. Sepertinya cara yang cukup mudah untuk melakukannya. Pendekatan lain mungkin menggunakan UNPIVOTklausa, tapi saya tidak yakin itu akan menjadi peningkatan kinerja. Saya menerapkan kedua pendekatan dengan kode di bawah ini (lebih dari 5 juta baris), dan masing-masing kembali dalam waktu sekitar 2 detik di laptop saya, jadi saya tidak yakin apa yang begitu berbeda tentang kumpulan data saya dibandingkan dengan yang asli. Saya bahkan tidak menambahkan indeks apa pun (selain kunci primer aktif 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

Sejauh pos pemeriksaan pergi, sepertinya ide yang masuk akal bagi saya. Karena Anda mengatakan bahwa pembaruan dan penghapusan benar-benar jarang terjadi, saya hanya akan menambahkan pemicu pada ProductPositionLogyang menyala pada pembaruan dan menghapus dan yang menyesuaikan tabel pos pemeriksaan dengan tepat. Dan hanya untuk lebih yakin, saya akan menghitung ulang pos pemeriksaan dan tabel cache dari awal sesekali.


Terima kasih untuk tes kamu! Ketika saya mengomentari pertanyaan saya di atas, saya menulis waktu eksekusi yang salah dalam pertanyaan saya (untuk permintaan khusus ini), ini mendekati 10 detik. Namun, ini sedikit lebih banyak daripada dalam tes Anda. Saya kira itu mungkin karena pemblokiran atau sesuatu seperti itu. Alasan untuk sistem pos pemeriksaan saya adalah untuk meminimalkan beban di server, dan itu akan menjadi cara untuk memastikan kinerja tetap baik saat log tumbuh. Saya mengirimkan rencana permintaan di atas jika Anda ingin melihatnya. Terima kasih.
Henrik
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.