前文阐述了创建索引要注意的索引宽度、索引顺序、索引字段的唯一值比例、索引字段的数据类型选择等,本文将重点说明索引类型的选择问题。
SQL Server 2012之前主要的索引为行索引,即我们常见的聚集索引和非聚集索引,SQL Server 2012及以后,增加列索引,包括聚集列存储索引和非聚集列存储索引。列索引主要使用在数据仓库中,不在本文考虑范围之内。下面我们主要考虑的行索引的聚集索引和非聚集索引。
聚集索引和非聚集索引都是B-tree结构。两者的主要不同之处在于聚集索引的叶页是表的数据页,和它们指向的数据顺序相同。这意味着聚集索引实质就是表。随着研究的深入,当你决定使用何种类型的索引时,你将会发现两种索引叶级别的不同变得非常重要。
聚集索引的叶页和聚集索引所在表的数据页是相同的。因为这样,表行按照聚集索引列物理排序,并且因为表数据只能有一个物理顺序,一个表仅仅只能有一个聚集索引。
注意:当你创建一个主键约束时,如果表上还没有聚集索引,或者没有显示的指定索引为唯一的非聚集索引时,SQL Server 在主键上自动创建一个唯一的聚集索引。这个是不需要的,仅仅是默认行为,你可以在创建表之前改变它。
如前文所述,一个没有聚集索引的表被称为堆表。堆表的数据行不是按照任何特定顺序存储的,或者链接表中的临近页。相比于大的非堆表(有聚集索引的表),堆表的这种无组织的结构通常会增加大的堆表的读取开销。
堆表dbcc ind()查询结果
创建非聚集索引后dbcc ind()堆表查询结果
使用dbcc page 查看非聚集索引节点9328页数据情况如下:
从上面非聚集索引页的查询结果可以看到,非聚集索引的键(Num)指向数据页的指针HEAP RID,HEAP RID 组成为28240000:页编号;0300 文件编号,0000 slot编号
HEAP RID(key):00002428=2*16^3+4*16^2+2*16+8=9256,非聚集索引中的两条记录均指向数据页9256;0000表示第一条数据存储在9256页第0个slot,0100表示第二条数据存储在9256页第一个slot;0300表示文件编号;
将HEAP RID 转换为 文件编号:页编号:槽编号的脚本如下:
DECLARE @HeapRid BINARY(8)
SET @HeapRid = 0x2824000003000000
SELECT
CONVERT (VARCHAR(5),
CONVERT(INT, SUBSTRING(@HeapRid, 6, 1)
+ SUBSTRING(@HeapRid, 5, 1)))
+ ':'
+ CONVERT(VARCHAR(10),
CONVERT(INT, SUBSTRING(@HeapRid, 4, 1)
+ SUBSTRING(@HeapRid, 3, 1)
+ SUBSTRING(@HeapRid, 2, 1)
+ SUBSTRING(@HeapRid, 1, 1)))
+ ':'
+ CONVERT(VARCHAR(5),
CONVERT(INT, SUBSTRING(@HeapRid, 8, 1)
+ SUBSTRING(@HeapRid, 7, 1)))
AS 'Fileid:Pageid:slot'
先创建非聚集索引,再创建聚集索引后发现,原数据页变为聚集索引页,同时会发现无论原堆页的编号(Page PID)还是非聚集索引页的编号(Page PID)均发生了变化。这是为何一个表最先需要考虑创建聚集索引的原因(创建聚集索引时,索引索引都会重新创建)。
创建聚集索引后,再查看索引页,发现,索引页指向数据指针变为聚集键(ID(key))
在聚集索引ID值1、3中间插入2
INSERT INTO test VALUES(2,3,'c');
可以看到插入新数据后,数据的顺序和数据页的物理顺序不一致,从而产生了索引碎片。
没有聚集索引的表,非聚集索引的HEAP RID可以直接定位数据所在的文件、页、槽;当在表上创建聚集索引后,非聚集索引是通过聚集索引键间接定位到数据所在的文件、页
聚集索引才会导致数据页拆分,导致数据页的物理顺序和逻辑顺序不一致,即产生内部索引碎片;堆表不会改变数据页的逻辑顺序;非聚集索引页的拆分,会产生非聚集索引页的索引碎片
因为所有非聚集索引在索引行中均包含聚集索引键,聚集索引和非聚集索引的创建顺序很重要。例如,如果非聚集索引先于聚集索引创建,那么非聚集索引的行定位将包含对应表的RID指针。然后创建聚集索引,将修改所有非聚集索引,包含聚集索引键,作为其新的定位值。这将引起所有非聚集索引重建。
考虑到最优性能,我建议在创建任何非聚集索引前,先创建聚集索引。这对最终的性能将没有影响,但是创建索引本身可能就需要大量的工作。
因为所有非聚集索引都以聚集索引键作为其行定位符,为了获得最好的性能,保持聚集索引的键的字节数尽可能小。例如,如果你创建一个宽的聚集索引,如CHAR(500),这将使得每个非聚集索引增加500字节。因此,保证聚集索引的键列尽可能少,仔细考虑聚集索引包含的每一列的大小。INTEGER类型数据通常是聚集索引的较优候选列,而字符数据类型列是次优选择。
大的聚集索引键列,不仅影响其本身的宽度,也加宽了表上所有非聚集索引。这增加了表上所有索引的页数,增加了逻辑读和硬盘I/O的需求。
因为非聚集索引和聚集索引的依赖关系,使用DROP INDEX 和CREATE INDEX语句重建聚集索引,将造成所有非聚集索引重建两次。为避免这种情形,使用CREATE INDEX的DROP_EXISTING语句在同一个步骤中重建聚集索引。同样也可以在重建非聚集索引时使用DROP_EXISTING 语句。
在特定情形下,使用聚集索引很有帮助。我将在下面的段落中讨论这些使用聚集索引的情景。
因为聚集索引的叶页和表的数据页相同,聚集索引列的顺序不仅对聚集索引进行排序,同时也对数据行的物理顺序进行了排序。如果数据行的物理顺序和请求的数据顺序一致,那么磁头可以顺序读取所有数据行,不需要太多的磁头移动。例如,如果一个查询请求属于数据库组中的所有雇员记录,并且Employees表在Group列有一个聚集索引,则索引相关雇员的行将会在磁盘上被物理的分配在一起。这运行磁头从第一行位置开始移动,然后用最少的物理移动磁头,以电子方式顺序读取所有数据。反之,如果数据行不是以正确的物理方式存储在磁盘上,磁头必须随机的从一个位置移动到另一个位置,获取相关的数据行。因为磁头的物理移动占用了磁盘操作大部分消耗,以合适的物理顺序存储在磁盘上(使用聚集索引),有利于优化I/O消耗。
一个范围的数据是在关系型系统中频繁的读取另外一个表的外键。这个数据依赖于应用的读取机制,是聚集索引很好的候选者。
当检索的数据需要排序时,使用聚集索引特别有效。如果你在一个或几个需要排序的列上创建聚集索引,则数据行 物理存储会按照那个顺序,减少了数据检索后排序的开销。
下面我们用一个实例来说明聚集索引对范围查找、排序查询的影响
IF (SELECT OBJECT_ID('od',N'U')) IS NOT NULL
DROP TABLE od;
GO
SELECT *
INTO dbo.od
FROM Purchasing.PurchaseOrders;
没有建聚集索引之前的范围查找od.PurchaseOrderID BETWEEN 500 AND 510
SET STATISTICS IO ON;
SELECT * FROM dbo.od
WHERE od.PurchaseOrderID BETWEEN 500 AND 510
没有建聚集索引之前的排序查找ORDER BY PurchaseOrderID DESC;
SELECT * FROM dbo.od
WHERE od.PurchaseOrderID BETWEEN 500 AND 510
ORDER BY PurchaseOrderID DESC;
CREATE CLUSTERED INDEX od_cl_POID ON od(PurchaseOrderID);
创建聚集索引后的范围查找:
创建聚集索引后的排序查找
比较创建聚集索引后的逻辑读均显著下降,并且创建聚集索引的排序查找,不需要再使用临时表(worktable)进行额外的排序。
现在把聚集索引删除,创建非聚集索引
DROP INDEX od_cl_POID ON dbo.od;
CREATE NONCLUSTERED INDEX od_ix_POID ON od(PurchaseOrderID);
并运行对应的查询,结果如下:
比较聚集索引、非聚集索引的逻辑读的情况,很明显,聚集索引下的范围查找、排序查找的性能都显著优于非聚集索引下的范围查找、排序查找
在某些特定的情形下,最好不要使用聚集索引,我将在下面讨论这些情形。
如果聚集索引列频繁更新,这将导致所有非聚集索引的行定位符随之更新,显著增加了相关语句的开销。这也会通过阻塞引用表和那个期间的非聚集索引相同内容而影响数据库的并发。
为了理解UPDATE语句的消耗,通过对比表上的非聚集索引,获得更新的性能增加因表中聚集索引键列而增加的。考虑如下例子。Sales.SpecialOfferProduct表在主键上有一个聚集索引,其也是两个不同表的外键;这是一个典型的many-to-many联查。在这个例子中,我使用下面的语句,更新聚集索引键列中的一列(注意,使用事务,保证测试数据完整)。
USE AdventureWorks2016CTP3
BEGIN TRANSACTION;
SET STATISTICS IO ON;
UPDATE Sales.SpecialOfferProduct
SET ProductId=345
WHERE SpecialOfferID=1
AND productid=720;
SET STATISTICS IO OFF;
ROLLBACK TRANSACTION;
如果你在表上增加一个非聚集索引,你将看到读在增长
CREATE NONCLUSTERED INDEX ixTest ON Sales.SpecialOfferProduct(ModifiedDate);
正如你所看到的,由更新聚集索引引起的读的数量因为其他的非聚集索引而增加。最后删除测试索引
DROP INDEX ixTest ON Sales.SpecialOfferProduct;
因为所有非聚集索引以聚集键作为其行定位符,考虑到性能因素,你应该避免创建宽列聚集索引,或者在多列上创建聚集索引。像前面所描述的,聚集索引应该尽可能的窄。
如果你想要并发的增加许多行新数据,那么,如果将其分布在表的不同数据页上,可能会提高性能。然而,如果你按照同一个顺序增加列,即按照聚集索引强加的顺序,那么所有的插入将试图写入表的最后一个数据页。这可能造成在对应磁盘扇形区域内出现大量的热点(hot spot)。为避免磁盘热点,你就不应该安排数据行逻辑顺序和其物理顺序相同。插入可以在整个表中随机进行,方法是在另一列上创建一个聚集索引,该索引不按与新行相同的顺序排列行。这个问题仅仅针对大量并发插入的情形。
这个建议有一个警告。允许在表底部插入,阻止了为适合新行而导致中间页的分页。如果并发插入数量较低,那么排序数据(使用聚集索引),按照新行的顺序将防止中间页分页。然而,如果磁盘热点变为性能瓶颈,那么新行可以调整到中间页,通过减少表填充因子减少页拆分。另外,热点页将会在内存中,这也有利于性能提升。
如果喜欢,可以搜索关注 MSSQLServer 公众号,将有更多精彩内容分享: