ผลรวมสะสมช่วงวันที่โดยใช้ฟังก์ชั่นหน้าต่าง


56

ฉันต้องคำนวณผลรวมสะสมในช่วงวันที่ เพื่อแสดงให้เห็นถึงการใช้ฐานข้อมูลตัวอย่าง AdventureWorksไวยากรณ์สมมุติฐานต่อไปนี้จะทำสิ่งที่ฉันต้องการ:

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 Server

ฉันรู้ว่าฉันสามารถเขียนวิธีการใช้แบบสอบถามย่อยและการรวมปกติ (ไม่ใช่หน้าต่าง):

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 สิ่งนี้มีแนวโน้มที่จะหมายถึงว่าส่วนOVERคำสั่งจะทำงานและแผนการดำเนินการจะนำเสนอ Window Spools และ Window Aggregates องค์ประกอบภาษาทั้งหมดที่ใช้ส่วนOVERคำสั่งเป็นเกมที่ยุติธรรม โซลูชัน SQLCLR เป็นที่ยอมรับหากมีการรับประกันว่าจะให้ผลลัพธ์ที่ถูกต้อง

สำหรับโซลูชัน T-SQL ยิ่ง Hashes, Sorts และ Window Spools / Aggregates น้อยลงในแผนการดำเนินการ อย่าลังเลที่จะเพิ่มดัชนี แต่ไม่อนุญาตให้ใช้โครงสร้างที่แยกต่างหาก (ดังนั้นจึงไม่มีตารางที่คำนวณล่วงหน้าซึ่งซิงค์กับทริกเกอร์ไว้) อนุญาตให้ใช้ตารางอ้างอิง (ตารางตัวเลขวันที่เป็นต้น)

ตามหลักการแล้วโซลูชั่นจะให้ผลลัพธ์ที่เหมือนกันในลำดับเดียวกันกับแบบสอบถามย่อยด้านบน แต่สิ่งใดก็ตามที่ถูกต้องก็เป็นที่ยอมรับเช่นกัน ประสิทธิภาพเป็นสิ่งที่ควรพิจารณาเสมอดังนั้นการแก้ปัญหาควรมีประสิทธิภาพอย่างน้อยที่สุดพอสมควร

ห้องสนทนาเฉพาะ:ฉันได้สร้างห้องแชทสาธารณะสำหรับการสนทนาที่เกี่ยวข้องกับคำถามนี้และคำตอบ ผู้ใช้ที่มีคะแนนชื่อเสียงอย่างน้อย 20 คะแนนสามารถเข้าร่วมได้โดยตรง กรุณา ping ฉันในความคิดเห็นด้านล่างหากคุณมีตัวแทนน้อยกว่า 20 และต้องการที่จะมีส่วนร่วม

คำตอบ:


42

คำถามที่ดีพอล! ฉันใช้วิธีที่ต่างกันสองวิธีหนึ่งใน T-SQL และอีกวิธีใน CLR

สรุปด่วน T-SQL

วิธีการ 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คำค้นหาดั้งเดิม

สรุปด่วน CLR

สรุป CLR สามารถสรุปเป็นขั้นตอนต่อไปนี้:

  • อ่านข้อมูลในหน่วยความจำเรียงตามผลิตภัณฑ์และวันที่
  • ในขณะที่ประมวลผลแต่ละรายการให้เพิ่มยอดรวมของต้นทุน เมื่อใดก็ตามที่การทำธุรกรรมเป็นผลิตภัณฑ์ที่แตกต่างจากการทำธุรกรรมก่อนหน้านี้ให้ตั้งค่าผลรวมการทำงานเป็น 0
  • รักษาตัวชี้ไปยังธุรกรรมแรกที่มีเหมือนกัน (ผลิตภัณฑ์วันที่) เป็นธุรกรรมปัจจุบัน เมื่อใดก็ตามที่มีการทำธุรกรรมครั้งสุดท้ายกับผลิตภัณฑ์ (วันที่) ให้คำนวณผลรวมสะสมสำหรับธุรกรรมนั้นและนำไปใช้กับธุรกรรมทั้งหมดด้วยผลิตภัณฑ์เดียวกัน (ผลิตภัณฑ์, วันที่)
  • ส่งคืนผลลัพธ์ทั้งหมดให้กับผู้ใช้!

