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

ナンバーシリーズジェネレータチャレンジソリューション–パート5

    これは、ナンバーシリーズジェネレータの課題に対するソリューションをカバーするシリーズの5番目で最後のパートです。パート1、パート2、パート3、およびパート4では、純粋なT-SQLソリューションについて説明しました。私がパズルを投稿した当初、何人かの人々は、最高のパフォーマンスのソリューションはおそらくCLRベースのソリューションであるとコメントしました。この記事では、この直感的な仮定をテストします。具体的には、KamilKosnoとAdamMachanicによって投稿されたCLRベースのソリューションについて説明します。

    Alan Burstein、Joe Obbish、Adam Machanic、Christopher Ford、Jeff Moden、Charlie、NoamGr、Kamil Kosno、Dave Mason、John Nelson#2、Ed Wagner、Michael Burbea、PaulWhiteのアイデアとコメントに感謝します。

    testdbというデータベースでテストを行います。次のコードを使用して、データベースが存在しない場合はデータベースを作成し、I/Oと時間の統計を有効にします。

    -DBおよびstatsSETNOCOUNTON; SET STATISTICS IO、TIME ON; GO IF DB_ID('testdb')IS NULL CREATE DATABASE testdb; GO USE testdb; GO 

    簡単にするために、次のコードを使用して、CLRの厳密なセキュリティを無効にし、データベースを信頼できるものにします。

    -CLRを有効にし、CLRの厳密なセキュリティを無効にして、db trustworthyEXEC sys.sp_configure'詳細設定を表示'、1; RECONFIGURE; EXEC sys.sp_configure'clr enabled'、1; EXEC sys.sp_configure'clr strict security'、0; RECONFIGURE; EXEC sys.sp_configure'詳細設定を表示'、0; RECONFIGURE; ALTER DATABASE testdb SET TRUSTWORTHY ON; GO 

    以前のソリューション

    CLRベースのソリューションについて説明する前に、最もパフォーマンスの高い2つのT-SQLソリューションのパフォーマンスを簡単に確認しましょう。

    永続化されたベーステーブル(バッチ処理を取得するためのダミーの空の列ストアテーブルを除く)を使用せず、したがってI / O操作を含まない最高のパフォーマンスのT-SQLソリューションは、関数dbo.GetNumsAlanCharlieItzikBatchに実装されたものでした。このソリューションについては、パート1で説明しました。

    関数のクエリが使用するダミーの空の列ストアテーブルを作成するコードは次のとおりです。

     DROP TABLE IF EXISTS dbo.BatchMe; GO CREATE TABLE dbo.BatchMe(col1 INT NOT NULL、INDEX idx_cs CLUSTERED COLUMNSTORE); GO 

    そして、関数の定義を含むコードは次のとおりです。

     CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT =1、@ high AS BIGINT)RETURNS TABLEASRETURN 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)rownum AS rn、@high + 1-rownum AS op、@ low-1 + rownum AS n FROM Nums LEFT OUTER JOIN dbo.BatchMe ON 1 =0 ORDER BY rownum; GO 

    最初に、MAX集計を列nに適用して、一連の1億個の数値を要求する関数をテストしましょう:

     SELECT MAX(n)AS mx FROM dbo.GetNumsAlanCharlieItzikBatch(1、100000000)OPTION(MAXDOP 1); 

    このテスト手法は、呼び出し元に1億行を送信することを回避し、変数割り当て手法を使用する場合の変数割り当てに伴う行モードの作業も回避します。

    自分のマシンでこのテストを行ったときに取得した時間統計は次のとおりです。

    CPU時間=6719ミリ秒、経過時間=6742ミリ秒

    もちろん、この関数を実行しても論理的な読み取りは行われません。

    次に、変数代入手法を使用して、順序でテストしてみましょう。

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNumsAlanCharlieItzikBatch(1、100000000)ORDER BY n OPTION(MAXDOP 1); 

    この実行について、次の時間統計を取得しました:

    CPU時間=9468ミリ秒、経過時間=9531ミリ秒

    この関数は、n順に並べられたデータを要求するときに並べ替えにはならないことを思い出してください。注文したデータをリクエストするかどうかに関係なく、基本的に同じプランが得られます。前のテストと比較して、このテストの余分な時間のほとんどは、1億行モードベースの変数割り当てに起因すると考えられます。

    永続化されたベーステーブルを使用したため、一部のI / O操作が発生した、最もパフォーマンスの高いT-SQLソリューションは、関数dbo.GetNums_SQLkiwiに実装されたPaulWhiteのソリューションでした。このソリューションについては、パート4で説明しました。

    関数が使用する列ストアテーブルと関数自体の両方を作成するPaulのコードは次のとおりです。

    -ヘルパー列ストアtableDROPTABLEIF EXISTS dbo.CS; --64K行(クロス結合の場合は4B行に十分)-列1は常にゼロ-列2は(1 ... 65536)SELECT-整数として入力NOT NULL-(すべてが64ビットに正規化されますとにかくcolumnstore/batchモード)n1 =ISNULL(CONVERT(integer、0)、0)、n2 =ISNULL(CONVERT(integer、N.rn)、0)INTO dbo.CSFROM(SELECT rn =ROW_NUMBER()OVER(ORDER BY @@ SPID)FROM master.dbo.spt_values AS SV1 CROSS JOIN master.dbo.spt_values AS SV2 ORDER BY rn ASC OFFSET 0 ROWS FETCH NEXT 65536 ROWS ONLY)AS N; --65,536行の単一の圧縮行グループCREATECLUSTEREDCOLUMNSTORE INDEX CCI ON dbo.CS WITH(MAXDOP =1); GO --functionCREATE OR ALTER FUNCTION dbo.GetNums_SQLkiwi(@low bigint =1、@ high bigint)RETURNS table ASRETURN SELECT N .rn、n =@low --1 + N.rn、op =@high + 1 --N.rn FROM(SELECT-すべてのクエリが好きな場合は、@@SPIDの代わりに@@TRANCOUNTを使用します。serialrn=ROW_NUMBER() OVER(ORDER BY @@ SPID ASC)FROM dbo.CS AS N1 JOIN dbo.CSASN2-バッチモードハッシュクロス結合-整数ではないnullデータ型回避ハッシュプローブ残差-これは常に0=0ONN2です。 n1 =N1.n1 WHERE-負の数でSQRTを回避し、単純化を有効にするようにしてください-@low> @high(リテラルを使用)の場合は単一の定数スキャンに-バッチモードでは起動フィルターなし@high> =@low -粗いフィルター:-の両側を制限しますSQRT(目標行数)へのクロス結合-IIFは、パラメーターAND N1.n2 <=CONVERT(integer、CEILING(SQRT(CONVERT(float、IIF(@high> =@low、@high) --@ low + 1、0)))))AND N2.n2 <=CONVERT(integer、CEILING(SQRT(CONVERT(float、IIF(@high> =@low、@high-@low + 1、0)) ))))AS N WHERE-正確なフィルター:-バッチモードは、必要な行の正確な数に制限されたクロス結合をフィルター処理します-オプティマイザーが行モードを導入することを回避します次の行モードでTopを計算します@low-2 + N.rn <@high; GO 

    まず、集計手法を使用して順序なしでテストし、すべてのバッチモードプランを作成します。

     SELECT MAX(n)AS mx FROM dbo.GetNums_SQLkiwi(1、100000000)OPTION(MAXDOP 1); 

    この実行の次の時間とI/O統計を取得しました:

    CPU時間=2922ミリ秒、経過時間=2943ミリ秒 。

    テーブル「CS」。スキャンカウント2、論理読み取り0、物理読み取り0、ページサーバー読み取り0、先読み読み取り0、ページサーバー先読み読み取り0、lob論理読み取り44 、lob物理読み取り0、lobページサーバー読み取り0、lob先読み読み取り0、lobページサーバー先読み読み取り0。

    テーブル「CS」。セグメントは2を読み取り、セグメントは0をスキップしました。

    変数代入手法を使用して、順序で関数をテストしてみましょう:

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNums_SQLkiwi(1、100000000)ORDER BY n OPTION(MAXDOP 1); 

    前のソリューションと同様に、このソリューションもプランでの明示的な並べ替えを回避するため、順序付けされたデータを要求するかどうかに関係なく、同じプランを取得します。ただし、このテストでも、主にここで使用されている変数割り当て手法が原因で追加のペナルティが発生し、プランの変数割り当て部分が行モードで処理されます。

    この実行で取得した時間とI/Oの統計は次のとおりです。

    CPU時間=6985ミリ秒、経過時間=7033ミリ秒

    テーブル「CS」。スキャンカウント2、論理読み取り0、物理読み取り0、ページサーバー読み取り0、先読み読み取り0、ページサーバー先読み読み取り0、lob論理読み取り44 、lob物理読み取り0、lobページサーバー読み取り0、lob先読み読み取り0、lobページサーバー先読み読み取り0。

    テーブル「CS」。セグメントは2を読み取り、セグメントは0をスキップしました。

    CLRソリューション

    KamilKosnoとAdamMachanicはどちらも、最初に単純なCLRのみのソリューションを提供し、その後、より洗練されたCLR+T-SQLコンボを考案しました。カミルのソリューションから始めて、次にアダムのソリューションについて説明します。

    KamilKosnoによるソリューション

    GetNums_KamilKosno1:

    という関数を定義するためのKamilの最初のソリューションで使用されたCLRコードは次のとおりです。
     using System; using System.Data.SqlTypes; using System.Collections;public部分クラスGetNumsKamil1{[Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_KamilKosno1_Fill"、TableDefinition ="n BIGINT")] public static IEnumerator GetNums_KamilKosno1 (SqlInt64 low、SqlInt64 high){return(low.IsNull || high.IsNull)? new GetNumsCS(0、0):new GetNumsCS(low.Value、high.Value); } public static void GetNums_KamilKosno1_Fill(Object o、out SqlInt64 n){n =(long)o; }プライベートクラスGetNumsCS:IEnumerator {public GetNumsCS(long from、long to){_lowrange =from; _current =_lowrange-1; _highrange =to; } public bool MoveNext(){_current + =1; (_current> _highrange)がfalseを返す場合;それ以外の場合はtrueを返します。 } public object Current {get {return _current; }} public void Reset(){_current =_lowrange-1; } long _lowrange; long _current; long _highrange; }} 

    この関数は、lowとhighという2つの入力を受け入れ、nというBIGINT列を持つテーブルを返します。この関数はストリーミングの種類であり、呼び出し元のクエリからの行ごとの要求のシリーズの次の番号を持つ行を返します。ご覧のとおり、Kamilは、IEnumeratorインターフェイスを実装するためのより正式な方法を選択しました。これには、MoveNext(列挙子を進めて次の行を取得する)、Current(現在の列挙子の位置に行を取得する)、およびReset(設定する)メソッドの実装が含まれます。列挙子を最初の行の前の初期位置に戻します)。

    シリーズの現在の番号を保持する変数は、_currentと呼ばれます。コンストラクターは、_currentを要求された範囲の下限から1を引いた値に設定し、Resetメソッドについても同じことが言えます。 MoveNextメソッドは_currentを1進めます。次に、_currentが要求された範囲の上限よりも大きい場合、メソッドはfalseを返します。これは、再度呼び出されないことを意味します。それ以外の場合はtrueを返し、再度呼び出されることを意味します。 Currentメソッドは自然に_currentを返します。ご覧のとおり、かなり基本的なロジックです。

    Visual StudioプロジェクトをGetNumsKamil1と呼び、パスC:\Temp\を使用しました。 testdbデータベースに関数をデプロイするために使用したコードは次のとおりです。

    存在する場合のドロップ関数dbo.GetNums_KamilKosno1; DROP ASSEMBLY IF EXISTS GetNumsKamil1; GO CREATE ASSEMBLY GetNumsKamil1 FROM'C:\ Temp \ GetNumsKamil1 \ GetNumsKamil1 \ bin \ Debug \ GetNumsKamil1.dll'; GO CREATE FUNCTION dbo.GetNums_KamilKosno1(@low AS BIGINT =1、 TABLE(n BIGINT)ORDER(n)AS EXTERNAL NAME GetNumsKamil1.GetNumsKamil1.GetNums_KamilKosno1; GO 

    CREATEFUNCTIONステートメントでのORDER句の使用に注意してください。この関数はnの順序で行を出力するため、行をnの順序でプランに取り込む必要がある場合、この句に基づいて、SQLServerはプランの並べ替えを回避できることを認識しています。

    順序付けが不要な場合は、最初に集計手法を使用して関数をテストしてみましょう。

     SELECT MAX(n)AS mx FROM dbo.GetNums_KamilKosno1(1、100000000); 

    図1に示す計画を取得しました。

    図1:dbo.GetNums_KamilKosno1関数の計画

    すべてのオペレーターが行実行モードを使用しているという事実を除けば、この計画について言うことはあまりありません。

    この実行について、次の時間統計を取得しました:

    CPU時間=37375ミリ秒、経過時間=37488ミリ秒

    そしてもちろん、論理的な読み取りは含まれていませんでした。

    変数代入手法を使用して、関数を順序でテストしてみましょう:

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNums_KamilKosno1(1、100000000)ORDER BY n; 

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

    図2:ORDERBYを使用したdbo.GetNums_KamilKosno1関数の計画

    関数はORDER(n)句を使用して作成されているため、プランには並べ替えがないことに注意してください。ただし、行が実際に関数から約束された順序で出力されるようにするための努力が必要です。これは、行番号の計算に使用されるセグメントおよびシーケンスプロジェクト演算子と、テストが失敗した場合にクエリの実行を中止するアサート演算子を使用して行われます。この作業には線形スケーリングがあります—ソートが必要だった場合に得られたであろうn log nスケーリングとは異なりますが、それでも安くはありません。このテストで次の時間統計を取得しました:

    CPU時間=51531ミリ秒、経過時間=51905ミリ秒

    一部の人、特にCLRベースのソリューションがT-SQLソリューションよりもパフォーマンスが優れていると直感的に想定した人にとっては、結果は驚くべきものになる可能性があります。ご覧のとおり、実行時間は、最高のパフォーマンスを発揮するT-SQLソリューションよりも桁違いに長くなっています。

    カミルの2番目のソリューションは、CLR-T-SQLハイブリッドです。低入力と高入力に加えて、CLR関数(GetNums_KamilKosno2)はステップ入力を追加し、互いにステップ離れた低と高の間の値を返します。 2番目のソリューションで使用されたCLRコードは次のとおりです。

     using System; using System.Data.SqlTypes; using System.Collections; public部分クラスGetNumsKamil2{[Microsoft.SqlServer.Server.SqlFunction(DataAccess =Microsoft.SqlServer.Server.DataAccessKind.None、IsDeterministic =true、IsPrecise =true、FillRowMethodName ="GetNums_Fill"、TableDefinition ="n BIGINT")] public static IEnumerator GetNums_KamilKosno2(SqlInt64 low、SqlInt64 high、SqlInt64 step){return(low.IsNull || high.IsNull)? new GetNumsCS(0、0、step.Value):new GetNumsCS(low.Value、high.Value、step.Value); } public static void GetNums_Fill(Object o、out SqlInt64 n){n =(long)o; }プライベートクラスGetNumsCS:IEnumerator {public GetNumsCS(long from、long to、long step){_lowrange =from; _step =step; _current =_lowrange --_ step; _highrange =to; } public bool MoveNext(){_current =_current + _step; (_current> _highrange)がfalseを返す場合;それ以外の場合はtrueを返します。 } public object Current {get {return _current; }} public void Reset(){_current =_lowrange --_ step; } long _lowrange; long _current; long _highrange; long _step; }} 

    VSプロジェクトにGetNumsKamil2という名前を付け、パスC:\ Temp \にも配置し、次のコードを使用してtestdbデータベースにデプロイしました。

    -アセンブリと関数を作成しますDROPFUNCTIONIF EXISTS dbo.GetNums_KamilKosno2; DROP ASSEMBLY IF EXISTS GetNumsKamil2; GO CREATE ASSEMBLY GetNumsKamil2 FROM'C:\ Temp \ GetNumsKamil2 \ GetNumsKamil2 \ bin \ Debug \ GetNumsKamil .GetNums_KamilKosno2(@low AS BIGINT =1、@ high AS BIGINT、@ step AS BIGINT)RETURNS TABLE(n BIGINT)ORDER(n)AS EXTERNAL NAME GetNumsKamil2.GetNumsKamil2.GetNums_KamilKosno2; GO 

    この関数を使用する例として、10のステップで5〜59の値を生成するリクエストを次に示します。

     SELECT n FROM dbo.GetNums_KamilKosno2(5、59、10); 

    このコードは次の出力を生成します:

     n --- 51525354555 

    T-SQLの部分については、Kamilはdbo.GetNums_Hybrid_Kamil2という関数を使用し、次のコードを使用しました。

     CREATE OR ALTER FUNCTION dbo.GetNums_Hybrid_Kamil2(@low AS BIGINT、@high AS BIGINT)RETURNS TABLEASRETURN SELECT TOP(@high-@low + 1)V.n FROM dbo.GetNums_KamilKosno2(@low、@high、10)AS GN CROSS APPLY(VALUES(0 + GN.n)、(1 + GN.n)、(2 + GN.n)、(3 + GN.n)、(4 + GN.n)、(5 + GN.n )、(6 + GN.n)、(7 + GN.n)、(8 + GN.n)、(9 + GN.n))AS V(n); GO 

    ご覧のとおり、T-SQL関数は取得したのと同じ@lowおよび@high入力でCLR関数を呼び出し、この例ではステップサイズ10を使用します。クエリはCLR関数の結果とステップの先頭に0から9の範囲の値を追加することにより、最終的な数値を生成するテーブル値コンストラクター。 TOPフィルターは、要求した数を超えないようにするために使用されます。

    重要: ここで、Kamilは、結果番号の順序に基づいて適用されるTOPフィルターについて仮定していることを強調しておきます。これは、クエリにORDER BY句がないため、実際には保証されません。 TOPをサポートするためにORDERBY句を追加するか、TOPをWHEREフィルターに置き換えて決定論的フィルターを保証すると、ソリューションのパフォーマンスプロファイルが完全に変わる可能性があります。

    とにかく、まず、集計手法を使用して、順序なしで関数をテストしましょう。

     SELECT MAX(n)AS mx FROM dbo.GetNums_Hybrid_Kamil2(1、100000000); 

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

    図3:dbo.GetNums_Hybrid_Kamil2関数の計画

    この場合も、プランのすべてのオペレーターは行実行モードを使用します。

    この実行について、次の時間統計を取得しました:

    CPU時間=13985ミリ秒、経過時間=14069ミリ秒

    そして当然、論理的な読み取りはありません。

    関数を順番にテストしてみましょう:

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNums_Hybrid_Kamil2(1、100000000)ORDER BY n; 

    図4に示す計画を取得しました。

    図4:ORDERBYを使用したdbo.GetNums_Hybrid_Kamil2関数の計画

    結果番号は、CLR関数によって返されるステップの下限の操作と、テーブル値コンストラクターに追加されたデルタの結果であるため、オプティマイザーは、結果番号が要求された順序で生成されることを信頼しません。プランに明示的な並べ替えを追加します。

    この実行について、次の時間統計を取得しました:

    CPU時間=68703ミリ秒、経過時間=84538ミリ秒 。

    したがって、注文が不要な場合、カミルの2番目の解決策は最初の解決策よりも優れているようです。しかし、注文が必要な場合は、その逆です。いずれにせよ、T-SQLソリューションの方が高速です。個人的には、最初の解決策の正しさを信頼しますが、2番目の解決策は信頼しません。

    アダム・マハニックによる解決策

    Adamの最初のソリューションは、カウンターをインクリメントし続ける基本的なCLR関数でもあります。カミルが行ったようなより複雑な形式化されたアプローチを使用する代わりに、アダムは返される必要がある行ごとにyieldコマンドを呼び出すより単純なアプローチを使用しました。

    GetNums_AdamMachanic1と呼ばれるストリーミング関数を定義する最初のソリューションのAdamのCLRコードは次のとおりです。

     using System.Data.SqlTypes; using System.Collections; public部分クラスGetNumsAdam1{[Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_AdamMachanic1_fill"、TableDefinition ="n BIGINT")] public static IEnumerable GetNums_AdamMachanic1(SqlInt64 min、SqlInt64 max){var min_int =min.Value; var max_int =max.Value; for(; min_int <=max_int; min_int ++){yield return(min_int); }} public static void GetNums_AdamMachanic1_fill(object o、out long i){i =(long)o; }}; 

    このソリューションは、そのシンプルさにおいて非常にエレガントです。ご覧のとおり、この関数は、要求された範囲の下限と上限を表すminとmaxという2つの入力を受け入れ、nというBIGINT列を持つテーブルを返します。この関数は、min_intおよびmax_intという変数を、それぞれの関数の入力パラメーター値で初期化します。次に、関数はmin_int <=max_intである限りループを実行します。これにより、各反復でmin_intの現在の値の行が生成され、min_intが1ずつ増加します。

    プロジェクトにVSでGetNumsAdam1という名前を付け、C:\ Temp \に配置し、次のコードを使用してデプロイしました:

    -アセンブリと関数を作成しますDROPFUNCTIONIF EXISTS dbo.GetNums_AdamMachanic1; DROP ASSEMBLY IF EXISTS GetNumsAdam1; GO CREATE ASSEMBLY GetNumsAdam1 FROM'C:\ Temp \ GetNumsAdam1 \ GetNumsAdam1 \ bin \ Debug \ GetNumsAdam .GetNums_AdamMachanic1(@low AS BIGINT =1、@ high AS BIGINT)RETURNS TABLE(n BIGINT)ORDER(n)AS EXTERNAL NAME GetNumsAdam1.GetNumsAdam1.GetNums_AdamMachanic1; GO 

    次のコードを使用して、順序が重要でない場合に備えて、集計手法でテストしました。

     SELECT MAX(n)AS mx FROM dbo.GetNums_AdamMachanic1(1、100000000); 

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

    図5:dbo.GetNums_AdamMachanic1関数の計画

    この計画は、カミルの最初のソリューションで以前に見た計画と非常によく似ており、同じことがそのパフォーマンスにも当てはまります。この実行について、次の時間統計を取得しました:

    CPU時間=36687ミリ秒、経過時間=36952ミリ秒

    そしてもちろん、論理的な読み取りは必要ありませんでした。

    変数代入手法を使用して、関数を順序でテストしてみましょう:

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNums_AdamMachanic1(1、100000000)ORDER BY n; 

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

    図6:ORDERBYを使用したdbo.GetNums_AdamMachanic1関数の計画

    繰り返しになりますが、計画は、カミルの最初のソリューションで以前に見たものと似ています。関数はORDER句を使用して作成されたため、明示的な並べ替えは必要ありませんでしたが、計画には、行が実際に約束どおりに順序付けられて返されることを確認するための作業が含まれています。

    この実行について、次の時間統計を取得しました:

    CPU時間=55047ミリ秒、経過時間=55498ミリ秒

    2番目のソリューションでは、AdamはCLR部分とT-SQL部分も組み合わせました。アダムがソリューションで使用したロジックについての説明は次のとおりです。

    「SQLCLRのおしゃべりの問題を回避する方法と、T-SQLでこの数値ジェネレーターの中心的な課題を回避する方法を考えようとしていました。これは、単純に行を作成することはできないという事実です。

    CLRは、2番目の部分の良い答えですが、もちろん最初の問題によって妨げられています。そこで、妥協案として、1から8192の値でハードコードされたT-SQL TVF[GetNums_AdamMachanic2_8192]を作成しました。 GetNums_AdamMachanic2_8192_base]という名前で、「max_base」と「base_add」の2つの列を出力し、次のような行を出力しました。

      max_base、base_add
      ——————
      8191、1
      8192、8192
      8192、16384

      8192、99991552
      257、99997744

    これで、単純なループになりました。 CLR出力はT-SQLTVFに送信されます。T-SQLTVFは、ハードコードされたセットの最大「max_base」行のみを返すように設定されています。そして、各行について、値に「base_add」を追加し、それによって必要な数を生成します。ここで重要なのは、論理クロス結合を1つだけ使用してN行を生成でき、CLR関数は1/8192の行を返すだけでよいため、ベースジェネレーターとして機能するのに十分な速度であるということです。」

    論理はかなり単純なようです。

    GetNums_AdamMachanic2_8192_baseというCLR関数を定義するために使用されるコードは次のとおりです。

     using System.Data.SqlTypes; using System.Collections; public部分クラスGetNumsAdam2{privatestruct row {public long max_base; public long base_add; } [Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_AdamMachanic2_8192_base_fill"、TableDefinition ="max_base int、base_add int")] public static IEnumerable GetNums_AdamMachanic2_8192_base(SqlInt64 min、SqlInt64 max){var min_ var max_int =max.Value; var min_group =min_int / 8192; var max_group =max_int / 8192; for(; min_group <=max_group; min_group ++){if(min_int> max_int)yield break; var max_base =8192-(min_int%8192); if(min_group ==max_group &&max_int <(((max_int / 8192)+ 1)* 8192)-1)max_base =max_int --min_int + 1;イールドリターン(new row(){max_base =max_base、base_add =min_int}); min_int =(min_group + 1)* 8192; }} public static void GetNums_AdamMachanic2_8192_base_fill(object o、out long max_base、out long base_add){var r =(row)o; max_base =r.max_base; base_add =r.base_add; }}; 

    VSプロジェクトにGetNumsAdam2という名前を付け、他のプロジェクトと同様にパスC:\Temp\に配置しました。 testdbデータベースに関数をデプロイするために使用したコードは次のとおりです。

    -アセンブリと関数を作成しますDROPFUNCTIONIF EXISTS dbo.GetNums_AdamMachanic2_8192_base; DROP ASSEMBLY IF EXISTS GetNumsAdam2; GO CREATE ASSEMBLY GetNumsAdam2 FROM'C:\ Temp \ GetNumsAdam2 \ GetNumsAdam2 \ bin \ Debug \ Get .GetNums_AdamMachanic2_8192_base(@max_base AS BIGINT、@ add_base AS BIGINT)RETURNS TABLE(max_base BIGINT、base_add BIGINT)ORDER(base_add)AS EXTERNAL NAME GetNumsAdam2.GetNumsAdam2.GetNums_AdamMachanic2 

    GetNums_AdamMachanic2_8192_baseを1〜100Mの範囲で使用する例を次に示します。

     SELECT * FROM dbo.GetNums_AdamMachanic2_8192_base(1、100000000); 

    このコードは、次の出力を生成します。ここでは省略形で示されています。

     max_base base_add -------------------- -------------------- 8191 18192 81928192 163848192 245768192 32768 ... 8192 999669768192 999751688192 999833608192 99991552257 99999744(12208行が影響を受ける)

    T-SQL関数GetNums_AdamMachanic2_8192(省略形)を定義したコードは次のとおりです。

     CREATE OR ALTER FUNCTION dbo.GetNums_AdamMachanic2_8192(@max_base AS BIGINT、@add_base AS BIGINT)RETURNS TABLEASRETURN SELECT TOP(@max_base)V.i + @add_base AS val FROM(VALUES(0)、(1)、(2)、 (3)、(4)、...(8187)、(8188)、(8189)、(8190)、(8191))AS V(i); GO 

    重要: また、ここで、Kamilの2番目のソリューションについて述べたのと同様に、Adamはここで、TOPフィルターがテーブル値コンストラクターの行の出現順序に基づいて最上位の行を抽出することを前提としていますが、これは実際には保証されません。 TOPをサポートするためにORDERBY句を追加するか、フィルターをWHEREフィルターに変更すると、決定論的なフィルターが得られますが、これにより、ソリューションのパフォーマンスプロファイルが完全に変わる可能性があります。

    最後に、これが最も外側のT-SQL関数dbo.GetNums_AdamMachanic2です。これは、エンドユーザーが一連の番号を取得するために呼び出します。

     CREATE OR ALTER FUNCTION dbo.GetNums_AdamMachanic2(@low AS BIGINT =1、@ high AS BIGINT)RETURNS TABLEASRETURN SELECT Y.val AS n FROM(SELECT max_base、base_add FROM dbo.GetNums_AdamMachanic2_8192_base(@low、@high))AS X CROSS APPLY dbo.GetNums_AdamMachanic2_8192(X.max_base、X.base_add)AS YGO 

    この関数は、CROSS APPLY演算子を使用して、内部CLR関数dbo.GetNums_AdamMachanic2_8192_baseによって返される行ごとに内部T-SQL関数dbo.GetNums_AdamMachanic2_8192を適用します。

    順序が重要でない場合は、最初に集計手法を使用してこのソリューションをテストしましょう。

     SELECT MAX(n)AS mx FROM dbo.GetNums_AdamMachanic2(1、100000000); 

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

    図7:dbo.GetNums_AdamMachanic2関数の計画

    このテストで次の時間統計を取得しました:

    SQLServer解析およびコンパイル時 :CPU時間=313ミリ秒、経過時間=339ミリ秒
    SQLServer実行時間 :CPU時間=8859ミリ秒、経過時間=8849ミリ秒

    論理的な読み取りは必要ありませんでした。

    実行時間は悪くありませんが、使用されるテーブル値コンストラクターが大きいため、コンパイル時間が長くなっていることに注意してください。要求する範囲サイズに関係なく、非常に長いコンパイル時間を支払うことになります。そのため、範囲が非常に狭い関数を使用する場合、これは特に注意が必要です。そして、このソリューションはまだT-SQLソリューションよりも低速です。

    関数を順番にテストしてみましょう:

     DECLARE @n AS BIGINT; SELECT @n =n FROM dbo.GetNums_AdamMachanic2(1、100000000)ORDER BY n; 

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

    図8:ORDERBYを使用したdbo.GetNums_AdamMachanic2関数の計画

    カミルの2番目のソリューションと同様に、計画には明示的な並べ替えが必要であり、パフォーマンスが大幅に低下します。このテストで得た時間統計は次のとおりです。

    実行時間:CPU時間=54891ミリ秒、経過時間=60981ミリ秒

    さらに、コンパイル時間には約3分の1秒という高いペナルティがあります。

    結論

    多くの人が当初、最高のパフォーマンスを発揮するソリューションはCLRベースのソリューションである可能性が高いと考えていたため、番号シリーズの課題に対するCLRベースのソリューションをテストすることは興味深いことでした。 KamilとAdamは同様のアプローチを使用しました。最初の試みは、カウンターをインクリメントして反復ごとに次の値を持つ行を生成する単純なループを使用し、2番目の試みはCLRとT-SQLの部分を組み合わせたより洗練された試みです。個人的には、KamilとAdamの2番目のソリューションの両方で、非決定論的なTOPフィルターに依存していたという事実に満足していません。また、自分のテストで決定論的なTOPフィルターに変換すると、ソリューションのパフォーマンスに悪影響を及ぼしました。 。いずれにせよ、2つのT-SQLソリューションはCLRソリューションよりもパフォーマンスが高く、行を並べ替える必要がある場合にプランで明示的な並べ替えが行われることはありません。ですから、CLRルートをこれ以上追求することの価値はあまりわかりません。図9に、この記事で紹介したソリューションのパフォーマンスの概要を示します。

    図9:時間パフォーマンスの比較

    私にとって、I / Oフットプリントがまったく必要ない場合は、GetNums_AlanCharlieItzikBatchが最適なソリューションであり、I / Oフットプリントが小さいことを気にしない場合は、GetNums_SQKWikiが推奨されます。もちろん、いつの日かMicrosoftがこの非常に便利なツールを組み込みツールとして追加することを常に期待できます。そうすれば、バッチ処理と並列処理をサポートするパフォーマンスの高いソリューションになることを願っています。したがって、この機能改善リクエストに投票することを忘れないでください。また、それがあなたにとって重要である理由についてコメントを追加することもできます。

    このシリーズの制作は本当に楽しかったです。その過程で多くのことを学びました。あなたもそうしてくれることを願っています。


    1. AndroidでJTDSドライバーを使用してSQLサーバーに接続する方法

    2. Windows認証またはSQLServer認証を使用してSQLServerインスタンスに接続する方法-SQLServer/T-SQLチュートリアルパート3

    3. MySQLのインストール-python

    4. ストアドプロシージャでテーブルを切り捨てる