はじめに
SQL Server 2005での導入以来、ウィンドウは ROW_NUMBERのように機能します。 およびRANK さまざまな一般的なT-SQLの問題を解決するのに非常に役立つことが証明されています。このようなソリューションを一般化するために、データベース設計者は、コードのカプセル化と再利用を促進するために、それらをビューに組み込むことを検討することがよくあります。残念ながら、SQL Serverクエリオプティマイザの制限により、ウィンドウ関数を含むビューが期待どおりに機能しないことがよくあります。この投稿は、問題の実例を示し、理由を詳しく説明し、いくつかの回避策を提供します。
この問題は、派生テーブル、一般的なテーブル式、インライン関数でも発生する可能性がありますが、より一般的なものとして意図的に記述されているため、ビューで最も頻繁に発生します。
ウィンドウ関数
ウィンドウ関数は、 OVER()の存在によって区別されます 条項と3つの種類があります:
- ランキングウィンドウ関数
-
ROW_NUMBER -
RANK -
DENSE_RANK -
NTILE
-
- 集約ウィンドウ関数
-
MIN、MAX、AVG、SUM -
COUNT、COUNT_BIG -
CHECKSUM_AGG -
STDEV、STDEVP、VAR、VARP
-
- 分析ウィンドウ関数
-
LAG、LEAD -
FIRST_VALUE、LAST_VALUE -
PERCENT_RANK、PERCENTILE_CONT、PERCENTILE_DISC、CUME_DIST
-
ランキングおよび集計ウィンドウ関数はSQLServer2005で導入され、SQLServer2012で大幅に拡張されました。分析ウィンドウ関数はSQLServer2012の新機能です。
上記のすべてのウィンドウ関数は、この記事で詳しく説明されているオプティマイザーの制限の影響を受けます。
例
AdventureWorksサンプルデータベースを使用して、当面のタスクは、利用可能な最新の日付に発生したすべての製品#878トランザクションを返すクエリを作成することです。 T-SQLでこの要件を表現するにはさまざまな方法がありますが、ウィンドウ関数を使用するクエリを作成することを選択します。最初のステップは、製品#878のトランザクションレコードを検索し、日付の降順でランク付けすることです。
SELECT th.TransactionID、th.ReferenceOrderID、th.TransactionDate、th.Quantity、rnk =RANK()OVER(ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY rnk;
クエリの結果は期待どおりであり、利用可能な最新の日付に6つのトランザクションが発生しています。実行プランには、インデックスが欠落していることを警告する警告トライアングルが含まれています:
不足しているインデックスの提案についてはいつものように、推奨はクエリの徹底的な分析の結果ではないことを覚えておく必要があります。これは、このクエリが必要なデータにどのようにアクセスするかについて少し考える必要があることを示しています。
提案されたインデックスは、テーブルを完全にスキャンするよりも確かに効率的です。これにより、関心のある特定の製品をインデックスで検索できるようになります。インデックスは必要なすべての列もカバーしますが、並べ替えを回避することはできません( TransactionDate 降順)。このクエリの理想的なインデックスは、 ProductIDでのシークを可能にします 、選択したレコードを逆に返します TransactionDate 注文し、返された他の列をカバーします:
CREATE NONCLUSTERED INDEX ixON Production.TransactionHistory(ProductID、TransactionDate DESC)INCLUDE(ReferenceOrderID、Quantity);
そのインデックスを設定すると、実行プランがはるかに効率的になります。クラスター化されたインデックススキャンは範囲シークに置き換えられ、明示的な並べ替えは不要になりました:
このクエリの最後のステップは、結果をランク1の行のみに制限することです。 WHEREで直接フィルタリングすることはできません ウィンドウ関数はSELECTにのみ表示される可能性があるため、クエリの句 およびORDERBY 条項。
この制限は、派生テーブル、共通テーブル式、関数、またはビューを使用して回避できます。この機会に、一般的なテーブル式(別名インラインビュー)を使用します:
WITH RankedTransactions AS(SELECT th.TransactionID、th.ReferenceOrderID、th.TransactionDate、th.Quantity、rnk =RANK()OVER(ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS th WHERE th.ProductID =878 )SELECT TransactionID、ReferenceOrderID、TransactionDate、QuantityFROM RankedTransactionsWHERE rnk =1;
実行プランは以前と同じですが、#1にランク付けされた行のみを返すための追加のフィルターがあります。
クエリは、期待される6つの等しくランク付けされた行を返します。
クエリの一般化
クエリが非常に役立つことがわかったので、クエリを一般化して定義をビューに格納することにしました。これをどの製品でも機能させるには、次の2つのことを行う必要があります。 ProductIDを返す ビューから、ランク付け機能を製品ごとに分割します:
CREATE VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT sq1.ProductID、sq1.TransactionID、sq1.ReferenceOrderID、sq1.TransactionDate、sq1.QuantityFROM(SELECT th.ProductID、th.TransactionID、th.ReferenceOrderID、th.Trans rnk =RANK()OVER(PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS th)AS sq1WHERE sq1.rnk =1;
ビューからすべての行を選択すると、次の実行プランと正しい結果が得られます。
これで、ビューではるかに簡単なクエリを使用して、製品878の最新のトランザクションを見つけることができます。
SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878;
この新しいクエリの実行プランは、ビューを作成する前とまったく同じになると予想されます。クエリオプティマイザは、 WHEREで指定されたフィルタをプッシュできる必要があります ビューに句を挿入すると、インデックスシークが発生します。
ただし、この時点で少し考えてみる必要があります。クエリオプティマイザは、論理クエリ仕様と同じ結果を生成することが保証されている実行プランのみを生成できます。 WHEREをプッシュしても安全ですか。 ビューに句を追加しますか?<フィルタリング対象の列が PARTITION BY に表示されている限り、答えは「はい」です。 ビュー内のウィンドウ関数の句。理由は、ウィンドウ関数から完全なグループ(パーティション)を削除しても、クエリによって返される行のランク付けには影響しないためです。問題は、SQLServerクエリオプティマイザがこれを知っているかどうかです。答えは、実行しているSQLServerのバージョンによって異なります。
SQLServer2005実行プラン
このプランのフィルタープロパティを見ると、2つの述語が適用されていることがわかります。
ProductID =878 述語がビューにプッシュダウンされていないため、インデックスをスキャンして、テーブル内のすべての行をランク付けしてから、製品#878とランク付けされた行をフィルタリングする計画が作成されます。
SQL Server 2005クエリオプティマイザは、適切な述語を下位のクエリスコープ(ビュー、共通テーブル式、インライン関数、または派生テーブル)のウィンドウ関数を超えてプッシュすることはできません。この制限は、すべてのSQLServer2005ビルドに適用されます。
SQLServer2008以降の実行プラン
これは、SQLServer2008以降での同じクエリの実行プランです。
ProductID 述語はランキング演算子を超えて正常にプッシュされ、インデックススキャンが効率的なインデックスシークに置き換えられました。
2008クエリオプティマイザには、新しい簡略化ルール SelOnSeqPrjが含まれています (シーケンスプロジェクトで選択)安全な外部スコープ述語を過去のウィンドウ関数にプッシュすることができます。 SQL Server 2008以降でこのクエリの効率の低いプランを作成するには、このクエリオプティマイザ機能を一時的に無効にする必要があります。
SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878OPTION(QUERYRULEOFF SelOnSeqPrj);
残念ながら、 SelOnSeqPrj 簡略化ルールのみ機能 述語が定数との比較を実行するとき 。そのため、次のクエリはSQLServer2008以降で次善の計画を生成します。
DECLARE @ProductID INT =878; SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;
述部が定数値を使用している場合でも、問題が発生する可能性があります。 SQL Serverは、些細なクエリ(明らかに最良の計画が存在するクエリ)を自動パラメーター化することを決定する場合があります。自動パラメーター化が成功すると、オプティマイザーは定数ではなくパラメーターを認識し、 SelOnSeqPrj ルールは適用されません。
自動パラメーター化が試行されない(または安全でないと判断された)クエリの場合、 FORCED PARAMETERIZATION のデータベースオプションを使用すると、最適化が失敗する可能性があります。 オンになっています。テストクエリ(定数値878)は自動パラメータ化には安全ではありませんが、強制的なパラメータ化設定はこれを上書きし、非効率的な計画になります:
ALTER DATABASE AdventureWorksSET PARAMETERIZATION FORCED; GOSELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =878; GOALTER DATABASE AdventureWorksSET PARAMETERIZATION SIM>>
![]()
SQLServer2008+の回避策
オプティマイザーがローカル変数またはパラメーターを参照するクエリの定数値を「確認」できるようにするために、
OPTION(RECOMPILE)を追加できます。 クエリのヒント:DECLARE @ProductID INT =878; SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductIDOPTION(RECOMPILE);注: 変数の値が実際にはまだ設定されていないため、実行前(「推定」)実行プランには引き続きインデックススキャンが表示されます。クエリが実行されたとき ただし、実行プランには、目的のインデックスシークプランが表示されます。
![]()
SelOnSeqPrjルールはSQLServer2005には存在しないため、OPTION(RECOMPILE)そこでは仕方がない。ご参考までに、<code> OPTION(RECOMPILE) 回避策により、強制パラメーター化のデータベースオプションがオンになっている場合でも、シークが発生します。すべてのバージョンの回避策#1
場合によっては、問題のあるビュー、共通テーブル式、または派生テーブルを、パラメーター化されたインラインテーブル値関数に置き換えることができます。
CREATE FUNCTION dbo.MostRecentTransactionsForProduct(@ProductID integer)RETURNS TABLEWITH SCHEMABINDING ASRETURN SELECT sq1.ProductID、sq1.TransactionID、sq1.ReferenceOrderID、sq1.TransactionDate、sq1.Quantity FROM(SELECT th.ProductID、th.Transaction ReferenceOrderID、th.TransactionDate、th.Quantity、rnk =RANK()OVER(PARTITION BY th.ProductID ORDER BY th.TransactionDate DESC)FROM Production.TransactionHistory AS th WHERE th.ProductID =@ProductID)AS sq1 WHERE sq1.rnk =1;この関数は、
ProductIDを明示的に配置します オプティマイザの制限を回避して、ウィンドウ関数と同じスコープ内の述語。インライン関数を使用するように記述されているため、クエリの例は次のようになります。SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsForProduct(878)AS mrt;これにより、ウィンドウ関数をサポートするすべてのバージョンのSQLServerで目的のインデックスシークプランが作成されます。この回避策は、述語がパラメーターまたはローカル変数を参照している場合でもシークを生成します–
OPTION(RECOMPILE)必須ではありません。<もちろん、関数本体を簡略化して、冗長になったPARTITION BYを削除することもできます。 句、およびProductIDを返さないようにします 桁。実行プランの違いの原因をより明確に示すために、置き換えたビューと同じ定義を残しました。すべてのバージョンの回避策#2
2番目の回避策は、(
ROW_NUMBERを使用して)番号1またはランク付けされた行を返すようにフィルター処理されたランク付けウィンドウ関数にのみ適用されます。 、RANK、またはDENSE_RANK)。ただし、これは非常に一般的な使用法であるため、言及する価値があります。追加の利点は、この回避策により、さらにより効率的な計画を作成できることです。 以前に見たインデックスシークプランよりも。念のため、以前の最良の計画は次のようになりました:
![]()
その実行プランは1,918にランク付けされます 最終的には6のみを返しますが、行 。
ORDER BYでウィンドウ関数を使用することで、この実行プランを改善できます。 行をランク付けしてからランク#1をフィルタリングする代わりに句:SELECT TOP(1)WITH TIES th.TransactionID、th.ReferenceOrderID、th.TransactionDate、th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY RANK()OVER(ORDER BY th.TransactionDate DESC);
![]()
このクエリは、
ORDER BYでのウィンドウ関数の使用をうまく示しています。 節ですが、ウィンドウ関数を完全に排除して、さらに改善することができます:SELECT TOP(1)WITH TIES th.TransactionID、th.ReferenceOrderID、th.TransactionDate、th.QuantityFROM Production.TransactionHistory AS thWHERE th.ProductID =878ORDER BY th.TransactionDate DESC;
![]()
このプランは、テーブルから7行のみを読み取り、同じ6行の結果セットを返します。なぜ7行なのですか? Topオペレーターは
WITHTIESで実行されています モード:
![]()
TransactionDateが変更されるまで、サブツリーから一度に1行ずつ要求し続けます。トップがこれ以上同値の行が適格でないことを確認するには、7番目の行が必要です。
上記のクエリのロジックを拡張して、問題のあるビュー定義を置き換えることができます。
ALTER VIEW dbo.MostRecentTransactionsPerProductWITH SCHEMABINDINGASSELECT p.ProductID、Rank1.TransactionID、Rank1.ReferenceOrderID、Rank1.TransactionDate、Rank1.QuantityFROM-製品IDのリスト(SELECT ProductID FROM Production.Product)AS pCROSS APPLY(-ランクを返します各製品IDの#1の結果SELECT TOP(1)WITH TIES th.TransactionID、th.ReferenceOrderID、th.TransactionDate、th.Quantity FROM Production.TransactionHistory AS th WHERE th.ProductID =p.ProductID ORDER BY th.TransactionDate DESC) ASランク1;ビューは
CROSSAPPLYを使用するようになりました 最適化されたORDERBYの結果を組み合わせる 各製品のクエリ。テストクエリは変更されていません:DECLARE @ProductID integer; SET @ProductID =878; SELECT mrt.ProductID、mrt.TransactionID、mrt.ReferenceOrderID、mrt.TransactionDate、mrt.QuantityFROM dbo.MostRecentTransactionsPerProduct AS mrt WHERE mrt.ProductID =@ProductID;実行前と実行後の両方のプランは、
OPTION(RECOMPILE)を必要とせずにインデックスシークを示します クエリヒント。以下は、実行後(「実際の」)計画です。
![]()
ビューが
ROW_NUMBERを使用していた場合RANKの代わりに 、置換ビューでは、WITH TIESが省略されているだけです。TOP(1)の句 。もちろん、新しいビューは、パラメーター化されたインラインのテーブル値関数として作成することもできます。
rnk =1を使用した元のインデックスシークプランと主張することができます。 述語は、7行のみをテストするように最適化することもできます。結局のところ、オプティマイザーは、ランキングが厳密な昇順でシーケンスプロジェクトオペレーターによって生成されることを知っている必要があります。そのため、ランクが1より大きい行が表示されるとすぐに実行が終了する可能性があります。ただし、オプティマイザには現在このロジックは含まれていません。最終的な考え
人々は、ウィンドウ関数を組み込んだビューのパフォーマンスに失望することがよくあります。この理由は、多くの場合、この投稿で説明されているオプティマイザーの制限にまでさかのぼることができます(または、ビューに適用される述語が
PARTITION BYに表示される必要があることをビューデザイナーが認識していなかったためかもしれません。 安全に押し下げる条項)この制限はビューだけに適用されるのではなく、
ROW_NUMBERにも制限されないことを強調したいと思います。 、RANK、およびDENSE_RANK。OVERで関数を使用する場合は、この制限に注意する必要があります。 ビュー内の句、共通テーブル式、派生テーブル、またはインラインテーブル値関数。この問題が発生したSQLServer2005ユーザーは、ビューをパラメーター化されたインラインテーブル値関数として書き換えるか、
APPLYを使用するかを選択する必要があります。 テクニック(該当する場合)。SQL Server 2008ユーザーには、
OPTION(RECOMPILE)を使用する追加のオプションがあります。 オプティマイザーが変数またはパラメーター参照の代わりに定数を表示できるようにすることで問題を解決できるかどうかのヒントを照会します。ただし、このヒントを使用する場合は、実行後の計画を確認することを忘れないでください。実行前の計画では、通常、最適な計画を示すことはできません。