การใช้SET STATISTICS IO ONวิธีการนี้จะรายงานว่าไม่มี I / O เชิงตรรกะเกิดขึ้น! ว้าวทางออกที่สมบูรณ์แบบ! (ที่จริงแล้วดูเหมือนว่าSET STATISTICS IOจะไม่รายงาน I / O ที่เกิดขึ้นภายใน CLR แต่จากรหัสมันง่ายที่จะเห็นว่าการสแกนหนึ่งตารางนั้นถูกสร้างขึ้นและดึงข้อมูลตามดัชนีที่ Paul แนะนำ

รายงานโดยSET STATISTICS TIME ONเวลา CPU 187msอยู่ในขณะนี้ ดังนั้นนี่เป็นการปรับปรุงวิธี T-SQL น่าเสียดายที่เวลาที่ผ่านไปโดยรวมของทั้งสองวิธีคล้ายกันมากประมาณครึ่งวินาทีต่อวินาที อย่างไรก็ตามวิธีการที่ใช้ CLR นั้นจะต้องส่งออกแถว 113K ไปยังคอนโซล (เทียบกับเพียง 52K สำหรับวิธี T-SQL ที่จัดกลุ่มตามผลิตภัณฑ์ / วันที่) ดังนั้นนั่นเป็นสาเหตุที่ฉันมุ่งเน้นเวลา CPU แทน

ข้อดีอีกข้อที่สำคัญของวิธีนี้คือมันให้ผลลัพธ์ที่เหมือนกันกับวิธีวนซ้ำ / ค้นหาแบบดั้งเดิมรวมถึงแถวสำหรับทุกธุรกรรมแม้ในกรณีที่สินค้าขายหลายครั้งในวันเดียวกัน (บน AdventureWorks ฉันเปรียบเทียบผลลัพธ์แบบแถวต่อแถวโดยเฉพาะและยืนยันว่าพวกเขาเชื่อมโยงกับแบบสอบถามต้นฉบับของ Paul)

ข้อเสียของวิธีนี้อย่างน้อยก็ในรูปแบบปัจจุบันคือมันอ่านข้อมูลทั้งหมดในหน่วยความจำ อย่างไรก็ตามอัลกอริทึมที่ได้รับการออกแบบเพียงต้องการกรอบหน้าต่างปัจจุบันในหน่วยความจำในเวลาใดก็ตามและสามารถอัปเดตให้ทำงานกับชุดข้อมูลที่เกินกว่าหน่วยความจำ พอลได้อธิบายประเด็นนี้ในคำตอบของเขาโดยสร้างอัลกอริธึมที่เก็บเฉพาะหน้าต่างแบบเลื่อนในหน่วยความจำ สิ่งนี้มาจากค่าใช้จ่ายในการอนุญาตการอนุญาตที่สูงขึ้นให้กับแอสเซมบลี CLR แต่จะคุ้มค่าในการปรับขนาดโซลูชันนี้เป็นชุดข้อมูลขนาดใหญ่โดยพลการ


T-SQL - สแกนหนึ่งครั้งจัดกลุ่มตามวันที่

ตั้งค่าเริ่มต้น

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; ถ้าเป็นเช่นนั้นการสร้างชุดผลิตภัณฑ์ / วันที่อย่างเต็มรูปแบบจะไม่สามารถทำได้อีกต่อไปหากไม่มีการส่งผ่านประวัติธุรกรรม
  • ฉันผลิต rowset ที่มีเพียงหนึ่งแถวต่อคู่ผลิตภัณฑ์ / วันที่ ฉันคิดว่านี่คือ "ถูกต้อง arguably" และในหลายกรณีผลลัพธ์ที่ต้องการกลับ สำหรับแต่ละผลิตภัณฑ์ / วันที่ฉันได้เพิ่มNumOrdersคอลัมน์เพื่อระบุจำนวนการขายที่เกิดขึ้น ดูภาพหน้าจอต่อไปนี้สำหรับการเปรียบเทียบผลลัพธ์ของการสืบค้นดั้งเดิมกับการสืบค้นที่เสนอในกรณีที่มีการขายผลิตภัณฑ์หลายครั้งในวันเดียวกัน (เช่น319/ 2007-09-05 00:00:00.000)

ป้อนคำอธิบายรูปภาพที่นี่


CLR - ชุดสแกนหนึ่งชุดผลลัพธ์ที่ไม่ได้จัดกลุ่มแบบสมบูรณ์

ฟังก์ชั่นร่างกายหลัก

ไม่มีตันให้ดูที่นี่; เนื้อความหลักของฟังก์ชันประกาศอินพุต (ซึ่งจะต้องตรงกับฟังก์ชัน SQL ที่สอดคล้องกัน) ตั้งค่าการเชื่อมต่อ SQL และเปิด SQLReader

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

คำเตือน

วิธี CLR ให้ความยืดหยุ่นมากขึ้นในการปรับแต่งอัลกอริทึมและอาจปรับได้โดยผู้เชี่ยวชาญใน C # อย่างไรก็ตามยังมีข้อเสียของกลยุทธ์ CLR สิ่งที่ควรทราบ:

  • วิธีการ CLR นี้เก็บสำเนาของชุดข้อมูลในหน่วยความจำ เป็นไปได้ที่จะใช้วิธีการสตรีม แต่ฉันพบปัญหาเริ่มแรกและพบว่ามีปัญหาการเชื่อมต่อที่โดดเด่นบ่นว่าการเปลี่ยนแปลงใน SQL 2008+ ทำให้ยากต่อการใช้วิธีการนี้มากขึ้น ยังคงเป็นไปได้ (ตามที่ Paul แสดง) แต่ต้องมีระดับการอนุญาตที่สูงขึ้นโดยการตั้งค่าฐานข้อมูลTRUSTWORTHYและอนุญาตให้EXTERNAL_ACCESSแอสเซมบลี CLR ดังนั้นจึงมีความยุ่งยากและความปลอดภัยที่อาจเกิดขึ้นได้ แต่ผลตอบแทนเป็นวิธีการสตรีมที่สามารถปรับขนาดให้ใหญ่กว่าชุดข้อมูลใน AdventureWorks ได้มากขึ้น
  • CLR อาจเข้าถึงได้น้อยลงสำหรับ DBA บางตัวทำให้ฟังก์ชั่นดังกล่าวเป็นกล่องดำที่ไม่โปร่งใสไม่แก้ไขง่ายไม่ปรับใช้ง่ายและอาจไม่ดีบั๊กได้ง่าย นี่เป็นข้อเสียที่ค่อนข้างใหญ่เมื่อเปรียบเทียบกับวิธี T-SQL


โบนัส: 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

สิ่งนี้จะให้แผนแบบสอบถามโดยรวมที่ค่อนข้างเรียบง่ายแม้เมื่อดูที่แบบสอบถามแบบสอบถามที่เกี่ยวข้องทั้งสองแผนพร้อมกัน:

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่

เหตุผลเล็ก ๆ น้อย ๆ ที่ฉันชอบวิธีนี้:

  • มันให้ผลการตั้งค่าเต็มรูปแบบที่ร้องขอในคำสั่งปัญหา (ตรงข้ามกับโซลูชั่น T-SQL อื่น ๆ ส่วนใหญ่ซึ่งส่งกลับผลลัพธ์รุ่นที่จัดกลุ่ม)
  • ง่ายต่อการอธิบายทำความเข้าใจและตรวจแก้จุดบกพร่อง ฉันจะไม่กลับมาอีกในปีต่อมาและสงสัยว่า heck ฉันสามารถเปลี่ยนแปลงได้เล็กน้อยโดยไม่ทำลายความถูกต้องหรือการแสดง
  • มันทำงานเกี่ยวกับ900msในชุดข้อมูลที่ให้มามากกว่า2700msของการค้นหาวนรอบเดิม
  • หากข้อมูลมีความหนาแน่นมากขึ้น (การทำธุรกรรมมากขึ้นต่อวัน) ความซับซ้อนในการคำนวณจะไม่เพิ่มขึ้นเป็นสองเท่ากับจำนวนการทำธุรกรรมในหน้าต่างบานเลื่อน (เช่นเดียวกับการสืบค้นดั้งเดิม) ฉันคิดว่านี่เป็นส่วนหนึ่งของความกังวลของ Paul เกี่ยวกับต้องการหลีกเลี่ยงการสแกนหลายครั้ง
  • มันส่งผลให้ไม่มี tempdb I / O ในการอัปเดตล่าสุดของ SQL 2012+ เนื่องจากการทำงานของtempdb สันหลังยาวเขียนใหม่
  • สำหรับชุดข้อมูลที่มีขนาดใหญ่มากมันไม่สำคัญเลยที่จะแบ่งงานออกเป็นแบตช์แยกกันสำหรับแต่ละผลิตภัณฑ์

คำเตือนที่อาจเป็นคู่:

  • ถึงแม้ว่าในทางเทคนิคแล้วมันจะทำการสแกน Production.TransactionHistory เพียงครั้งเดียว แต่ก็ไม่ใช่วิธีการ "one scan" เนื่องจากตาราง #temp ที่มีขนาดใกล้เคียงกันและจะต้องใช้ I / O ลอจิกเพิ่มเติมในตารางนั้น อย่างไรก็ตามฉันไม่เห็นสิ่งนี้แตกต่างจากตารางงานที่เรามีการควบคุมด้วยตนเองมากกว่าเนื่องจากเราได้กำหนดโครงสร้างที่แม่นยำ
  • ขึ้นอยู่กับสภาพแวดล้อมของคุณการใช้งานของ tempdb อาจถูกมองว่าเป็นบวก (เช่นอยู่ในชุดไดรฟ์ SSD ที่แยกต่างหาก) หรือลบ (เกิดพร้อมกันสูงบนเซิร์ฟเวอร์มีการช่วงชิงเนื้อหา tempdb จำนวนมาก)

25

นี่เป็นคำตอบที่ยาวดังนั้นฉันตัดสินใจที่จะเพิ่มบทสรุปที่นี่

  • ตอนแรกฉันนำเสนอวิธีแก้ปัญหาที่ให้ผลลัพธ์ที่เหมือนกันในลำดับเดียวกันกับคำถาม มันสแกนตารางหลัก 3 ครั้ง: เพื่อรับรายการที่ProductIDsมีช่วงวันที่สำหรับแต่ละผลิตภัณฑ์เพื่อสรุปค่าใช้จ่ายในแต่ละวัน (เนื่องจากมีหลายธุรกรรมที่มีวันที่เดียวกัน) เพื่อเข้าร่วมกับแถวเดิม
  • ต่อไปฉันเปรียบเทียบสองวิธีที่ทำให้งานง่ายขึ้นและหลีกเลี่ยงการสแกนครั้งสุดท้ายของตารางหลัก ผลลัพธ์ของพวกเขาคือสรุปรายวันคือถ้ามีหลายธุรกรรมในผลิตภัณฑ์มีวันที่เหมือนกันพวกเขาจะถูกรีดเป็นแถวเดียว แนวทางของฉันจากขั้นตอนก่อนหน้าสแกนตารางสองครั้ง วิธีการโดย Geoff Patterson สแกนตารางหนึ่งครั้งเนื่องจากเขาใช้ความรู้ภายนอกเกี่ยวกับช่วงวันที่และรายการผลิตภัณฑ์
  • ที่สุดท้ายที่ผมนำเสนอวิธีการแก้ปัญหาผ่านเดียวอีกครั้งว่าผลตอบแทนที่ได้สรุปทุกวัน ProductIDsแต่ก็ไม่จำเป็นต้องมีความรู้เกี่ยวกับภายนอกช่วงของวันที่หรือรายการ

ฉันจะใช้ฐานข้อมูลAdventureWorks2014และ SQL Server Express 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);

