এটি একটি দীর্ঘ উত্তর, তাই আমি এখানে একটি সংক্ষিপ্ত যুক্ত করার সিদ্ধান্ত নিয়েছি।
- প্রথমে আমি একটি সমাধান উপস্থাপন করি যা প্রশ্নের অনুরূপ একই ক্রমে একই ফলাফল তৈরি করে। এটি মূল টেবিলটি 3 বার স্ক্যান করে:
ProductIDs
প্রতিটি পণ্যটির তারিখের সীমা সহ একটি তালিকা পেতে , প্রতিটি দিনের জন্য ব্যয় যোগ করতে (কারণ একই তারিখের সাথে বেশ কয়েকটি লেনদেন রয়েছে), আসল সারিগুলির সাথে পরিণতিতে যোগ দিতে।
- এরপরে আমি দুটি পদ্ধতির তুলনা করব যা কার্যকে সহজতর করে এবং প্রধান টেবিলের একটি শেষ স্ক্যান এড়ায়। তাদের ফলাফল হ'ল একটি দৈনিক সারসংক্ষেপ, অর্থাত্ যদি কোনও পণ্যের বেশ কয়েকটি লেনদেনের একই তারিখ থাকে তবে সেগুলি একক সারিতে পরিণত হয়। পূর্ববর্তী পদক্ষেপ থেকে আমার পদ্ধতির টেবিলটি দুইবার স্ক্যান করে। জিওফ প্যাটারসনের অ্যাপ্রোচটি একবার টেবিলটি স্ক্যান করে, কারণ তিনি তারিখের পরিসীমা এবং পণ্যগুলির তালিকা সম্পর্কে বাহ্যিক জ্ঞান ব্যবহার করেন।
- শেষ পর্যন্ত আমি একটি একক পাসের সমাধানটি উপস্থাপন করি যা আবার একটি দৈনিক সারাংশ দেয়, তবে এটির তারিখের তালিকা বা তালিকা সম্পর্কে বাহ্যিক জ্ঞানের প্রয়োজন হয় না
ProductIDs
।
আমি অ্যাডভেঞ্চার ওয়ার্স2014 ডাটাবেস এবং এসকিউএল সার্ভার এক্সপ্রেস 2014 ব্যবহার করব ।
মূল ডাটাবেসে পরিবর্তনগুলি:
- পরিবর্তন টাইপ
[Production].[TransactionHistory].[TransactionDate]
থেকে datetime
থেকে date
। সময় উপাদানটি যাইহোক শূন্য ছিল।
- যোগ করা ক্যালেন্ডার সারণী
[dbo].[Calendar]
- সূচীতে যুক্ত হয়েছে
[Production].[TransactionHistory]
।
CREATE TABLE [dbo].[Calendar]
(
[dt] [date] NOT NULL,
CONSTRAINT [PK_Calendar] PRIMARY KEY CLUSTERED
(
[dt] ASC
))
CREATE UNIQUE NONCLUSTERED INDEX [i] ON [Production].[TransactionHistory]
(
[ProductID] ASC,
[TransactionDate] ASC,
[ReferenceOrderID] ASC
)
INCLUDE ([ActualCost])
-- Init calendar table
INSERT INTO dbo.Calendar (dt)
SELECT TOP (50000)
DATEADD(day, ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1, '2000-01-01') AS dt
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);
OVER
ক্লজ সম্পর্কিত এমএসডিএন নিবন্ধে ইটজিক বেন-গানের উইন্ডো ফাংশন সম্পর্কে একটি দুর্দান্ত ব্লগ পোস্টের লিঙ্ক রয়েছে । সেই পোস্টে তিনি ব্যাখ্যা করেন যে কীভাবে OVER
কাজ করে, পার্থক্য ROWS
এবং RANGE
বিকল্পগুলির মধ্যে পার্থক্য রয়েছে এবং একটি তারিখের সীমাতে রোলিংয়ের যোগফল গণনা করার এই সমস্যাটি উল্লেখ করে। তিনি উল্লেখ করেছেন যে এসকিউএল সার্ভারের বর্তমান সংস্করণ RANGE
পুরোপুরি কার্যকর হয় না এবং অস্থায়ী ব্যবস্থার ডেটা ধরণের প্রয়োগ করে না। তার মধ্যে পার্থক্য সম্পর্কে তাঁর ব্যাখ্যা ROWS
এবং RANGE
আমাকে একটি ধারণা দিয়েছে।
ফাঁক এবং নকল ছাড়া তারিখ
যদি TransactionHistory
টেবিলটিতে ফাঁক ছাড়াই এবং ডুপ্লিকেট ছাড়াই তারিখ থাকে তবে নিম্নলিখিত প্রশ্নের সাথে সঠিক ফলাফল পাওয়া যাবে:
SELECT
TH.ProductID,
TH.TransactionDate,
TH.ActualCost,
RollingSum45 = SUM(TH.ActualCost) OVER (
PARTITION BY TH.ProductID
ORDER BY TH.TransactionDate
ROWS BETWEEN
45 PRECEDING
AND CURRENT ROW)
FROM Production.TransactionHistory AS TH
ORDER BY
TH.ProductID,
TH.TransactionDate,
TH.ReferenceOrderID;
প্রকৃতপক্ষে, 45 টি সারির একটি উইন্ডো ঠিক 45 দিন 45েকে রাখবে।
সদৃশ ছাড়াই ফাঁক সহ তারিখগুলি
দুর্ভাগ্যক্রমে, আমাদের ডেটাতে তারিখের ফাঁক রয়েছে। এই সমস্যাটি সমাধান করার জন্য আমরা Calendar
কোনও ফাঁক ছাড়াই তারিখের সেট তৈরি করতে একটি টেবিল ব্যবহার করতে পারি , তারপরে LEFT JOIN
এই সেটে মূল ডেটা এবং এর সাথে একই কোয়েরিটি ব্যবহার করতে পারি ROWS BETWEEN 45 PRECEDING AND CURRENT ROW
। তারিখগুলি পুনরাবৃত্তি না করে তবেই এটির সঠিক ফলাফল হবে (একই মধ্যে ProductID
)।
নকল সহ ফাঁক সহ তারিখগুলি
দুর্ভাগ্যক্রমে, আমাদের ডেটাতে তারিখ এবং তারিখের উভয় ফাঁক রয়েছে একই সাথে পুনরাবৃত্তি করতে পারে ProductID
। এই সমস্যা সমাধানের জন্য আমরা নকল ছাড়াই তারিখের সেট তৈরি GROUP
করে মূল ডেটা করতে পারি ProductID, TransactionDate
। তারপরে Calendar
কোনও ফাঁক ছাড়াই তারিখের সেট তৈরি করতে টেবিলটি ব্যবহার করুন । তারপরে আমরা ROWS BETWEEN 45 PRECEDING AND CURRENT ROW
ঘূর্ণায়মান গণনার জন্য কোয়েরিটি ব্যবহার করতে পারি SUM
। এটি সঠিক ফলাফল আনতে পারে। নীচের প্রশ্নের মধ্যে মন্তব্য দেখুন।
WITH
-- calculate Start/End dates for each product
CTE_Products
AS
(
SELECT TH.ProductID
,MIN(TH.TransactionDate) AS MinDate
,MAX(TH.TransactionDate) AS MaxDate
FROM [Production].[TransactionHistory] AS TH
GROUP BY TH.ProductID
)
-- generate set of dates without gaps for each product
,CTE_ProductsWithDates
AS
(
SELECT CTE_Products.ProductID, C.dt
FROM
CTE_Products
INNER JOIN dbo.Calendar AS C ON
C.dt >= CTE_Products.MinDate AND
C.dt <= CTE_Products.MaxDate
)
-- generate set of dates without duplicates for each product
-- calculate daily cost as well
,CTE_DailyCosts
AS
(
SELECT TH.ProductID, TH.TransactionDate, SUM(ActualCost) AS DailyActualCost
FROM [Production].[TransactionHistory] AS TH
GROUP BY TH.ProductID, TH.TransactionDate
)
-- calculate rolling sum over 45 days
,CTE_Sum
AS
(
SELECT
CTE_ProductsWithDates.ProductID
,CTE_ProductsWithDates.dt
,CTE_DailyCosts.DailyActualCost
,SUM(CTE_DailyCosts.DailyActualCost) OVER (
PARTITION BY CTE_ProductsWithDates.ProductID
ORDER BY CTE_ProductsWithDates.dt
ROWS BETWEEN 45 PRECEDING AND CURRENT ROW) AS RollingSum45
FROM
CTE_ProductsWithDates
LEFT JOIN CTE_DailyCosts ON
CTE_DailyCosts.ProductID = CTE_ProductsWithDates.ProductID AND
CTE_DailyCosts.TransactionDate = CTE_ProductsWithDates.dt
)
-- remove rows that were added by Calendar, which fill the gaps in dates
-- add back duplicate dates that were removed by GROUP BY
SELECT
TH.ProductID
,TH.TransactionDate
,TH.ActualCost
,CTE_Sum.RollingSum45
FROM
[Production].[TransactionHistory] AS TH
INNER JOIN CTE_Sum ON
CTE_Sum.ProductID = TH.ProductID AND
CTE_Sum.dt = TH.TransactionDate
ORDER BY
TH.ProductID
,TH.TransactionDate
,TH.ReferenceOrderID
;
আমি নিশ্চিত করেছি যে এই কোয়েরিটি subquery ব্যবহার করে এমন প্রশ্ন থেকে পদ্ধতির মতো একই ফলাফল তৈরি করে।
কার্যকর করার পরিকল্পনা রয়েছে
প্রথম ক্যোয়ারী subquery ব্যবহার করে, দ্বিতীয় - এই পদ্ধতির। আপনি দেখতে পারেন যে সময়কাল এবং পাঠের সংখ্যা এই পদ্ধতির তুলনায় অনেক কম। এই পদ্ধতির আনুমানিক ব্যয়ের বেশিরভাগটি চূড়ান্ত ORDER BY
, নীচে দেখুন।
উপজাত পদ্ধতির নেস্টেড লুপ এবং O(n*n)
জটিলতার সাথে একটি সহজ পরিকল্পনা রয়েছে ।
এই পদ্ধতির জন্য পরিকল্পনা TransactionHistory
কয়েকবার স্ক্যান করে , তবে কোনও লুপ নেই। আপনি দেখতে পাচ্ছেন 70% এরও বেশি ব্যয় হ'ল Sort
ফাইনালের জন্য ORDER BY
।
শীর্ষ ফলাফল - subquery
, নীচে - OVER
।
অতিরিক্ত স্ক্যান এড়ানো
উপরের পরিকল্পনায় সর্বশেষ সূচক স্ক্যান, মার্জ INNER JOIN
জোইন এবং বাছাইয়ের কারণটি মূল টেবিলের সাথে ফাইনালের ফলে চূড়ান্ত ফলাফলটিকে সাবকিউয়ের সাথে ধীর পন্থার মতো করে তোলে। ফিরে আসা সারিগুলির সংখ্যা TransactionHistory
টেবিলের মতোই same TransactionHistory
একই পণ্যটিতে একই দিনে বেশ কয়েকটি লেনদেন ঘটলে সেখানে সারি রয়েছে । যদি ফলাফলটিতে কেবল প্রতিদিনের সংক্ষিপ্তসারটি দেখানো ঠিক হয় তবে এই চূড়ান্তটি JOIN
সরিয়ে ফেলা যায় এবং ক্যোয়ারিটি কিছুটা সহজ এবং কিছুটা দ্রুত হয়ে যায়। পূর্ববর্তী পরিকল্পনা থেকে শেষ সূচক স্ক্যান, মার্জ যোগ দিন এবং বাছাই করা ফিল্টার দ্বারা প্রতিস্থাপিত হয়, যা যোগ করা সারিগুলি সরিয়ে দেয় Calendar
।
WITH
-- two scans
-- calculate Start/End dates for each product
CTE_Products
AS
(
SELECT TH.ProductID
,MIN(TH.TransactionDate) AS MinDate
,MAX(TH.TransactionDate) AS MaxDate
FROM [Production].[TransactionHistory] AS TH
GROUP BY TH.ProductID
)
-- generate set of dates without gaps for each product
,CTE_ProductsWithDates
AS
(
SELECT CTE_Products.ProductID, C.dt
FROM
CTE_Products
INNER JOIN dbo.Calendar AS C ON
C.dt >= CTE_Products.MinDate AND
C.dt <= CTE_Products.MaxDate
)
-- generate set of dates without duplicates for each product
-- calculate daily cost as well
,CTE_DailyCosts
AS
(
SELECT TH.ProductID, TH.TransactionDate, SUM(ActualCost) AS DailyActualCost
FROM [Production].[TransactionHistory] AS TH
GROUP BY TH.ProductID, TH.TransactionDate
)
-- calculate rolling sum over 45 days
,CTE_Sum
AS
(
SELECT
CTE_ProductsWithDates.ProductID
,CTE_ProductsWithDates.dt
,CTE_DailyCosts.DailyActualCost
,SUM(CTE_DailyCosts.DailyActualCost) OVER (
PARTITION BY CTE_ProductsWithDates.ProductID
ORDER BY CTE_ProductsWithDates.dt
ROWS BETWEEN 45 PRECEDING AND CURRENT ROW) AS RollingSum45
FROM
CTE_ProductsWithDates
LEFT JOIN CTE_DailyCosts ON
CTE_DailyCosts.ProductID = CTE_ProductsWithDates.ProductID AND
CTE_DailyCosts.TransactionDate = CTE_ProductsWithDates.dt
)
-- remove rows that were added by Calendar, which fill the gaps in dates
SELECT
CTE_Sum.ProductID
,CTE_Sum.dt AS TransactionDate
,CTE_Sum.DailyActualCost
,CTE_Sum.RollingSum45
FROM CTE_Sum
WHERE CTE_Sum.DailyActualCost IS NOT NULL
ORDER BY
CTE_Sum.ProductID
,CTE_Sum.dt
;
তবুও, TransactionHistory
দু'বার স্ক্যান করা হয়। প্রতিটি পণ্যের জন্য তারিখের ব্যাপ্তি পেতে একটি অতিরিক্ত স্ক্যান প্রয়োজন। কিভাবে আমি এর আরেকটি পন্থা, যেখানে আমরা তারিখ বিশ্বব্যাপী পরিসীমা সম্পর্কে বাহ্যিক জ্ঞান ব্যবহার সঙ্গে তুলনা করে দেখতে আগ্রহী ছিলেন TransactionHistory
, প্লাস অতিরিক্ত টেবিল Product
সব আছে ProductIDs
যে অতিরিক্ত স্ক্যান এড়ানো। তুলনা বৈধ করতে আমি এই কোয়েরি থেকে প্রতিদিন লেনদেনের সংখ্যার গণনা সরিয়েছি। এটি উভয় প্রশ্নের যোগ করা যেতে পারে, তবে আমি তুলনা করার জন্য এটি সহজ রাখতে চাই। আমাকে অন্যান্য তারিখগুলিও ব্যবহার করতে হয়েছিল, কারণ আমি ডেটাবেসের 2014 সংস্করণ ব্যবহার করি।
DECLARE @minAnalysisDate DATE = '2013-07-31',
-- Customizable start date depending on business needs
@maxAnalysisDate DATE = '2014-08-03'
-- Customizable end date depending on business needs
SELECT
-- one scan
ProductID, TransactionDate, ActualCost, RollingSum45
--, NumOrders
FROM (
SELECT ProductID, TransactionDate,
--NumOrders,
ActualCost,
SUM(ActualCost) OVER (
PARTITION BY ProductId ORDER BY TransactionDate
ROWS BETWEEN 45 PRECEDING AND CURRENT ROW
) AS RollingSum45
FROM (
-- The full cross-product of products and dates,
-- combined with actual cost information for that product/date
SELECT p.ProductID, c.dt AS TransactionDate,
--COUNT(TH.ProductId) AS NumOrders,
SUM(TH.ActualCost) AS ActualCost
FROM Production.Product p
JOIN dbo.calendar c
ON c.dt BETWEEN @minAnalysisDate AND @maxAnalysisDate
LEFT OUTER JOIN Production.TransactionHistory TH
ON TH.ProductId = p.productId
AND TH.TransactionDate = c.dt
GROUP BY P.ProductID, c.dt
) aggsByDay
) rollingSums
--WHERE NumOrders > 0
WHERE ActualCost IS NOT NULL
ORDER BY ProductID, TransactionDate
-- MAXDOP 1 to avoid parallel scan inflating the scan count
OPTION (MAXDOP 1);
উভয় প্রশ্নের একই ক্রমে একই ফলাফল ফিরে আসে।
তুলনা
এখানে সময় এবং আইও পরিসংখ্যান রয়েছে।
দ্বি-স্ক্যান বৈকল্পিকটি কিছুটা দ্রুত এবং এর পাঠ্য কম হয়, কারণ ওয়ান-স্ক্যান বৈকল্পিকটি ওয়ার্কটেবলকে প্রচুর ব্যবহার করতে হয়। এছাড়াও, ওয়ান-স্ক্যান বৈকল্পিক আপনার পরিকল্পনাগুলিতে দেখতে পাওয়ায় প্রয়োজনের চেয়ে আরও বেশি সারি তৈরি করে। এটি টেবিলে থাকা প্রত্যেকটির জন্য খেজুর তৈরি ProductID
করে Product
, এমনকি যদি ProductID
কোনও লেনদেন নাও করে। Product
সারণীতে 504 সারি রয়েছে, তবে কেবল 441 পণ্যগুলিতে লেনদেন রয়েছে TransactionHistory
। এছাড়াও, এটি প্রতিটি পণ্যের জন্য একই পরিসরের তারিখ তৈরি করে, যা প্রয়োজনের চেয়ে বেশি। TransactionHistory
প্রতিটি স্বতন্ত্র পণ্য তুলনামূলকভাবে সংক্ষিপ্ত ইতিহাসের সাথে যদি দীর্ঘতর সামগ্রিক ইতিহাস থাকে তবে অতিরিক্ত অনিবদ্ধ সারিগুলির সংখ্যা আরও বেশি হবে।
অন্যদিকে, কেবলমাত্র আরও সরু সূচক তৈরি করে দ্বি-স্ক্যান বৈকল্পিকটি আরও কিছুটা অনুকূল করা সম্ভব (ProductID, TransactionDate)
। এই সূচকটি প্রতিটি পণ্যের জন্য প্রারম্ভিক / শেষ তারিখগুলি গণনা করতে ব্যবহৃত হত ( CTE_Products
) এবং এতে সূচকে আচ্ছাদন করার চেয়ে কম পৃষ্ঠাগুলি থাকবে এবং ফলস্বরূপ কম পাঠ করা হবে।
সুতরাং, আমরা বেছে নিতে পারি, হয় একটি অতিরিক্ত স্পষ্টত সরল স্ক্যান, অথবা একটি অন্তর্নিহিত ওয়ার্কটেবল।
বিটিডাব্লু, যদি কেবলমাত্র দৈনিক সংক্ষিপ্তসারগুলির সাথে ফলাফল নেওয়া ঠিক হয় তবে অন্তর্ভুক্ত না হওয়া সূচি তৈরি করা ভাল ReferenceOrderID
। এটি কম পৃষ্ঠা => কম আইও ব্যবহার করবে।
CREATE NONCLUSTERED INDEX [i2] ON [Production].[TransactionHistory]
(
[ProductID] ASC,
[TransactionDate] ASC
)
INCLUDE ([ActualCost])
ক্রস প্রয়োগের মাধ্যমে একক পাসের সমাধান
এটি সত্যিই দীর্ঘ উত্তর হয়ে ওঠে, তবে এখানে আরও একটি বৈকল্পিক যা কেবলমাত্র প্রতিদিনের সারসংক্ষেপ আবার ফেরত দেয় তবে এটি কেবলমাত্র একটি স্ক্যান করে এবং এটি তারিখের পরিসীমা বা প্রোডাক্টআইডির তালিকা সম্পর্কে বাহ্যিক জ্ঞানের প্রয়োজন হয় না। এটি পাশাপাশি মধ্যবর্তী বাছাই করে না। সামগ্রিক পারফরম্যান্স পূর্ববর্তী রূপগুলির মতো, যদিও এটি কিছুটা খারাপ বলে মনে হচ্ছে।
মূল ধারণাটি হ'ল শূন্যস্থানগুলি পূরণ করার জন্য সারি তৈরি করতে সংখ্যার একটি সারণী ব্যবহার করা। প্রতিটি বিদ্যমান তারিখের জন্য LEAD
দিনের মধ্যে ফাঁকের আকার গণনা করতে এবং তারপরে CROSS APPLY
ফলাফলের সেটে প্রয়োজনীয় সংখ্যক সারি যুক্ত করতে ব্যবহার করুন। প্রথমে আমি স্থায়ী সংখ্যার টেবিল দিয়ে চেষ্টা করেছিলাম। পরিকল্পনাটি এই টেবিলটিতে প্রচুর পরিমাণে পাঠ্য দেখিয়েছিল, যদিও প্রকৃত সময়কালটি অনেকটা একই রকম ছিল, যখন আমি উড়ে ব্যবহার করে সংখ্যা তৈরি করেছি CTE
।
WITH
e1(n) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
) -- 10
,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b) -- 10*10
,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
,CTE_Numbers
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY n) AS Number
FROM e3
)
,CTE_DailyCosts
AS
(
SELECT
TH.ProductID
,TH.TransactionDate
,SUM(ActualCost) AS DailyActualCost
,ISNULL(DATEDIFF(day,
TH.TransactionDate,
LEAD(TH.TransactionDate)
OVER(PARTITION BY TH.ProductID ORDER BY TH.TransactionDate)), 1) AS DiffDays
FROM [Production].[TransactionHistory] AS TH
GROUP BY TH.ProductID, TH.TransactionDate
)
,CTE_NoGaps
AS
(
SELECT
CTE_DailyCosts.ProductID
,CTE_DailyCosts.TransactionDate
,CASE WHEN CA.Number = 1
THEN CTE_DailyCosts.DailyActualCost
ELSE NULL END AS DailyCost
FROM
CTE_DailyCosts
CROSS APPLY
(
SELECT TOP(CTE_DailyCosts.DiffDays) CTE_Numbers.Number
FROM CTE_Numbers
ORDER BY CTE_Numbers.Number
) AS CA
)
,CTE_Sum
AS
(
SELECT
ProductID
,TransactionDate
,DailyCost
,SUM(DailyCost) OVER (
PARTITION BY ProductID
ORDER BY TransactionDate
ROWS BETWEEN 45 PRECEDING AND CURRENT ROW) AS RollingSum45
FROM CTE_NoGaps
)
SELECT
ProductID
,TransactionDate
,DailyCost
,RollingSum45
FROM CTE_Sum
WHERE DailyCost IS NOT NULL
ORDER BY
ProductID
,TransactionDate
;
এই পরিকল্পনাটি "দীর্ঘ", কারণ ক্যোয়ারিতে দুটি উইন্ডো ফাংশন ( LEAD
এবং SUM
) ব্যবহার করা হয়েছে।
RunningTotal.TBE IS NOT NULL
শর্ত (এবং, অতএব,TBE
কলাম) অপ্রয়োজনীয়। আপনি যদি এটি ফেলে দেন তবে আপনি অনর্থক সারিগুলি পাচ্ছেন না, কারণ আপনার অভ্যন্তরীণ যোগদানের শর্তে তারিখের কলামটি অন্তর্ভুক্ত রয়েছে - ফলস্বরূপ ফলাফলের সেটগুলিতে তারিখগুলি থাকতে পারে না যা উত্সটিতে মূলত ছিল না।