ช่องว่างและเกาะ: โซลูชันไคลเอ็นต์เทียบกับข้อความค้นหา T-SQL


10

โซลูชัน T-SQL สำหรับช่องว่างและหมู่เกาะสามารถทำงานได้เร็วกว่าโซลูชัน C # ที่ทำงานบนไคลเอนต์หรือไม่

หากต้องการเจาะจงให้เราให้ข้อมูลการทดสอบบางอย่าง:

CREATE TABLE dbo.Numbers
  (
    n INT NOT NULL
          PRIMARY KEY
  ) ; 
GO 

INSERT  INTO dbo.Numbers
        ( n )
VALUES  ( 1 ) ; 
GO 
DECLARE @i INT ; 
SET @i = 0 ; 
WHILE @i < 21 
  BEGIN 
    INSERT  INTO dbo.Numbers
            ( n 
            )
            SELECT  n + POWER(2, @i)
            FROM    dbo.Numbers ; 
    SET @i = @i + 1 ; 
  END ;  
GO

CREATE TABLE dbo.Tasks
  (
    StartedAt SMALLDATETIME NOT NULL ,
    FinishedAt SMALLDATETIME NOT NULL ,
    CONSTRAINT PK_Tasks PRIMARY KEY ( StartedAt, FinishedAt ) ,
    CONSTRAINT UNQ_Tasks UNIQUE ( FinishedAt, StartedAt )
  ) ;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

ข้อมูลการทดสอบชุดแรกมีช่องว่างหนึ่งช่อง:

SELECT  StartedAt ,
        FinishedAt
FROM    dbo.Tasks
WHERE   StartedAt BETWEEN DATEADD(MINUTE, 499999, '20100101')
                  AND     DATEADD(MINUTE, 500006, '20100101')

ข้อมูลทดสอบชุดที่สองมีช่องว่าง 2M -1 ช่องว่างระหว่างแต่ละช่วงเวลาที่อยู่ติดกัน:

TRUNCATE TABLE dbo.Tasks;
GO

INSERT  INTO dbo.Tasks
        ( StartedAt ,
          FinishedAt
        )
        SELECT  DATEADD(MINUTE, 3*n, '20100101') AS StartedAt ,
                DATEADD(MINUTE, 3*n + 2, '20100101') AS FinishedAt
        FROM    dbo.Numbers
        WHERE   ( n < 500000
                  OR n > 500005
                )
GO

ขณะนี้ฉันใช้ 2008 R2 แต่โซลูชั่นปี 2012 ยินดีต้อนรับอย่างมาก ฉันโพสต์คำตอบ C # ของฉันเป็นคำตอบ

คำตอบ:


4

และทางออกที่ 1 1 ...

;WITH cteSource(StartedAt, FinishedAt)
AS (
    SELECT      s.StartedAt,
            e.FinishedAt
    FROM        (
                SELECT  StartedAt,
                    ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
                FROM    dbo.Tasks
            ) AS s
    INNER JOIN  (
                SELECT  FinishedAt,
                    ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
                FROM    dbo.Tasks
            ) AS e ON e.rn = s.rn
    WHERE       s.StartedAt > e.FinishedAt

    UNION ALL

    SELECT  MIN(StartedAt),
        MAX(FinishedAt)
    FROM    dbo.Tasks
), cteGrouped(theTime, grp)
AS (
    SELECT  u.theTime,
        (ROW_NUMBER() OVER (ORDER BY u.theTime) - 1) / 2
    FROM    cteSource AS s
    UNPIVOT (
            theTime
            FOR theColumn IN (s.StartedAt, s.FinishedAt)
        ) AS u
)
SELECT      MIN(theTime),
        MAX(theTime)
FROM        cteGrouped
GROUP BY    grp
ORDER BY    grp

เร็วกว่าโซลูชันอื่นของคุณประมาณ 30% 1 ช่องว่าง: (00: 00: 12.1355011 00: 00: 11.6406581), 2M-1 ช่องว่าง (00: 00: 12.4526817 00: 00: 11.7442217) ยังคงช้ากว่าโซลูชันฝั่งไคลเอ็นต์ประมาณ 25% ในกรณีที่เลวร้ายที่สุดตามที่ Adam Machanic ทำนายไว้บนทวิตเตอร์
AK

