索引列顺序的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 Server捕获理想的执行计划,并且可能会错过我的新索引。但是我不明白为什么索引[IX_I_Status_2]似乎更适合查询并且更有效。

由于查询首先按状态STATUS列过滤表I,然后与表IP联接,所以我不明白为什么[IX_I_Status_2]更好并且被Sql Server代替[IX_I_Status_1]使用?


是的,如果满足过滤条件,它将使用此索引。它执行索引扫描(与IX_I_Status_2相同),与之相比,它节省了1次物理读取。但我必须将该索引“包含(status)”,因为状态在输出中,并且在之前再次进行过查找。
马吉尔

有趣的旁注:在我现在应用到最佳索引之后,我可以弄清楚([IX_I_Status_2])并再次运行查询,现在我得到了缺少的索引建议:CREATE NONCLUSTERED INDEX [<缺少索引的名称,系统名称,>] ON [ dbo]。[I]([状态])包含([ID])这是一个较差的建议,并降低了查询性能。TY Sql server :)
Magier

Answers:


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

...产生的计划如下:

可能的计划

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

另一种选择(只是为了展示众多其他可能性之一):

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

给出:

替代方案

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.