विंडो फ़ंक्शंस का उपयोग करके तिथि सीमा रोलिंग राशि


56

मुझे एक तिथि सीमा से अधिक रोलिंग राशि की गणना करने की आवश्यकता है। एडवेंचरवर्क्स सैंपल डेटाबेस का उपयोग करने के लिए, निम्नलिखित काल्पनिक वाक्यविन्यास ठीक वही होगा जो मुझे चाहिए:

SELECT
    TH.ProductID,
    TH.TransactionDate,
    TH.ActualCost,
    RollingSum45 = SUM(TH.ActualCost) OVER (
        PARTITION BY TH.ProductID
        ORDER BY TH.TransactionDate
        RANGE BETWEEN 
            INTERVAL 45 DAY PRECEDING
            AND CURRENT ROW)
FROM Production.TransactionHistory AS TH
ORDER BY
    TH.ProductID,
    TH.TransactionDate,
    TH.ReferenceOrderID;

अफसोस की बात है, RANGEविंडो फ्रेम सीमा वर्तमान में SQL सर्वर में अंतराल की अनुमति नहीं देती है।

मुझे पता है कि मैं एक उपकुंजी और एक नियमित (गैर-खिड़की) कुल का उपयोग कर एक समाधान लिख सकता हूं:

SELECT 
    TH.ProductID,
    TH.TransactionDate,
    TH.ActualCost,
    RollingSum45 =
    (
        SELECT SUM(TH2.ActualCost)
        FROM Production.TransactionHistory AS TH2
        WHERE
            TH2.ProductID = TH.ProductID
            AND TH2.TransactionDate <= TH.TransactionDate
            AND TH2.TransactionDate >= DATEADD(DAY, -45, TH.TransactionDate)
    )
FROM Production.TransactionHistory AS TH
ORDER BY
    TH.ProductID,
    TH.TransactionDate,
    TH.ReferenceOrderID;

निम्नलिखित सूचकांक को देखते हुए:

CREATE UNIQUE INDEX i
ON Production.TransactionHistory
    (ProductID, TransactionDate, ReferenceOrderID)
INCLUDE
    (ActualCost);

निष्पादन योजना है:

निष्पादन योजना

हालांकि यह बहुत ही अयोग्य नहीं है, लेकिन ऐसा लगता है कि SQL Server 2012, 2014 या 2016 (अब तक) में समर्थित केवल विंडो एग्रीगेट और एनालिटिक फ़ंक्शन का उपयोग करके इस क्वेरी को व्यक्त करना संभव है।

स्पष्टता के लिए, मैं एक ऐसे समाधान की तलाश कर रहा हूं जो डेटा पर एकल पास करता है ।

T-SQL में इसका मतलब यह है कि संभावना है खंड काम करते हैं, और खिड़की Spools और खिड़की समुच्चय की सुविधा होगी कार्य योजना लागू करेंगे। क्लॉज का उपयोग करने वाले सभी भाषा तत्व निष्पक्ष खेल हैं। SQLCLR समाधान स्वीकार्य है, बशर्ते कि सही परिणाम देने की गारंटी हो।OVEROVER

T-SQL समाधान के लिए, निष्पादन योजना में कम Hashes, Sorts और Window Spools / Aggregates, बेहतर। अनुक्रमणिका को जोड़ने के लिए स्वतंत्र महसूस करें, लेकिन अलग-अलग संरचनाओं की अनुमति नहीं है (इसलिए ट्रिगर्स के साथ सिंक में रखी गई कोई पूर्व-गणना की गई तालिकाएं, उदाहरण के लिए)। संदर्भ तालिकाओं की अनुमति है (संख्या, दिनांक आदि की तालिकाएं)

आदर्श रूप से, समाधान उसी क्रम में ठीक उसी परिणाम का उत्पादन करेंगे जैसे उपरी संस्करण के रूप में, लेकिन यकीनन सही कुछ भी स्वीकार्य है। प्रदर्शन हमेशा एक विचार है, इसलिए समाधान कम से कम उचित रूप से कुशल होना चाहिए।

समर्पित चैट रूम: मैंने इस प्रश्न और इसके उत्तरों से संबंधित चर्चा के लिए एक सार्वजनिक चैट रूम बनाया है। कम से कम 20 प्रतिष्ठा बिंदुओं वाला कोई भी उपयोगकर्ता सीधे भाग ले सकता है। कृपया मुझे नीचे टिप्पणी में पिंग करें यदि आपके पास 20 से कम प्रतिनिधि हैं और भाग लेना चाहते हैं।

जवाबों:


42

महान सवाल, पॉल! मैंने कुछ अलग-अलग तरीकों का इस्तेमाल किया, एक टी-एसक्यूएल में और एक सीएलआर में।

टी-एसक्यूएल त्वरित सारांश

T-SQL दृष्टिकोण को निम्न चरणों के रूप में संक्षेपित किया जा सकता है:

  • उत्पादों / तिथियों के क्रॉस-उत्पाद को लें
  • मनाया बिक्री डेटा में विलय
  • उस डेटा को उत्पाद / दिनांक स्तर तक एकत्र करें
  • इस समग्र डेटा के आधार पर पिछले 45 दिनों में रोलिंग रकम की गणना करें (जिसमें कोई "गायब" दिन भरा हो)
  • उन परिणामों को केवल उस उत्पाद / दिनांक युग्मन को फ़िल्टर करें जिसमें एक या अधिक बिक्री थी

उपयोग करते हुए SET STATISTICS IO ON, यह दृष्टिकोण रिपोर्ट Table 'TransactionHistory'. Scan count 1, logical reads 484करता है, जो टेबल पर "सिंगल पास" की पुष्टि करता है। संदर्भ के लिए, मूल पाश की तलाश क्वेरी रिपोर्ट Table 'TransactionHistory'. Scan count 113444, logical reads 438366

जैसा कि बताया गया है SET STATISTICS TIME ON, CPU समय है 514ms। यह 2231msमूल क्वेरी के लिए अनुकूल रूप से तुलना करता है ।

सीएलआर त्वरित सारांश

सीएलआर सारांश को निम्नलिखित चरणों के रूप में संक्षेपित किया जा सकता है:

  • डेटा को मेमोरी में पढ़ें, उत्पाद और तिथि के अनुसार
  • प्रत्येक लेन-देन को संसाधित करते समय, लागतों का एक कुल भाग जोड़ें। जब भी कोई लेनदेन पिछले लेनदेन की तुलना में एक अलग उत्पाद होता है, तो चल रहे कुल को 0 पर रीसेट करें।
  • पहले लेनदेन के लिए एक संकेतक बनाए रखें जिसमें वर्तमान लेनदेन के समान (उत्पाद, दिनांक) हो। जब भी उस (उत्पाद, तिथि) के साथ अंतिम लेनदेन का सामना किया जाता है, तो उस लेनदेन के लिए रोलिंग राशि की गणना करें और इसे उसी (उत्पाद, तिथि) के साथ सभी लेनदेन पर लागू करें
  • उपयोगकर्ता को सभी परिणाम लौटाएं!

