Khoảng trống và đảo: giải pháp máy khách so với truy vấn T-SQL


10

Giải pháp T-SQL cho các khoảng trống và đảo có thể chạy nhanh hơn giải pháp C # chạy trên máy khách không?

Để cụ thể, hãy để chúng tôi cung cấp một số dữ liệu thử nghiệm:

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

Tập dữ liệu thử nghiệm đầu tiên này có chính xác một khoảng cách:

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

Tập dữ liệu thử nghiệm thứ hai có các khoảng trống 2M -1, khoảng cách giữa hai khoảng thời gian liền kề nhau:

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

Hiện tại tôi đang chạy 2008 R2, nhưng các giải pháp 2012 rất được hoan nghênh. Tôi đã đăng giải pháp C # của mình như một câu trả lời.

Câu trả lời:


4

Và giải pháp 1 giây ...

;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

Điều này nhanh hơn khoảng 30% so với giải pháp khác của bạn. 1 khoảng cách: (00: 00: 12.1355011 00: 00: 11.6406581), khoảng trống 2M-1 (00: 00: 12,4526817 00: 00: 11.7442217). Tuy nhiên, điều này chậm hơn khoảng 25% so với giải pháp phía khách hàng trong trường hợp xấu nhất của nó, chính xác như dự đoán của Adam Machanic trên twitter.
AK

4

Mã C # sau đây giải quyết vấn đề:

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

Mã này gọi thủ tục được lưu trữ này:

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

Nó tìm và in một khoảng cách trong các khoảng thời gian 2M trong các lần sau, bộ đệm ấm:

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

Nó tìm và in các khoảng trống 2M-1 trong các khoảng thời gian 2M trong các lần sau, bộ đệm ấm:

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

Đây là một giải pháp rất đơn giản - tôi mất 10 phút để phát triển. Một sinh viên tốt nghiệp đại học gần đây có thể đến với nó. Về phía cơ sở dữ liệu, kế hoạch thực hiện là một phép nối hợp nhất tầm thường sử dụng rất ít CPU và bộ nhớ.

Chỉnh sửa: để thực tế, tôi đang chạy máy khách và máy chủ trên các hộp riêng biệt.


Có, nhưng nếu tôi muốn kết quả trở lại dưới dạng tập dữ liệu, không phải là tệp thì sao?
Peter Larsson

Hầu hết các ứng dụng muốn sử dụng IEnumerable <someClassOrSturation> - trong trường hợp này, chúng tôi chỉ mang lại lợi nhuận thay vì thêm một dòng vào danh sách. Để giữ ví dụ này ngắn, tôi đã loại bỏ rất nhiều thứ không cần thiết để đo hiệu suất thô.
AK

Và đó là miễn phí của cpu? Hay nó thêm thời gian cho giải pháp của bạn?
Peter Larsson

@PeterLarsson bạn có thể đề xuất một cách tốt hơn để điểm chuẩn? Viết vào một tệp bắt chước việc khách hàng tiêu thụ dữ liệu khá chậm.
AK

3

Tôi nghĩ rằng tôi đã sử dụng hết giới hạn kiến ​​thức của mình trong máy chủ SQL trên cái này ....

