উইন্ডো ফাংশন ব্যবহার করে তারিখের পরিসীমা রোলিংয়ের যোগফল


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উইন্ডো ফ্রেমের পরিমাণ বর্তমানে এসকিউএল সার্ভারে একটি অন্তরালের অনুমতি দেয় না।

আমি জানি আমি সাবকিউরি এবং একটি নিয়মিত (উইন্ডোবিহীন) সামগ্রিক ব্যবহার করে একটি সমাধান লিখতে পারি:

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

কার্যকর করার পরিকল্পনাটি হ'ল:

হত্যা পরিকল্পনা

মারাত্মকভাবে অদক্ষ না হলেও, মনে হচ্ছে এসকিউএল সার্ভার 2012, 2014 বা 2016 (এখন পর্যন্ত) সমর্থিত কেবল উইন্ডো সমষ্টি এবং বিশ্লেষণমূলক ফাংশন ব্যবহার করে এই কোয়েরিটি প্রকাশ করা সম্ভব হবে possible

স্পষ্টতার জন্য, আমি এমন একটি সমাধান খুঁজছি যা ডেটা থেকে একক পাস করে।

টি-SQL এর যে এর মানে এই যে সম্ভাবনা রয়েছে দফা কাজ, এবং উইন্ডো spools এবং উইন্ডো দলা বৈশিষ্ট্য হবে মৃত্যুদন্ড পরিকল্পনা করতে হবে। ক্লজটি ব্যবহার করে এমন সমস্ত ভাষার উপাদানগুলি ন্যায্য খেলা। একটি এসকিউএলসিএলআর সমাধান গ্রহণযোগ্য, তবে এটি সঠিক ফলাফল উত্থাপনের গ্যারান্টিযুক্ত providedOVEROVER

টি-এসকিউএল সমাধানগুলির জন্য, এক্সিকিউশন প্ল্যানে কম হ্যাশ, বাছাই এবং উইন্ডো স্পুলস / সমষ্টিগুলি আরও ভাল। সূচকগুলি নির্দ্বিধায় অনুভব করুন তবে পৃথক কাঠামো অনুমোদিত নয় (উদাহরণস্বরূপ, ট্রিগারগুলির সাথে সিঙ্কে রাখা কোনও প্রাক-গণিত টেবিল নেই)। রেফারেন্স সারণী অনুমোদিত (সংখ্যার টেবিল, তারিখ ইত্যাদি)

আদর্শভাবে, সমাধানগুলি উপরের সাবকোরি সংস্করণের মতো একই ক্রমে একই ফলাফল তৈরি করবে, তবে যুক্তিযুক্তভাবে সঠিক কিছু গ্রহণযোগ্যও। পারফরম্যান্স সর্বদা বিবেচ্য, তাই সমাধানগুলি কমপক্ষে যুক্তিসঙ্গতভাবে দক্ষ হওয়া উচিত।

উত্সর্গীকৃত চ্যাট রুম: আমি এই প্রশ্ন এবং এর উত্তর সম্পর্কিত আলোচনার জন্য একটি পাবলিক চ্যাট রুম তৈরি করেছি। কমপক্ষে 20 খ্যাতি পয়েন্ট সহ যে কোনও ব্যবহারকারী সরাসরি অংশ নিতে পারেন। আপনার যদি কম 20 জন প্রতিনিধি থাকে এবং অংশ নিতে চান তবে দয়া করে নীচের মন্তব্যে আমাকে পিং করুন।

উত্তর:


42

দুর্দান্ত প্রশ্ন, পল! আমি কয়েকটি ভিন্ন পদ্ধতির ব্যবহার করেছি, একটি টি-এসকিউএল এবং একটি সিএলআর-এ।

টি এসকিউএল দ্রুত সংক্ষিপ্তসার

টি-এসকিউএল পদ্ধতির সংক্ষিপ্ত বিবরণ নিম্নলিখিত পদক্ষেপ হিসাবে দেওয়া যেতে পারে:

  • পণ্য / তারিখের ক্রস-পণ্যটি নিন
  • পর্যবেক্ষণ বিক্রয় ডেটাতে মার্জ করুন
  • সেই ডেটাটিকে পণ্য / তারিখ স্তরে একত্রিত করুন
  • এই সামগ্রিক ডেটার উপর ভিত্তি করে গত 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, সিপিইউ সময় হয় 514ms। এটি 2231msমূল প্রশ্নের সাথে অনুকূলভাবে তুলনা করে ।

