SQL Server索引管理——索引创建建议和经验

SQL Server索引管理——索引创建建议和经验

 

索引创建的建议

  • 检查WHERE语句和JOIN关联列

  • 使用窄索引

  • 检查列的唯一值(基数)

  • 考虑列的顺序

  • 考虑索引类型(行索引 VS. 列索引;聚集索引 VS 非聚集索引)

如果一个表的数据较少,小于8KB,所有数据在一页上,那么表扫描可能比索引查找更适合

使用窄索引

你可以使用表中的多列组合创建索引。为获取最好的性能,使索引中所包含的列尽可能的少。你也应该避免在索引中国使用宽数据类型列。拥有数据类型(CHAR,VARCHAR、NCHAR、NVARCHAR)的列,有时会很宽,就像二进制一样;除非它们绝对需要,否则尽量减少在索引中使用大尺寸的宽数据类型列。

相比较于宽列索引,窄列索引在一个8KB的页中,可以容纳 更多的行,这有如下影响:

  • 减少I/O(通过读取较少8KB页)

  • 使数据库缓存更有效,因为SQL Server可以缓存较少的索引页,因此可以减少内存中索引页所需要的逻辑读

  • 减少数据库的存储空间

为理解窄列索引如何影响逻辑读,创建一个有20行数据,和一个索引,脚本如下:

IF(SELECT OBJECT_ID('t1')) IS NOT NULL
       DROP TABLE dbo.t1;
GO
CREATE TABLE dbo.t1(c1 INT, c2 INT);
WITH Nums AS
       (SELECT 1 AS n
        UNION ALL
        SELECT n+1
        FROM Nums
        WHERE n<20
        )
        INSERT INTO t1(c1,c2)
        SELECT n,2 FROM Nums;
CREATE INDEX i1 ON t1(c1);

因为索引列为窄列(INT数据类型为4字节),所有索引行可以容纳在一个8KB的索引页中。可以使用如下动态视图确认这个结论:

SELECT
       i.name
       ,i.type_desc
       ,s.page_count
       ,s.record_count
       ,s.index_level
       ,s.index_depth
FROM sys.indexes i
JOIN sys.dm_db_index_physical_stats(DB_ID(N'WideWorldImporters'),OBJECT_ID(N'dbo.t1'),NULL,NULL,'DETAILED') AS s
       ON i.index_id=s.index_id
WHERE i.[object_id]=OBJECT_ID(N'dbo.t1');

为了理解宽列索引的缺点,修改索引列c1的类型,将其由INT改为CHAR(500):

DROP  INDEX t1.i1;
ALTER TABLE t1 ALTER COLUMN c1 CHAR(500);
CREATE INDEX i1 ON t1(c1);

INT类型数据的宽度是4字节,CHAR(500)数据类型的宽度是500字节。因为索引列的较大的宽度,需要两个索引页来容纳20个索引行。你可以通过再次运行上面动态视图脚本进行确认,结果如下:

大的索引键尺寸增加了索引页数,因此增加了索引所需要的内存和硬盘。所以总是建议索引键列尽可能窄。

检查列的唯一值

在小范围可能值的列上创建索引(如性别),对性能没有益处,因为查询优化器将不能使用这种索引有效的减少返回的行数。考虑性别列,仅仅只有两个唯一值:男和女。当你执行一个在WHERE中有性别列作为筛选条件的语句时,最终会从表中获得大量的行(假定男女分布是平均的),导致了代价昂贵的表扫描或者索引扫描。总是选择WHERE语句中,有较多唯一行的列(high selectivity),来限制读取的行数。你应该在那些列上创建索引,帮助优化器读取较少的结果集。

进一步,当在多个列上创建索引时,通常被称为组合索引列顺序问题。在某些情况下,使用最多唯一值的列作为索引的第一列,将更有效的过滤数据。

注意:组合索引列顺序的重要性将在后面的“考虑列顺序”部分解释。

从这点上,你可以看到,了解需要创建索引的列的唯一值的多少是很重要的,你可以通过执行类似如下脚本,获得相关的信息,只需要调整表名和列名即可:

SELECT
       COUNT(DISTINCT Gender) AS DistinctColValuse
       ,COUNT(Gender) AS NumberOfRows
       ,COUNT(DISTINCT Gender)*1.0/COUNT(Gender)
FROM [HumanResources].[Employee];

WHERE语句或者JOIN关系中,拥有最多唯一值的列将是索引最好的候选者。

为了理解唯一值是如何影响索引的使用的,看一下HumanResources.Employee 表的Gender列,如果你运行前面的查询,你将看到其有290行,只包含2个唯一值,selectivity 为 0.006,一个仅仅查询Gender值为F的查询如下:

SELECT * FROM HumanResources.Employee with(index(IX_Employee_Test))
WHERE SickLeaveHours=59
       AND Gender='F'
       AND MaritalStatus='M';

执行计划和IO消耗如下:

数据通过扫描聚集索引(数据存储)获得适合条件 Gender='F' 的值。如果你在Gender上创建索引如下:

CREATE INDEX IX_Employee_Test ON HumanResources.Employee(Gender)

并再次运行上面的查询,其执行计划不变。列中数据的selectivity不足够支撑索引被使用,独立使用。如果使用下面的组合索引

CREATE INDEX IX_Employee_Test ON HumanResources.Employee(Gender,SickLeaveHours,MaritalStatus)
with(drop_existing=on);

再次执行查询,执行计划及IO消耗如下:

