SQL Server临界点游戏——为什么非聚集索引被忽略!

当我们进行SQL Server问题处理的时候,有时候会发现一个很有意思的现象:SQL Server完全忽略现有定义好的非聚集索引,直接使用表扫描来获取数据。我们来看看下面的表和索引定义:

 1 CREATE TABLE Customers
 2 (
 3    CustomerID INT NOT NULL,
 4    CustomerName CHAR(100) NOT NULL,
 5    CustomerAddress CHAR(100) NOT NULL,
 6    Comments CHAR(185) NOT NULL,
 7    Value INT NOT NULL
 8 )
 9 GO
10
11 CREATE UNIQUE CLUSTERED INDEX idx_Customers ON Customers(CustomerID)
12 GO
13
14 CREATE UNIQUE NONCLUSTERED INDEX idx_Test ON Customers(Value)
15 GO

我们往表里插入80000条记录:

 1 DECLARE @i INT = 1
 2 WHILE (@i <= 80000)
 3 BEGIN
 4 INSERT INTO Customers VALUES
 5 (
 6    @i,
 7    ‘CustomerName‘ + CAST(@i AS CHAR),
 8    ‘CustomerAddress‘ + CAST(@i AS CHAR),
 9    ‘Comments‘ + CAST(@i AS CHAR),
10    @i
11 )
12
13 SET @i += 1
14 END
15 GO

执行下列查询,就会发现SQL Server完全忽略非聚集索引,而使用表扫描来获取数据,点击工具栏的显示包含实际的执行计划:

1 SELECT * FROM Customers
2 WHERE Value < 1267
3 GO

而当我们把查询条件修改为1266时,我们惊奇的发现,SQL Server又重新使用非聚集索引来获取数据了:

1 SELECT * FROM Customers
2 WHERE Value < 1266
3 GO

很多人估计会很兴奋,因为他们认为它们找到了SQL Server里的一个BUG,用指定索引来查询就可以避免这个问题:

1 SELECT * FROM Customers
2 WITH (INDEX(idx_Test))
3 WHERE Value < 1267
4 GO

从执行计划里我们可以看到,SQL Server需要进行书签查找,因为针对这个查询,我们并没有定义对应的覆盖非聚集索引。当你进行全表聚集索引扫描时,SQL Server这里帮了你一个大忙:用书签查找获取每条记录成本太高,因此SQL Server使用了全表扫描,这样就只需要较少的IO和CPU占用,因为书签查找都要通过内循环运算符完成。

在SQL Server里,这个行为被称为临界点(Tipping Point) 。我们再详细解释下这个概念。简单来说,临界点定义了SQL Server是使用书签查找还是全表/索引扫描。这也意味着临界点只与非覆盖非聚集索引有关。一个对指定查询扮演覆盖非聚集索引的角色的话,不会有临界点,也就不会有刚才介绍的问题。

在有书签查找的查询时,SQL Server使用书签查找还是全表扫描取决于获取的页数。是的,你没看错!获取的页数决定了书签查找是好的还是不好的!这与查询返回的记录条数完全无关,唯一有关就是页数。临界点出现在查询需要读取的24%-33%页数之间。

在这范围之前,查询优化器会选择书签查找,在这范围之后,查询优化器会选择全表扫描(在全表扫描运算符里会有谓语定义)。

这也意味着你记录的大小决定了临界点的位置。在查询越过临界点进行全表扫描时,小记录,你就只能从表获取小数量的记录,大记录,你就能够获得大量的记录。下图就是对临界点的一个图示。

在我们刚才的例子里,每条记录是400 bytes长,因此8kb的页面里可以保存20条记录,当我们进行全表扫描时,SQL Server会产生4016个逻辑读。

1 SET STATISTICS IO ON
2 SELECT * FROM Customers

刚才的例子里,我们的表在聚集索引的叶子层有4000个数据页,也就是说临界点在1000与1333页之间的某个地方。在优化器选择进行全表扫描前,你只能读取1.25%-1.67%(1000/80000,1333/80000)的表数据。

下面这个查询会用到书签查找:

1 SET STATISTICS IO ON
2 SELECT * FROM Customers
3 WHERE Value < 1266
4 GO

可以看到,这个查询需要3887个IO操作,而全表扫描只需要4016个IO,这里的书签查找成本(IO和CPU消耗)越来越昂贵了。超过了这个点,SQL Server就决定不使用书签查找,改用全表扫描了。

1 SET STATISTICS IO ON
2 SELECT * FROM Customers
3 WHERE Value < 1267
4 GO

我们一起执行看下:

1 SELECT * FROM Customers
2 WHERE Value < 1266
3 GO
4 SELECT * FROM Customers
5 WHERE Value < 1267
6 GO

2个近乎一样的查询,却有完全不同的执行计划,这在性能调优的时候是个大问题,因为你的执行计划失去了稳定性。

