索引列顺序的 WHERE-JOIN-ORDER-(SELECT) 规则是否错误?

Mag*_*ier 9 index sql-server sql-server-2008-r2 index-tuning index-design

我正在尝试改进这个(子)查询作为更大查询的一部分:

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
Run Code Online (Sandbox Code Playgroud)

Sentry Plan Explorer 指出了由上述查询执行的表 dbo.[I] 的一些相对昂贵的 Key Lookups。

表 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);
Run Code Online (Sandbox Code Playgroud)

表 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);
Run Code Online (Sandbox Code Playgroud)

表“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])
Run Code Online (Sandbox Code Playgroud)

提取的查询立即使用此索引。但它是原始较大查询的一部分,没有。当我强迫它使用 WITH(INDEX(IX_I_Status_1)) 时,它甚至没有使用它。

一段时间后,我决定尝试另一个新索引并更改为索引列的顺序:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])
Run Code Online (Sandbox Code Playgroud)

哇!这个索引被提取的查询使用,也被更大的查询使用!

然后我通过强制它使用 [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
Run Code Online (Sandbox Code Playgroud)

结果 [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
Run Code Online (Sandbox Code Playgroud)

好的,我可以理解 mega-large-monster 查询可能太复杂了,无法让 SQL Server 捕获理想的执行计划,并且可能会错过我的新索引。但是我不明白为什么索引 [IX_I_Status_2] 似乎更适合查询,也更高效。

由于查询首先按列 STATUS 过滤表 I,然后与表 IP 连接,我不明白为什么 [IX_I_Status_2] 更好并由 Sql Server 使用而不是 [IX_I_Status_1]?

Pau*_*ite 19

索引列顺序的 WHERE-JOIN-ORDER-(SELECT) 规则是否错误?

至少它是不完整的并且可能具有误导性的建议(我没有费心阅读整篇文章)。如果您要在 Internet 上阅读内容(包括此内容),您应该根据您对作者的了解和信任程度来调整您的信任程度,但始终要亲自验证。

创建索引有许多“经验法则”,具体取决于具体场景,但没有一个能真正替代您自己了解核心问题。阅读 SQL Server 中索引和执行计划运算符的实现,通过一些练习,并对如何使用索引使执行计划更有效有一个很好的理解。获得这些知识和经验没有有效的捷径。

一般来说,我可以说您的索引最常首先包含用于相等性测试的列,最后使用任何不等式,和/或由索引的过滤器提供。这不是一个完整的陈述,因为索引也可以提供顺序,这在某些情况下可能比直接查找一个或多个键更有用。例如,排序可用于避免排序、降低物理连接选项(如合并连接)的成本、启用流聚合、快速找到前几个符合条件的行……等等。

我在这里有点含糊,因为为查询选择理想的索引取决于很多因素 - 这是一个非常广泛的主题。

无论如何,在查询中找到“最佳”索引的冲突信号并不罕见。例如,您的连接谓词希望行以一种方式排序以进行合并连接,分组依据希望行以另一种方式排序以用于流聚合,并且使用 where 子句谓词查找符合条件的行会建议其他索引。

索引既是一门艺术又是一门科学的原因在于,理想的组合在逻辑上并不总是可能的。为工作负载(不仅仅是单个查询)选择最佳折衷索引需要分析技能、经验和特定于系统的知识。如果这很容易,自动化工具将是完美的,性能调优顾问的需求将大大减少。

就缺失的索引建议而言:这些都是机会主义的。当优化器尝试将谓词和所需的排序顺序与不存在的索引进行匹配时,它会引起您的注意。因此,这些建议基于当时正在考虑的特定子计划变体的特定上下文中的特定匹配尝试。

在上下文中,根据优化器的模型,这些建议在降低数据访问的估计成本方面总是有意义的。它并没有做查询的更广泛的分析,作为一个整体(更不用说更广泛的工作负载),所以你应该考虑这些建议作为一个委婉地暗示,一个技术人员需要查看可用的指标,与建议作为出发点(通常仅此而已)。

在您的情况下,该(Status) INCLUDE (ID)建议可能是在查看散列或合并连接的可能性时提出的(稍后举例)。在这种狭隘的背景下,这个建议是有道理的。对于整个查询,也许不是。索引(ID, Status)启用嵌套循环连接,ID作为外部引用:每次迭代的相等查找ID和不相等Status

一种可能的索引选择是:

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

...产生如下计划:

可能的计划

我并不是说这些索引适合您;他们碰巧为我制定了一个合理的计划,而无法查看所涉及表的统计信息,或完整定义和现有索引。此外,我对更广泛的工作量或实际查询一无所知。

或者(只是为了显示无数其他可能性之一):

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

给出:

替代计划

执行计划是使用SQL Sentry Plan Explorer生成的。