インデックス列の順序のWHERE-JOIN-ORDER-(SELECT)ルールは間違っていますか?


9

より大きなクエリの一部であるこの(サブ)クエリを改善しようとしています。

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

Sentry Plan Explorerは、上記のクエリによって実行された、テーブルdbo。[I]の比較的コストのかかるキールックアップを指摘しました。

テーブルdbo.I

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

テーブルdbo.IP

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

テーブル "I"には約100,000行あり、クラスター化インデックスには9,386ページあります。
テーブルIPは「子」-Iのテーブルで、約175,000行あります。

インデックス列の順序ルール「WHERE-JOIN-ORDER-(SELECT)」に従って新しいインデックスを追加しようとしました

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

キー検索に対処し、インデックスシークを作成するには:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

抽出されたクエリはすぐにこのインデックスを使用しました。しかし、それが含まれる元の大きなクエリは、そうではありませんでした。WITH(INDEX(IX_I_Status_1))を使用するように強制した場合も、使用しませんでした。

しばらくして、別の新しいインデックスを試すことにし、インデックス付きの列の順序を変更しました。

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

うわ!このインデックスは、抽出されたクエリとより大きなクエリによって使用されました!

次に、[IX_I_Status_1]および[IX_I_Status_2]を使用するように強制することにより、抽出されたクエリのIO統計を比較しました。

結果[IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

結果[IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

わかりました。メガラージモンスタークエリは複雑すぎて、SQLサーバーが理想的な実行プランをキャッチできず、新しいインデックスを取得できない可能性があることを理解できました。しかし、インデックス[IX_I_Status_2]がクエリにとってより適切で効率的であると思われる理由がわかりません。

最初にすべてのクエリが列STATUSでテーブルIをフィルターし、次にテーブルIPと結合するので、SQL Serverが[IX_I_Status_1]ではなく[IX_I_Status_2]を使用して使用する理由がわかりません。


はい、フィルター基準が満たされた場合にこのインデックスを使用します。インデックススキャンを実行し(IX_I_Status_2と同じ)、これと比較して1つの物理読み取りを保存します。しかし、ステータスが出力にあり、以前に再度検索されたため、このインデックスに「include(status)」する必要がありました。
Magier

おもしろいメモ:([IX_I_Status_2])と判断できる最高のインデックスに適用してクエリを再度実行すると、不足しているインデックスの提案が表示されます:CREATE NONCLUSTERED INDEX [<Name of Missing Index、sysname、>] ON [ dbo]。[I]([Status])INCLUDE([ID])これは不適切な提案であり、クエリのパフォーマンスを低下させます。TY
Sql

回答:


19

インデックス列の順序のWHERE-JOIN-ORDER-(SELECT)ルールは間違っていますか?

少なくとも、それは不完全であり、誤解を招く可能性のあるアドバイスです(私は記事全体を読むことを気にしませんでした)。あなたがインターネット(これを含む)で物事を読むつもりなら、あなたはあなたがその作者を既に知っていて、どれだけ信頼しているかに従ってあなたの信頼の量を調整すべきですが、常にあなた自身のために確かめてください。

正確なシナリオに応じて、インデックスを作成するためのいくつかの「経験則」がありますが、自分にとっての中心的な問題を理解するのに適したものはありません。SQL Serverでのインデックスと実行プラン演算子の実装について読んで、いくつかの演習を行い、インデックスを使用して実行プランをより効率的にする方法をしっかりと理解してください。この知識と経験を得るための効果的な近道はありません。

一般的に、ほとんどの場合、インデックスには、最初に等値性テストに使用される列があり、不等式が最後にあるか、インデックスのフィルターによって提供される列が必要です。これは完全なステートメントではありません。インデックスも順序を提供できるため、状況によっては1つ以上のキーを直接シークするよりも便利な場合があります。たとえば、並べ替えを使用して、マージ結合などの物理結合オプションのコストを削減し、ストリームの集約を有効にして、最初の数行をすばやく特定するなどの順序付けを使用できます。

クエリの理想的なインデックスの選択は非常に多くの要因に依存するため、私はここでは少し漠然としています。これは非常に広範なトピックです。

とにかく、クエリ内の「最適な」インデックスの競合するシグナルを見つけることは珍しいことではありません。たとえば、結合述語では、マージ結合の行を1つの方法で並べ替え、group byでは、ストリーム集計の別の方法で行を並べ替え、where句の述語を使用して条件を満たす行を検索すると、他のインデックスが提案されます。

索引付けが芸術であり科学である理由は、理想的な組み合わせが常に論理的に可能であるとは限らないためです。(単一のクエリだけでなく)ワークロードに最適な妥協インデックスを選択するには、分析スキル、経験、およびシステム固有の知識が必要です。それが簡単だったら、自動化ツールは完璧で、パフォーマンス調整コンサルタントの需要ははるかに少なくなります。

欠落しているインデックスの提案に関する限り、これらは日和見的です。オプティマイザは、述語と必要なソート順を存在しないインデックスに一致させようとするときに、注意を引き付けます。したがって、提案は、当時検討していた特定のサブプランのバリエーションの特定のコンテキストにおける特定のマッチングの試みに基づいています。

コンテキストでは、オプティマイザのモデルによると、データアクセスの推定コストを削減するという観点から、提案は常に意味があります。クエリ全体の分析は広く行われませ(ワークロードが大幅に減少します)ため、これらの提案は、熟練者が利用可能なインデックスを検討する必要がある穏やかなヒントであり、提案を最初に考える必要があります。ポイント(そして通常それ以上)。

あなたの場合、(Status) INCLUDE (ID)おそらく提案は、ハッシュまたはマージ結合の可能性を検討しているときに発生しました(後で例を示します)。その狭い文脈では、提案は理にかなっています。クエリ全体としては、そうではないかもしれません。インデックス(ID, Status)ID、外部参照としてのネストされたループ結合を可能にします:反復ごとの等価シークIDおよび不等価オンStatus

可能なインデックスの1つの選択は次のとおりです。

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

...次のような計画が作成されます。

可能な計画

これらのインデックスがあなたにとって最適であるとは言っていません。それらはたまたま、関係するテーブルの統計や完全な定義と既存のインデックス付けを見ることができずに、合理的に見えるプランを作成するために働いています。また、より広いワークロードや実際のクエリについては何も知りません。

代わりに(無数の追加可能性の1つを示すためだけに):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

与える:

代替計画

実行プランは、SQL Sentry Plan Explorerを使用して生成されました。

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