บทความ MSDN เกี่ยวกับOVERclause มีลิงค์ไปยังบล็อกโพสต์ที่ยอดเยี่ยมเกี่ยวกับฟังก์ชั่นหน้าต่างโดย Itzik Ben-Gan ในโพสต์นั้นเขาอธิบายถึงวิธีการOVERทำงานความแตกต่างระหว่างROWSและRANGEตัวเลือกและกล่าวถึงปัญหานี้อย่างมากในการคำนวณผลรวมสะสมในช่วงวันที่ เขากล่าวว่า SQL Server เวอร์ชันปัจจุบันไม่สามารถใช้งานได้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% ของค่าใช้จ่ายประมาณเป็นสุดท้ายSortORDER BY

io

ผลด้านบน - 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จะไม่มีธุรกรรมใด ๆ มี 504 แถวในมีProductโต๊ะ แต่เพียง 441 TransactionHistoryผลิตภัณฑ์ในการทำธุรกรรมได้ นอกจากนี้มันยังสร้างช่วงวันที่เดียวกันสำหรับแต่ละผลิตภัณฑ์ซึ่งเกินความจำเป็น หากTransactionHistoryมีประวัติโดยรวมที่ยาวนานขึ้นโดยแต่ละผลิตภัณฑ์มีประวัติค่อนข้างสั้นจำนวนแถวที่ไม่จำเป็นเพิ่มขึ้นจะยิ่งสูงขึ้น