4

รหัส C # ต่อไปนี้สามารถแก้ไขปัญหาได้:

    var connString =
        "Initial Catalog=MyDb;Data Source=MyServer;Integrated Security=SSPI;Application Name=Benchmarks;";

    var stopWatch = new Stopwatch();
    stopWatch.Start();

    using (var conn = new SqlConnection(connString))
    {
        conn.Open();
        var command = conn.CreateCommand();
        command.CommandText = "dbo.GetAllTaskEvents";
        command.CommandType = CommandType.StoredProcedure;
        var gaps = new List<string>();
        using (var dr = command.ExecuteReader())
        {
            var currentEvents = 0;
            var gapStart = new DateTime();
            var gapStarted = false;
            while (dr.Read())
            {
                var change = dr.GetInt32(1);
                if (change == -1 && currentEvents == 1)
                {
                    gapStart = dr.GetDateTime(0);
                    gapStarted = true;
                }
                else if (change == 1 && currentEvents == 0 && gapStarted)
                {
                    gaps.Add(string.Format("({0},{1})", gapStart, dr.GetDateTime(0)));
                    gapStarted = false;
                }
                currentEvents += change;
            }
        }
        File.WriteAllLines(@"C:\Temp\Gaps.txt", gaps);
    }

    stopWatch.Stop();
    System.Console.WriteLine("Elapsed: " + stopWatch.Elapsed);

รหัสนี้จะเรียกขั้นตอนที่เก็บไว้นี้:

CREATE PROCEDURE dbo.GetAllTaskEvents
AS 
  BEGIN ;
    SELECT  EventTime ,
            Change
    FROM    ( SELECT  StartedAt AS EventTime ,
                      1 AS Change
              FROM    dbo.Tasks
              UNION ALL
              SELECT  FinishedAt AS EventTime ,
                      -1 AS Change
              FROM    dbo.Tasks
            ) AS TaskEvents
    ORDER BY EventTime, Change DESC ;
  END ;
GO

มันค้นหาและพิมพ์ช่องว่างหนึ่งช่วงเวลา 2M ในเวลาต่อไปนี้แคชอุ่น:

1 gap: Elapsed: 00:00:01.4852029 00:00:01.4444307 00:00:01.4644152

พบและพิมพ์ช่องว่าง 2M-1 ในช่วงเวลา 2M ในเวลาต่อไปนี้แคชอุ่น:

2M-1 gaps Elapsed: 00:00:08.8576637 00:00:08.9123053 00:00:09.0372344 00:00:08.8545477

นี่เป็นทางออกที่ง่ายมาก - ฉันใช้เวลา 10 นาทีในการพัฒนา บัณฑิตวิทยาลัยล่าสุดสามารถเกิดขึ้นกับมัน ในด้านฐานข้อมูลแผนการดำเนินการเป็นการรวมแบบเล็กน้อยซึ่งใช้ CPU และหน่วยความจำน้อยมาก

แก้ไข:เป็นจริงฉันใช้ไคลเอนต์และเซิร์ฟเวอร์ในกล่องแยกต่างหาก


ใช่ แต่ถ้าฉันต้องการให้ resultset กลับมาเป็นชุดข้อมูลไม่ใช่ไฟล์?
Peter Larsson

แอปพลิเคชันส่วนใหญ่ต้องการใช้ IEnumerable <SomeClassOrStruct> ในกรณีนี้เราเพียงแค่ให้ผลตอบแทนแทนการเพิ่มบรรทัดลงในรายการ เพื่อให้ตัวอย่างนี้สั้นฉันได้ลบหลายสิ่งที่ไม่จำเป็นสำหรับการวัดประสิทธิภาพดิบ
AK

และนั่นเป็น CPU ฟรีหรือไม่? หรือเพิ่มเวลาในการแก้ปัญหาของคุณ?
Peter Larsson

@PeterLarsson คุณสามารถแนะนำวิธีที่ดีกว่าในการวัดมาตรฐานหรือไม่? การเขียนไปยังไฟล์เลียนแบบการใช้ข้อมูลของลูกค้าค่อนข้างช้า
AK

3

ฉันคิดว่าฉันหมดความรู้ใน SQL Server ของฉันไปแล้ว ...