针对输入参数的不同,却有完全不同的计划!这也是书签查找的重大缺陷!用了书签查找,你就不能获得稳定的执行计划。如果这个执行计划被缓存(或你的统计信息过期了),你用它获取大量数据的时候就会有性能上的问题,因为低效的书签查找被SQL Server盲目重用了!这会造成原先只要几秒的查询,要花好几分钟才能完成!

我们说过,临界点取决于查询的读取页数。我们对刚才的表做下一点改动,每条记录40 bytes长,8k的页里能存储200条的记录,同样我们也插入80000条记录(记得关掉IO统计:SET STATISTICS IO OFF和执行计划显示,否则电脑蜗牛了-_-)。

 1 CREATE TABLE Customers3
 2 (
 3    CustomerID INT NOT NULL,
 4    CustomerName CHAR(10) NOT NULL,
 5    CustomerAddress CHAR(10) NOT NULL,
 6    Comments CHAR(5) NOT NULL,
 7    Value INT NOT NULL
 8 )
 9 GO
10
11 CREATE UNIQUE CLUSTERED INDEX idx_Customers ON Customers3(CustomerID)
12 GO
13
14 CREATE UNIQUE NONCLUSTERED INDEX idx_Test ON Customers3(Value)
15 GO
16
17
18 DECLARE @i INT = 1
19 WHILE (@i <= 80000)
20 BEGIN
21 INSERT INTO Customers3 VALUES
22 (
23    @i,
24    ‘C2‘,
25    ‘C3‘,
26    ‘C4‘,
27    @i
28 )
29
30 SET @i += 1
31 END
32 GO

这样的话,我们需要400页来存储这些数据。我们来看下临界点位置:临界点在100-133页读取的位置,也就是说通过非聚集索引,你只能读取0.125%-0.167%的数据,对于80000条数据的表来说,这几乎就是没数据你的非聚集索引毫无用处!

我们来看下临界点的2个不同查询,这里我们可以打开执行计划显示。

 1 SET STATISTICS IO ON
 2 -- 书签查找会产生332个逻辑读。
 3 SELECT * FROM Customers3
 4 WHERE Value < 157
 5 GO
 6
 7 -- 聚集索引扫描会产生419个逻辑读。
 8 -- The query produces 419 I/Os.
 9 SELECT * FROM Customers3
10 WHERE Value < 158
11 GO

我们来看第2个查询,我们只选择80000条记录的157条,我们只选择了很少的数据,但是SQL Server在这里就非常聪明,完全忽略你的的非聚集索引,使用表扫描来获取数据。但对于整个查询来说,这个非聚集索引设计并不完美,因为不是覆盖的非聚集索引,如果有人用指定索引来查找数据,就会非常恐怖:

1 SELECT * FROM Customers3 WITH(INDEX(idx_Test))
2 WHERE Value < 80001
3 GO

这个查询产生了165120个逻辑读,把聚集索引全表扫描需要的IO数直接秒杀!从这个例子我们可以看出,临界点是SQL Server里的性能保障,它阻止着使用书签查找,造成占用昂贵资源的查询发生。但这些和记录数完全无关。这2个例子里的表记录数都是80000。我们只修改了表记录的大小,因此我们就改变了表的大小,最后临界点也跟着改变,SQL Server就会忽略我们的非聚集索引。

寓意:非聚集索引,不是覆盖非聚集索引的话,在SQL Server里是非常,非常,非常,非常有选择性的用例!下次当你碰到这个情况的时候,想下你要怎么处理这个问题!

时间: 2024-08-29 00:44:59

SQL Server临界点游戏——为什么非聚集索引被忽略!的相关文章

创建非聚集索引

创建非聚集索引 SQL Server 2014 其他版本 2(共 2)对本文的评价是有帮助 - 评价此主题 您可以使用 SQL Server Management Studio 或 Transact-SQL 在 SQL Server 2014 中创建非聚集索引. 非聚集索引是一种与存储在表中的数据相分离的索引结构,可对一个或多个选定列重新排序. 非聚集索引通常可帮助您通过比搜索基础表更快的速度查找数据:有时可以完全由非聚集索引中的数据回答查询,或非聚集索引可将数据库引擎指向基础表中的行. 一般来

SQL Server 性能调优2 之索引(Index)的建立

前言 索引是关系数据库中最重要的对象之一,他能显著减少磁盘I/O及逻辑读取的消耗,并以此来提升 SELECT 语句的查找性能.但它是一把双刃剑,使用不当反而会影响性能:他需要额外的控件来存放这些索引信息,并且当数据更新时需要一些额外开销来保持索引的同步. 形象的来说索引就像字典里的目录,你要查找某一个字的时候可以根据它的比划/拼音先在目录中找到对应的页码范围,然后在该范围中找到这个字.如果没有这个目录(索引),你可能需要翻遍整本字典来找到要找的字. SQL Server 中的索引以 B-Tree

第7周 非聚集索引

上个星期我讨论了SQL Server里的聚集索引.当你在表上定义了一个聚集索引,你是物理上把你的表数据按提供的聚集键列的顺序存储.在SQL Server里,一个表只能定义一个聚集索引,非聚集索引可以定义多个(最多999个). 非聚集索引是第二索引,你可以在表上列进行定义.你也可以把非聚集索引与书比较.但是这次你把它认为类似T-SQL 语言参考的书.书本身就是一个聚集索引,不同的T-SQL命令是按它们的名字物理排序的.在书的最后,你会看到一个索引.当你查找一个T-SQL 命令(例如 CREATE

索引键的唯一性(4/4):非唯一聚集索引上的唯一和非唯一非聚集索引

在上一篇文章里,我谈了唯一聚集索引上的唯一和非唯一非聚集索引的区别.在这篇文章里,我想谈下非唯一聚集索引上的唯一和非唯一聚集索引的区别.我们都知道,SQL Server内部把非唯一聚集索引当作唯一聚集索引处理的.如果你定义了一个非唯一聚集索引,SQL Server会增加叫做uniquifier到你的索引记录,它导致你聚集索引的导航结构(B树的非叶子层)里,每条索引行都要用到4 bytes的开销. 下列代码再次创建我们的Customers表,这次在它上面定义非唯一聚集索引,最后定义2个非聚集索引,

程序员眼中的 SQL Server-非聚集索引能给我们带来什么?

 我不会说一些数据库概念,所以只能用做一些实践来理解概念的意义,以下应用场景中的用例是虚拟出来的,只是作为个人研究使用. 程序员应该有刨根问底的怪癖,虽然这是个数据库问题. 应用场景 有一个 Product 表,字段如下: 数据添加脚本: begin tran declare @index int set @index=0 while(@index<1000000) begin insert into [dbo].[Product]([Name],Remarks,ProviderID,[Ti

SQL Server中的联合主键、聚集索引、非聚集索引

我们都知道在一个表中当需要2列以上才能确定记录的唯一性的时候,就需要用到联合主键,当建立联合主键以后,在查询数据的时候性能就会有很大的提升,不过并不是对联合主键的任何列单独查询的时候性能都会提升,但我们依然可以通过对联合主键中的首列除外的其他列建立非聚集索引来提高性能.本文将对联合主键.聚集索引.非聚集索引对查询性能的影响举例说明.步骤一,建立一个测试表,并且插入350万条以上的数据. /*创建测试数据表*/create table MyTestTable(id varchar(10)not n

SQL Server索引 - 非聚集索引 &lt;第七篇&gt;

一.非聚集索引维护 非聚集索引的行定位器值保持相同的聚集索引值,即使该聚集索引列物理上重新定位后,也是如此. 为了优化这个维护开销,SQL Server添加一个指向旧数据页的指针,以在页面分割之后指向新的数据页面,而不是更新所有相关非聚集索引的行定位器.这样,虽然降低了非聚集索引的维护开销,但是增加了从非聚集索引行到数据行的导航开销,因为添加了一个旧数据页面和信数据页面之间的连接.因此,将聚集索引作为行定位器降低了非聚集索引相关的开销. 二.定义书签查找 当一个查询请求不是优化器选择的非聚集索引

SQL Server 非聚集索引的覆盖,连接,交叉和过滤 &lt;第二篇&gt;

在SQL Server中,非聚集索引其实可以看做是一个含有聚集索引的表,但相对实际的表来说,非聚集索引中所存储的表的列数要少得多,一般就是索引列,聚集键(或RID).非聚集索引仅仅包含源表中的非聚集索引的列和指向实际物理表的指针. 一.非聚集索引之INCLUDE 非聚集索引其实可以看做一个含有聚集索引的列表,当这个非聚集索引中包含了查询所需要的所有信息的时候,则就不再需要去查基本表,仅仅做非聚集索引就能够得到所需要的数据.INCLUDE实际上也能称为覆盖索引,但它不影响索引键的大小. 先来看下面

SQL Server索引 - 聚集索引、非聚集索引、非聚集唯一索引 &lt;第八篇&gt;

聚集索引.非聚集索引.非聚集唯一索引 我们都知道建立适当的索引能够提高查询速度,优化查询.先说明一下,无论是聚集索引还是非聚集索引都是B树结构. 聚集索引默认与主键相匹配,在设置主键时,SQL Server会默认在主键列创建聚集索引.但是可以手动更改为在任意一个列创建聚集索引,然后在另一个字段或多个字段上定义主键.这时主键将会被作为一个唯一的非聚集索引(唯一索引)被创建.通过指定NONCLUSTERED关键字就可以做到. CREATE TABLE MyTableKeyExample { Colu