সিএলআর দ্রুত সংক্ষিপ্তসার

সিএলআর সারাংশ নিম্নলিখিত পদক্ষেপ হিসাবে সংক্ষিপ্ত করা যেতে পারে:

  • পণ্য এবং তারিখ অনুসারে মেমরিতে ডেটা পড়ুন
  • প্রতিটি লেনদেন প্রক্রিয়া করার সময়, ব্যয় চলমান মোট যোগ করুন। যখনই কোনও লেনদেন আগের লেনদেনের চেয়ে আলাদা পণ্য হয়, চলমান মোট 0 এ পুনরায় সেট করুন।
  • বর্তমান লেনদেনের মতো একই (পণ্য, তারিখ) থাকা প্রথম লেনদেনের একটি পয়েন্টার বজায় রাখুন। যখনই সেই (পণ্য, তারিখ) সহ শেষ লেনদেনের মুখোমুখি হয়, তখন সেই লেনদেনের জন্য রোলিংয়ের অঙ্কটি গণনা করুন এবং এটি একই (পণ্য, তারিখ) দিয়ে সমস্ত লেনদেনে প্রয়োগ করুন
  • সমস্ত ফলাফল ব্যবহারকারীর কাছে ফিরিয়ে দিন!

ব্যবহার করে SET STATISTICS IO ON, এই পদ্ধতির প্রতিবেদন করা হয়েছে যে কোনও যৌক্তিক I / O ঘটেনি! বাহ, একটি নিখুঁত সমাধান! (আসলে, এটা মনে হচ্ছে যে SET STATISTICS IOরিপোর্ট না, I / O CLR মধ্যে যথাযোগ্য। কিন্তু কোড থেকে, এটি দেখতে যে ঠিক টেবিলের এক স্ক্যান তৈরি করা হয় সহজ এবং সূচক পল প্রস্তাব দ্বারা অনুক্রমে তথ্য প্রাপ্ত করে।

হিসাবে রিপোর্ট করা হয়েছে SET STATISTICS TIME ON, সিপিইউ সময় এখন 187ms। সুতরাং এটি টি-এসকিউএল পদ্ধতির চেয়ে বেশ উন্নতি। দুর্ভাগ্যক্রমে, উভয় পদ্ধতির সামগ্রিকভাবে অতিবাহিত সময় প্রতিটি প্রায় অর্ধেক সেকেন্ডে খুব অনুরূপ। যাইহোক, সিএলআর ভিত্তিক পদ্ধতির 113K সারিগুলি কনসোলে আউটপুট করতে হবে (বনাম। টি-এসকিউএল পদ্ধতির জন্য কেবল 52K যা পণ্য / তারিখ অনুসারে গ্রুপ করে), সুতরাং সে কারণেই আমি পরিবর্তে সিপিইউয়ের দিকে মনোনিবেশ করেছি।

এই পদ্ধতির আর একটি বড় সুবিধা হ'ল এটি একই সময়ে ফলাফল লুপ / ​​সিক অ্যাপ্রোচ হিসাবে ঠিক একই ফলাফল দেয়, প্রতি লেনদেনের জন্য একটি সারি সহ এমনকি যেখানে একই দিনে কোনও পণ্য একাধিকবার বিক্রি হয়। (অ্যাডভেঞ্চার ওয়ার্কসে, আমি বিশেষভাবে সারি-সারি সারি ফলাফলগুলির তুলনা করেছি এবং নিশ্চিত করেছি যে তারা পলের মূল প্রশ্নের সাথে তাল মিলিয়েছে))

কমপক্ষে বর্তমান রূপে এই পদ্ধতির একটি অসুবিধা হ'ল এটি মেমরিতে সমস্ত ডেটা পড়ে। তবে যে অ্যালগরিদমটি কেবলমাত্র কঠোরভাবে ডিজাইন করা হয়েছে তা কোনও নির্দিষ্ট সময়ে মেমরিতে বর্তমান উইন্ডো ফ্রেমের প্রয়োজন এবং মেমরির অতিক্রমকারী ডেটা সেটগুলির জন্য কাজ করার জন্য আপডেট করা যেতে পারে। পল এই উত্তরটিকে এই অ্যালগরিদমের একটি বাস্তবায়ন উত্পাদন দ্বারা চিত্রিত করেছেন যা কেবল স্মৃতিতে স্লাইডিং উইন্ডো সঞ্চয় করে। এটি সিএলআর অ্যাসেমব্লিকে উচ্চতর অনুমতি দেওয়ার ব্যয়ে আসে, তবে এ সমাধানটি নির্বিচারে বড় ডেটা সেট পর্যন্ত স্কেলিংয়ে অবশ্যই কার্যকর হবে।


টি-এসকিউএল - একটি স্ক্যান, তারিখ অনুসারে গোষ্ঠীভুক্ত

প্রাথমিক সেটআপ

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, তাই আমি এটিকে ন্যায্য খেলা হিসাবে ব্যাখ্যা করেছি pre
  • এই পদ্ধতির উপর নির্ভর করে যে লেনদেনগুলির একটি সময় উপাদান থাকে না AdventureWorks2012; যদি তারা তা করে থাকে তবে লেনদেনের ইতিহাসটি প্রথমে পাস না করেই পণ্য / তারিখের সমন্বয়গুলির সম্পূর্ণ সেট তৈরি করা আর সম্ভব হবে না।
  • আমি একটি রোসেট তৈরি করছি যা প্রতি পণ্য / তারিখের জুটিতে কেবল একটি সারি থাকে। আমি মনে করি এটি "যুক্তিযুক্তভাবে সঠিক" এবং অনেক ক্ষেত্রে ফিরে আসার জন্য আরও পছন্দসই ফলাফল। প্রতিটি পণ্য / তারিখের জন্য, NumOrdersকতগুলি বিক্রয় হয়েছে তা নির্দেশ করার জন্য আমি একটি কলাম যুক্ত করেছি । মূল ক্যোয়ারির বনাম প্রস্তাবিত ক্যোয়ারির ফলাফলগুলির তুলনার জন্য নিম্নলিখিত স্ক্রিনশটটি দেখুন যেখানে একই তারিখে কোনও পণ্য একাধিকবার বিক্রি হয়েছিল (যেমন, 319/ 2007-09-05 00:00:00.000)

এখানে চিত্র বর্ণনা লিখুন


সিএলআর - একটি স্ক্যান, সম্পূর্ণ গ্রুপহীন ফলাফল সেট

প্রধান ফাংশন বডি

এখানে দেখার মতো কোনও টন নেই; ফাংশনটির প্রধান অংশটি ইনপুটগুলি ঘোষণা করে (যা অবশ্যই এসকিউএল ফাংশনের সাথে মেলে), একটি এসকিউএল সংযোগ স্থাপন করে এবং এসকিউএলআরডার খোলে।

// 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;
}