ในทางตรงกันข้ามมันเป็นไปได้ที่จะเพิ่มประสิทธิภาพตัวแปรสแกนสองตัวเพิ่มขึ้นอีกเล็กน้อยโดยการสร้างดัชนีที่แคบ(ProductID, TransactionDate)กว่า ดัชนีนี้จะใช้ในการคำนวณวันที่เริ่มต้น / สิ้นสุดสำหรับแต่ละผลิตภัณฑ์ ( CTE_Products) และมันจะมีหน้าน้อยกว่าที่ครอบคลุมดัชนีและเป็นผลทำให้อ่านน้อยลง

ดังนั้นเราสามารถเลือกได้ว่าจะสแกนอย่างชัดเจนเป็นพิเศษอย่างชัดเจนหรือมี Worktable โดยนัย

BTW ReferenceOrderIDถ้ามันเป็นความตกลงที่จะมีผลให้มีการสรุปรายวันแล้วมันจะดีกว่าที่จะสร้างดัชนีที่ไม่รวม มันจะใช้หน้าน้อย = = IO น้อย

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

ทางออกเดียวที่ใช้ CROSS ใช้เท่านั้น

มันกลายเป็นคำตอบที่ยาวมาก ๆ แต่นี่เป็นอีกหนึ่งตัวแปรที่คืนค่าสรุปรายวันอีกครั้ง แต่ทำการสแกนข้อมูลเพียงครั้งเดียวและไม่ต้องการความรู้ภายนอกเกี่ยวกับช่วงวันที่หรือรายการ ProductID มันไม่ได้เรียงลำดับกลางเช่นกัน ประสิทธิภาพโดยรวมคล้ายกับรุ่นก่อนหน้า แต่ดูเหมือนว่าจะแย่ลงเล็กน้อย

