変数をインライン化するときにSQL Serverがより良い実行計画を使用するのはなぜですか?


32

最適化しようとしている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がより良い計画を立てるのでしょうか?

回答:


44

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

リテラル値:

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;

成果

リテラル値を使用し、プランがa)Trivialおよびb)Simple Parameterizedまたはc)Forced Parameterizationをオンにしていない場合、オプティマイザーはその値に対して非常に特別なプランを作成します。

パラメーターを使用すると、オプティマイザーはそのパラメーターのプランを作成し(これはパラメータースニッフィングと呼ばれます)、そのプランを再利用し、再コンパイルヒントがない場合、プランキャッシュエビクションなどを行います。

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

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

DECLARE @Reputation INT = 1

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

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

ナッツ

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

ナッツ

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

ローカル変数は、未知であるため、カーディナリティの推定にヒストグラムの「良い」部分を使用しません。密度ベクトルに基づいた推測を使用します。

ナッツ

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値が適度に選択的であると仮定すると、これは適切なプランを提供し、オプティマイザーに「明白な」データアクセス方法を提供することで役立ちます。

もっと読む

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


12

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

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

以下は私のテストデータです。テーブルに13 M行を配置し、その半分に列の値を設定し'3A35EA17-CE7E-4637-8319-4C517B6E48CA'ましたId

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

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

ここに画像の説明を入力してください

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

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

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

ここに画像の説明を入力してください

キー検索は不要になりました。すべての入力に対して適切に機能するより良いアクセスパスを使用すると、密度ベクトルのためにオプティマイザーが誤ったクエリプランを選択することを心配する必要はありません。ただし、一般的な@Id値を検索する場合、このクエリとインデックスは他のクエリとインデックスほど効率的ではありません。

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


2

ここで理由をお答えすることはできませんが、クエリが意図したとおりに実行されるようにするための迅速で汚れた方法は次のとおりです。

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.