এসকিউএল এ সব একসাথে বেঁধে রাখা

এই অবধি সমস্ত কিছু সি # তে ছিল, সুতরাং আসল এসকিউএল জড়িত দেখি। (বিকল্পভাবে, আপনি এই সংস্থার স্ক্রিপ্টটি নিজেকে সংকলনের পরিবর্তে আমার সমাবেশের বিট থেকে সরাসরি সমাবেশ তৈরি করতে ব্যবহার করতে পারেন ))

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

আদেশ সহকারে

সিএলআর পদ্ধতির মাধ্যমে অ্যালগরিদমকে অনুকূল করতে আরও অনেক নমনীয়তা পাওয়া যায় এবং এটি সম্ভবত সি # এর একজন বিশেষজ্ঞ দ্বারা আরও টিউন করতে পারেন। তবে, সিএলআর কৌশলটির ডাউনসাইডও রয়েছে। কিছু বিষয় মনে রাখতে হবে:

  • এই সিএলআর পদ্ধতির মেমরিতে সেট করা ডেটার অনুলিপি রাখে। স্ট্রিমিং পদ্ধতির ব্যবহার করা সম্ভব, তবে আমি প্রাথমিক সমস্যার মুখোমুখি হয়েছি এবং দেখেছি যে এখানে একটি অসামান্য সংযোগ সমস্যা রয়েছে যা অভিযোগ করেছে যে এসকিউএল ২০০+ এর পরিবর্তনগুলি এই ধরণের পদ্ধতির ব্যবহারকে আরও কঠিন করে তুলেছে। এটি এখনও সম্ভব (পল যেমন দেখিয়েছেন) তবে ডাটাবেস সেট করে সিএলআর সমাবেশকে TRUSTWORTHYমঞ্জুর করে উচ্চ স্তরের অনুমতি প্রয়োজন EXTERNAL_ACCESS। সুতরাং কিছু ঝামেলা এবং সম্ভাব্য সুরক্ষা জড়িত রয়েছে, কিন্তু পরিশোধটি একটি স্ট্রিমিং অ্যাপ্রোচ যা অ্যাডভেঞ্চার ওয়ার্কসের তুলনায় অনেক বড় ডেটা সেটগুলিতে আরও ভাল স্কেল করতে পারে।
  • সিএলআর কিছু ডিবিএর কাছে কম অ্যাক্সেসযোগ্য হতে পারে, এমন একটি ফাংশন এমন একটি কালো বাক্স তৈরি করে যা স্বচ্ছ নয়, সহজেই সংশোধিত হয় না, সহজেই মোতায়েন হয় না এবং সম্ভবত সহজে ডিবাগ হয় না। টি-এসকিউএল পদ্ধতির সাথে তুলনা করার সময় এটি বেশ বড় অসুবিধা।