Để tìm khoảng trống trong máy chủ SQL (mã C # làm gì) và bạn không quan tâm đến việc bắt đầu hoặc kết thúc các khoảng trống (những khoảng trống trước khi bắt đầu đầu tiên hoặc sau khi kết thúc cuối cùng), thì truy vấn sau (hoặc các biến thể) là nhanh nhất tôi có thể tìm thấy:

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

Điều này có hiệu quả mặc dù đối với mỗi bộ bắt đầu-kết thúc, bạn có thể coi bắt đầu và kết thúc là các chuỗi riêng biệt, bù lại kết thúc bằng một và các khoảng trống được hiển thị.

ví dụ: lấy (S1, F1), (S2, F2), (S3, F3) và sắp xếp theo thứ tự: {S1, S2, S3, null} và {null, F1, F2, F3} Sau đó so sánh hàng n với hàng n trong mỗi bộ và các khoảng trống là nơi giá trị bộ F nhỏ hơn giá trị bộ S ... vấn đề tôi nghĩ là trong máy chủ SQL không có cách nào để nối hoặc so sánh hai bộ riêng biệt hoàn toàn theo thứ tự của các giá trị trong tập hợp ... do đó việc sử dụng hàm row_number để cho phép chúng ta hợp nhất hoàn toàn dựa trên số hàng ... nhưng không có cách nào để nói với máy chủ SQL rằng các giá trị này là duy nhất (không chèn chúng vào var bảng với chỉ mục trên đó - mất nhiều thời gian hơn - tôi đã thử nó), vì vậy tôi nghĩ rằng kết hợp hợp nhất là ít hơn tối ưu? (mặc dù khó chứng minh khi nó nhanh hơn bất kỳ điều gì khác tôi có thể làm)

Tôi đã có thể nhận được các giải pháp bằng cách sử dụng các hàm 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

(bằng cách này, tôi không đảm bảo kết quả - có vẻ như nó hoạt động, nhưng tôi nghĩ dựa vào StartedAt theo thứ tự trong bảng Nhiệm vụ ... và nó chậm hơn)

Sử dụng thay đổi tổng:

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

(không có gì bất ngờ, cũng chậm hơn)

Tôi thậm chí đã thử một hàm tổng hợp CLR (để thay thế tổng - nó chậm hơn tổng và dựa vào row_number () để giữ thứ tự của dữ liệu) và CLR một hàm có giá trị bảng (để mở hai tập kết quả và so sánh hoàn toàn các giá trị theo trình tự) ... và nó cũng chậm hơn. Tôi đã đập đầu rất nhiều lần vào SQL và các hạn chế CLR, thử nhiều phương pháp khác ...

Và để làm gì?

Chạy trên cùng một máy và nhổ cả dữ liệu C # và dữ liệu đã lọc SQL vào một tệp (theo mã C # gốc), thời gian gần như giống nhau .... khoảng 2 giây cho dữ liệu 1 khoảng cách (C # thường nhanh hơn ), 8-10 giây cho tập dữ liệu nhiều khoảng cách (SQL thường nhanh hơn).

LƯU Ý : Không sử dụng Môi trường phát triển máy chủ SQL để so sánh thời gian, vì việc hiển thị trên lưới sẽ mất thời gian. Như đã thử nghiệm với SQL 2012, VS2010, .net 4.0 Cấu hình máy khách

Tôi sẽ chỉ ra rằng cả hai giải pháp thực hiện khá nhiều cách sắp xếp dữ liệu giống nhau trên máy chủ SQL để tải máy chủ cho fetch-sort sẽ tương tự nhau, cho dù bạn sử dụng giải pháp nào, sự khác biệt duy nhất là xử lý trên máy khách (chứ không phải máy chủ) và chuyển qua mạng.

Tôi không biết sự khác biệt có thể là gì khi phân vùng bởi các nhân viên khác nhau, hoặc khi bạn có thể cần thêm dữ liệu với thông tin về khoảng trống (mặc dù tôi không thể nghĩ gì khác ngoài id nhân viên), hoặc tất nhiên nếu có một kết nối dữ liệu chậm giữa máy chủ SQL và máy khách (hoặc máy khách chậm ) ... Tôi cũng chưa thực hiện so sánh các vấn đề về thời gian khóa hoặc tranh chấp hoặc các vấn đề CPU / NETWORK cho nhiều người dùng ... Vì vậy, tôi không biết cái nào có nhiều khả năng là nút cổ chai trong trường hợp này.

Những gì tôi biết, là có, máy chủ SQL không tốt trong loại so sánh được đặt này và nếu bạn không viết đúng truy vấn, bạn sẽ phải trả giá đắt cho nó.

Nó dễ hơn hay khó hơn viết phiên bản C #? Tôi không hoàn toàn chắc chắn, Thay đổi +/- 1, chạy giải pháp tổng thể cũng không hoàn toàn trực quan, và tôi nhưng đó không phải là giải pháp đầu tiên mà một sinh viên tốt nghiệp trung bình sẽ đến ... một khi thực hiện nó đủ dễ để sao chép, nhưng cần có cái nhìn sâu sắc để viết ở vị trí đầu tiên ... có thể nói tương tự cho phiên bản SQL. Cái nào khó hơn? Cái nào mạnh hơn để lừa đảo dữ liệu? Cái nào có nhiều tiềm năng cho các hoạt động song song? Có thực sự quan trọng khi sự khác biệt quá nhỏ so với nỗ lực lập trình?

Một lưu ý cuối cùng; có một ràng buộc không có căn cứ đối với dữ liệu - StartedAt phải nhỏ hơn DoneAt , nếu không bạn sẽ nhận được kết quả xấu.


3

Đây là một giải pháp chạy trong 4 giây.

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, trên một tập dữ liệu có một khoảng cách, tốc độ này chậm hơn 10 lần: (00: 00: 18.1016745 - 00: 00: 17.8190959) Trên dữ liệu có các khoảng trống 2M-1, tốc độ chậm hơn 2 lần: (00:00 : 17.2409640 00: 00: 17.6068879)
AK
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.