web-dev-qa-db-ja.com

変数をインライン化したときにSQL Serverがより優れた実行プランを使用するのはなぜですか?

最適化しようとしているSQLクエリがあります。

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTableには2つのインデックスがあります。

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

上記とまったく同じようにクエリを実行すると、SQL Serverは最初のインデックスをスキャンし、189,703の論理読み取りと2〜3秒の継続時間になります。

@Id変数をインライン化してクエリを再度実行すると、SQL Serverは2番目のインデックスを検索し、104の論理読み取りと0.001秒の継続時間(基本的には瞬時)をもたらします。

変数が必要ですが、SQLで適切なプランを使用してください。一時的な解決策として、クエリにインデックスヒントを付けました。クエリは基本的にインスタントです。ただし、可能な場合は、インデックスのヒントに近づかないようにしています。通常、クエリオプティマイザーがその仕事を実行できない場合、何をすべきかを明示的に指示せずにそれを助けるために私ができる(またはやめる)ことができる何かがあると思います。

では、なぜ変数をインライン化したときにSQL Serverがより優れた計画を立てるのでしょうか。

32
Rainbolt

SQL Serverには、非結合述語の3つの一般的な形式があります。

literal値:

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

パラメータで:

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

ローカル変数

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

成果

literal値を使用し、プランがa) Trivial およびb)Simple Parameterizedまたはc)でない場合- Forced Parameterization がオンになっていると、オプティマイザはその値についてのみ特別な計画を作成します。

parameterを使用すると、オプティマイザーはそのパラメーターのプランを作成し(これは parameter sniffing と呼ばれます)、そのプランを再利用しますが、再コンパイルは行いません。ヒント、計画キャッシュの削除など.

ローカル変数を使用すると、オプティマイザは... Something の計画を立てます。

このクエリを実行する場合:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

計画は次のようになります。

NUTS

そして、そのローカル変数の推定行数は次のようになります。

NUTS

クエリは4,744,427のカウントを返しますが。

不明なローカル変数は、カーディナリティの推定にヒストグラムの「良い」部分を使用しません。それらは、密度ベクトルに基づく推測を使用します。

NUTS

SELECT 5.280389E-05 * 7250739 AS [poo]

382.86722457471、これはオプティマイザが行う推測です。

これらの未知の推測は通常、非常に悪い推測であり、多くの場合、悪い計画や悪いインデックスの選択につながる可能性があります。

修正していますか?

あなたのオプションは一般的に:

  • 脆弱なインデックスのヒント
  • 潜在的に高価な再コンパイルのヒント
  • パラメータ化された動的SQL
  • ストアドプロシージャ
  • 現在のインデックスを改善する

具体的には次のオプションがあります:

現在のインデックスを改善するということは、クエリに必要なすべての列をカバーするようにインデックスを拡張することを意味します。

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Idの値がかなり選択的であると仮定すると、これは適切な計画を提供し、オプティマイザに「明白な」データアクセスメソッドを提供することで支援します。

もっと読む

パラメータの埋め込みについて詳しくは、こちらをご覧ください。

45
Erik Darling

データが歪んでいる、クエリヒントを使用してオプティマイザに強制的に実行させたくない、そして@Idのすべての可能な入力値に対して良好なパフォーマンスを得る必要があると仮定します。次のインデックスのペア(または同等のもの)を作成する場合は、可能な入力値に対して少数の論理読み取りを必要とすることが保証されたクエリプランを取得できます。

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

以下は私のテストデータです。テーブルに1,300万行を入れて、それらの半分にId列の値'3A35EA17-CE7E-4637-8319-4C517B6E48CA'を設定しました。

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

このクエリは、最初は少し奇妙に見えるかもしれません:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

これは、いくつかの論理読み取りでインデックスの順序を利用して最小値または最大値を見つけるように設計されています。 CROSS JOINは、@Id値に一致する行がない場合に正しい結果を取得するためにあります。テーブルで最も人気のある値(650万行に一致)でフィルター処理しても、8つの論理読み取りしか得られません。

テーブル「MyTable」。スキャンカウント2、論理読み取り8

クエリプランは次のとおりです。

enter image description here

両方のインデックスシークは0または1行を見つけます。これは非常に効率的ですが、2つのインデックスを作成するのは、シナリオにとってはやり過ぎかもしれません。代わりに、次のインデックスを検討できます。

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

これで、元のクエリのクエリプラン(オプションのMAXDOP 1ヒントを使用)は少し異なります。

enter image description here

キー検索は必要なくなりました。すべての入力に対して適切に機能する、より優れたアクセスパスがあれば、密度ベクトルが原因でオプティマイザが間違ったクエリプランを選択することを心配する必要はありません。ただし、このクエリとインデックスは、人気のある@Id値を探す場合、他のクエリとインデックスほど効率的ではありません。

テーブル「MyTable」。スキャンカウント1、論理読み取り33757

12
Joe Obbish

ここではwhyと答えることはできませんが、クエリが意図したとおりに実行されることを確認するための簡単な方法は次のとおりです。

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

これにより、テーブルまたはインデックスが将来変更され、この最適化が機能しなくなるというリスクが発生しますが、必要に応じて利用できます。願わくば、この回避策ではなく、誰かが根本原因の答えを提供できることを願っています。

2