web-dev-qa-db-ja.com

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

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

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])

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

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

結果[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]を使用して使用する理由がわかりません。

9
Magier

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

少なくとも、それは不完全であり、誤解を招く可能性のあるアドバイスです(私は記事全体を読むのに迷惑をかけませんでした)。インターネット(これを含む)で物事を読む場合は、作成者の知識と信頼度に応じて信頼度を調整する必要がありますが、常に自分で確認してください。

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

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

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

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

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

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

コンテキストでは、オプティマイザのモデルによると、データアクセスの推定コストを削減するという観点から、提案alwaysは理にかなっています。これはしないクエリ全体をより広範囲に分析するため(ワークロードが大幅に減少する)、これらの提案を穏やかなヒント提案を出発点として(通常はそれ以上)、利用可能なインデックスを調べる必要があることを示します。

あなたの場合、(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);

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

---(Possible plan

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

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

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

与える:

Alternate plan

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

19
Paul White 9