แนวคิดหลักคือการใช้ตารางตัวเลขเพื่อสร้างแถวที่เติมเต็มช่องว่างในวันที่ สำหรับแต่ละวันที่มีอยู่ใช้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 ซอร์ส

วางแผนประสิทธิภาพสถิติของ Explorer

ตรรกะของ Profiler อ่าน: 481

ประโยชน์หลักของการใช้งานนี้คือเร็วกว่าการใช้การเชื่อมต่อบริบทและใช้หน่วยความจำน้อยลง มันเก็บสองสิ่งในความทรงจำได้ตลอดเวลา:

  1. แถวที่ซ้ำกันใด ๆ (ผลิตภัณฑ์เดียวกันและวันที่ทำธุรกรรม) สิ่งนี้เป็นสิ่งจำเป็นเนื่องจากจนกระทั่งผลิตภัณฑ์หรือวันที่เปลี่ยนแปลงเราไม่ทราบว่าผลรวมสะสมสุดท้ายจะเป็นอย่างไร ในข้อมูลตัวอย่างมีการรวมกันหนึ่งของผลิตภัณฑ์และวันที่ที่มี 64 แถว
  2. การเลื่อนช่วงเวลา 45 วันของค่าใช้จ่ายและวันที่ทำธุรกรรมเท่านั้นสำหรับผลิตภัณฑ์ปัจจุบัน นี่เป็นสิ่งจำเป็นในการปรับผลรวมการทำงานอย่างง่ายสำหรับแถวที่ออกจากหน้าต่างการเลื่อน 45 วัน

การแคชที่น้อยที่สุดนี้ควรตรวจสอบให้แน่ใจว่าวิธีนี้ขยายได้ดี แน่นอนดีกว่าพยายามเก็บชุดอินพุตทั้งหมดในหน่วยความจำ CLR

รหัสแหล่งที่มา


17