สำหรับการค้นหาช่องว่างในเซิร์ฟเวอร์ SQL (สิ่งที่โค้ด C # ทำ) และคุณไม่สนใจเกี่ยวกับการเริ่มต้นหรือสิ้นสุดช่องว่าง (สิ่งเหล่านั้นก่อนที่จะเริ่มต้นครั้งแรกหรือหลังจากจบครั้งสุดท้าย) ดังนั้นแบบสอบถามต่อไปนี้ (หรือตัวแปร) เร็วที่สุดที่ฉันสามารถหา:

SELECT e.FinishedAt as GapStart, s.StartedAt as GapEnd
FROM 
(
    SELECT StartedAt, ROW_NUMBER() OVER (ORDER BY StartedAt) AS rn
    FROM dbo.Tasks
) AS s
INNER JOIN  
(
    SELECT  FinishedAt, ROW_NUMBER() OVER (ORDER BY FinishedAt) + 1 AS rn
    FROM    dbo.Tasks
) AS e ON e.rn = s.rn and s.StartedAt > e.FinishedAt

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

เช่นใช้ (S1, F1), (S2, F2), (S3, F3) และเรียงลำดับดังนี้: {S1, S2, S3, null} และ {null, F1, F2, F3} แล้วเปรียบเทียบแถว n กับแถว n ในแต่ละชุดและช่องว่างคือที่ค่าชุด F น้อยกว่าค่าชุด S ... ปัญหาที่ฉันคิดว่าในเซิร์ฟเวอร์ SQL ไม่มีทางที่จะเข้าร่วมหรือเปรียบเทียบสองชุดแยกกันตามลำดับของค่าใน set ... ดังนั้นการใช้ฟังก์ชั่น row_number เพื่อให้เราสามารถผสานโดยอิงตามหมายเลขแถว ... แต่ไม่มีวิธีใดที่จะบอกเซิร์ฟเวอร์ SQL ว่าค่าเหล่านี้มีค่าไม่ซ้ำกัน (โดยไม่ต้องแทรกลงในตาราง var ด้วยดัชนี บน - ซึ่งใช้เวลานานกว่า - ฉันลองแล้ว) ดังนั้นฉันคิดว่าการรวมการเข้าร่วมนั้นน้อยกว่าความเหมาะสมหรือไม่ (แม้ว่าจะยากที่จะพิสูจน์ว่ามันเร็วกว่าสิ่งอื่นใดที่ฉันสามารถทำได้)

ฉันสามารถรับโซลูชันโดยใช้ฟังก์ชั่น LAG / LEAD:

select * from
(
    SELECT top (100) percent StartedAt, FinishedAt, LEAD(StartedAt, 1, null) OVER (Order by FinishedAt) as NextStart
    FROM dbo.Tasks
) as x
where NextStart > FinishedAt

(ซึ่งโดยวิธีฉันไม่รับประกันผลลัพธ์ - ดูเหมือนว่าจะทำงาน แต่ฉันคิดว่าพึ่งเริ่มที่อยู่ในลำดับในตารางงาน ... และมันช้าลง)

ใช้การเปลี่ยนแปลงผลรวม:

select * from
(
    SELECT EventTime, Change, SUM(Change) OVER (ORDER BY EventTime, Change desc ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as RunTotal --, x.*
    FROM    
    ( 
        SELECT StartedAt AS EventTime, 1 AS Change
        FROM dbo.Tasks
    UNION ALL
        SELECT  FinishedAt AS EventTime, -1 AS Change
        FROM dbo.Tasks
    ) AS TaskEvents
) as x
where x.RunTotal = 0 or (x.RunTotal = 1 and x.Change = 1)
ORDER BY EventTime, Change DESC

(ไม่แปลกใจเลยช้ากว่า)

ฉันได้ลองใช้ฟังก์ชันการรวม CLR (เพื่อแทนที่ผลรวม - มันช้ากว่าผลรวมและอาศัยแถว row_number () เพื่อรักษาลำดับของข้อมูล) และ CLR เป็นฟังก์ชันที่มีค่าตาราง (เพื่อเปิดชุดผลลัพธ์สองชุดและเปรียบเทียบค่าที่อิงตามหมดจด ตามลำดับ) ... และมันก็ช้าลงเช่นกัน ฉันกระแทกหัวของฉันหลายครั้งบน SQL และข้อ จำกัด CLR ลองวิธีอื่น ๆ ...

และเพื่ออะไร

ทำงานบนเครื่องเดียวกันและแยกทั้งข้อมูล C # และ SQL กรองข้อมูลลงในไฟล์ (ตามรหัสต้นฉบับ C #) เวลาจะเหมือนกันจริง .... ประมาณ 2 วินาทีสำหรับ 1 ช่องว่างข้อมูล (C # มักจะเร็วกว่า ) 8-10 วินาทีสำหรับชุดข้อมูลแบบหลายช่องว่าง (SQL มักจะเร็วกว่า)

หมายเหตุ : อย่าใช้สภาพแวดล้อมการพัฒนาเซิร์ฟเวอร์ SQL สำหรับการเปรียบเทียบเวลาเนื่องจากมันแสดงผลกับกริดต้องใช้เวลา ทดสอบกับ SQL 2012, VS2010, .net 4.0 Client Profile

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

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

สิ่งที่ฉันรู้คือใช่เซิร์ฟเวอร์ SQL ไม่ดีในการเปรียบเทียบชุดนี้และถ้าคุณไม่เขียนแบบสอบถามที่ถูกต้องคุณจะจ่ายมันอย่างสุดซึ้ง

ง่ายกว่าหรือยากกว่าการเขียน C # หรือไม่ ฉันไม่แน่ใจทั้งหมดการเปลี่ยนแปลง +/- 1 การรันโซลูชันทั้งหมดนั้นไม่ง่ายนัก แต่ฉันก็ไม่ใช่โซลูชันแรกที่บัณฑิตทั่วไปจะมา ... เมื่อทำเสร็จแล้วก็ง่ายพอที่จะคัดลอก แต่ ต้องใช้ความเข้าใจอย่างลึกซึ้งในการเขียนตั้งแต่แรก ... สามารถพูดได้เหมือนกันสำหรับเวอร์ชัน SQL ไหนยากกว่ากัน ข้อมูลอันไหนที่มีประสิทธิภาพมากกว่าในการโกงข้อมูล สิ่งใดมีศักยภาพมากขึ้นสำหรับการดำเนินการแบบขนาน เป็นเรื่องสำคัญหรือไม่เมื่อความแตกต่างนั้นเล็กมากเมื่อเทียบกับความพยายามในการเขียนโปรแกรม?

หนึ่งบันทึกล่าสุด; มีข้อ จำกัด ของข้อมูลที่ไม่ได้ระบุไว้ - StartedAt จะต้องน้อยกว่าFinishAt มิฉะนั้นคุณจะได้รับผลลัพธ์ที่ไม่ดี


3

นี่คือโซลูชันที่ทำงานใน 4 วินาที

WITH cteRaw(ts, type, e, s)
AS (
    SELECT  StartedAt,
        1 AS type,
        NULL,
        ROW_NUMBER() OVER (ORDER BY StartedAt)
    FROM    dbo.Tasks

    UNION ALL

    SELECT  FinishedAt,
        -1 AS type, 
        ROW_NUMBER() OVER (ORDER BY FinishedAt),
        NULL
    FROM    dbo.Tasks
), cteCombined(ts, e, s, se)
AS (
    SELECT  ts,
        e,
        s,
        ROW_NUMBER() OVER (ORDER BY ts, type DESC)
    FROM    cteRaw
), cteFiltered(ts, grpnum)
AS (
    SELECT  ts, 
        (ROW_NUMBER() OVER (ORDER BY ts) - 1) / 2 AS grpnum
    FROM    cteCombined
    WHERE   COALESCE(s + s - se - 1, se - e - e) = 0
)
SELECT      MIN(ts) AS starttime,
        MAX(ts) AS endtime
FROM        cteFiltered
GROUP BY    grpnum;

Peter บนชุดข้อมูลที่มีหนึ่งช่องว่างจะช้ากว่า 10 เท่า: (00: 00: 18.1016745 - 00: 00: 17.8190959) บนข้อมูลที่มีช่องว่าง 2M-1 จะช้าลง 2 เท่า: (00:00 : 17.2409640 00: 00: 17.6068879)
AK
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.