বোনাস: টি-এসকিউএল # 2 - বাস্তবিক ব্যবহারিক পদ্ধতির আমি ব্যবহার করব

কিছুক্ষণ সৃজনশীলভাবে সমস্যাটি নিয়ে চিন্তা করার চেষ্টা করার পরে, আমি ভেবেছিলাম যে আমি যদি আমার প্রতিদিনের কাজটি সামনে আসে তবে সমস্যাটি সমাধান করার পক্ষে আমি মোটামুটি সহজ, ব্যবহারিক উপায়ও পোস্ট করব। এটি এসকিউএল 2012+ উইন্ডো কার্যকারিতা ব্যবহার করে না, তবে প্রশ্নটি যে প্রত্যাশার প্রত্যাশায় ছিল তার ভিত্তিতে নয় not

-- 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
  • যদি ডেটা অনেক বেশি ঘনঘন (দিনে বেশি লেনদেন) হত, স্লাইডিং উইন্ডোতে লেনদেনের সংখ্যার সাথে গণনা সংক্রান্ত জটিলতা চারদিকে বৃদ্ধি পাবে না (যেমন এটি মূল জিজ্ঞাসার ক্ষেত্রে রয়েছে); আমি মনে করি এটি একাধিক স্ক্যান এড়ানোর জন্য পলের উদ্বেগের অংশকে সম্বোধন করেছে
  • এটি নতুন টেম্পডিবি অলস লেখার কার্যকারিতার কারণে এসকিউএল 2012+ এর সাম্প্রতিক আপডেটগুলিতে মূলত কোনও টেম্পডিবি আই / ও এর ফলাফল দেয় না
  • খুব বড় ডেটা সেটগুলির জন্য, মেমরির চাপটি যদি উদ্বেগের বিষয় হয়ে থাকে তবে প্রতিটি পণ্যের জন্য কাজটিকে পৃথক ব্যাচে ভাগ করা তুচ্ছ is

একটি দম্পতি সম্ভাব্য সতর্কতা:

  • যদিও এটি প্রযুক্তিগতভাবে প্রোডাকশনটিকে ট্রান্সঅ্যাকশন হিস্টোরি মাত্র একবার স্ক্যান করেছে, এটি সত্যিকার অর্থে একটি "একটি স্ক্যান" পদ্ধতির নয় কারণ # টিম টেস্টের একই আকারের টেবিল এবং সেই টেবিলেও অতিরিক্ত লজিকেশন I / O করতে হবে। যাইহোক, আমি এটি কোনও কাজের টেবিলের থেকে খুব বেশি আলাদা দেখতে পাচ্ছি না যে এটির সুনির্দিষ্ট কাঠামোটি আমরা সংজ্ঞায়িত করার কারণে আমাদের আরও ম্যানুয়াল নিয়ন্ত্রণ রয়েছে
  • আপনার পরিবেশের উপর নির্ভর করে, টেম্পডিবির ব্যবহারকে ইতিবাচক হিসাবে দেখা যেতে পারে (উদাহরণস্বরূপ, এটি এসএসডি ড্রাইভের একটি পৃথক সেট) অথবা একটি নেতিবাচক (সার্ভারে উচ্চ সম্মতি, ইতিমধ্যে প্রচুর টেম্পডিবি কনটেন্ট)