उपयोग करते हुए SET STATISTICS IO ON, यह दृष्टिकोण बताता है कि कोई तार्किक I / O नहीं हुआ है! वाह, एक सही समाधान! (वास्तव में, ऐसा लगता है कि SET STATISTICS IOसीएलआर के भीतर I / O की रिपोर्ट नहीं करता है। लेकिन कोड से, यह देखना आसान है कि तालिका का ठीक एक स्कैन किया गया है और सूचकांक पॉल द्वारा सुझाए गए क्रम में डेटा को पुनर्प्राप्त करता है।

जैसा कि बताया गया है SET STATISTICS TIME ON, CPU समय अब ​​है 187ms। तो यह T-SQL दृष्टिकोण पर काफी सुधार है। दुर्भाग्य से, दोनों दृष्टिकोणों का समग्र बीता हुआ समय लगभग आधा दूसरे के समान है। हालाँकि, CLR आधारित दृष्टिकोण को 113K पंक्तियों को कंसोल (बनाम T-SQL दृष्टिकोण के लिए केवल 52K उत्पाद / दिनांक द्वारा समूह) में आउटपुट करना पड़ता है, इसलिए मैंने इसके बजाय CPU समय पर ध्यान केंद्रित किया है।

इस दृष्टिकोण का एक और बड़ा लाभ यह है कि यह मूल लूप / तलाश दृष्टिकोण के समान परिणाम प्राप्त करता है, जिसमें प्रत्येक लेनदेन के लिए एक पंक्ति भी शामिल है, यहां तक ​​कि उन मामलों में जहां एक ही दिन में कई बार उत्पाद बेचा जाता है। (एडवेंचरवर्क्स पर, मैंने विशेष रूप से पंक्ति-दर-पंक्ति परिणामों की तुलना की और पुष्टि की कि वे पॉल की मूल क्वेरी के साथ टाई करते हैं)

इस दृष्टिकोण का एक नुकसान, कम से कम अपने वर्तमान रूप में, यह है कि यह मेमोरी में सभी डेटा को पढ़ता है। हालाँकि, जो एल्गोरिथम केवल डिज़ाइन किया गया है, उसे किसी भी समय मेमोरी में वर्तमान विंडो फ़्रेम की आवश्यकता होती है और मेमोरी से अधिक डेटा सेट के लिए काम करने के लिए अद्यतन किया जा सकता है। पॉल ने अपने एल्गोरिथ्म के कार्यान्वयन का निर्माण करके अपने उत्तर में इस बिंदु को चित्रित किया है जो स्मृति में केवल स्लाइडिंग विंडो को संग्रहीत करता है। यह सीएलआर असेंबली को उच्च अनुमति देने की कीमत पर आता है, लेकिन निश्चित रूप से इस समाधान को बड़े पैमाने पर डेटा सेटों में स्केल करने में सार्थक होगा।


टी-एसक्यूएल - एक स्कैन, तिथि के अनुसार समूहीकृत

प्रारंभिक व्यवस्था

USE AdventureWorks2012
GO
-- Create Paul's index
CREATE UNIQUE INDEX i
ON Production.TransactionHistory (ProductID, TransactionDate, ReferenceOrderID)
INCLUDE (ActualCost);
GO
-- Build calendar table for 2000 ~ 2020
CREATE TABLE dbo.calendar (d DATETIME NOT NULL CONSTRAINT PK_calendar PRIMARY KEY)
GO
DECLARE @d DATETIME = '1/1/2000'
WHILE (@d < '1/1/2021')
BEGIN
    INSERT INTO dbo.calendar (d) VALUES (@d)
    SELECT @d =  DATEADD(DAY, 1, @d)
END
GO

पूछताछ

DECLARE @minAnalysisDate DATE = '2007-09-01', -- Customizable start date depending on business needs
        @maxAnalysisDate DATE = '2008-09-03'  -- Customizable end date depending on business needs
SELECT 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.d AS TransactionDate,
            COUNT(TH.ProductId) AS NumOrders, SUM(TH.ActualCost) AS ActualCost
        FROM Production.Product p
        JOIN dbo.calendar c
            ON c.d BETWEEN @minAnalysisDate AND @maxAnalysisDate
        LEFT OUTER JOIN Production.TransactionHistory TH
            ON TH.ProductId = p.productId
            AND TH.TransactionDate = c.d
        GROUP BY P.ProductID, c.d
    ) aggsByDay
) rollingSums
WHERE NumOrders > 0
ORDER BY ProductID, TransactionDate
-- MAXDOP 1 to avoid parallel scan inflating the scan count
OPTION (MAXDOP 1)

निष्पादन योजना

निष्पादन योजना से, हम देखते हैं कि पॉल द्वारा प्रस्तावित मूल सूचकांक हमें Production.TransactionHistoryप्रत्येक संभावित उत्पाद / तिथि संयोजन के साथ लेनदेन के इतिहास को संयोजित करने के लिए मर्ज में शामिल होने के लिए एकल ऑर्डर किए गए स्कैन को करने की अनुमति देने के लिए पर्याप्त है ।

यहाँ छवि विवरण दर्ज करें

मान्यताओं

इस दृष्टिकोण में पके हुए कुछ महत्वपूर्ण धारणाएं हैं। मुझे लगता है कि पॉल के लिए यह तय करना होगा कि वे स्वीकार्य हैं :)

  • मैं Production.Productतालिका का उपयोग कर रहा हूं । यह तालिका स्वतंत्र रूप से उपलब्ध है AdventureWorks2012और इस संबंध को एक विदेशी कुंजी द्वारा लागू किया गया है Production.TransactionHistory, इसलिए मैंने इसे उचित खेल के रूप में व्याख्या किया।
  • यह दृष्टिकोण इस तथ्य पर निर्भर करता है कि लेनदेन पर एक समय घटक नहीं है AdventureWorks2012; यदि उन्होंने किया, तो उत्पाद / तिथि संयोजनों का पूरा सेट तैयार करना पहले लेन-देन के इतिहास से गुजरने के बिना संभव नहीं होगा।
  • मैं एक ऐसी पंक्तियों का निर्माण कर रहा हूं जिसमें प्रति उत्पाद / तिथि जोड़ी में सिर्फ एक पंक्ति है। मुझे लगता है कि यह "यकीनन सही है" और कई मामलों में लौटने के लिए एक अधिक वांछनीय परिणाम है। प्रत्येक उत्पाद / तिथि के लिए, मैंने यह NumOrdersइंगित करने के लिए एक कॉलम जोड़ा है कि कितनी बिक्री हुई। मूल क्वेरी बनाम प्रस्तावित क्वेरी के परिणामों की तुलना के लिए निम्नलिखित स्क्रीनशॉट देखें उन मामलों में जहां एक ही तारीख में एक उत्पाद को कई बार बेचा गया था (जैसे, 319/ 2007-09-05 00:00:00.000)

यहाँ छवि विवरण दर्ज करें


सीएलआर - एक स्कैन, पूर्ण अनियंत्रित परिणाम सेट

मुख्य कार्य शरीर

यहाँ देखने के लिए एक टन नहीं है; फ़ंक्शन का मुख्य निकाय इनपुट की घोषणा करता है (जो संबंधित SQL फ़ंक्शन से मेल खाना चाहिए), SQL कनेक्शन सेट करता है और SQLRder को खोलता है।

// SQL CLR function for rolling SUMs on AdventureWorks2012.Production.TransactionHistory
[SqlFunction(DataAccess = DataAccessKind.Read,
    FillRowMethodName = "RollingSum_Fill",
    TableDefinition = "ProductId INT, TransactionDate DATETIME, ReferenceOrderID INT," +
                      "ActualCost FLOAT, PrevCumulativeSum FLOAT, RollingSum FLOAT")]
public static IEnumerable RollingSumTvf(SqlInt32 rollingPeriodDays) {
    using (var connection = new SqlConnection("context connection=true;")) {
        connection.Open();
        List<TrxnRollingSum> trxns;
        using (var cmd = connection.CreateCommand()) {
            //Read the transaction history (note: the order is important!)
            cmd.CommandText = @"SELECT ProductId, TransactionDate, ReferenceOrderID,
                                    CAST(ActualCost AS FLOAT) AS ActualCost 
                                FROM Production.TransactionHistory 
                                ORDER BY ProductId, TransactionDate";
            using (var reader = cmd.ExecuteReader()) {
                trxns = ComputeRollingSums(reader, rollingPeriodDays.Value);
            }
        }

        return trxns;
    }
}

मूल तर्क

मैंने मुख्य तर्क को अलग कर दिया है ताकि ध्यान केंद्रित करना आसान हो:

// Given a SqlReader with transaction history data, computes / returns the rolling sums
private static List<TrxnRollingSum> ComputeRollingSums(SqlDataReader reader,
                                                        int rollingPeriodDays) {
    var startIndexOfRollingPeriod = 0;
    var rollingSumIndex = 0;
    var trxns = new List<TrxnRollingSum>();

    // Prior to the loop, initialize "next" to be the first transaction
    var nextTrxn = GetNextTrxn(reader, null);
    while (nextTrxn != null)
    {
        var currTrxn = nextTrxn;
        nextTrxn = GetNextTrxn(reader, currTrxn);
        trxns.Add(currTrxn);

        // If the next transaction is not the same product/date as the current
        // transaction, we can finalize the rolling sum for the current transaction
        // and all previous transactions for the same product/date
        var finalizeRollingSum = nextTrxn == null || (nextTrxn != null &&
                                (currTrxn.ProductId != nextTrxn.ProductId ||
                                currTrxn.TransactionDate != nextTrxn.TransactionDate));
        if (finalizeRollingSum)
        {
            // Advance the pointer to the first transaction (for the same product)
            // that occurs within the rolling period
            while (startIndexOfRollingPeriod < trxns.Count
                && trxns[startIndexOfRollingPeriod].TransactionDate <
                    currTrxn.TransactionDate.AddDays(-1 * rollingPeriodDays))
            {
                startIndexOfRollingPeriod++;
            }

            // Compute the rolling sum as the cumulative sum (for this product),
            // minus the cumulative sum for prior to the beginning of the rolling window
            var sumPriorToWindow = trxns[startIndexOfRollingPeriod].PrevSum;
            var rollingSum = currTrxn.ActualCost + currTrxn.PrevSum - sumPriorToWindow;
            // Fill in the rolling sum for all transactions sharing this product/date
            while (rollingSumIndex < trxns.Count)
            {
                trxns[rollingSumIndex++].RollingSum = rollingSum;
            }
        }

        // If this is the last transaction for this product, reset the rolling period
        if (nextTrxn != null && currTrxn.ProductId != nextTrxn.ProductId)
        {
            startIndexOfRollingPeriod = trxns.Count;
        }
    }

    return trxns;
}

सहायकों

निम्नलिखित तर्क को इनलाइन लिखा जा सकता है, लेकिन जब वे अपने स्वयं के तरीकों से अलग हो जाते हैं, तो पढ़ना थोड़ा आसान होता है।

private static TrxnRollingSum GetNextTrxn(SqlDataReader r, TrxnRollingSum currTrxn) {
    TrxnRollingSum nextTrxn = null;
    if (r.Read()) {
        nextTrxn = new TrxnRollingSum {
            ProductId = r.GetInt32(0),
            TransactionDate = r.GetDateTime(1),
            ReferenceOrderId = r.GetInt32(2),
            ActualCost = r.GetDouble(3),
            PrevSum = 0 };
        if (currTrxn != null) {
            nextTrxn.PrevSum = (nextTrxn.ProductId == currTrxn.ProductId)
                    ? currTrxn.PrevSum + currTrxn.ActualCost : 0;
        }
    }
    return nextTrxn;
}

// Represents the output to be returned
// Note that the ReferenceOrderId/PrevSum fields are for debugging only
private class TrxnRollingSum {
    public int ProductId { get; set; }
    public DateTime TransactionDate { get; set; }
    public int ReferenceOrderId { get; set; }
    public double ActualCost { get; set; }
    public double PrevSum { get; set; }
    public double RollingSum { get; set; }
}

// The function that generates the result data for each row
// (Such a function is mandatory for SQL CLR table-valued functions)
public static void RollingSum_Fill(object trxnWithRollingSumObj,
                                    out int productId,
                                    out DateTime transactionDate, 
                                    out int referenceOrderId, out double actualCost,
                                    out double prevCumulativeSum,
                                    out double rollingSum) {
    var trxn = (TrxnRollingSum)trxnWithRollingSumObj;
    productId = trxn.ProductId;
    transactionDate = trxn.TransactionDate;
    referenceOrderId = trxn.ReferenceOrderId;
    actualCost = trxn.ActualCost;
    prevCumulativeSum = trxn.PrevSum;
    rollingSum = trxn.RollingSum;
}

यह सब एक साथ SQL में बांधना

इस बिंदु तक सब कुछ C # में रहा है, तो चलिए वास्तविक SQL को शामिल करते हैं। (वैकल्पिक रूप से, आप इस परिनियोजन स्क्रिप्ट का उपयोग स्वयं को संकलित करने के बजाय मेरी असेंबली के बिट्स से असेंबली बनाने के लिए कर सकते हैं )

USE AdventureWorks2012; /* GPATTERSON2\SQL2014DEVELOPER */
GO

-- Enable CLR
EXEC sp_configure 'clr enabled', 1;
GO
RECONFIGURE;
GO

-- Create the assembly based on the dll generated by compiling the CLR project
-- I've also included the "assembly bits" version that can be run without compiling
CREATE ASSEMBLY ClrPlayground
-- See http://pastebin.com/dfbv1w3z for a "from assembly bits" version
FROM 'C:\FullPathGoesHere\ClrPlayground\bin\Debug\ClrPlayground.dll'
WITH PERMISSION_SET = safe;
GO

--Create a function from the assembly
CREATE FUNCTION dbo.RollingSumTvf (@rollingPeriodDays INT)
RETURNS TABLE ( ProductId INT, TransactionDate DATETIME, ReferenceOrderID INT,
                ActualCost FLOAT, PrevCumulativeSum FLOAT, RollingSum FLOAT)
-- The function yields rows in order, so let SQL Server know to avoid an extra sort
ORDER (ProductID, TransactionDate, ReferenceOrderID)
AS EXTERNAL NAME ClrPlayground.UserDefinedFunctions.RollingSumTvf;
GO

-- Now we can actually use the TVF!
SELECT * 
FROM dbo.RollingSumTvf(45) 
ORDER BY ProductId, TransactionDate, ReferenceOrderId
GO

चेतावनियां

सीएलआर दृष्टिकोण एल्गोरिथ्म को अनुकूलित करने के लिए बहुत अधिक लचीलापन प्रदान करता है, और इसे संभवतः C # में एक विशेषज्ञ द्वारा आगे भी ट्यून किया जा सकता है। हालांकि, सीएलआर रणनीति के लिए डाउनसाइड भी हैं। ध्यान रखने योग्य कुछ बातें:

  • यह सीएलआर दृष्टिकोण मेमोरी में सेट किए गए डेटा की एक प्रति रखता है। स्ट्रीमिंग दृष्टिकोण का उपयोग करना संभव है, लेकिन मैंने शुरुआती कठिनाइयों का सामना किया और पाया कि एक बकाया कनेक्ट समस्या है जो शिकायत करती है कि SQL 2008+ में परिवर्तन इस प्रकार के दृष्टिकोण का उपयोग करना अधिक कठिन बनाते हैं। यह अभी भी संभव है (जैसा कि पॉल प्रदर्शित करता है), लेकिन सीएलआर असेंबली के रूप में डेटाबेस सेट करके TRUSTWORTHYऔर अनुदान देकर उच्च स्तर की अनुमति की आवश्यकता होती है EXTERNAL_ACCESS। तो कुछ परेशानी और संभावित सुरक्षा निहितार्थ है, लेकिन भुगतान एक स्ट्रीमिंग दृष्टिकोण है जो एडवेंचरवर्क्स पर उन लोगों की तुलना में बहुत बड़े डेटा सेट को बेहतर बना सकता है।
  • सीएलआर कुछ डीबीए के लिए कम सुलभ हो सकता है, इस तरह के एक फ़ंक्शन को एक ब्लैक बॉक्स का अधिक बनाता है जो पारदर्शी नहीं है, आसानी से संशोधित नहीं है, आसानी से तैनात नहीं है, और शायद उतनी आसानी से डिबग नहीं किया जाता है। T-SQL दृष्टिकोण की तुलना में यह एक बहुत बड़ा नुकसान है।


बोनस: टी-एसक्यूएल # 2 - व्यावहारिक दृष्टिकोण जो मैं वास्तव में उपयोग करूंगा

