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

挑戦が始まりました!最速の数級数ジェネレータを作成するためのコミュニティの呼びかけ

    テーブル式に関するシリーズのパート5では、CTE、テーブル値コンストラクター、および相互結合を使用して一連の数値を生成するための次のソリューションを提供しました。

    DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
     
    WITH
      L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
      L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L5 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;

    このようなツールには、一連の日付と時刻の値の生成、サンプルデータの作成など、多くの実用的なユースケースがあります。一般的なニーズを認識して、一部のプラットフォームには、PostgreSQLのgenerate_series関数などの組み込みツールが用意されています。執筆時点では、T-SQLはそのような組み込みツールを提供していませんが、将来追加されるそのようなツールをいつでも期待して投票することができます。

    私の記事へのコメントで、Marcos Kirchnerは、テーブル値コンストラクターのカーディナリティを変えて私のソリューションをテストし、カーディナリティごとに異なる実行時間を取得したと述べました。

    私は常に、ベーステーブル値コンストラクターのカーディナリティが2のソリューションを使用していましたが、Marcosのコメントから考えさせられました。このツールは非常に便利なので、コミュニティとして力を合わせて、可能な限り最速のバージョンを作成する必要があります。さまざまなベーステーブルのカーディナリティをテストすることは、試すべき1つの側面にすぎません。他にもたくさんあるかもしれません。ソリューションで行ったパフォーマンステストを紹介します。私は主に、さまざまなテーブル値コンストラクターのカーディナリティ、シリアル処理と並列処理、および行モードとバッチモードの処理を試しました。ただし、まったく異なるソリューションが私の最高のバージョンよりもさらに高速である可能性があります。だから、挑戦はオンです!私はすべてのジェダイ、パダワン、ウィザード、そして見習いを同じように呼んでいます。あなたが思いつくことができる最高のパフォーマンスのソリューションは何ですか?これまでに投稿された最速のソリューションを打ち負かすことがあなたの中にありますか?もしそうなら、この記事へのコメントとしてあなたを共有し、他の人によって投稿された解決策を自由に改善してください。

    要件:

    • ソリューションを、パラメーター@lowASBIGINTおよび@highASBIGINTを使用してdbo.GetNumsYourNameという名前のインラインテーブル値関数(iTVF)として実装します。例として、この記事の最後に提出したものを参照してください。
    • 必要に応じて、ユーザーデータベースにサポートテーブルを作成できます。
    • 必要に応じてヒントを追加できます。
    • 前述のように、ソリューションはBIGINTタイプの区切り文字をサポートする必要がありますが、シリーズの最大カーディナリティは4,294,967,296と想定できます。
    • ソリューションのパフォーマンスを評価して他のソリューションと比較するために、1〜100,000,000の範囲でテストし、SSMSで実行後の結果を破棄します。

    私たち全員に頑張ってください!最高のコミュニティが勝ちますように。;)

    ベーステーブル値コンストラクターのさまざまなカーディナリティ

    基本CTEのカーディナリティを変化させて、2から始めて対数スケールで進め、各ステップで前のカーディナリティを2乗しました:2、4、16、256。

    さまざまな基本カーディナリティーの実験を開始する前に、基本カーディナリティーと最大範囲カーディナリティーが必要なCTEのレベル数を示す式を用意しておくと役立つ場合があります。準備段階として、基本カーディナリティーとCTEのレベル数を指定して、結果として得られる最大範囲カーディナリティーを計算する式を最初に作成する方が簡単です。 T-SQLで表現されるそのような式は次のとおりです。

    DECLARE @basecardinality AS INT = 2, @levels AS INT = 5;
     
    SELECT POWER(1.*@basecardinality, POWER(2., @levels));

    上記のサンプル入力値を使用すると、この式は4,294,967,296の最大範囲カーディナリティを生成します。

    次に、必要なCTEレベルの数を計算するための逆式には、次のように2つの対数関数をネストすることが含まれます。

    DECLARE @basecardinality AS INT = 2, @seriescardinality AS BIGINT = 4294967296;
     
    SELECT CEILING(LOG(LOG(@seriescardinality, @basecardinality), 2));

    上記のサンプル入力値を使用すると、この式は5になります。この数値は、ソリューションでL0(レ​​ベル0の場合)と名付けたテーブル値コンストラクターを持つベースCTEに追加されることに注意してください。

    どうやってこれらの公式にたどり着いたのか私に聞かないでください。私が固執している話は、ガンダルフが私の夢の中でエルフ語で私にそれらを発声したということです。

    パフォーマンステストに進みましょう。 [グリッド]の[結果]の下にある[SSMSクエリオプション]ダイアログで、実行後に結果を破棄するが有効になっていることを確認してください。次のコードを使用して、基本CTEカーディナリティが2のテストを実行します(5つの追加レベルのCTEが必要です):

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
      L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L5 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;

    この実行について、図1に示す計画を取得しました。

    図1:基本CTEカーディナリティ2の計画

    プランはシリアルであり、プラン内のすべてのオペレーターはデフォルトで行モード処理を使用します。ソリューションをiTVFにカプセル化し、広い範囲を使用する場合など、デフォルトで並列プランを取得している場合は、今のところ、MAXDOP1ヒントを使用してシリアルプランを強制します。

    CTEをアンパックした結果、Constant Scanオペレーターのインスタンスが32個発生し、それぞれが2行のテーブルを表していることを確認してください。

    この実行について、次のパフォーマンス統計を取得しました。

    CPU time = 30188 ms,  elapsed time = 32844 ms.

    次のコードを使用して、基本CTEカーディナリティが4のソリューションをテストします。これは、式ごとに4つのレベルのCTEが必要です。

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L4 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;

    この実行について、図2に示す計画を取得しました。

    図2:基本CTEカーディナリティ4の計画

    CTEをアンパックすると、16個のコンスタントスキャン演算子が生成され、それぞれが4行のテーブルを表します。

    この実行について、次のパフォーマンス統計を取得しました。

    CPU time = 23781 ms,  elapsed time = 25435 ms.

    これは、以前のソリューションに比べて22.5%の適切な改善です。

    クエリについて報告された待機統計を調べると、主要な待機タイプはSOS_SCHEDULER_YIELDです。実際、待機回数は最初のソリューションと比較して不思議なことに22.8%減少しました(待機回数は15,280対19,800)。

    次のコードを使用して、基本CTEカーディナリティが16のソリューションをテストします。これは、式ごとに3つのレベルのCTEが必要です。

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c 
              FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L3 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;

    この実行について、図3に示す計画を取得しました。

    図3:基本CTEカーディナリティ16の計画

    今回、CTEをアンパックすると、8つのコンスタントスキャン演算子が生成され、それぞれが16行のテーブルを表します。

    この実行について、次のパフォーマンス統計を取得しました。

    CPU time = 22968 ms,  elapsed time = 24409 ms.

    このソリューションは、わずか数パーセントの追加ではありますが、経過時間をさらに短縮し、最初のソリューションと比較して25.7パーセントの削減になります。この場合も、SOS_SCHEDULER_YIELD待機タイプの待機カウントは減少し続けます(12,938)。

    対数目盛で進むと、次のテストには256の基本CTEカーディナリティが含まれます。これは長くて醜いですが、試してみてください:

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c 
              FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L2 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;

    この実行について、図4に示す計画を取得しました。

    図4:基本CTEカーディナリティ256の計画

    今回、CTEをアンパックした結果、それぞれ256行のコンスタントスキャン演算子が4つだけになりました。

    この実行で次のパフォーマンス数値を取得しました:

    CPU time = 23516 ms,  elapsed time = 25529 ms.

    今回は、ベースCTEカーディナリティが16の以前のソリューションと比較して、パフォーマンスが少し低下したようです。実際、SOS_SCHEDULER_YIELD待機タイプの待機カウントは13,176に少し増加しました。ですから、私たちの黄金の数である16を見つけたようです!

    並列プランとシリアルプラン

    ヒントENABLE_PARALLEL_PLAN_PREFERENCEを使用して並列プランを強制することを試みましたが、パフォーマンスが低下しました。実際、ソリューションをiTVFとして実装する場合、デフォルトで広い範囲のマシンで並列プランを取得し、最適なパフォーマンスを得るには、MAXDOP1ヒントを使用してシリアルプランを強制する必要がありました。

    バッチ処理

    私のソリューションの計画で使用される主なリソースはCPUです。バッチ処理はCPU効率の向上に関係していることを考えると、特に多数の行を処理する場合は、このオプションを試す価値があります。バッチ処理の恩恵を受けることができるここでの主な活動は、行番号の計算です。 SQL Server2019Enterpriseエディションでソリューションをテストしました。 SQL Serverは、以前に示したすべてのソリューションに対してデフォルトで行モード処理を選択しました。どうやら、このソリューションは、行ストアでバッチモードを有効にするために必要なヒューリスティックを通過しませんでした。ここでは、SQLServerでバッチ処理を使用する方法がいくつかあります。

    オプション1は、ソリューションに列ストアインデックスを持つテーブルを含めることです。これを実現するには、列ストアインデックスを使用してダミーテーブルを作成し、NumsCTEとそのテーブルの間の最も外側のクエリにダミーの左結合を導入します。ダミーテーブルの定義は次のとおりです。

    CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);

    次に、Numsに対する外部クエリを修正して、FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1=0を使用します。基本CTEカーディナリティが16の例を次に示します。

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c 
              FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L3 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
    ORDER BY rownum;

    この実行について、図5に示す計画を取得しました。

    図5:バッチ処理を使用した計画

    行番号を計算するためのバッチモードのWindowAggregate演算子の使用を観察します。また、計画にはダミーテーブルが含まれていないことにも注意してください。オプティマイザーはそれを最適化しました。

    オプション1の利点は、すべてのSQL Serverエディションで機能し、SQLServer2016以降に関連することです。バッチモードのWindowAggregateオペレーターがSQLServer2016で導入されたためです。欠点は、ダミーテーブルを作成して含める必要があることです。ソリューションに含まれています。

    SQL Server 2019 Enterpriseエディションを使用している場合、ソリューションのバッチ処理を取得するためのオプション2は、次のように、文書化されていない自明のヒントOVERRIDE_BATCH_MODE_HEURISTICS(Dmitry Piluginの記事の詳細)を使用することです。

    DECLARE @low AS BIGINT = 1, @high AS BIGINT = 100000000;
     
    WITH
      L0 AS ( SELECT 1 AS c 
              FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                          (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
      L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
      L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
      L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
      Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                FROM L3 )
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum
    OPTION(USE HINT('OVERRIDE_BATCH_MODE_HEURISTICS'));

    オプション2の利点は、ダミーテーブルを作成してソリューションに含める必要がないことです。欠点は、Enterpriseエディションを使用する必要があり、行ストアでバッチモードが導入されたSQL Server 2019を少なくとも使用する必要があることです。このソリューションでは、文書化されていないヒントを使用する必要があります。これらの理由から、私はオプション1を好みます。

    さまざまな基本CTEカーディナリティについて取得したパフォーマンスの数値は次のとおりです。

    Cardinality 2:   CPU time = 21594 ms,  elapsed time = 22743 ms (down from 32844).
    
    Cardinality 4:   CPU time = 18375 ms,  elapsed time = 19394 ms (down from 25435).
    
    Cardinality 16:  CPU time = 17640 ms,  elapsed time = 18568 ms (down from 24409).
    
    Cardinality 256: CPU time = 17109 ms,  elapsed time = 18507 ms (down from 25529).

    図6に、さまざまなソリューション間のパフォーマンスの比較を示します。

    図6:パフォーマンスの比較

    行モードの対応するものと比較して、20〜30パーセントの適切なパフォーマンスの向上を観察できます。

    不思議なことに、バッチモード処理では、基本CTEカーディナリティが256のソリューションが最適でした。ただし、基本CTEカーディナリティが16のソリューションよりもわずかに高速です。違いはごくわずかであり、コードの簡潔さの点で後者には明らかな利点があるため、16に固執します。

    そのため、私の調整作業により、行モード処理を使用した基本カーディナリティーが2の元のソリューションから43.5%の改善が得られました。

    チャレンジが始まりました!

    この課題へのコミュニティの貢献として、2つのソリューションを提出します。 SQL Server 2016以降で実行していて、ユーザーデータベースにテーブルを作成できる場合は、次のダミーテーブルを作成します。

    CREATE TABLE dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);

    そして、次のiTVF定義を使用します。

    CREATE OR ALTER FUNCTION dbo.GetNumsItzikBatch(@low AS BIGINT, @high AS BIGINT)
      RETURNS TABLE
    AS
    RETURN
      WITH
        L0 AS ( SELECT 1 AS c 
                FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                            (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
        L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
        L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
        L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
        Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                  FROM L3 )
     
      SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
      FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 = 0
      ORDER BY rownum;
    GO

    次のコードを使用してテストします(実行後に結果を破棄するチェックボックスがオンになっていることを確認してください):

    SELECT n FROM dbo.GetNumsItzikBatch(1, 100000000) OPTION(MAXDOP 1);

    このコードは私のマシンでは18秒で終了します。

    何らかの理由でバッチ処理ソリューションの要件を満たせない場合は、2番目のソリューションとして次の関数定義を送信します。

    CREATE OR ALTER FUNCTION dbo.GetNumsItzik(@low AS BIGINT, @high AS BIGINT)
      RETURNS TABLE
    AS
    RETURN
      WITH
        L0 AS ( SELECT 1 AS c 
                FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
                            (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
        L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
        L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
        L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
        Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
                  FROM L3 )
      SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
      FROM Nums
      ORDER BY rownum;
    GO

    次のコードを使用してテストします。

    SELECT n FROM dbo.GetNumsItzik(1, 100000000) OPTION(MAXDOP 1);

    このコードは私のマシンでは24秒で終了します。

    あなたの番です!


    1. Oracle SQL Developerでパッケージを作成する方法は?

    2. 列が存在しない場合は、mysqlテーブルに列を追加します

    3. SQLServerデータベースにすべてのストアドプロシージャを一覧表示する3つの方法

    4. mysql_real_escape_string()はSQLインジェクションから完全に保護しますか?