同じクエリであると信じているものに対して2つの異なる実行プランを表示しているときに、SQLServerで苦労している人をよく目にします。通常、これは、実行時間が大幅に異なるなど、他の観察の後に発見されます。同じクエリであると信じているのは、そうである場合とそうでない場合があるからです。
最も一般的なケースの1つは、SSMSでクエリをテストしていて、アプリケーションから取得したものとは異なるプランを取得している場合です。ここでは、潜在的に2つの要因が関係しています(アプリケーションとSSMSの比較が行われていない場合にも関連する可能性があります)。
- アプリケーションには、ほとんどの場合、異なる
SET
があります SSMSよりも設定(これらはARITHABORT
のようなものです 、ANSI_NULLS
およびQUOTED_IDENTIFIER
)。これにより、SQLServerは2つのプランを別々に保存します。 Erland Sommarskogは、彼の記事「Slow in the Application、Fast in SSMS?」でこれを詳細に扱っています。
- プランのコピーが最初にコンパイルされたときにアプリケーションで使用されたパラメーターは、SSMSからクエリが最初に実行されたときに使用されたパラメーターとは大きく異なり、異なるプランにつながる可能性があります。これはパラメータースニッフィングと呼ばれます。 。アーランドはそれについても詳しく話します。彼の推奨事項を逆戻りさせるつもりはありませんが、SSMSでアプリケーションのクエリをテストすることは、リンゴ同士のテストである可能性が非常に低いため、必ずしも役立つとは限らないことを思い出して要約します。
Bad Habits&Best Practicesの講演で取り上げた、もう少しわかりにくいシナリオが他にもいくつかあります。これらは、プランに違いはありませんが、プランのキャッシュを肥大化させる同じプランの複数のコピーがある場合です。彼らはいつも多くの人を驚かせるので、ここで言及する必要があると思いました。
cAsEと空白は重要です
SQL Serverは、クエリテキストをバイナリ形式にハッシュします。これは、クエリテキストのすべての文字が重要であることを意味します。次の簡単なクエリを見てみましょう:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO SELECT StoreID FROM Sales.Customer; GO -- original query GO SELECT StoreID FROM Sales.Customer; GO ----^---- extra space GO SELECT storeid FROM sales.customer; GO ---- lower case names GO select StoreID from Sales.Customer; GO ---- lower case keywords GO
これらは明らかにまったく同じ結果を生成し、まったく同じ計画を生成します。ただし、プランキャッシュにあるものを見ると、次のようになります。
SELECT t.[text], p.size_in_bytes, p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE N'%sales'+'.'+'customer%';
結果は残念です:
したがって、この場合、大文字と小文字と空白が非常に重要であることは明らかです。これについては、昨年5月にさらに詳しく話しました。
スキーマ参照は重要です
以前、オブジェクトを参照するときにスキーマプレフィックスを指定することの重要性についてブログを書きましたが、当時は、プランキャッシュにも影響があることを十分に認識していませんでした。
デフォルトのスキーマが異なる2人のユーザーがいて、まったく同じクエリテキストを実行し、そのスキーマでオブジェクトを参照できないという非常に単純なケースを見てみましょう。
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO CREATE USER SQLPerf1 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Sales; CREATE USER SQLPerf2 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Person; GO CREATE TABLE dbo.AnErrorLog(id INT); GRANT SELECT ON dbo.AnErrorLog TO SQLPerf1, SQLPerf2; GO EXECUTE AS USER = N'SQLPerf1'; GO SELECT id FROM AnErrorLog; GO REVERT; GO EXECUTE AS USER = N'SQLPerf2'; GO SELECT id FROM AnErrorLog; GO REVERT; GO
ここで、プランキャッシュを見ると、sys.dm_exec_plan_attributes
を取り込むことができます。 同一のクエリに対して2つの異なるプランを取得している理由を正確に確認するには:
SELECT t.[text], p.size_in_bytes, p.usecounts, [schema_id] = pa.value, [schema] = s.name FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS pa INNER JOIN sys.schemas AS s ON s.[schema_id] = pa.value WHERE t.[text] LIKE N'%AnError'+'Log%' AND pa.attribute = N'user_id';
結果:
そして、それをすべてもう一度実行するが、dbo.
を追加する場合 両方のクエリのプレフィックスとして、2回使用されるプランは1つだけであることがわかります。これは、常にオブジェクトを完全に参照するための非常に説得力のある議論になります。
SET設定の重複
補足として、同様のアプローチを使用して、SET
かどうかを判断できます。 同じクエリの2つ以上のバージョンでは、設定が異なります。この場合、同じストアドプロシージャへの異なる呼び出しによって生成された複数のプランに関連するクエリを調査していますが、クエリテキストまたはクエリハッシュによってそれらを識別することもできます。
SELECT p.plan_handle, p.usecounts, p.size_in_bytes, set_options = MAX(a.value) FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS a WHERE t.objectid = OBJECT_ID(N'dbo.procedure_name') AND a.attribute = N'set_options' GROUP BY p.plan_handle, p.usecounts, p.size_in_bytes;
ここに複数の結果がある場合は、set_options
に異なる値が表示されるはずです。 (これはビットマスクです)。それはほんの始まりにすぎません。ここで説明します。ここの「セットオプションの評価」セクションに従って値をアンパックすることで、各プランで有効になっているオプションのセットを決定できることをお伝えします。はい、私はとても怠け者です。
結論
同じクエリ(または同じクエリだと思うもの)に対して異なる計画が表示される理由はいくつかあります。ほとんどの場合、原因を簡単に特定できます。課題は、そもそもそれを探すことを知っていることです。次の投稿では、少し異なるテーマについて説明します。「同一の」サーバーに復元されたデータベースが、同じクエリに対して異なるプランを生成する可能性がある理由です。