कुछ समय के लिए समस्या के बारे में रचनात्मक रूप से सोचने की कोशिश करने के बाद, मैंने सोचा कि मैं काफी सरल, व्यावहारिक तरीका पोस्ट करूंगा, जो कि मैं इस समस्या से निपटने के लिए चुनूंगा अगर यह मेरे दैनिक काम में आया। यह SQL 2012+ विंडो की कार्यक्षमता का उपयोग करता है, लेकिन उस प्रकार के ज़मीनी तरीके से नहीं जिस तरह से यह प्रश्न अपेक्षित था:

-- Compute all running costs into a #temp table; Note that this query could simply read
-- from Production.TransactionHistory, but a CROSS APPLY by product allows the window 
-- function to be computed independently per product, supporting a parallel query plan
SELECT t.*
INTO #runningCosts
FROM Production.Product p
CROSS APPLY (
    SELECT t.ProductId, t.TransactionDate, t.ReferenceOrderId, t.ActualCost,
        -- Running sum of the cost for this product, including all ties on TransactionDate
        SUM(t.ActualCost) OVER (
            ORDER BY t.TransactionDate 
            RANGE UNBOUNDED PRECEDING) AS RunningCost
    FROM Production.TransactionHistory t
    WHERE t.ProductId = p.ProductId
) t
GO

-- Key the table in our output order
ALTER TABLE #runningCosts
ADD PRIMARY KEY (ProductId, TransactionDate, ReferenceOrderId)
GO

SELECT r.ProductId, r.TransactionDate, r.ReferenceOrderId, r.ActualCost,
    -- Cumulative running cost - running cost prior to the sliding window
    r.RunningCost - ISNULL(w.RunningCost,0) AS RollingSum45
FROM #runningCosts r
OUTER APPLY (
    -- For each transaction, find the running cost just before the sliding window begins
    SELECT TOP 1 b.RunningCost
    FROM #runningCosts b
    WHERE b.ProductId = r.ProductId
        AND b.TransactionDate < DATEADD(DAY, -45, r.TransactionDate)
    ORDER BY b.TransactionDate DESC
) w
ORDER BY r.ProductId, r.TransactionDate, r.ReferenceOrderId
GO

यह वास्तव में काफी सरल समग्र क्वेरी योजना देता है, यहां तक ​​कि जब दोनों एक साथ दोनों प्रासंगिक क्वेरी प्लान को देखते हैं:

यहाँ छवि विवरण दर्ज करें यहाँ छवि विवरण दर्ज करें

इस दृष्टिकोण को पसंद करने वाले कुछ कारण:

  • यह समस्या कथन में अनुरोध किए गए पूर्ण परिणाम सेट करता है (जैसा कि अधिकांश अन्य टी-एसक्यूएल समाधानों के विपरीत है, जो परिणामों के एक समूहीकृत संस्करण को लौटाता है)।
  • व्याख्या करना, समझना और डीबग करना आसान है; मैं एक साल बाद वापस नहीं आऊंगा और आश्चर्यचकित रहूंगा कि कैसे हेक मैं शुद्धता या प्रदर्शन को बर्बाद किए बिना एक छोटा बदलाव कर सकता हूं
  • यह मूल लूप-सीक के 900msबजाय प्रदान किए गए डेटा सेट पर चलता है2700ms
  • यदि डेटा बहुत अधिक सघन था (प्रति दिन अधिक लेन-देन), कम्प्यूटेशनल जटिलता स्लाइडिंग विंडो में लेनदेन की संख्या के साथ नहीं बढ़ती है (जैसा कि यह मूल क्वेरी के लिए करता है); मुझे लगता है कि यह पॉल की चिंता का एक हिस्सा है, जो कई स्कैन से बचना चाहता था
  • इसका परिणाम अनिवार्य रूप से SQL 2012 के हाल के अपडेट में कोई tempdb I / O नहीं है, नई tempdb आलसी कार्यक्षमता के कारण
  • बहुत बड़े डेटा सेटों के लिए, प्रत्येक उत्पाद के लिए कार्य को अलग-अलग बैचों में विभाजित करना तुच्छ है यदि स्मृति दबाव चिंता का विषय बन जाता है

एक जोड़ी संभावित चेतावनी:

  • हालांकि यह तकनीकी रूप से स्कैन करता है। केवल एक बार ही प्रोडक्शन। ट्रांसशिपहिस्ट्रेशन, यह वास्तव में "वन स्कैन" दृष्टिकोण नहीं है क्योंकि समान आकार की # टैम्प टेबल और उस टेबल पर भी अतिरिक्त तर्क I / O करने की आवश्यकता होगी। हालाँकि, मैं इसे एक कार्य तालिका से बहुत अलग नहीं देखता कि हमारे पास इसकी सटीक संरचना को परिभाषित करने के बाद से अधिक मैनुअल नियंत्रण है
  • आपके वातावरण के आधार पर, tempdb के उपयोग को सकारात्मक के रूप में देखा जा सकता है (उदाहरण के लिए, यह SSD ड्राइव के एक अलग सेट पर है) या एक नकारात्मक (सर्वर पर उच्च संगामिति, पहले से ही कई tempdb विवाद)

25

यह एक लंबा जवाब है, इसलिए मैंने यहां एक सारांश जोड़ने का फैसला किया।

  • सबसे पहले मैं एक समाधान प्रस्तुत करता हूं जो प्रश्न में उसी क्रम में समान परिणाम उत्पन्न करता है। यह मुख्य तालिका को 3 बार स्कैन करता है: ProductIDsमूल पंक्तियों के साथ परिणाम में शामिल होने के लिए, प्रत्येक उत्पाद के लिए तारीखों की सीमा के साथ एक सूची प्राप्त करने के लिए, प्रत्येक दिन (क्योंकि समान तिथियों के साथ कई लेनदेन होते हैं)।
  • आगे मैं दो दृष्टिकोणों की तुलना करता हूं जो कार्य को सरल बनाते हैं और मुख्य तालिका के एक अंतिम स्कैन से बचते हैं। उनका परिणाम एक दैनिक सारांश है, अर्थात यदि किसी उत्पाद पर कई लेन-देन एक ही तिथि में हैं तो उन्हें एकल पंक्ति में रोल किया गया है। पिछले चरण से मेरा दृष्टिकोण दो बार तालिका को स्कैन करता है। ज्योफ पैटरसन द्वारा दृष्टिकोण एक बार तालिका को स्कैन करता है, क्योंकि वह उत्पादों की तारीखों और सूची की सीमा के बारे में बाहरी ज्ञान का उपयोग करता है।
  • अंत में मैं एक एकल पास समाधान प्रस्तुत करता हूं जो फिर से एक दैनिक सारांश देता है, लेकिन इसके लिए तारीखों की सूची या सूची के बारे में बाहरी ज्ञान की आवश्यकता नहीं होती है ProductIDs

मैं AdventureWorks2014 डेटाबेस और SQL सर्वर एक्सप्रेस 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खंड के बारे में MSDN लेख में इटज़िक बेन-गण द्वारा विंडो कार्यों के बारे में एक उत्कृष्ट ब्लॉग पोस्ट का लिंक है । उस पोस्ट में वह बताता है कि कैसे OVERकाम करता है, बीच का अंतर ROWSऔर RANGEविकल्प और एक तिथि सीमा से अधिक रोलिंग राशि की गणना की इस बहुत समस्या का उल्लेख करता है। उन्होंने उल्लेख किया है कि SQL सर्वर का वर्तमान संस्करण 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 दिनों में कवर होगी।

नकल के बिना अंतराल के साथ दिनांक

दुर्भाग्य से, हमारे डेटा की तारीखों में अंतराल है। इस समस्या को हल करने के लिए हम 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
;

मैंने पुष्टि की कि यह प्रश्न उप-प्रश्न का उपयोग करने वाले प्रश्न के दृष्टिकोण के समान परिणाम उत्पन्न करता है।

निष्पादन की योजना

आँकड़े