หากคุณใช้ SQL Server 2014 รุ่น 64 บิตองค์กรผู้พัฒนาหรือรุ่นทดลองใช้คุณสามารถใช้งานIn-Memory OLTP โซลูชันจะไม่สแกนเพียงครั้งเดียวและจะใช้ฟังก์ชันหน้าต่างใด ๆ เลย แต่อาจเพิ่มคุณค่าให้กับคำถามนี้และอัลกอริทึมที่ใช้อาจนำไปใช้เป็นแรงบันดาลใจให้กับโซลูชันอื่น ๆ

ก่อนอื่นคุณต้องเปิดใช้งาน In-Memory OLTP ในฐานข้อมูล AdventureWorks

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

ID ไม่ซ้ำกันในตารางนี้มันเป็นเรื่องที่ไม่ซ้ำกันสำหรับการรวมกันของแต่ละและProductIDTransactionDate

มีความคิดเห็นบางส่วนในโพรซีเดอร์ที่บอกคุณว่ามันทำอะไร แต่โดยรวมแล้วมันกำลังคำนวณผลรวมสะสมในลูปและสำหรับการวนซ้ำแต่ละครั้งมันจะทำการค้นหาผลรวมที่กำลังทำงานเหมือนเดิมเมื่อ 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

สนุกดี :) วิธีแก้ปัญหาของฉันช้ากว่า @ GeoffPatterson เล็กน้อย แต่เป็นส่วนหนึ่งของความจริงที่ว่าฉันคาดกลับไปที่ตารางดั้งเดิมเพื่อกำจัดสมมติฐานของ Geoff (เช่นหนึ่งแถวต่อคู่ / วันที่ผลิตภัณฑ์) . ฉันไปกับข้อสันนิษฐานว่านี่เป็นเวอร์ชันที่ง่ายของเคียวรีสุดท้ายและอาจต้องการข้อมูลเพิ่มเติมจากตารางดั้งเดิม

หมายเหตุ: ฉันยืมตารางปฏิทินของ Geoff และในความเป็นจริงแล้วมีโซลูชันที่คล้ายกันมาก:

-- 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 อื่นเพื่อสร้างข้อมูลการหมุน ฉันต้องทำเช่นนี้เพราะถ้าฉันเข้าร่วมมันกลับตารางเดิมโดยตรงฉันได้รับการกำจัดแถวที่โยนออกผลลัพธ์ของฉัน หลังจากนั้นมันเป็นเรื่องง่าย ๆ ที่จะเข้าร่วม CTE ตัวที่สองของฉันกลับไปที่ตารางดั้งเดิม ฉันเพิ่มTBEคอลัมน์ (เพื่อถูกกำจัด) เพื่อกำจัดแถวว่างที่สร้างใน CTE นอกจากนี้ฉันใช้CROSS APPLYใน CTE เริ่มต้นเพื่อสร้างขอบเขตสำหรับตารางปฏิทินของฉัน

ฉันเพิ่มดัชนีที่แนะนำแล้ว:

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คอลัมน์) ที่ไม่จำเป็น คุณจะไม่ได้รับแถวที่ซ้ำซ้อนหากคุณวางไว้เนื่องจากเงื่อนไขการรวมภายในของคุณมีคอลัมน์วันที่ดังนั้นชุดผลลัพธ์ไม่สามารถมีวันที่ที่ไม่ได้อยู่ในแหล่งที่มา
Andriy M

2
อ๋อ ฉันเห็นด้วยอย่างสมบูรณ์ และมันก็ยังทำให้ฉันได้รับประมาณ 0.2 วินาที ฉันคิดว่ามันช่วยให้เครื่องมือเพิ่มประสิทธิภาพทราบข้อมูลเพิ่มเติมบางอย่าง
Kenneth Fisher

4

ฉันมีทางเลือกอื่น ๆ ที่ไม่ใช้ดัชนีหรือตารางอ้างอิง บางทีพวกเขาอาจมีประโยชน์ในสถานการณ์ที่คุณไม่สามารถเข้าถึงตารางเพิ่มเติมใด ๆ และไม่สามารถสร้างดัชนี ดูเหมือนจะเป็นไปได้ที่จะได้รับผลลัพธ์ที่ถูกต้องเมื่อจัดกลุ่มตามTransactionDateด้วยการส่งผ่านข้อมูลเพียงครั้งเดียวและเพียงแค่ฟังก์ชั่นหน้าต่างเดียว TransactionDateแต่ผมไม่สามารถคิดหาวิธีที่จะทำมันได้ด้วยฟังก์ชั่นเพียงหน้าต่างเดียวเมื่อคุณไม่สามารถจัดกลุ่มโดย

