ギャップとアイランド:クライアントソリューションと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

この最初のテストデータセットには、1つのギャップがあります。

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

テストデータの2番目のセットには、2M -1のギャップがあり、隣接する2つの間隔の間にギャップがあります。

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)。それでも、これは最悪の場合、クライアント側のソリューションよりも約25%遅く、Twitterの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つのギャップを見つけて印刷し、キャッシュをウォームします。

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 2013

ほとんどのアプリケーションはIEnumerable <SomeClassOrStruct>を使用したいと考えています。この場合、リストに行を追加するのではなく、単にreturnを生成します。この例を短くするために、私は生のパフォーマンスを測定するのに不可欠ではない多くのことを削除しました。
AK

そして、それはCPUから解放されていますか?それとも、ソリューションに時間を追加しますか?
Peter Larsson 2013

@PeterLarssonは、ベンチマークのより良い方法を提案できますか?ファイルへの書き込みは、クライアントによるデータの非常に遅い消費を模倣します。
AK

3

私はこれでSQLサーバーに関する知識の限界を使い果たしたと思います...

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

これは、わずかな手作業で機能しますが、開始と終了のセットごとに、開始と終了を別々のシーケンスとして扱い、終了を1つずらしてギャップを表示できます。

たとえば、(S1、F1)、(S2、F2)、(S3、F3)を取り、{S1、S2、S3、null}および{null、F1、F2、F3}の順序で行nと行nを比較します。各セットで、ギャップはFセット値がSセット値よりも小さい場所にあります...問題は、SQLサーバーでは2つの個別のセットを純粋に次の値の順序で結合または比較する方法がないということですセット...したがって、row_number関数を使用して、純粋に行番号に基づいてマージできるようにします...しかし、これらの値が一意であることをSQLサーバーに伝える方法はありません(インデックス付きのテーブル変数に挿入しないと)その上で-時間がかかります-私はそれを試しました)、それでマージ結合は最適ではないと思いますか?(他の何よりも速いと証明するのは難しいですが)

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

(ちなみに、私は結果を保証するものではありません-うまくいくようですが、TasksテーブルでStartedAtが順​​番に並んでいることに依存していると思います...そしてそれはより遅くなりました)

合計変更の使用:

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して(2つの結果セットを開き、純粋に基づいて値を比較しました)シーケンス上)...そしてそれも遅かった。SQLとCLRの制限に頭を何度も叩き、他の多くの方法を試しました...

そして何のために?

同じマシン上で実行し、C#データとSQLフィルター処理されたデータの両方をファイルに(元のC#コードに従って)吐き出すと、時間は実質的に同じです.... 1ギャップデータの場合は約2秒(C#は通常より高速) )、マルチギャップデータセットの場合は8〜10秒(SQLは通常高速)。

:グリッドへの表示には時間がかかるため、タイミング比較にSQL Server開発環境を使用しないでください。SQL 2012、VS2010、.net 4.0クライアントプロファイルでテスト済み

どちらのソリューションもSQLサーバーでデータの並べ替えがほぼ同じであることを指摘します。そのため、フェッチソートのサーバー負荷はどちらのソリューションを使用しても似ています。唯一の違いは、サーバーではなくクライアントでの処理です。 、およびネットワーク経由の転送。

おそらく異なるスタッフメンバーでパーティション分割するとき、またはギャップ情報を含む追加のデータが必要になるとき(私はスタッフID以外は考えられませんが)、またはもちろんそこにある遅い SQLサーバーとクライアント・マシン(またはとの間のデータ接続が遅いクライアント)が...また私は、ロック時間の比較を行っている、または競合の問題、または複数のユーザーのためのCPU /ネットワークの問題...だから私はこの場合、どちらがボトルネックになる可能性が高いかはわかりません。

私が知っていることは、はい、SQLサーバーはこの種のセット比較が得意ではありません。クエリを正しく記述しないと、コストが高くつきます。

C#バージョンを書くよりも簡単ですか、難しいですか?私は完全にはわかりません、変更+/- 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;

ピーター、1つのギャップがあるデータセットでは、これは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.