पहली क्वेरी सबक्विरी का उपयोग करती है, दूसरी - यह दृष्टिकोण। आप देख सकते हैं कि इस दृष्टिकोण में अवधि और पढ़ने की संख्या बहुत कम है। इस दृष्टिकोण में अनुमानित लागत का अधिकांश अंतिम है ORDER BY, नीचे देखें।

सबक्वेरी

सुबक्वेरी दृष्टिकोण में नेस्टेड छोरों और O(n*n)जटिलता के साथ एक सरल योजना है।

ऊपर

इस दृष्टिकोण की योजना TransactionHistoryकई बार स्कैन की जाती है, लेकिन लूप नहीं होते हैं। जैसा कि आप देख सकते हैं कि अनुमानित लागत का 70% से अधिक Sortअंतिम के लिए है ORDER BY

कब

शीर्ष परिणाम - subquery, नीचे - OVER


अतिरिक्त स्कैन से बचना

अंतिम सूचकांक स्कैन, मर्ज ज्वाइन और सॉर्ट उपरोक्त योजना INNER JOINमें मूल तालिका के साथ अंतिम परिणाम के कारण होता है, जिससे अंतिम परिणाम उपशम के साथ एक धीमी गति के समान होता है। लौटी हुई पंक्तियों की संख्या TransactionHistoryतालिका के समान है । 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);

एक-स्कैन

दोनों प्रश्न समान क्रम में एक ही परिणाम देते हैं।

तुलना

यहाँ समय और IO आँकड़े हैं।

stats2

io2

टू-स्कैन वेरिएंट थोड़ा तेज है और इसमें बहुत कम रीड हैं, क्योंकि वन-स्कैन वेरिएंट में वर्कटेब का काफी उपयोग करना है। इसके अलावा, एक-स्कैन वैरिएंट जरूरत से ज्यादा पंक्तियों को उत्पन्न करता है जैसा कि आप योजनाओं में देख सकते हैं। यह तालिका ProductIDमें शामिल प्रत्येक के लिए तिथियां उत्पन्न करता है Product, भले ही ProductIDउसका कोई लेनदेन न हो। Productतालिका में 504 पंक्तियाँ हैं , लेकिन केवल 441 उत्पादों में लेनदेन होता है TransactionHistory। इसके अलावा, यह प्रत्येक उत्पाद के लिए समान तारीखें बनाता है, जो आवश्यकता से अधिक है। यदि TransactionHistoryएक लंबा समग्र इतिहास होता, तो प्रत्येक व्यक्ति के उत्पाद में अपेक्षाकृत कम इतिहास होता, अतिरिक्त अनावश्यक पंक्तियों की संख्या और भी अधिक होती।

दूसरी ओर, बस एक और, अधिक संकीर्ण सूचकांक बनाकर दो-स्कैन संस्करण को थोड़ा और अनुकूलित करना संभव है (ProductID, TransactionDate)। इस सूचकांक का उपयोग प्रत्येक उत्पाद के लिए प्रारंभ / समाप्ति तिथियों की गणना करने के लिए किया जाएगा ( CTE_Productsऔर इसमें अनुक्रमणिका को कवर करने की तुलना में कम पृष्ठ होंगे और इसके परिणामस्वरूप परिणाम पढ़ता है।

तो, हम चुन सकते हैं, या तो एक अतिरिक्त स्पष्ट सरल स्कैन है, या एक अंतर्निहित कार्य करने योग्य है।

BTW, यदि केवल दैनिक सारांश के साथ परिणाम देना ठीक है, तो एक इंडेक्स बनाना बेहतर होता है जिसमें शामिल नहीं है ReferenceOrderID। यह कम पृष्ठों => कम IO का उपयोग करेगा।

CREATE NONCLUSTERED INDEX [i2] ON [Production].[TransactionHistory]
(
    [ProductID] ASC,
    [TransactionDate] ASC
)
INCLUDE ([ActualCost])

CROSS APPLY का उपयोग करके एकल पास समाधान

यह वास्तव में एक लंबा जवाब हो जाता है, लेकिन यहां एक और संस्करण है जो केवल दैनिक सारांश को फिर से लौटाता है, लेकिन यह डेटा का केवल एक स्कैन करता है और इसके लिए किसी तिथि या उत्पाद की सूची की बाहरी जानकारी की आवश्यकता नहीं होती है। यह मध्यवर्ती सॉर्ट भी नहीं करता है। कुल मिलाकर प्रदर्शन पिछले वेरिएंट की तरह ही है, हालांकि यह थोड़ा खराब लगता है।

मुख्य विचार पंक्तियों को उत्पन्न करने के लिए संख्याओं की तालिका का उपयोग करना है जो तारीखों में अंतराल को भर देगा। प्रत्येक मौजूदा तिथि के लिए 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) का उपयोग करती है।

क्रॉस आवेदन

ca आँकड़े

ca io


23

एक वैकल्पिक SQLCLR समाधान जो तेजी से निष्पादित होता है और कम मेमोरी की आवश्यकता होती है:

परिनियोजन स्क्रिप्ट

इसके लिए EXTERNAL_ACCESSअनुमति सेट की आवश्यकता होती है क्योंकि यह (धीमी) संदर्भ कनेक्शन के बजाय लक्ष्य सर्वर और डेटाबेस में लूपबैक कनेक्शन का उपयोग करता है। यह फ़ंक्शन को कॉल करने का तरीका है:

SELECT 
    RS.ProductID,
    RS.TransactionDate,
    RS.ActualCost,
    RS.RollingSum45
FROM dbo.RollingSum
(
    N'.\SQL2014',           -- Instance name
    N'AdventureWorks2012'   -- Database name
) AS RS 
ORDER BY
    RS.ProductID,
    RS.TransactionDate,
    RS.ReferenceOrderID;

सवाल के रूप में, ठीक उसी क्रम में, उसी क्रम में उत्पादन करता है।

निष्पादन योजना:

SQLCLR TVF निष्पादन योजना

SQLCLR स्रोत क्वेरी निष्पादन योजना

एक्सप्लोरर के प्रदर्शन के आंकड़े की योजना बनाएं

प्रोफाइलर तार्किक पढ़ता है: 481

इस कार्यान्वयन का मुख्य लाभ यह है कि यह संदर्भ कनेक्शन का उपयोग करने की तुलना में तेज़ है, और यह कम मेमोरी का उपयोग करता है। यह केवल एक ही समय में दो चीजों को स्मृति में रखता है:

  1. कोई भी डुप्लिकेट पंक्तियाँ (समान उत्पाद और लेनदेन की तारीख)। इसकी आवश्यकता है क्योंकि जब तक कि उत्पाद या तिथि नहीं बदलती है, तब तक हम नहीं जानते कि अंतिम रनिंग योग क्या होगा। नमूना डेटा में, उत्पाद और दिनांक का एक संयोजन होता है जिसमें 64 पंक्तियाँ होती हैं।
  2. वर्तमान उत्पाद के लिए केवल लागत और लेनदेन की 45 दिन की स्लाइडिंग रेंज। 45-दिन की स्लाइडिंग विंडो को छोड़ने वाली पंक्तियों के लिए सरल रनिंग योग को समायोजित करना आवश्यक है।

यह न्यूनतम कैशिंग इस पद्धति को अच्छी तरह से सुनिश्चित करना चाहिए; सीएलआर मेमोरी में पूरे इनपुट सेट को रखने की कोशिश करने से बेहतर है।

सोर्स कोड


17

यदि आप SQL सर्वर 2014 के 64-बिट एंटरप्राइज, डेवलपर या मूल्यांकन संस्करण पर हैं, तो आप इन-मेमोरी ओएलटीपी का उपयोग कर सकते हैं । समाधान एक भी स्कैन नहीं होगा और शायद ही किसी भी विंडो फ़ंक्शन का उपयोग करेगा, लेकिन यह इस प्रश्न के लिए कुछ मूल्य जोड़ सकता है और उपयोग किए गए एल्गोरिदम को संभवतः अन्य समाधानों के लिए प्रेरणा के रूप में उपयोग किया जा सकता है।

सबसे पहले आपको एडवेंचरवर्क्स डेटाबेस पर इन-मेमोरी ओएलटीपी को सक्षम करना होगा।

alter database AdventureWorks2014 
  add filegroup InMem contains memory_optimized_data;

alter database AdventureWorks2014 
  add file (name='AW2014_InMem', 
            filename='D:\SQL Server\MSSQL12.MSSQLSERVER\MSSQL\DATA\AW2014') 
    to filegroup InMem;

alter database AdventureWorks2014 
  set memory_optimized_elevate_to_snapshot = on;

प्रक्रिया का पैरामीटर एक इन-मेमोरी टेबल चर है और इसे एक प्रकार के रूप में परिभाषित किया जाना है।

create type dbo.TransHistory as table
(
  ID int not null,
  ProductID int not null,
  TransactionDate datetime not null,
  ReferenceOrderID int not null,
  ActualCost money not null,
  RunningTotal money not null,
  RollingSum45 money not null,

  -- Index used in while loop
  index IX_T1 nonclustered hash (ID) with (bucket_count = 1000000),

  -- Used to lookup the running total as it was 45 days ago (or more)
  index IX_T2 nonclustered (ProductID, TransactionDate desc)
) with (memory_optimized = on);

आईडी इस तालिका में अद्वितीय नहीं है, इसके बारे में प्रत्येक संयोजन के लिए अद्वितीय है ProductIDऔर TransactionDate

प्रक्रिया में कुछ टिप्पणियां हैं जो आपको बताती हैं कि यह क्या करता है लेकिन कुल मिलाकर यह एक लूप में चल रहे कुल की गणना कर रहा है और प्रत्येक पुनरावृत्ति के लिए यह रनिंग कुल के लिए एक लुकअप करता है जैसा कि यह 45 दिन पहले (या अधिक) था।

वर्तमान में चल रहा कुल ऋण शून्य से कुल चल रहा है जैसा कि 45 दिन पहले था, रोलिंग 45 दिन का योग है जिसकी हम तलाश कर रहे हैं।

create procedure dbo.GetRolling45
  @TransHistory dbo.TransHistory readonly
with native_compilation, schemabinding, execute as owner as
begin atomic with(transaction isolation level = snapshot, language = N'us_english')

  -- Table to hold the result
  declare @TransRes dbo.TransHistory;

  -- Loop variable
  declare @ID int = 0;

  -- Current ProductID
  declare @ProductID int = -1;

  -- Previous ProductID used to restart the running total
  declare @PrevProductID int;

  -- Current transaction date used to get the running total 45 days ago (or more)
  declare @TransactionDate datetime;

  -- Sum of actual cost for the group ProductID and TransactionDate
  declare @ActualCost money;

  -- Running total so far
  declare @RunningTotal money = 0;

  -- Running total as it was 45 days ago (or more)
  declare @RunningTotal45 money = 0;

  -- While loop for each unique occurence of the combination of ProductID, TransactionDate
  while @ProductID <> 0
  begin
    set @ID += 1;
    set @PrevProductID = @ProductID;

    -- Get the current values
    select @ProductID = min(ProductID),
           @TransactionDate = min(TransactionDate),
           @ActualCost = sum(ActualCost)
    from @TransHistory 
    where ID = @ID;

    if @ProductID <> 0
    begin
      set @RunningTotal45 = 0;

      if @ProductID <> @PrevProductID
      begin
        -- New product, reset running total
        set @RunningTotal = @ActualCost;
      end
      else
      begin
        -- Same product as last row, aggregate running total
        set @RunningTotal += @ActualCost;

        -- Get the running total as it was 45 days ago (or more)
        select top(1) @RunningTotal45 = TR.RunningTotal
        from @TransRes as TR
        where TR.ProductID = @ProductID and
              TR.TransactionDate < dateadd(day, -45, @TransactionDate)
        order by TR.TransactionDate desc;

      end;

      -- Add all rows that match ID to the result table
      -- RollingSum45 is calculated by using the current running total and the running total as it was 45 days ago (or more)
      insert into @TransRes(ID, ProductID, TransactionDate, ReferenceOrderID, ActualCost, RunningTotal, RollingSum45)
      select @ID, 
             @ProductID, 
             @TransactionDate, 
             TH.ReferenceOrderID, 
             TH.ActualCost, 
             @RunningTotal, 
             @RunningTotal - @RunningTotal45
      from @TransHistory as TH
      where ID = @ID;

    end
  end;

  -- Return the result table to caller
  select TR.ProductID, TR.TransactionDate, TR.ReferenceOrderID, TR.ActualCost, TR.RollingSum45
  from @TransRes as TR
  order by TR.ProductID, TR.TransactionDate, TR.ReferenceOrderID;

end;

इस तरह की प्रक्रिया को लागू करें।

-- Parameter to stored procedure GetRollingSum
declare @T dbo.TransHistory;

-- Load data to in-mem table
-- ID is unique for each combination of ProductID, TransactionDate
insert into @T(ID, ProductID, TransactionDate, ReferenceOrderID, ActualCost, RunningTotal, RollingSum45)
select dense_rank() over(order by TH.ProductID, TH.TransactionDate),
       TH.ProductID, 
       TH.TransactionDate, 
       TH.ReferenceOrderID,
       TH.ActualCost,
       0, 
       0
from Production.TransactionHistory as TH;

-- Get the rolling 45 days sum
exec dbo.GetRolling45 @T;

मेरे कंप्यूटर पर यह परीक्षण करना ग्राहक सांख्यिकी के बारे में 750 मिलीसेकंड का कुल निष्पादन समय रिपोर्ट करता है। तुलना के लिए उप-क्वेरी संस्करण 3.5 सेकंड लेता है।

अतिरिक्त रंबलिंग:

इस एल्गोरिथ्म का उपयोग नियमित T-SQL द्वारा भी किया जा सकता है। rangeपंक्तियों का उपयोग करके, चल रहे कुल की गणना करें , और परिणाम को एक अस्थायी तालिका में संग्रहीत करें। फिर आप उस तालिका को स्वयं से जोड़कर रनिंग कुल में शामिल कर सकते हैं क्योंकि यह 45 दिन पहले था और रोलिंग योग की गणना करता है। हालाँकि, की rangeतुलना में कार्यान्वयन rowsइस तथ्य के कारण काफी धीमा है कि आदेश द्वारा डुप्लिकेट के उपचार को अलग-अलग तरीके से करने की आवश्यकता है, इसलिए मुझे इस दृष्टिकोण के साथ यह सब अच्छा प्रदर्शन नहीं मिला। उस पर एक वर्कअराउंड दूसरे विंडो फ़ंक्शन का उपयोग करने के लिए हो सकता है जैसे last_value()कि एक गणना चल रही कुल का उपयोग करके चल रहे कुल का rowsअनुकरण करने के लिए range। दूसरा तरीका उपयोग करना है max() over()। दोनों में कुछ मुद्दे थे। प्रकारों से बचने और स्पूल से बचने के लिए उपयोग करने के लिए उपयुक्त इंडेक्स का पता लगानाmax() over()संस्करण। मैंने उन चीजों का अनुकूलन करना छोड़ दिया, लेकिन अगर आप अभी तक मेरे द्वारा बताए गए कोड में रुचि रखते हैं तो कृपया मुझे बताएं।


13

वैसे यह मजेदार था :) मेरा समाधान @ जियोफैप्टर्सन की तुलना में थोड़ा धीमा है लेकिन इसका एक हिस्सा यह तथ्य है कि मैं मूल तालिका में वापस आ रहा हूं ताकि ज्योफ की मान्यताओं में से एक को समाप्त कर सके (यानी प्रति उत्पाद / तिथि जोड़ी एक पंक्ति) । मैं इस धारणा के साथ गया कि यह एक अंतिम क्वेरी का सरलीकृत संस्करण है और मूल तालिका से बाहर अतिरिक्त जानकारी की आवश्यकता हो सकती है।

नोट: मैं जियोफ़ की कैलेंडर तालिका उधार ले रहा हूं और वास्तव में एक समान समाधान के साथ समाप्त हुआ:

-- Build calendar table for 2000 ~ 2020
CREATE TABLE dbo.calendar (d DATETIME NOT NULL CONSTRAINT PK_calendar PRIMARY KEY)
GO
DECLARE @d DATETIME = '1/1/2000'
WHILE (@d < '1/1/2021')
BEGIN
    INSERT INTO dbo.calendar (d) VALUES (@d)
    SELECT @d =  DATEADD(DAY, 1, @d)
END

यहाँ क्वेरी ही है:

WITH myCTE AS (SELECT PP.ProductID, calendar.d AS TransactionDate, 
                    SUM(ActualCost) AS CostPerDate
                FROM Production.Product PP
                CROSS JOIN calendar
                LEFT OUTER JOIN Production.TransactionHistory PTH
                    ON PP.ProductID = PTH.ProductID
                    AND calendar.d = PTH.TransactionDate
                CROSS APPLY (SELECT MAX(TransactionDate) AS EndDate,
                                MIN(TransactionDate) AS StartDate
                            FROM Production.TransactionHistory) AS Boundaries
                WHERE calendar.d BETWEEN Boundaries.StartDate AND Boundaries.EndDate
                GROUP BY PP.ProductID, calendar.d),
    RunningTotal AS (
        SELECT ProductId, TransactionDate, CostPerDate AS TBE,
                SUM(myCTE.CostPerDate) OVER (
                    PARTITION BY myCTE.ProductID
                    ORDER BY myCTE.TransactionDate
                    ROWS BETWEEN 
                        45 PRECEDING
                        AND CURRENT ROW) AS RollingSum45
        FROM myCTE)
SELECT 
    TH.ProductID,
    TH.TransactionDate,
    TH.ActualCost,
    RollingSum45
FROM Production.TransactionHistory AS TH
JOIN RunningTotal
    ON TH.ProductID = RunningTotal.ProductID
    AND TH.TransactionDate = RunningTotal.TransactionDate
WHERE RunningTotal.TBE IS NOT NULL
ORDER BY
    TH.ProductID,
    TH.TransactionDate,
    TH.ReferenceOrderID;

मूल रूप से मैंने फैसला किया कि इससे निपटने का सबसे आसान तरीका था ROWS क्लॉज का विकल्प। लेकिन यह आवश्यक है कि मेरे पास केवल एक पंक्ति प्रति है ProductID, TransactionDateसंयोजन और सिर्फ इतना ही नहीं, बल्कि मुझे प्रति पंक्ति एक पंक्ति ProductIDऔर होनी चाहिए possible date। मैंने उस उत्पाद, कैलेंडर और TransactionHistory तालिकाओं को एक CTE में मिलाकर किया। फिर मुझे रोलिंग जानकारी उत्पन्न करने के लिए एक और CTE बनाना पड़ा। मुझे ऐसा करना पड़ा क्योंकि अगर मैंने इसे मूल तालिका में वापस शामिल कर लिया तो सीधे मुझे पंक्ति उन्मूलन मिल गया जिसने मेरे परिणामों को फेंक दिया। उसके बाद मूल तालिका में मेरे दूसरे सीटीई में शामिल होने का एक साधारण मामला था। मैंने सीटीई में बनाई गई रिक्त पंक्तियों TBEसे छुटकारा पाने के लिए कॉलम (समाप्त होने के लिए) को जोड़ा । इसके अलावा, मैंने अपने कैलेंडर तालिका के लिए सीमाएँ बनाने के लिए प्रारंभिक CTE में उपयोग किया ।CROSS APPLY

मैंने तब अनुशंसित सूचकांक जोड़ा:

CREATE NONCLUSTERED INDEX [TransactionHistory_IX1]
ON [Production].[TransactionHistory] ([TransactionDate])
INCLUDE ([ProductID],[ReferenceOrderID],[ActualCost])

और अंतिम निष्पादन योजना मिली:

यहाँ छवि विवरण दर्ज करें यहाँ छवि विवरण दर्ज करें यहाँ छवि विवरण दर्ज करें

EDIT: अंत में मैंने कैलेंडर टेबल पर एक इंडेक्स जोड़ा है जो एक उचित मार्जिन द्वारा प्रदर्शन को बढ़ाता है।

CREATE INDEX ix_calendar ON calendar(d)

2
RunningTotal.TBE IS NOT NULLहालत (है, परिणामस्वरूप, TBEस्तंभ) अनावश्यक है। यदि आप इसे छोड़ देते हैं तो आप निरर्थक पंक्तियाँ प्राप्त नहीं करने वाले हैं, क्योंकि आपकी आंतरिक जुड़ने की स्थिति में दिनांक स्तंभ शामिल है - इसलिए परिणाम सेट में वे दिनांक नहीं हो सकते हैं जो मूल रूप से स्रोत में नहीं थे।
एंड्री एम

2
हां। मैं पूरी तरह से सहमत हूँ। और फिर भी इसने मुझे लगभग 2 सेकंड तक लाभ पहुँचाया। मुझे लगता है कि यह आशावादी को कुछ अतिरिक्त जानकारी बताने देता है।
केनेथ फिशर

4

मेरे पास कुछ वैकल्पिक समाधान हैं जो अनुक्रमित या संदर्भ तालिकाओं का उपयोग नहीं करते हैं। शायद वे उन स्थितियों में उपयोगी हो सकते हैं जिनमें आपके पास किसी भी अतिरिक्त तालिकाओं तक पहुंच नहीं है और अनुक्रमणिका नहीं बना सकते हैं। यह TransactionDateडेटा के सिर्फ एक पास और सिर्फ एक विंडो फ़ंक्शन के साथ समूहीकरण करते समय सही परिणाम प्राप्त करना संभव प्रतीत होता है । हालाँकि, मैं इसे केवल एक विंडो फ़ंक्शन के साथ करने का एक तरीका नहीं खोज सका जब आप समूह द्वारा नहीं कर सकते TransactionDate

मेरी मशीन पर संदर्भ का एक फ्रेम प्रदान करने के लिए, प्रश्न में पोस्ट किए गए मूल समाधान में कवरिंग इंडेक्स के बिना 2808 एमएस का सीपीयू समय और कवरिंग इंडेक्स के साथ 1950 एमएस है। मैं AdventureWorks2014 डेटाबेस और SQL सर्वर एक्सप्रेस 2014 के साथ परीक्षण कर रहा हूं।

आइए हम एक समाधान के साथ शुरुआत करें जब हम समूह बना सकते हैं TransactionDate। पिछले एक्स दिनों में एक रनिंग राशि को निम्न तरीके से भी व्यक्त किया जा सकता है:

एक पंक्ति के लिए राशि दौड़ना = पिछली सभी पंक्तियों का जोड़-भाग - सभी पिछली पंक्तियों का योग। जिसके लिए तारीख खिड़की के बाहर है।

SQL में, इसे व्यक्त करने का एक तरीका आपके डेटा की दो प्रतियाँ बनाना और दूसरी प्रतिलिपि के लिए, लागत को -1 से गुणा करना और तिथि कॉलम में X + 1 दिन जोड़ना है। सभी डेटा पर एक रनिंग योग की गणना करने से उपरोक्त फॉर्मूला लागू होगा। मैं इसे कुछ उदाहरण डेटा के लिए दिखाऊंगा। नीचे एकल के लिए कुछ नमूना तिथि दी गई है ProductID। मैं गणना को आसान बनाने के लिए संख्याओं के रूप में तारीखों का प्रतिनिधित्व करता हूं। डेटा शुरू करना:

╔══════╦══════╗
 Date  Cost 
╠══════╬══════╣
    1     3 
    2     6 
   20     1 
   45    -4 
   47     2 
   64     2 
