SQL Serverで2つのテーブルを結合するために使用できるアルゴリズムの1つは、ネストされたループです。ネストされたループの結合は、1つの結合入力を外部入力テーブルとして使用し、もう1つを内部入力テーブルとして使用します。外側のループは、外側の入力テーブルを行ごとに繰り返します。外側の行ごとに実行される内側のループは、内側の入力テーブルで一致する行を検索します。
これは、単純なネストされたループ結合と呼ばれます。
内部入力テーブルに結合条件のインデックスがある場合は、外部テーブルの各行に対して内部ループを実行する必要はありません。代わりに、外部テーブルの値を検索引数として渡し、返されたすべての内部テーブルの行を外部テーブルの行に接続できます。
内部テーブルによる検索はランダムアクセスです。バージョン2005以降のSQLServerには、バッチソートの最適化があります(列ストアインデックスのバッチモードのソート演算子と混同しないでください)。最適化の目的は、内部テーブルからデータを取得する前に、外部テーブルから検索キーを並べ替えることです。したがって、ランダムアクセスはシーケンシャルになります。
実行プランには、バッチソート操作が個別の演算子として表示されません。代わりに、ネストされたループ演算子でOptimized=trueプロパティを確認できます。計画内でバッチソートを個別の演算子として表示できる場合は、次のようになります。
この疑似プランでは、クラスター化されていないix_CustomerIDインデックスから、このCustomerIDインデックスキーの順序でデータを読み取ります。次に、ix_CustomerIDはカバーするインデックスではないため、クラスター化されたインデックスでキールックアップを実行する必要があります。キールックアップは、クラスター化インデックスキー検索操作(ランダムアクセス)です。順次作成するために、SQLServerはクラスター化インデックスキーによるバッチソートを実行する場合があります。
バッチソートの詳細については、私の記事「バッチソートとネストされたループ」を参照してください。
この最適化により、十分な数の行が大幅に向上します。そのテスト結果の詳細については、オプティマイザー開発者であるCraigFreedmanによって作成されたブログOPTIMIZEDNestedLoopsJoinsを参照してください。
ただし、実際の行数が予想よりも少ない場合、この種類を構築するためのCPUの追加コストにより、その利点が隠され、CPUの消費量が増加し、パフォーマンスが低下する可能性があります。
この特定の例を考えてみましょう:
use tempdb; go -- create a test table (SalesOrderID - clustered PK) create table dbo.SalesOrder(SalesOrderID int identity primary key, CustomerID int not null, SomeData char(200) not null); go -- add test data with n as (select top(1000000) rn = row_number() over(order by (select null)) from sys.all_columns c1,sys.all_columns c2) insert dbo.SalesOrder(CustomerID, SomeData) select rn%500000, str(rn,100) from n; -- create a clustered index create index ix_c on dbo.Salesorder(CustomerID); go -- the batch sort optimization is enabled by default (Nested Loops: Optimized = true) select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000; -- disable it with the DISABLE_OPTIMIZED_NESTED_LOOP hint (Nested Loops: Optimized = false) select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000 option(use hint('DISABLE_OPTIMIZED_NESTED_LOOP')); go
返された結果:
出力の行の順序の違いに注意を向けたいと思います。 ORDER BYを明示的に指定していないため、サーバーは行を処理する順序で返します。最初のケースでは、ix_cインデックスから徐々に読み取ります。ただし、クラスター化インデックスからのランダムな読み取りを最適化するために、クラスター化されたSalesOrderIDインデックスキーで行をフィルター処理します。 2番目のケースでは、ソートは行われず、読み取りは非クラスター化インデックスix_cのCustomerIDキーの順序で行われます。
2340トレースフラグとの違い
ドキュメントでは、2340トレースフラグがDISABLE_OPTIMIZED_NESTED_LOOPヒントと同等であると指定されていますが、実際には正しくありません。
次の例を考えてみましょう。UPDATESTATISTICS…WITHPAGECOUNTundocumentedコマンドを使用して、テーブルが実際よりも多くのページを使用していると言って、オプティマイザーをだまします。次に、これらのクエリを見てください:
- ヒントなし(簡単な計画を維持するためにMAXDOPを追加しました);
- DISABLE_OPTIMIZED_NESTED_LOOPヒントを使用;
- 2340トレースフラグを使用します。
-- create a huge table update statistics dbo.SalesOrder with pagecount = 100000; go set showplan_xml on; go -- 1. without hints select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(maxdop 1); -- 2. hint select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(use hint('DISABLE_OPTIMIZED_NESTED_LOOP'), maxdop 1); -- 3. trace flag select * from dbo.SalesOrder with(index(ix_c)) where CustomerID < 1000000 option(querytraceon 2340, maxdop 1); go set showplan_xml off; go
その結果、次の計画があります。
ネストされたループには、3つのプランすべてでOptimized=falseプロパティがあります。実際には、テーブルの幅を増やすことで、データアクセスコストも増加します。コストが十分に高い場合、SQL Serverは、暗黙的なバッチ並べ替え演算子ではなく、明示的な並べ替え演算子を使用する場合があります。これは、最初のクエリプランで確認できます。
2番目のクエリでは、暗黙のバッチソートをオフにするDISABLE_OPTIMIZED_NESTED_LOOPヒントを使用しました。ただし、別の演算子による明示的な並べ替えは削除されます。
3番目の計画では、2340トレースフラグを追加したにもかかわらず、並べ替え演算子が存在することがわかります。
したがって、ヒントとフラグの違いは次のとおりです。ヒントは、サーバーが暗黙のバッチソートで実装するか、個別のソート演算子で実装するかに応じて、ランダムアクセスをシリアルアクセスに変換することで最適化を無効にします。
P.S.計画は機器によって異なる場合があります。したがって、これらのクエリの実行に失敗した場合は、dbo.SalesOrderテーブルの説明でSomeData char(200)列のサイズを増減してみてください。
この記事は、著者の許可を得てCodingsightチームによって翻訳されました。