เพื่อให้กรอบการอ้างอิงบนเครื่องของฉันโซลูชันดั้งเดิมที่โพสต์ในคำถามมีเวลา CPU ที่ 2808 ms โดยไม่มีดัชนีครอบคลุมและ 1950 ms พร้อมดัชนีครอบคลุม ฉันกำลังทดสอบกับฐานข้อมูล AdventureWorks2014 และ SQL Server Express 2014

มาเริ่มกันด้วยวิธีแก้ปัญหาเมื่อเราสามารถจัดกลุ่มตาม TransactionDateขอเริ่มต้นด้วยวิธีการแก้ปัญหาเมื่อเราสามารถจัดกลุ่มโดยผลรวมสะสมในช่วง X วันที่ผ่านมาสามารถแสดงด้วยวิธีต่อไปนี้:

ผลรวมสะสมสำหรับแถว = ผลรวมของแถวก่อนหน้าทั้งหมด - ผลรวมการเรียกใช้ของแถวก่อนหน้าทั้งหมดซึ่งวันที่อยู่นอกหน้าต่างวันที่

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

บนเครื่องของฉันใช้เวลา CPU 702 มิลลิวินาทีโดยมีดัชนีครอบคลุมและเวลา CPU 734 มิลลิวินาทีโดยไม่มีดัชนี แผนการค้นหาสามารถพบได้ที่นี่: https://www.brentozar.com/pastetheplan/?id=SJdCsGVSl

ข้อเสียอย่างหนึ่งของการแก้ปัญหานี้ก็คือดูเหมือนจะมีการเรียงลำดับที่หลีกเลี่ยงไม่ได้เมื่อสั่งซื้อโดยTransactionDateคอลัมน์ใหม่ ฉันไม่คิดว่าการเรียงลำดับนี้สามารถแก้ไขได้โดยการเพิ่มดัชนีเนื่องจากเราจำเป็นต้องรวมข้อมูลสองชุดก่อนทำการสั่งซื้อ ฉันสามารถกำจัดการเรียงลำดับในตอนท้ายของแบบสอบถามโดยการเพิ่มคอลัมน์อื่นลงใน ORDER BY ถ้าฉันสั่งโดยFilterFlagฉันพบว่า SQL Server จะปรับคอลัมน์ให้เหมาะสมจากการเรียงลำดับและจะเรียงลำดับอย่างชัดเจน

วิธีแก้เมื่อเราต้องการส่งคืนชุดผลลัพธ์ด้วยTransactionDateค่าซ้ำสำหรับชุดเดียวกันProductIdนั้นซับซ้อนกว่ามาก ฉันจะสรุปปัญหาที่เกิดขึ้นพร้อมกันโดยต้องแบ่งพาร์ติชันและเรียงตามคอลัมน์เดียวกัน ไวยากรณ์ที่ Paul ให้ไว้แก้ไขปัญหาดังกล่าวดังนั้นจึงไม่น่าแปลกใจเลยที่มันจะยากที่จะแสดงด้วยฟังก์ชั่นหน้าต่างปัจจุบันที่มีอยู่ใน SQL Server (ถ้ามันไม่ยากที่จะแสดงก็ไม่จำเป็นต้องขยายไวยากรณ์)

หากฉันใช้แบบสอบถามดังกล่าวโดยไม่มีการจัดกลุ่มแล้วฉันจะได้รับค่าที่แตกต่างกันสำหรับผลรวมกลิ้งเมื่อมีหลายแถวด้วยเหมือนกันและ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);  

บนเครื่องของฉันใช้เวลา 2464 มิลลิวินาทีของเวลา CPU โดยไม่มีดัชนีครอบคลุม เหมือนก่อนหน้านี้ดูเหมือนจะเป็นสิ่งที่หลีกเลี่ยงไม่ได้ แผนการค้นหาสามารถพบได้ที่นี่: https://www.brentozar.com/pastetheplan/?id=HyWxhGVBl

ฉันคิดว่ามีห้องพักสำหรับการปรับปรุงในแบบสอบถามด้านบน มีวิธีอื่น ๆ ในการใช้ฟังก์ชั่น windows เพื่อให้ได้ผลลัพธ์ตามที่ต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.