25

এটি একটি দীর্ঘ উত্তর, তাই আমি এখানে একটি সংক্ষিপ্ত যুক্ত করার সিদ্ধান্ত নিয়েছি।

  • প্রথমে আমি একটি সমাধান উপস্থাপন করি যা প্রশ্নের অনুরূপ একই ক্রমে একই ফলাফল তৈরি করে। এটি মূল টেবিলটি 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, নীচে দেখুন।

subquery

উপজাত পদ্ধতির নেস্টেড লুপ এবং O(n*n)জটিলতার সাথে একটি সহজ পরিকল্পনা রয়েছে ।

উপর

এই পদ্ধতির জন্য পরিকল্পনা TransactionHistoryকয়েকবার স্ক্যান করে , তবে কোনও লুপ নেই। আপনি দেখতে পাচ্ছেন 70% এরও বেশি ব্যয় হ'ল Sortফাইনালের জন্য ORDER BY

IO

শীর্ষ ফলাফল - 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);

এক-স্ক্যান

উভয় প্রশ্নের একই ক্রমে একই ফলাফল ফিরে আসে।

তুলনা

এখানে সময় এবং আইও পরিসংখ্যান রয়েছে।

stats2

io2

দ্বি-স্ক্যান বৈকল্পিকটি কিছুটা দ্রুত এবং এর পাঠ্য কম হয়, কারণ ওয়ান-স্ক্যান বৈকল্পিকটি ওয়ার্কটেবলকে প্রচুর ব্যবহার করতে হয়। এছাড়াও, ওয়ান-স্ক্যান বৈকল্পিক আপনার পরিকল্পনাগুলিতে দেখতে পাওয়ায় প্রয়োজনের চেয়ে আরও বেশি সারি তৈরি করে। এটি টেবিলে থাকা প্রত্যেকটির জন্য খেজুর তৈরি 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) ব্যবহার করা হয়েছে।

ক্রস প্রয়োগ

সিএ পরিসংখ্যান

ca io


23

একটি বিকল্প এসকিউএলসিএলআর সমাধান যা দ্রুত কার্যকর করে এবং কম মেমরির প্রয়োজন:

স্থাপনার স্ক্রিপ্ট

এর জন্য 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;

প্রশ্ন হিসাবে একই ক্রমে ঠিক একই ফলাফল, উত্পাদন করে।

হত্যা পরিকল্পনা:

এসকিউএলসিএলআর টিভিএফ বাস্তবায়ন পরিকল্পনা

এসকিউএলসিএলআর সোর্স ক্যোয়ারী এক্সিকিউশন প্ল্যান

এক্সপ্লোরার কর্মক্ষমতা পরিসংখ্যান পরিকল্পনা করুন

প্রোফাইলার যৌক্তিক পাঠ: 481

এই প্রয়োগের মূল সুবিধাটি হ'ল প্রসঙ্গ সংযোগটি ব্যবহার করার চেয়ে এটি দ্রুত এবং এটি কম স্মৃতি ব্যবহার করে। এটি যে কোনও সময়ে কেবল দুটি জিনিস স্মৃতিতে রাখে:

  1. যে কোনও সদৃশ সারি (একই পণ্য এবং লেনদেনের তারিখ)। এটি প্রয়োজনীয় কারণ কোনও পণ্য বা তারিখ পরিবর্তিত হওয়া অবধি চূড়ান্ত চলমান যোগফলটি কী হবে তা আমরা জানি না। নমুনা ডেটাতে, পণ্য এবং তারিখের একটি সমন্বয় রয়েছে যার মধ্যে 64 টি সারি রয়েছে।
  2. বর্তমান পণ্যটির জন্য কেবল 45 দিনের ব্যয় এবং লেনদেনের তারিখের স্লাইডিং। 45 দিনের স্লাইডিং উইন্ডো ছেড়ে যাওয়া সারিগুলির জন্য সহজ চলমান যোগফলকে সামঞ্জস্য করতে এটি প্রয়োজনীয়।

