sql >> データベース >  >> RDS >> Database

供給と需要のマッチング—ソリューション、パート3

    [ジャンプ先:元のチャレンジ|ソリューション:パート1 |パート2|パート3]

    この記事では、需要と供給を一致させるための解決策を引き続き模索します。ソリューションを送信してくれたLuca、Kamil Kosno、Daniel Brown、Brian Walker、Joe Obbish、Rainer Hoffmann、Paul White、Charlie、Ian、PeterLarssonに感謝します。

    先月、従来のアプローチと比較して修正されたインターバル交差点アプローチに基づくソリューションについて説明しました。これらのソリューションの中で最速のものは、カミル、ルカ、ダニエルのアイデアを組み合わせたものです。これは、2つのクエリをばらばらの引数可能な述語で統合しました。 400K行の入力に対してソリューションが完了するまでに1.34秒かかりました。従来のインターバル交差点アプローチに基づくソリューションが同じ入力に対して完了するのに931秒かかったことを考えると、これはそれほど粗雑ではありません。また、ジョーが古典的な間隔交差アプローチに依存するが、最大の間隔の長さに基づいて間隔をバケット化することによってマッチングロジックを最適化する優れたソリューションを思いついたことを思い出してください。同じ400K行の入力で、Joeのソリューションが完了するまでに0.9秒かかりました。このソリューションで注意が必要なのは、最大間隔の長さが長くなるとパフォーマンスが低下することです。

    今月は、Kamil / Luca / Daniel Revised Intersectionsソリューションよりも高速で、区間の長さに中立な魅力的なソリューションを検討します。この記事のソリューションは、Brian Walker、Ian、Peter Larsson、Paul White、および私によって作成されました。

    この記事のすべてのソリューションを、100K、200K、300K、および400K行で、次のインデックスを使用してオークション入力テーブルに対してテストしました。

    -- Index to support solution
     
    CREATE UNIQUE NONCLUSTERED INDEX idx_Code_ID_i_Quantity
      ON dbo.Auctions(Code, ID)
      INCLUDE(Quantity);
     
    -- Enable batch-mode Window Aggregate
     
    CREATE NONCLUSTERED COLUMNSTORE INDEX idx_cs
      ON dbo.Auctions(ID)
      WHERE ID = -1 AND ID = -2;

    ソリューションの背後にあるロジックを説明するときは、オークションテーブルに次の小さなサンプルデータのセットが入力されていると仮定します。

    ID          Code Quantity
    ----------- ---- ---------
    1           D    5.000000
    2           D    3.000000
    3           D    8.000000
    5           D    2.000000
    6           D    8.000000
    7           D    4.000000
    8           D    2.000000
    1000        S    8.000000
    2000        S    6.000000
    3000        S    2.000000
    4000        S    2.000000
    5000        S    4.000000
    6000        S    3.000000
    7000        S    2.000000

    このサンプルデータの望ましい結果は次のとおりです。

    DemandID    SupplyID    TradeQuantity
    ----------- ----------- --------------
    1           1000        5.000000
    2           1000        3.000000
    3           2000        6.000000
    3           3000        2.000000
    5           4000        2.000000
    6           5000        4.000000
    6           6000        3.000000
    6           7000        1.000000
    7           7000        1.000000

    ブライアンウォーカーのソリューション

    外部結合はSQLクエリソリューションでかなり一般的に使用されますが、これらを使用する場合は、片側結合を使用します。外部結合について教えるとき、完全な外部結合の実際の使用例の例を尋ねる質問をよく受けますが、それほど多くはありません。ブライアンのソリューションは、完全外部結合の実際の使用例の美しい例です。

    ブライアンの完全なソリューションコードは次のとおりです。

    DROP TABLE IF EXISTS #MyPairings;
     
    CREATE TABLE #MyPairings
    ( 
      DemandID       INT            NOT NULL, 
      SupplyID       INT            NOT NULL, 
      TradeQuantity  DECIMAL(19,06) NOT NULL
    );
     
    WITH D AS
    (
      SELECT A.ID,
        SUM(A.Quantity) OVER (PARTITION BY A.Code 
                              ORDER BY A.ID ROWS UNBOUNDED PRECEDING) AS Running
      FROM dbo.Auctions AS A
      WHERE A.Code = 'D'
    ),
    S AS
    (
      SELECT A.ID, 
        SUM(A.Quantity) OVER (PARTITION BY A.Code 
                              ORDER BY A.ID ROWS UNBOUNDED PRECEDING) AS Running
      FROM dbo.Auctions AS A
      WHERE A.Code = 'S'
    ),
    W AS
    (
      SELECT D.ID AS DemandID, S.ID AS SupplyID, ISNULL(D.Running, S.Running) AS Running
      FROM D
        FULL JOIN S
          ON D.Running = S.Running
    ),
    Z AS
    (
      SELECT 
        CASE 
          WHEN W.DemandID IS NOT NULL THEN W.DemandID 
          ELSE MIN(W.DemandID) OVER (ORDER BY W.Running 
                                     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
        END AS DemandID,
        CASE
          WHEN W.SupplyID IS NOT NULL THEN W.SupplyID 
          ELSE MIN(W.SupplyID) OVER (ORDER BY W.Running 
                                     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) 
        END AS SupplyID,
        W.Running
      FROM W
    )
    INSERT #MyPairings( DemandID, SupplyID, TradeQuantity )
      SELECT Z.DemandID, Z.SupplyID,
        Z.Running - ISNULL(LAG(Z.Running) OVER (ORDER BY Z.Running), 0.0) AS TradeQuantity
      FROM Z
      WHERE Z.DemandID IS NOT NULL
        AND Z.SupplyID IS NOT NULL;

    わかりやすくするために、CTEを使用した派生テーブルのブライアンの元々の使用法を改訂しました。

    CTE Dは、D.Runningと呼ばれる結果列で現在の総需要量を計算し、CTE Sは、S.Runningと呼ばれる結果列で現在の総供給量を計算します。次に、CTE Wは、D.RunningとS.Runningを照合して、DとSの間で完全外部結合を実行し、D.RunningとS.Runningの最初の非NULLをW.Runningとして返します。実行順にWからすべての行をクエリした場合に得られる結果は次のとおりです。

    DemandID    SupplyID    Running
    ----------- ----------- ----------
    1           NULL         5.000000
    2           1000         8.000000
    NULL        2000        14.000000
    3           3000        16.000000
    5           4000        18.000000
    NULL        5000        22.000000
    NULL        6000        25.000000
    6           NULL        26.000000
    NULL        7000        27.000000
    7           NULL        30.000000
    8           NULL        32.000000 

    需要と供給の現在の合計を比較する述語に基づいて完全な外部結合を使用するというアイデアは、天才のストロークです!この結果のほとんどの行(この場合は最初の9行)は、少し余分な計算が欠落している結果のペアを表しています。いずれかの種類の末尾にNULLIDがある行は、一致できないエントリを表します。この場合、最後の2行は、供給エントリと一致できない需要エントリを表します。したがって、ここに残っているのは、結果のペアのDemandID、SupplyID、TradeQuantityを計算し、一致しないエントリを除外することです。

    結果のDemandIDとSupplyIDを計算するロジックは、CTE Zで次のように実行されます(実行によるWでの順序付けを想定):

    • DemandID:DemandIDがNULLでない場合はDemandID、それ以外の場合は現在の行から始まる最小のDemandID
    • SupplyID:SupplyIDがNULLでない場合は、SupplyID、それ以外の場合は、現在の行から始まる最小のSupplyID

    Zをクエリし、実行して行を並べ替えると、次のような結果が得られます。

    DemandID    SupplyID    Running
    ----------- ----------- ----------
    1           1000         5.000000
    2           1000         8.000000
    3           2000        14.000000
    3           3000        16.000000
    5           4000        18.000000
    6           5000        22.000000
    6           6000        25.000000
    6           7000        26.000000
    7           7000        27.000000
    7           NULL        30.000000
    8           NULL        32.000000

    外部クエリは、DemandIDとSupplyIDの両方がNULLでないことを確認することにより、他の種類のエントリと一致できない1つの種類のエントリを表すZから行を除外します。結果のペアリングの取引量は、LAG関数を使用して、現在のランニング値から前のランニング値を引いたものとして計算されます。

    #MyPairingsテーブルに書き込まれる内容は次のとおりです。これは、望ましい結果です。

    DemandID    SupplyID    TradeQuantity
    ----------- ----------- --------------
    1           1000        5.000000
    2           1000        3.000000
    3           2000        6.000000
    3           3000        2.000000
    5           4000        2.000000
    6           5000        4.000000
    6           6000        3.000000
    6           7000        1.000000
    7           7000        1.000000

    このソリューションの計画を図1に示します。

    図1:ブライアンのソリューションのクエリプラン

    計画の上位2つのブランチは、それぞれがサポートインデックスからそれぞれのエントリを取得した後、バッチモードのWindowAggregate演算子を使用して需要と供給の現在の合計を計算します。シリーズの以前の記事で説明したように、インデックスにはすでにWindow Aggregate演算子が必要とするように順序付けられた行があるため、明示的なSort演算子は必要ないと思われます。ただし、SQL Serverは現在、並列バッチモードのWindow Aggregate演算子の前に、並列順序保持インデックス操作の効率的な組み合わせをサポートしていません。その結果、明示的な並列Sort演算子が各並列WindowAggregate演算子の前にあります。

    このプランでは、バッチモードのハッシュ結合を使用して完全な外部結合を処理します。このプランでは、明示的な並べ替え演算子が前に付いた2つの追加のバッチモードウィンドウ集計演算子を使用して、MINおよびLAGウィンドウ関数を計算します。

    これはかなり効率的な計画です!

    100Kから400K行の範囲の入力サイズに対して、このソリューションで得られた実行時間(秒単位)は次のとおりです。

    100K: 0.368
    200K: 0.845
    300K: 1.255
    400K: 1.315

    Itzikのソリューション

    課題の次の解決策は、それを解決するための私の試みの1つです。完全なソリューションコードは次のとおりです。

    DROP TABLE IF EXISTS #MyPairings;
     
    WITH C1 AS
    (
      SELECT *,
        SUM(Quantity)
          OVER(PARTITION BY Code 
               ORDER BY ID 
               ROWS UNBOUNDED PRECEDING) AS SumQty
      FROM dbo.Auctions
    ),
    C2 AS
    (
      SELECT *,
        SUM(Quantity * CASE Code WHEN 'D' THEN -1 WHEN 'S' THEN 1 END)
          OVER(ORDER BY SumQty, Code 
               ROWS UNBOUNDED PRECEDING) AS StockLevel,
        LAG(SumQty, 1, 0.0) OVER(ORDER BY SumQty, Code) AS PrevSumQty,
        MAX(CASE WHEN Code = 'D' THEN ID END)
          OVER(ORDER BY SumQty, Code 
               ROWS UNBOUNDED PRECEDING) AS PrevDemandID,
        MAX(CASE WHEN Code = 'S' THEN ID END)
          OVER(ORDER BY SumQty, Code 
               ROWS UNBOUNDED PRECEDING) AS PrevSupplyID,
        MIN(CASE WHEN Code = 'D' THEN ID END)
          OVER(ORDER BY SumQty, Code 
               ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS NextDemandID,
        MIN(CASE WHEN Code = 'S' THEN ID END)
          OVER(ORDER BY SumQty, Code 
               ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS NextSupplyID
      FROM C1
    ),
    C3 AS
    (
      SELECT *,
        CASE Code
          WHEN 'D' THEN ID
          WHEN 'S' THEN
            CASE WHEN StockLevel > 0 THEN NextDemandID ELSE PrevDemandID END
        END AS DemandID,
        CASE Code
          WHEN 'S' THEN ID
          WHEN 'D' THEN
            CASE WHEN StockLevel <= 0 THEN NextSupplyID ELSE PrevSupplyID END
        END AS SupplyID,
        SumQty - PrevSumQty AS TradeQuantity
      FROM C2
    )
    SELECT DemandID, SupplyID, TradeQuantity
    INTO #MyPairings
    FROM C3
    WHERE TradeQuantity > 0.0
      AND DemandID IS NOT NULL
      AND SupplyID IS NOT NULL;

    CTE C1はAuctionsテーブルにクエリを実行し、ウィンドウ関数を使用して現在の総需要と供給量を計算し、結果列SumQtyを呼び出します。

    CTE C2はC1にクエリを実行し、SumQtyとコードの順序に基づくウィンドウ関数を使用していくつかの属性を計算します。

    • StockLevel:各エントリを処理した後の現在の在庫レベル。これは、需要数量に負の符号を割り当て、供給数量に正の符号を割り当て、それらの数量を現在のエントリまで合計することによって計算されます。
    • PrevSumQty:前の行のSumQty値。
    • PrevDemandID:最後のNULL以外のデマンドID。
    • PrevSupplyID:最後のNULL以外のサプライID。
    • NextDemandID:次のNULL以外のデマンドID。
    • NextSupplyID:次のNULL以外のサプライID。

    SumQtyとCodeで並べ替えられたC2の内容は次のとおりです。

    ID    Code Quantity  SumQty     StockLevel  PrevSumQty  PrevDemandID PrevSupplyID NextDemandID NextSupplyID
    ----- ---- --------- ---------- ----------- ----------- ------------ ------------ ------------ ------------
    1     D    5.000000   5.000000  -5.000000    0.000000   1            NULL         1            1000
    2     D    3.000000   8.000000  -8.000000    5.000000   2            NULL         2            1000
    1000  S    8.000000   8.000000   0.000000    8.000000   2            1000         3            1000
    2000  S    6.000000  14.000000   6.000000    8.000000   2            2000         3            2000
    3     D    8.000000  16.000000  -2.000000   14.000000   3            2000         3            3000
    3000  S    2.000000  16.000000   0.000000   16.000000   3            3000         5            3000
    5     D    2.000000  18.000000  -2.000000   16.000000   5            3000         5            4000
    4000  S    2.000000  18.000000   0.000000   18.000000   5            4000         6            4000
    5000  S    4.000000  22.000000   4.000000   18.000000   5            5000         6            5000
    6000  S    3.000000  25.000000   7.000000   22.000000   5            6000         6            6000
    6     D    8.000000  26.000000  -1.000000   25.000000   6            6000         6            7000
    7000  S    2.000000  27.000000   1.000000   26.000000   6            7000         7            7000
    7     D    4.000000  30.000000  -3.000000   27.000000   7            7000         7            NULL
    8     D    2.000000  32.000000  -5.000000   30.000000   8            7000         8            NULL

    CTE C3はC2にクエリを実行し、結果のペアリングのDemandID、SupplyID、およびTradeQuantityを計算してから、余分な行を削除します。

    結果のC3.DemandID列は次のように計算されます:

    • 現在のエントリがデマンドエントリの場合は、DemandIDを返します。
    • 現在のエントリが供給エントリであり、現在の在庫レベルが正の場合、NextDemandIDを返します。
    • 現在のエントリが供給エントリであり、現在の在庫レベルが正でない場合は、PrevDemandIDを返します。

    結果のC3.SupplyID列は次のように計算されます:

    • 現在のエントリが供給エントリの場合は、SupplyIDを返します。
    • 現在のエントリが需要エントリであり、現在の在庫レベルが正でない場合は、NextSupplyIDを返します。
    • 現在のエントリが需要エントリであり、現在の在庫レベルが正の場合、PrevSupplyIDを返します。

    結果のTradeQuantityは、現在の行のSumQtyからPrevSumQtyを引いたものとして計算されます。

    C3の結果に関連する列の内容は次のとおりです。

    DemandID    SupplyID    TradeQuantity
    ----------- ----------- --------------
    1           1000        5.000000
    2           1000        3.000000
    2           1000        0.000000
    3           2000        6.000000
    3           3000        2.000000
    3           3000        0.000000
    5           4000        2.000000
    5           4000        0.000000
    6           5000        4.000000
    6           6000        3.000000
    6           7000        1.000000
    7           7000        1.000000
    7           NULL        3.000000
    8           NULL        2.000000

    外部クエリに残されているのは、C3から余分な行を除外することです。これには2つのケースが含まれます:

    • 両方の種類の累計が同じである場合、供給エントリの取引数量はゼロになります。順序はSumQtyとCodeに基づいているため、現在の合計が同じ場合、需要エントリが供給エントリの前に表示されることに注意してください。
    • ある種類の末尾のエントリで、他の種類のエントリと一致させることはできません。このようなエントリは、DemandIDがNULLまたはSupplyIDがNULLであるC3の行で表されます。

    このソリューションの計画を図2に示します。

    図2:Itzikのソリューションのクエリプラン

    このプランでは、入力データに1つのパスを適用し、4つのバッチモードのWindow Aggregate演算子を使用して、さまざまなウィンドウ計算を処理します。ここでは避けられないのはそのうちの2つだけですが、すべてのWindowAggregate演算子の前に明示的なSort演算子があります。他の2つは、並列バッチモードのWindow Aggregate演算子の現在の実装と関係があり、並列順序保持入力に依存することはできません。この理由によるソート演算子を確認する簡単な方法は、シリアルプランを強制し、どのソート演算子が消えるかを確認することです。このソリューションでシリアルプランを強制すると、1番目と3番目の並べ替え演算子が消えます。

    このソリューションで得た実行時間(秒単位)は次のとおりです。

    100K: 0.246
    200K: 0.427
    300K: 0.616
    400K: 0.841>

    イアンのソリューション

    イアンのソリューションは短く効率的です。完全なソリューションコードは次のとおりです。

    DROP TABLE IF EXISTS #MyPairings;
     
    WITH A AS (
      SELECT *,
        SUM(Quantity) OVER (PARTITION BY Code 
                            ORDER BY ID 
                            ROWS UNBOUNDED PRECEDING) AS CumulativeQuantity
      FROM dbo.Auctions
    ), B AS (
      SELECT CumulativeQuantity,
        CumulativeQuantity 
          - LAG(CumulativeQuantity, 1, 0) 
              OVER (ORDER BY CumulativeQuantity) AS TradeQuantity,
        MAX(CASE WHEN Code = 'D' THEN ID END) AS DemandID,
        MAX(CASE WHEN Code = 'S' THEN ID END) AS SupplyID
      FROM A
      GROUP BY CumulativeQuantity, ID/ID -- bogus grouping to improve row estimate 
                                         -- (rows count of Auctions instead of 2 rows)
    ), C AS (
      -- fill in NULLs with next supply / demand
      -- FIRST_VALUE(ID) IGNORE NULLS OVER ... 
      -- would be better, but this will work because the IDs are in CumulativeQuantity order
      SELECT
        MIN(DemandID) OVER (ORDER BY CumulativeQuantity 
                            ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS DemandID,
        MIN(SupplyID) OVER (ORDER BY CumulativeQuantity 
                            ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS SupplyID,
        TradeQuantity
      FROM B
    )
    SELECT DemandID, SupplyID, TradeQuantity
    INTO #MyPairings
    FROM C
    WHERE SupplyID IS NOT NULL  -- trim unfulfilled demands
      AND DemandID IS NOT NULL; -- trim unused supplies

    CTE Aのコードは、Auctionsテーブルにクエリを実行し、ウィンドウ関数を使用して現在の総需要と供給量を計算し、結果列にCumulativeQuantityという名前を付けます。

    CTEBのコードはCTEAにクエリを実行し、CumulativeQuantityによって行をグループ化します。このグループ化は、需要と供給の現在の合計に基づいて、ブライアンの外部結合と同様の効果を実現します。 Ianはまた、推定中のグループ化の元のカーディナリティを改善するために、ダミー式ID/IDをグループ化セットに追加しました。私のマシンでは、これにより、シリアルプランではなくパラレルプランが使用されるようになりました。

    SELECTリストでは、コードは、MAX集計とCASE式を使用して、グループ内のそれぞれのエントリタイプのIDを取得することにより、DemandIDとSupplyIDを計算します。 IDがグループに存在しない場合、結果はNULLになります。このコードは、TradeQuantityという結果列を、LAGウィンドウ関数を使用して取得した現在の累積数量から前の累積数量を引いたものとして計算します。

    Bの内容は次のとおりです。

    CumulativeQuantity  TradeQuantity  DemandId  SupplyId
    ------------------- -------------- --------- ---------
     5.000000           5.000000       1         NULL
     8.000000           3.000000       2         1000
    14.000000           6.000000       NULL      2000
    16.000000           2.000000       3         3000
    18.000000           2.000000       5         4000
    22.000000           4.000000       NULL      5000
    25.000000           3.000000       NULL      6000
    26.000000           1.000000       6         NULL
    27.000000           1.000000       NULL      7000
    30.000000           3.000000       7         NULL
    32.000000           2.000000       8         NULL
    >

    次に、CTECのコードはCTEBにクエリを実行し、現在の行とバインドされていないフォローの間のフレームROWSでMINウィンドウ関数を使用して、NULLの需要と供給IDに次の非NULLの需要と供給IDを入力します。

    Cの内容は次のとおりです:

    DemandID  SupplyID  TradeQuantity 
    --------- --------- --------------
    1         1000      5.000000      
    2         1000      3.000000      
    3         2000      6.000000      
    3         3000      2.000000      
    5         4000      2.000000      
    6         5000      4.000000      
    6         6000      3.000000      
    6         7000      1.000000      
    7         7000      1.000000      
    7         NULL      3.000000      
    8         NULL      2.000000

    Cに対する外部クエリによって処理される最後のステップは、他の種類のエントリと一致できない1つの種類のエントリを削除することです。これは、SupplyIDがNULLまたはDemandIDがNULLのいずれかの行を除外することによって行われます。

    このソリューションの計画を図3に示します。

    図3:Ianのソリューションのクエリプラン

    このプランは、入力データの1回のスキャンを実行し、3つの並列バッチモードウィンドウ集計演算子を使用してさまざまなウィンドウ関数を計算します。これらはすべて、並列ソート演算子が前に付きます。シリアルプランを強制することで確認できるため、そのうちの2つは避けられません。この計画では、ハッシュ集計演算子を使用して、CTEBでのグループ化と集計も処理します。

    このソリューションで得た実行時間(秒単位)は次のとおりです。

    100K: 0.214
    200K: 0.363
    300K: 0.546
    400K: 0.701

    PeterLarssonのソリューション

    Peter Larssonのソリューションは、驚くほど短く、甘く、非常に効率的です。 Peterの完全なソリューションコードは次のとおりです。

    DROP TABLE IF EXISTS #MyPairings;
     
    WITH cteSource(ID, Code, RunningQuantity)
    AS
    (
      SELECT ID, Code,
        SUM(Quantity) OVER (PARTITION BY Code ORDER BY ID) AS RunningQuantity
      FROM dbo.Auctions
    )
    SELECT DemandID, SupplyID, TradeQuantity
    INTO #MyPairings
    FROM (
           SELECT
             MIN(CASE WHEN Code = 'D' THEN ID ELSE 2147483648 END)
               OVER (ORDER BY RunningQuantity, Code 
                     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS DemandID,
             MIN(CASE WHEN Code = 'S' THEN ID ELSE 2147483648 END) 
               OVER (ORDER BY RunningQuantity, Code 
                     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS SupplyID,
             RunningQuantity
               - COALESCE(LAG(RunningQuantity) OVER (ORDER BY RunningQuantity, Code), 0.0)
                 AS TradeQuantity
           FROM cteSource
         ) AS d
    WHERE DemandID < 2147483648
      AND SupplyID < 2147483648
      AND TradeQuantity > 0.0;

    CTE cteSourceはAuctionsテーブルにクエリを実行し、ウィンドウ関数を使用して現在の総需要と供給量を計算し、結果列RunningQuantityを呼び出します。

    派生テーブルdを定義するコードは、cteSourceにクエリを実行し、結果のペアのDemandID、SupplyID、およびTradeQuantityを計算してから、余分な行を削除します。これらの計算で使用されるすべてのウィンドウ関数は、RunningQuantityとコードの順序に基づいています。

    結果列d.DemandIDは、現在のIDから始まる最小需要IDとして計算されます。見つからない場合は、2147483648として計算されます。

    結果列d.SupplyIDは、現在の供給IDから始まる最小供給IDとして計算され、見つからない場合は2147483648として計算されます。

    結果のTradeQuantityは、現在の行のRunningQuantity値から前の行のRunningQuantity値を引いたものとして計算されます。

    dの内容は次のとおりです:

    DemandID  SupplyID    TradeQuantity
    --------- ----------- --------------
    1         1000        5.000000
    2         1000        3.000000
    3         1000        0.000000
    3         2000        6.000000
    3         3000        2.000000
    5         3000        0.000000
    5         4000        2.000000
    6         4000        0.000000
    6         5000        4.000000
    6         6000        3.000000
    6         7000        1.000000
    7         7000        1.000000
    7         2147483648  3.000000
    8         2147483648  2.000000

    外側のクエリに残されているのは、dから余分な行を除外することです。これらは、取引数量がゼロである場合、またはある種類のエントリが他の種類のエントリ(ID 2147483648)と一致しない場合です。

    このソリューションの計画を図4に示します。

    図4:Peterのソリューションのクエリプラン

    Ianの計画と同様に、Peterの計画は、入力データの1回のスキャンに依存し、3つの並列バッチモードウィンドウ集計演算子を使用してさまざまなウィンドウ関数を計算します。すべての前に並列Sort演算子があります。シリアルプランを強制することで確認できるため、そのうちの2つは避けられません。 Peterの計画では、Ianの計画のようにグループ化された集計演算子は必要ありません。

    このような短い解決策を可能にしたPeterの重要な洞察は、いずれかの種類の関連するエントリについて、もう一方の種類の一致するIDが常に後で表示されるという認識でした(RunningQuantityとコードの順序に基づく)。ピーターの解決策を見た後、私は自分の中で物事を複雑にしすぎたように感じました!

    このソリューションで得た実行時間(秒単位)は次のとおりです。

    100K: 0.197
    200K: 0.412
    300K: 0.558
    400K: 0.696

    ポールホワイトのソリューション

    最後のソリューションはPaulWhiteによって作成されました。完全なソリューションコードは次のとおりです。

    DROP TABLE IF EXISTS #MyPairings;
     
    CREATE TABLE #MyPairings
    (
      DemandID integer NOT NULL,
      SupplyID integer NOT NULL,
      TradeQuantity decimal(19, 6) NOT NULL
    );
    GO
     
    INSERT #MyPairings 
        WITH (TABLOCK)
    (
        DemandID,
        SupplyID,
        TradeQuantity
    )
    SELECT 
        Q3.DemandID,
        Q3.SupplyID,
        Q3.TradeQuantity
    FROM 
    (
        SELECT
            Q2.DemandID,
            Q2.SupplyID,
            TradeQuantity =
                -- Interval overlap
                CASE
                    WHEN Q2.Code = 'S' THEN
                        CASE
                            WHEN Q2.CumDemand >= Q2.IntEnd THEN Q2.IntLength
                            WHEN Q2.CumDemand > Q2.IntStart THEN Q2.CumDemand - Q2.IntStart
                            ELSE 0.0
                        END
                    WHEN Q2.Code = 'D' THEN
                        CASE
                            WHEN Q2.CumSupply >= Q2.IntEnd THEN Q2.IntLength
                            WHEN Q2.CumSupply > Q2.IntStart THEN Q2.CumSupply - Q2.IntStart
                            ELSE 0.0
                        END
                END
        FROM
        (
            SELECT 
                Q1.Code, 
                Q1.IntStart, 
                Q1.IntEnd, 
                Q1.IntLength, 
                DemandID = MAX(IIF(Q1.Code = 'D', Q1.ID, 0)) OVER (
                        ORDER BY Q1.IntStart, Q1.ID 
                        ROWS UNBOUNDED PRECEDING),
                SupplyID = MAX(IIF(Q1.Code = 'S', Q1.ID, 0)) OVER (
                        ORDER BY Q1.IntStart, Q1.ID 
                        ROWS UNBOUNDED PRECEDING),
                CumSupply = SUM(IIF(Q1.Code = 'S', Q1.IntLength, 0)) OVER (
                        ORDER BY Q1.IntStart, Q1.ID 
                        ROWS UNBOUNDED PRECEDING),
                CumDemand = SUM(IIF(Q1.Code = 'D', Q1.IntLength, 0)) OVER (
                        ORDER BY Q1.IntStart, Q1.ID 
                        ROWS UNBOUNDED PRECEDING)
            FROM 
            (
                -- Demand intervals
                SELECT 
                    A.ID, 
                    A.Code, 
                    IntStart = SUM(A.Quantity) OVER (
                        ORDER BY A.ID 
                        ROWS UNBOUNDED PRECEDING) - A.Quantity,
                    IntEnd = SUM(A.Quantity) OVER (
                        ORDER BY A.ID 
                        ROWS UNBOUNDED PRECEDING),
                    IntLength = A.Quantity
                FROM dbo.Auctions AS A
                WHERE 
                    A.Code = 'D'
     
                UNION ALL 
     
                -- Supply intervals
                SELECT 
                    A.ID, 
                    A.Code, 
                    IntStart = SUM(A.Quantity) OVER (
                        ORDER BY A.ID 
                        ROWS UNBOUNDED PRECEDING) - A.Quantity,
                    IntEnd = SUM(A.Quantity) OVER (
                        ORDER BY A.ID 
                        ROWS UNBOUNDED PRECEDING),
                    IntLength = A.Quantity
                FROM dbo.Auctions AS A
                WHERE 
                    A.Code = 'S'
            ) AS Q1
        ) AS Q2
    ) AS Q3
    WHERE
        Q3.TradeQuantity > 0;

    派生テーブルQ1を定義するコードは、2つの別々のクエリを使用して、現在の合計に基づいて需要と供給の間隔を計算し、2つを統合します。コードは、間隔ごとに、開始(IntStart)、終了(IntEnd)、および長さ(IntLength)を計算します。 IntStartとIDで並べ替えられたQ1の内容は次のとおりです。

    ID    Code IntStart   IntEnd     IntLength
    ----- ---- ---------- ---------- ----------
    1     D     0.000000   5.000000  5.000000
    1000  S     0.000000   8.000000  8.000000
    2     D     5.000000   8.000000  3.000000
    3     D     8.000000  16.000000  8.000000
    2000  S     8.000000  14.000000  6.000000
    3000  S    14.000000  16.000000  2.000000
    5     D    16.000000  18.000000  2.000000
    4000  S    16.000000  18.000000  2.000000
    6     D    18.000000  26.000000  8.000000
    5000  S    18.000000  22.000000  4.000000
    6000  S    22.000000  25.000000  3.000000
    7000  S    25.000000  27.000000  2.000000
    7     D    26.000000  30.000000  4.000000
    8     D    30.000000  32.000000  2.000000

    派生テーブルQ2を定義するコードはQ1を照会し、DemandID、SupplyID、CumSupply、およびCumDemandと呼ばれる結果列を計算します。このコードで使用されるすべてのウィンドウ関数は、IntStartとIDの順序、およびフレームROWS UNBOUNDED PRECEDING(現在までのすべての行)に基づいています。

    DemandIDは、現在の行までの最大デマンドIDであり、存在しない場合は0です。

    SupplyIDは、現在の行までの最大供給ID、または存在しない場合は0です。

    CumSupplyは、現在の行までの累積供給量(供給間隔の長さ)です。

    CumDemandは、現在の行までの累積需要量(需要間隔の長さ)です。

    Q2の内容は次のとおりです。

    Code IntStart   IntEnd     IntLength  DemandID  SupplyID  CumSupply  CumDemand
    ---- ---------- ---------- ---------- --------- --------- ---------- ----------
    D     0.000000   5.000000  5.000000   1         0          0.000000   5.000000
    S     0.000000   8.000000  8.000000   1         1000       8.000000   5.000000
    D     5.000000   8.000000  3.000000   2         1000       8.000000   8.000000
    D     8.000000  16.000000  8.000000   3         1000       8.000000  16.000000
    S     8.000000  14.000000  6.000000   3         2000      14.000000  16.000000
    S    14.000000  16.000000  2.000000   3         3000      16.000000  16.000000
    D    16.000000  18.000000  2.000000   5         3000      16.000000  18.000000
    S    16.000000  18.000000  2.000000   5         4000      18.000000  18.000000
    D    18.000000  26.000000  8.000000   6         4000      18.000000  26.000000
    S    18.000000  22.000000  4.000000   6         5000      22.000000  26.000000
    S    22.000000  25.000000  3.000000   6         6000      25.000000  26.000000
    S    25.000000  27.000000  2.000000   6         7000      27.000000  26.000000
    D    26.000000  30.000000  4.000000   7         7000      27.000000  30.000000
    D    30.000000  32.000000  2.000000   8         7000      27.000000  32.000000

    Q2 already has the final result pairings’ correct DemandID and SupplyID values. The code defining the derived table Q3 queries Q2 and computes the result pairings’ TradeQuantity values as the overlapping segments of the demand and supply intervals. Finally, the outer query against Q3 filters only the relevant pairings where TradeQuantity is positive.

    The plan for this solution is shown in Figure 5.

    Figure 5:Query plan for Paul’s solution

    The top two branches of the plan are responsible for computing the demand and supply intervals. Both rely on Index Seek operators to get the relevant rows based on index order, and then use parallel batch-mode Window Aggregate operators, preceded by Sort operators that theoretically could have been avoided. The plan concatenates the two inputs, sorts the rows by IntStart and ID to support the subsequent remaining Window Aggregate operator. Only this Sort operator is unavoidable in this plan. The rest of the plan handles the needed scalar computations and the final filter. That’s a very efficient plan!

    Here are the run times in seconds I got for this solution:

    100K: 0.187
    200K: 0.331
    300K: 0.369
    400K: 0.425

    These numbers are pretty impressive!

    Performance Comparison

    Figure 6 has a performance comparison between all solutions covered in this article.

    Figure 6:Performance comparison

    At this point, we can add the fastest solutions I covered in previous articles. Those are Joe’s and Kamil/Luca/Daniel’s solutions. The complete comparison is shown in Figure 7.

    Figure 7:Performance comparison including earlier solutions

    These are all impressively fast solutions, with the fastest being Paul’s and Peter’s.

    結論

    When I originally introduced Peter’s puzzle, I showed a straightforward cursor-based solution that took 11.81 seconds to complete against a 400K-row input. The challenge was to come up with an efficient set-based solution. It’s so inspiring to see all the solutions people sent. I always learn so much from such puzzles both from my own attempts and by analyzing others’ solutions. It’s impressive to see several sub-second solutions, with Paul’s being less than half a second!

    It's great to have multiple efficient techniques to handle such a classic need of matching supply with demand. Well done everyone!

    [ Jump to:Original challenge | Solutions:Part 1 | Part 2 | Part 3 ]
    1. mysql全文検索の失敗

    2. SQLServerのサブクエリを使用してクエリを更新する

    3. Oracleでバージョン4(ランダム)UUIDを生成するにはどうすればよいですか?

    4. OracleSQLの連続または重複する日付範囲の開始日と終了日を1行に出力します