간격 및 아일랜드 : 클라이언트 솔루션 및 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 초 해결책 ...

;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). 여전히 트위터에서 Adam Machanic에 의해 예측 된 것과 같이 최악의 경우 클라이언트 측 솔루션보다 약 25 % 느립니다.
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 간격으로 2M-1 간격을 찾아 인쇄합니다.

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

이것은 매우 간단한 솔루션입니다. 개발하는데 10 분이 걸렸습니다. 최근의 대학 졸업생이 그것을 생각 해낼 수 있습니다. 데이터베이스 측면에서 실행 계획은 CPU와 메모리를 거의 사용하지 않는 간단한 병합 조인입니다.

편집 : 현실적으로, 나는 별도의 상자에서 클라이언트와 서버를 실행하고 있습니다.


예. 그러나 결과 집합을 파일이 아닌 데이터 집합으로 되돌리려면 어떻게해야합니까?
Peter Larsson

대부분의 응용 프로그램은 IEnumerable <SomeClassOrStruct>를 사용하려고합니다.이 경우에는 목록에 줄을 추가하는 대신 반환 만 발생합니다. 이 예제를 짧게 유지하기 위해 원시 성능 측정에 필수적이지 않은 많은 것을 제거했습니다.
AK

그리고 그것은 CPU가 없습니까? 아니면 솔루션에 시간이 추가됩니까?
피터 라르손

@PeterLarsson 더 나은 벤치마킹 방법을 제안 할 수 있습니까? 파일에 쓰면 클라이언트의 데이터 소비 속도가 상당히 느려집니다.
AK

3

나는 이것에 대한 SQL 서버에 대한 나의 지식의 한계를 모두 소진했다고 생각합니다 ....

SQL Server에서 차이를 발견하고 (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

약간의 손으로도 작동하지만 각 시작-마침 세트마다 시작 및 마무리를 별도의 시퀀스로 취급하고 마무리를 1만큼 오프셋하면 간격이 표시됩니다.

예를 들어 (S1, F1), (S2, F2), (S3, F3)을 취하고 {S1, S2, S3, null} 및 {null, F1, F2, F3}과 같이 주문한 다음 행 n과 행 n을 비교하십시오. 각 세트에서 간격은 F 세트 값이 S 세트 값보다 작은 곳입니다 ... 문제는 SQL Server에서 값의 순서대로 두 개의 개별 세트를 결합하거나 비교할 방법이 없다는 것입니다 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 # 코드에 따라 분리하면 시간이 거의 동일합니다. ... 1 차이 데이터의 경우 약 2 초 ), 멀티 갭 데이터 세트의 경우 8-10 초 (일반적으로 SQL이 더 빠름).

참고 : 그리드에 표시하는 데 시간이 걸리므로 타이밍 비교를 위해 SQL Server 개발 환경을 사용하지 마십시오. SQL 2012, VS2010, .net 4.0 클라이언트 프로파일로 테스트 한 결과

두 솔루션 모두 SQL 서버에서 거의 동일한 데이터 정렬을 수행하므로 페치 정렬에 대한 서버로드는 비슷합니다. 어떤 솔루션을 사용하든간에 차이점은 클라이언트가 아닌 서버에서의 처리입니다 (서버가 아닌) 네트워크를 통한 전송.

아마도 다른 직원들에 의해 파티션 할 때 또는 격차 정보가있는 추가 데이터가 필요할 때 (직원 ID 이외의 다른 많은 것을 생각할 수는 없지만), 또는 거기에있다 느린 SQL 서버 및 클라이언트 컴퓨터 (또는 사이의 데이터 연결 속도가 느린 클라이언트) ... 아니다 나는 여러 사용자 잠금 시간, 또는 경합 문제, 또는 CPU / 네트워크 문제의 비교를 만들었습니다 ... 그래서 이 경우 어떤 병목 현상이 발생할 가능성이 높은지 알 수 없습니다.

내가 아는 것은 그렇습니다 .SQL 서버는 이러한 종류의 세트 비교에 적합하지 않으며 쿼리를 올바르게 작성하지 않으면 대가를 지불합니다.

C # 버전을 작성하는 것보다 쉬울까요? 전체 솔루션을 실행하는 Change +/- 1도 완전히 직관적 인 것은 아니며, 일반 졸업생이 처음으로 오는 솔루션은 아닙니다. 처음부터 쓰려면 통찰력이 필요합니다 ... SQL 버전에서도 마찬가지입니다. 어느 것이 더 어렵습니까? 불량 데이터에 더 강력한 것은 무엇입니까? 병렬 작업의 가능성이 더 높은 것은 무엇입니까? 프로그래밍 노력과 비교할 때 차이가 너무 작은 경우에는 실제로 중요합니까?

마지막 메모; 데이터에 대한 제한되지 않은 제약 조건 있습니다 . StartedAt FinishedAt보다 작아야 합니다 . 그렇지 않으면 잘못된 결과가 나타납니다.


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.