现在效果要比聚集索引扫描好的多了。一个较为清晰索引查找操作将搜集数据的IO操作将近减半。其他的都花费在键查找上。

尽管问题中没有一个单独的列具有足够的selectivity,作为有效的索引,但他们结合在一起,为优化器提供了足够的selectivity,从而采用他们提供的索引。

有必要强制使用第一个创建的测试索引,如果你删除组合索引,再次创建最初的索引,并修改查询,使用查询提示,强制使用最初的索引,如下:

SELECT * FROM HumanResources.Employee with(index(IX_Employee_Test))
WHERE SickLeaveHours=59
       AND Gender='F'
       AND MaritalStatus='M';

你看到同样是索引查找,但是逻辑读次数多了近20倍。尽管强制优化器选择索引是可能的,很清晰其不是最优的方法。

在SQL Server 2008以上另一个强制的不同行为是 FORCESEEK 查询提示,FORCESEEK使得优化器仅选择查找的操作。如果我们像下面重写查询:

SELECT * FROM HumanResources.Employee with(FORCESEEK)
WHERE SickLeaveHours=59
       AND Gender='F'
       AND MaritalStatus='M';

结果和强制使用索引一样

限制优化器的选项,强制行为在某些情形可能有帮助,但是通常,如这里的结果,其增加了读,对整个查询无益。

检查数据类型

索引数据类型问题,如,整数键上的索引搜索很快,因为其尺寸较小,并且容易在整数(INT)上进行算术运行。你也可以使用其他类型的整数类型,如BIGINT,SMALLINT和TINYINT作为索引列,然而字符类型,如CHAR、VARCHAR、NCHAR和NVARCHAR ,因为其需要字符匹配,通常消耗要比整数要高。

假设你想要在一个列上创建索引,你有两个候选列:一个是INT数据类型,另外一个是CHAR(4)数据类型。尽管在SQL Server中,两个类型均占用4个字节,你将仍然选择整数列作为索引列。以算术运算操作为例。CHAR(4)数据类型中的1,实际上是以1开头,后面跟三个空格的方式存储的,是这样四个字节的结合:0x35、0x20、0x20、0x20. CPU不能理解这样数据如何进行算术操作,因此,首先将其转化为整数数据类型,1在整数类型下是这样存储的 0x00000001.CPU可以很容易的对这类数据执行算术操作。

当然,大多数时候,你没有这种在数据类型尺寸相等的情况下进行简单最优选择。当设计或创建你的索引时,考虑这个情况。

考虑列顺序

索引键先按照第一列进行排序,然后再按照下一列包含于前一列的值进行排序。组合索引中的第一列通常被称为索引的leading edge。例如,考虑如下表:

如果在表上创建组合索引(c1,c2),那么索引将排如下:

如上表展示的,数据首先按照组合索引中第一列c1列进行排序。第一列中的各值,数据在按照第二列(c2)进行排序。

因此,组合索引中列的顺序是一个影响索引效率的重要因素。你可以这样考虑:

  • 列唯一值

  • 列宽

  • 列数据类型

例如,假设在表t1上的查询均和下面的类似:

SELECT * FROM t1 WHERE c2=12;
SELECT * FROM t1 WHERE c2=12 AND c1=11;

一个如(c2,c1)的索引,将对两个查询都有益。但是索引(c1,c2)将不适合,因为它将按照c1列进行初始化排序,然而第一个查询语句需要数据按照c2列进行排序。

为了理解索引列顺序的重要性,考虑如下例子

SELECT COUNT(OrderQty) NumberOfOrderQtyRows
     ,COUNT(DISTINCT OrderQty) DistinctOrderQtyColValuse
     ,COUNT(DISTINCT OrderQty)*1.0/COUNT(OrderQty) OrderQtySelectivity
    ,COUNT(ProductID) NumberOfProductIDRows
     ,COUNT(DISTINCT ProductID) DistinctProductIDValuse
     ,COUNT(DISTINCT ProductID)*1.0/COUNT(ProductID) SalesProductIDSelectivity
FROM [Sales].[SalesOrderDetail];

WHERE语句或者JOIN关系中,拥有最多唯一值的列将是索引最好的候选者。

为了理解唯一值是如何影响索引的使用的,看一下[Sales].[SalesOrderDetail]表的QrderQty列,如果你运行上面的脚本,你将看到,它包含41个唯一值,121317行数据,其selectivity是0.0003.而ProductID列包含266个唯一值,其selectivity为0.002,一个包含QrderQty,ProductID列条件查询如下:

SELECT  *
FROM [Sales].[SalesOrderDetail]
WHERE ProductID=714 AND OrderQty<=10 and OrderQty>5  ;

其查询计划如为:

在表上创建索引如下:

CREATE INDEX IX_SalesOrderDetail_Test ON [Sales].[SalesOrderDetail](OrderQty,ProductID)

再次执行查询脚本,执行计划及开销如下:

CREATE INDEX IX_SalesOrderDetail_Test ON [Sales].[SalesOrderDetail](ProductID,OrderQty)
WITH(DROP_EXISTING=ON);

可以看到逻辑读的次数有所降低。《SQL Server 索引优化—— 查询条件中等于、大于或小于条件在索引中的顺序对性能的影响》一文中阐述了索引顺序对性能影响的另一种形式,有兴趣者可以参考。

对于索引类型的选择,将在后续文章中给出,敬请期待……

如果喜欢,可以搜索关注 MSSQLServer 公众号,将有更多精彩内容分享:

                                                                 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章