╚══════╩══════╝

डेटा की दूसरी प्रति में जोड़ें। दूसरी कॉपी में तारीख में 46 दिन और लागत में 1 गुना का इजाफा हुआ है:

╔══════╦══════╦═══════════╗
 Date  Cost  CopiedRow 
╠══════╬══════╬═══════════╣
    1     3          0 
    2     6          0 
   20     1          0 
   45    -4          0 
   47    -3          1 
   47     2          0 
   48    -6          1 
   64     2          0 
   66    -1          1 
   91     4          1 
   93    -2          1 
  110    -2          1 
╚══════╩══════╩═══════════╝

Dateआरोही और CopiedRowअवरोही द्वारा आदेशित रन राशि लें :

╔══════╦══════╦═══════════╦════════════╗
 Date  Cost  CopiedRow  RunningSum 
╠══════╬══════╬═══════════╬════════════╣
    1     3          0           3 
    2     6          0           9 
   20     1          0          10 
   45    -4          0           6 
   47    -3          1           3 
   47     2          0           5 
   48    -6          1          -1 
   64     2          0           1 
   66    -1          1           0 
   91     4          1           4 
   93    -2          1           0 
  110    -2          1           0 
╚══════╩══════╩═══════════╩════════════╝

वांछित परिणाम प्राप्त करने के लिए कॉपी की गई पंक्तियों को फ़िल्टर करें:

╔══════╦══════╦═══════════╦════════════╗
 Date  Cost  CopiedRow  RunningSum 
╠══════╬══════╬═══════════╬════════════╣
    1     3          0           3 
    2     6          0           9 
   20     1          0          10 
   45    -4          0           6 
   47     2          0           5 
   64     2          0           1 
╚══════╩══════╩═══════════╩════════════╝

निम्नलिखित एल्गोरिथ्म को लागू करने के लिए निम्न SQL एक तरीका है:

WITH THGrouped AS 
(
    SELECT
    ProductID,
    TransactionDate,
    SUM(ActualCost) ActualCost
    FROM Production.TransactionHistory
    GROUP BY ProductID,
    TransactionDate
)
SELECT
ProductID,
TransactionDate,
ActualCost,
RollingSum45
FROM
(
    SELECT
    TH.ProductID,
    TH.ActualCost,
    t.TransactionDate,
    SUM(t.ActualCost) OVER (PARTITION BY TH.ProductID ORDER BY t.TransactionDate, t.OrderFlag) AS RollingSum45,
    t.OrderFlag,
    t.FilterFlag -- define this column to avoid another sort at the end
    FROM THGrouped AS TH
    CROSS APPLY (
        VALUES
        (TH.ActualCost, TH.TransactionDate, 1, 0),
        (-1 * TH.ActualCost, DATEADD(DAY, 46, TH.TransactionDate), 0, 1)
    ) t (ActualCost, TransactionDate, OrderFlag, FilterFlag)
) tt
WHERE tt.FilterFlag = 0
ORDER BY
tt.ProductID,
tt.TransactionDate,
tt.OrderFlag
OPTION (MAXDOP 1);

मेरी मशीन पर इसने अनुक्रमित सूचकांक के साथ सीपीयू समय का 702 एमएस लिया और सूचकांक के बिना सीपीयू समय के 734 एमएस। क्वेरी प्लान यहां पाया जा सकता है: https://www.brentozar.com/pastetheplan/?id=SJdCs_VLl

इस समाधान का एक पहलू यह है कि नए TransactionDateकॉलम द्वारा ऑर्डर करने पर यह एक अपरिहार्य प्रकार प्रतीत होता है । मुझे नहीं लगता कि अनुक्रम को जोड़कर इस तरह का समाधान किया जा सकता है क्योंकि हमें ऑर्डर करने से पहले डेटा की दो प्रतियों को संयोजित करने की आवश्यकता है। मैं क्वेरी के अंत में ORDER BY के एक अलग कॉलम में जोड़कर एक तरह से छुटकारा पाने में सक्षम था। अगर मैंने आदेश दिया है FilterFlagकि SQL सर्वर उस कॉलम को सॉर्ट से ऑप्टिमाइज़ करेगा और एक स्पष्ट सॉर्ट करेगा।

जब हमें TransactionDateउसी के लिए डुप्लिकेट मानों के साथ सेट किए गए परिणाम को वापस करने की आवश्यकता होती है, तो समाधान ProductIdअधिक जटिल थे। मैं एक ही कॉलम द्वारा विभाजन और आदेश की आवश्यकता के साथ समस्या को संक्षेप में बताऊंगा। पॉल ने जो सिंटैक्स प्रदान किया है वह उस मुद्दे को हल करता है, इसलिए यह आश्चर्यजनक नहीं है कि SQL सर्वर में उपलब्ध वर्तमान विंडो फ़ंक्शन के साथ व्यक्त करना इतना मुश्किल है (यदि यह व्यक्त करना मुश्किल नहीं था तो सिंटैक्स का विस्तार करने की आवश्यकता नहीं होगी)।

यदि मैं समूह के बिना उपरोक्त क्वेरी का उपयोग करता हूं तो मुझे रोलिंग योग के लिए अलग-अलग मान मिलते हैं जब एक ही के साथ कई पंक्तियाँ होती हैं ProductIdऔर TransactionDate। इसे हल करने का एक तरीका यह है कि ऊपर की तरह एक ही चल रहे योग की गणना करें लेकिन विभाजन में अंतिम पंक्ति को भी चिह्नित करें। यह एक अतिरिक्त प्रकार के बिना LEAD(माना ProductIDजाता है कि NULL कभी नहीं) के साथ किया जा सकता है । अंतिम चल रहे योग मूल्य के लिए, मैं MAXविभाजन के सभी पंक्तियों में विभाजन की अंतिम पंक्ति में मान को लागू करने के लिए एक विंडो फ़ंक्शन के रूप में उपयोग करता हूं ।

SELECT
ProductID,
TransactionDate,
ReferenceOrderID,
ActualCost,
MAX(CASE WHEN LasttRowFlag = 1 THEN RollingSum ELSE NULL END) OVER (PARTITION BY ProductID, TransactionDate) RollingSum45
FROM
(
    SELECT
    TH.ProductID,
    TH.ActualCost,
    TH.ReferenceOrderID,
    t.TransactionDate,
    SUM(t.ActualCost) OVER (PARTITION BY TH.ProductID ORDER BY t.TransactionDate, t.OrderFlag, TH.ReferenceOrderID) RollingSum,
    CASE WHEN LEAD(TH.ProductID) OVER (PARTITION BY TH.ProductID, t.TransactionDate ORDER BY t.OrderFlag, TH.ReferenceOrderID) IS NULL THEN 1 ELSE 0 END LasttRowFlag,
    t.OrderFlag,
    t.FilterFlag -- define this column to avoid another sort at the end
    FROM Production.TransactionHistory AS TH
    CROSS APPLY (
        VALUES
        (TH.ActualCost, TH.TransactionDate, 1, 0),
        (-1 * TH.ActualCost, DATEADD(DAY, 46, TH.TransactionDate), 0, 1)
    ) t (ActualCost, TransactionDate, OrderFlag, FilterFlag)
) tt
WHERE tt.FilterFlag = 0
ORDER BY
tt.ProductID,
tt.TransactionDate,
tt.OrderFlag,
tt.ReferenceOrderID
OPTION (MAXDOP 1);  

मेरी मशीन पर यह 2464ms सीपीयू समय को कवर किए बिना सूचकांक में ले गया। जैसा कि पहले एक अपरिहार्य प्रकार प्रतीत होता है। क्वेरी प्लान यहां पाया जा सकता है: https://www.brentozar.com/pastetheplan/?id=HyWxhGGBl

मुझे लगता है कि उपरोक्त प्रश्न में सुधार की गुंजाइश है। वांछित परिणाम प्राप्त करने के लिए निश्चित रूप से विंडोज़ कार्यों का उपयोग करने के अन्य तरीके हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.