এই সর্বনিম্ন ক্যাচিংয়ের এই পদ্ধতিটি ভালভাবে স্কেল করা উচিত; পুরো ইনপুট সেটটি সিএলআর মেমরিতে রাখার চেষ্টা করার চেয়ে অবশ্যই ভাল।

সোর্স কোড


17

আপনি যদি 64-বিট এন্টারপ্রাইজ, বিকাশকারী বা এসকিউএল সার্ভার 2014 এর মূল্যায়ন সংস্করণে থাকেন তবে আপনি ইন-মেমরি ওলটিপি ব্যবহার করতে পারেন । সমাধানটি একটি একক স্ক্যান হবে না এবং এটি খুব সহজেই কোনও উইন্ডো ফাংশন ব্যবহার করবে তবে এটি এই প্রশ্নের কিছুটা মূল্য দিতে পারে এবং ব্যবহৃত অ্যালগরিদম সম্ভবত অন্যান্য সমাধানগুলিতে অনুপ্রেরণা হিসাবে ব্যবহৃত হতে পারে।

প্রথমে আপনাকে অ্যাডভেঞ্চার ওয়ার্কস ডাটাবেসে ইন-মেমরি ওলটিপি সক্ষম করতে হবে।

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 দিনের আগে (বা আরও বেশি) ছিল ততক্ষণ অনুসন্ধান করে।

৪৫ দিন আগে যেমন চলমান চলমান মোট বিয়োগফল তা হ'ল ৪৫ দিনের সমষ্টি যা আমরা খুঁজছি is

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 সেকেন্ড সময় নেয়।

অতিরিক্ত র‌্যাম্পিং:

এই অ্যালগরিদমটি নিয়মিত টি এসকিউএল দ্বারাও ব্যবহার করা যেতে পারে। চলমান মোট গণনা ব্যবহার করে rangeনা সারি, এবং একটি টেম্প টেবিলে ফলাফলের সংরক্ষণ করি। তারপরে আপনি সেই টেবিলটি 45 দিনের আগে যেমন চলমান মোট হিসাবে স্ব-যোগ দিয়ে জিজ্ঞাসা করতে পারেন এবং রোলিংয়ের সমষ্টি গণনা করতে পারেন। যাইহোক, rangeতুলনায় বাস্তবায়নটি rowsধীরে ধীরে ধীরে ধীরে ধীরে ধীরে শৃঙ্খলা দ্বারা আদেশের সদৃশ আচরণ করা প্রয়োজন তাই আমি এই পদ্ধতির সাথে সমস্ত ভাল পারফরম্যান্স পাইনি। এটির মতো কাজটি একটি চলমান মোট অনুকরণের জন্য last_value()গণনা করা চলমান মোটের মতো অন্য উইন্ডো ফাংশনটি ব্যবহার করা হতে পারে । অন্য উপায় ব্যবহার করা হয় । দুজনেরই কিছু সমস্যা ছিল। বিভিন্ন ধরণের এড়াতে এবং এর সাথে স্পুলগুলি এড়ানোর জন্য উপযুক্ত সূচকটি সন্ধান করাrowsrangemax() over()max() over()সংস্করণ। আমি এই জিনিসগুলি অপ্টিমাইজিং ছেড়ে দিয়েছি তবে আপনি এখন পর্যন্ত আমার কাছে কোডটিতে আগ্রহী থাকলে দয়া করে আমাকে জানান let


13

ওয়েল এটি মজাদার ছিল :) আমার সমাধানটি @ জেফপ্যাটারসনের চেয়ে কিছুটা ধীরে ধীরে তবে এর অংশটি হ'ল আমি জিফের অনুমানগুলি (যেমন পণ্য / তারিখের জুড়ে প্রতি এক সারি) মুছে ফেলার জন্য মূল টেবিলে ফিরে বেড়াচ্ছি fact । আমি অনুমান দিয়েছিলাম যে এটি একটি চূড়ান্ত ক্যোয়ারির সরলিকৃত সংস্করণ এবং মূল টেবিলের বাইরে অতিরিক্ত তথ্যের প্রয়োজন হতে পারে।

দ্রষ্টব্য: আমি জেফের ক্যালেন্ডার টেবিল ধার নিয়েছি এবং বাস্তবে একটি খুব অনুরূপ সমাধান দিয়ে শেষ করেছি:

-- 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। আমি একটি সিটিইতে পণ্য, ক্যালেন্ডার এবং লেনদেনের ইতিহাস সারণীগুলির সংমিশ্রণটি করেছি। তারপরে রোলিংয়ের তথ্য উত্পন্ন করতে আমাকে আরও একটি সিটিই তৈরি করতে হয়েছিল। আমাকে এটি করতে হয়েছিল কারণ আমি যদি এটির মূল টেবিলটিতে সরাসরি যোগদান করি তবে আমি সারি নির্মূল পেয়েছি যা আমার ফলাফলগুলি ছুঁড়ে ফেলেছে। এর পরে এটি আমার দ্বিতীয় সিটিইতে মূল টেবিলে ফিরে আসার একটি সহজ বিষয় ছিল। আমি সিটিইগুলিতে তৈরি ফাঁকা সারিগুলি TBEথেকে মুক্তি পেতে কলামটি (মুছে ফেলার জন্য) যুক্ত করেছি। এছাড়াও আমি আমার ক্যালেন্ডার সারণির সীমানা তৈরি করতে প্রাথমিক সিটিইতে একটি ব্যবহার করেছি।CROSS APPLY

আমি তখন প্রস্তাবিত সূচকটি যুক্ত করেছি:

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

এবং চূড়ান্ত কার্যকরকরণ পরিকল্পনা পেয়েছে:

এখানে চিত্র বর্ণনা লিখুন এখানে চিত্র বর্ণনা লিখুন এখানে চিত্র বর্ণনা লিখুন

সম্পাদনা: শেষ পর্যন্ত আমি ক্যালেন্ডার টেবিলে একটি সূচক যুক্ত করেছি যা যুক্তিসঙ্গত মার্জিন দ্বারা কর্মক্ষমতা বাড়িয়ে তোলে।

CREATE INDEX ix_calendar ON calendar(d)

2
RunningTotal.TBE IS NOT NULLশর্ত (এবং, অতএব, TBEকলাম) অপ্রয়োজনীয়। আপনি যদি এটি ফেলে দেন তবে আপনি অনর্থক সারিগুলি পাচ্ছেন না, কারণ আপনার অভ্যন্তরীণ যোগদানের শর্তে তারিখের কলামটি অন্তর্ভুক্ত রয়েছে - ফলস্বরূপ ফলাফলের সেটগুলিতে তারিখগুলি থাকতে পারে না যা উত্সটিতে মূলত ছিল না।
অ্যান্ড্রি এম

2
হাঁ। অামি সম্পূর্ণ একমত. এবং এখনও এটি প্রায় 2 সেকেন্ডের মধ্যে আমার লাভ করেছে। আমি মনে করি এটি অপ্টিমাইজারটিকে কিছু অতিরিক্ত তথ্য জানুক।
কেনেথ ফিশার

4

আমার কাছে কয়েকটি বিকল্প সমাধান রয়েছে যা সূচিপত্র বা রেফারেন্স সারণী ব্যবহার করে না। সম্ভবত এটি সেই পরিস্থিতিতে কার্যকর হতে পারে যেখানে আপনার কোনও অতিরিক্ত সারণীতে অ্যাক্সেস নেই এবং সূচি তৈরি করতে পারবেন না। TransactionDateকেবলমাত্র একক পাসের ডেটা এবং কেবল একটি একক উইন্ডো ফাংশন দিয়ে গোষ্ঠীকরণ করার সময় সঠিক ফলাফল পাওয়া সম্ভব বলে মনে হচ্ছে । যাইহোক, যখন আপনি দলবদ্ধ করতে পারবেন না তখন কেবল একটি উইন্ডো ফাংশন দিয়ে এটি করার কোনও উপায় আমি বের করতে পারিনি TransactionDate

রেফারেন্সের একটি ফ্রেম সরবরাহ করতে, আমার মেশিনে প্রশ্নের পোস্ট করা মূল সমাধানটির প্রচ্ছদ সূচক ছাড়াই 2808 এমএস এবং প্রচ্ছদ সূচক সহ 1950 এমএসের সিপিইউ সময় থাকে। আমি অ্যাডভেঞ্চার ওয়ার্কস ২০১৪ ডাটাবেস এবং এসকিউএল সার্ভার এক্সপ্রেস ২০১৪ দিয়ে পরীক্ষা করছি।

আসুন একটি সমাধান দিয়ে শুরু করা যাক আমরা কখন গ্রুপ করব TransactionDate। গত এক্স দিনগুলিতে চলমান যোগফলটি নিম্নলিখিত উপায়ে প্রকাশ করা যেতে পারে:

একটি সারির জন্য চলমান সমষ্টি = সমস্ত পূর্ববর্তী সারির চলমান যোগফল - পূর্ববর্তী সমস্ত সারিগুলির চলমান যোগফল যার জন্য তারিখটি উইন্ডোর বাইরে।

এসকিউএল এ প্রকাশ করার একটি উপায় হ'ল আপনার ডেটার দুটি অনুলিপি এবং দ্বিতীয় অনুলিপির জন্য, ব্যয়কে -1 দ্বারা গুণিত করা এবং তারিখের কলামে এক্স + 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 
╚══════╩══════╩═══════════╩════════════╝

উপরের অ্যালগরিদমটি প্রয়োগ করার জন্য নিম্নলিখিত এসকিউএল একটি উপায় way

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=SJdCsGVSl

এই সমাধানটির একটি নেতিবাচক দিকটি হ'ল নতুন TransactionDateকলাম দ্বারা অর্ডার করার সময় একটি অনিবার্য সাজানোর উপস্থিতি উপস্থিত হয় । আমি মনে করি না যে এই সাজানোর সূচকগুলি যুক্ত করে সমাধান করা যেতে পারে কারণ অর্ডার দেওয়ার আগে আমাদের দুটি কপি ডেটা একত্রিত করতে হবে। অর্ডার বাইয়ের সাথে একটি আলাদা কলামে যুক্ত করে আমি ক্যোয়ারির শেষে একটি বাছাই থেকে মুক্তি পেতে সক্ষম হয়েছি। আমি যদি আদেশ FilterFlagকরেছিলাম যে এসকিউএল সার্ভারটি এই কলামটিকে বাছাই করা থেকে অনুকূল করবে এবং একটি স্পষ্ট বাছাই করবে।

যখন ডুপ্লিকেট TransactionDateমানগুলির সাথে আমাদের ফলাফল সেট করতে হবে তখন এর সমাধানগুলি ProductIdআরও জটিল ছিল। আমি একই সাথে কলাম দ্বারা বিভাজন এবং একই কলাম দ্বারা অর্ডার করার প্রয়োজন হিসাবে সমস্যার সংক্ষিপ্ত বিবরণ করব। পল যে সিনট্যাক্সটি সরবরাহ করেছিল তা সেই সমস্যার সমাধান করে তাই অবাক হওয়ার মতো বিষয় নয় যে এসকিউএল সার্ভারে বিদ্যমান উইন্ডো ফাংশনগুলির সাথে প্রকাশ করা এতটাই কঠিন (যদি প্রকাশ করা কঠিন না হত তবে সিনট্যাক্সটি প্রসারিত করার প্রয়োজন হবে না)।

আমি যদি গ্রুপিং না করে উপরের ক্যোয়ারীটি ব্যবহার করি তবে আমি যখন একই ProductIdএবং একাধিক সারি থাকি তখন রোলিংয়ের যোগফলের জন্য আমি বিভিন্ন মান পাই TransactionDate। এটি সমাধানের একটি উপায় উপরের মত একই চলমান যোগ গণনা করা কিন্তু পার্টিশনের শেষ সারিটি পতাকাঙ্কিত করা। এটি অতিরিক্ত ধরণের ছাড়াই LEAD(ধরে ProductIDনেওয়া কখনই নুল হয় না) দিয়ে করা যায়। চূড়ান্ত চলমান যোগফলের জন্য, আমি 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=HyWxhGVBl

আমি মনে করি যে উপরের ক্যোয়ারিতে উন্নতির কোনও জায়গা আছে। পছন্দসই ফলাফল পেতে উইন্ডোজ ফাংশন ব্যবহার করার অবশ্যই অন্যান্য উপায় রয়েছে।

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.