索引和查询优化

时间:2024-04-06 22:04:26

概述

在SQL server中,一个页的最小单位占用8KB的物理空间.当一个页存满时,数据会自动存入下一个新建页.通常,数据之间都是杂乱无章的存放,并不是按照逻辑结构一一对应到物理存储上来,这样的方式称为堆放,对应的结构叫做堆.一个数据页存满而自动放入另一个数据页的行为,称为页分解.

索引是一种与表或视图关联的物理结构,可以加快检索数据的速度.创建索引的几大优势如下:

  • 创建唯一索引,可以保证数据唯一性.
  • 索引可以大大加快数据检索速度.
  • 索引可以加速表和表之间的连接,实现参考完整性方面意义重大.
  • 索引可以显著减少分组和排序的时间(执行GROUP BYORDER BY时).
  • 索引可以调用优化隐藏器,提高系统性能.

相应的,索引也有其自身的局限性:

  • 索引是物理结构,自身有空间开销.
  • 创建和维护索引需要时间.
  • 改变表/视图时,索引也需要动态维护,需要人力和时间.

因此,索引的建立是需要经过一定的考虑的,原则大概有以下几条(注意索引是建立在列上的):

  • 经常被查询的列上,应该建立索引且应该在主键上建立索引. \checkmark
  • 经常用于连接的列上,应该建立索引且应该在外键上建立索引. \checkmark
  • 经常排序的列上建立索引,通过索引可以优化排序时间. \checkmark
  • 经常作为WHERE的条件列上建立索引. \checkmark
  • 不要在很少被查询到/经常被修改的列上建立索引.
  • 不要在取值很少的列上建立索引,如性别这样的列.
  • 当某些列上UPDATEINSERTDELETE的性能远大于SELECT时,不应该建立索引.

说明: UPDATEINSERTDELETE的性能刚好和SELECT成反比,因此以上条件如成立,应该舍弃SELECT的性能.

索引的类型和特点

SQL server中有多种索引,其中聚集索引和非聚集索引是基础,在本小节我们只讨论这两种索引,顺便介绍一下堆.

堆是不含聚集索引的表,表中数据没有任何顺序.堆的信息记录在sys.partitions目录视图里.每个堆有多个分区,每个分区都有一个堆结构,每个分区在sys.partitions里占有一行,且index_id = 0,这就是说,每一个堆可能包含多个堆结构.堆的结构描述如下:

sys.system_internal_allocation_units系统视图中的first_iam_page指向堆结构中的一系列IAM(Index Allocation Map,即: 索引分配图),IAM存放着数据堆所在区域的存储信息.系统通过IAM可以在数据堆中寻找到可供新数据插入的空闲空间.通常IAM中记录的这些数据堆中含有很多数据页,这些页彼此并无联系,仅仅是通过IAM随机地使用.

聚集索引

聚集索引指的是数据表的物理结构和逻辑结构顺序相同的索引.在聚集索引中,页是有序的,应当在经常检索的/按照顺序访问的列上建立聚集索引.其中,用于指定聚集索引第一页地址信息的root_page来自于sys.system_internal_allocation_units系统视图中.

创建聚集索引时,应考虑如下因素:

  • 一个表至多至多只能有一个聚集索引.
  • 表中聚集索引行的物理顺序和表的物理顺序一致,在建立非聚集索引之前应先建立聚集索引,因为聚集索引改变表的物理顺序,并自动维护该顺序.
  • 索引的唯一性可使用UNIQUE维护,或使用一个内部标识符维护.这些标识符是系统使用的,用户无法访问.
  • 聚集索引大小随着索引列的大小而变化,平均大小是数据表的50%.
  • 对于频繁更改的列不应使用聚集索引,大量的数据变更使得聚集索引自动维护时排序时间开销很大.

说明: 索引创建过程中,系统临时使用当前数据库的磁盘空间,聚集索引需要表大小的1.2倍空间,因此务必确保空间充足.

非聚集索引

非聚集索引指的是数据表的物理结构和逻辑结构顺序不同的索引.非聚集索引可以建立在表/视图的聚集索引上,也可以建立在表/视图的堆上.由于非聚集索引表示行的逻辑结构,因此当需要以多种方式检索数据时,非聚集索引就会派上大用场.例如,某个咸鱼喜欢玩游戏,经常搜索有关游戏的书籍,并且希望能够按照游戏的原名和译名来检索.那么这时就可以以原名创建一个聚集索引,为译名创建一个非聚集索引.当创建非聚集索引时,应考虑如下情况:

  • 默认情况下,创建的索引就是非聚集索引.
  • 在每一个页上,可以创建249个非聚集索引.
  • 索引页只包含索引关键字,不包含实际数据.

其他类型的索引

除了聚集索引和非聚集索引,SQL server还有其他类型的索引,如:唯一性索引、包含性索引、索引视图、全文索引、XML索引.

创建聚集索引和非聚集索引时,索引键值既可以不同,也可以相同,若需要创建不重复的键值,则需要创建唯一性索引.当然,在创建聚集索引/非聚集索引时,都应该使用唯一性约束.这种唯一性约束和之前的主键唯一性约束是一样的,从某种意义上说,主键可以看作唯一性的聚集索引.索引中的索引列的数量和字节数是受到限制的.SQL server中,索引列的最大个数是16个,索引列字节总数不超过900字节.

一般的,把视图当作一个虚拟表看待,因为视图中没有存物理数据.但是如果希望提高视图的查询效率,则可以将视图的索引物理化,也就是将结果集永久存在索引中.视图索引的存储方法和表索引的存储方法是一样的.如果很少更新视图的基表数据,那么使用视图索引的效果更好.

全文索引是一种特殊类型的基于标记的索引.是通过SQL server的全文引擎服务创建、使用和维护,其目的是为用户提供在字符串数据中高效率地搜索复杂的词语.

访问数据的方式

  • 表扫描

表扫描是指系统将指针放在表头数据所在的数据页上,然后按照数据的排列方式,一页一页地扫描整个表所在的数据页,直到扫描完全部数据,在扫描时将符合查询条件的结果挑选出来,最后,将挑选出来的结果返回到select中去.

  • 索引查找

索引是一种树状结构,存储了关键字和包含关键字所在记录的数据页的指针.使用索引时,系统将顺着树的结构,找到符合条件的记录,并在最后全部显示出来.当系统沿着树形结构查找下去,使用搜索值和索引值依次比较,直到下面的两种情况之一发生:

  1. 搜索值\le索引值.
  2. 搜索值\ge索引页的最后一个值.

在SQL server中,由系统自动确定索引的存在,若不存在索引,则自动按照表扫描的方式进行查找,显然索引将会提升查询效率.

创建索引

直接/间接法

创建索引分为直接创建和间接创建,直接法指的是直接用命令/GUI创建索引,间接法指的是在创建其它对象时附加创建了索引.

使用CREATE INDEX语句来直接创建索引,这样创建出来的索引有很大的弹性,可以根据需求自订,这个命令我们将在后面讨论.

通过定义主键/唯一性约束,可以间接创建索引.创建主键约束时,系统自动创建了一个唯一性聚集索引.这是因为主键虽然是逻辑上的约束,但是对应的物理结构却是唯一性聚集索引.同样创建唯一性约束,也会同时创建一个唯一性非聚集索引.可见间接创建的索引类型是固定的,可供定制的空间就小了.

注意: 主键的优先级高于CREATE INDEX创建的索引,它会自动覆盖该索引.

使用CREATE INDEX语句

使用CREATE INDEX语句可以创建索引,其基本语法如下:

CREATE [UNIQUE] | [CLUSTERED | NONCLUSTERED] INDEX index_name
ON tb_name_or_view_name (COLUMN [ASC | DESC] [,...,n])
[INCLUDE (column_name[,...,n])]
[WITH
		(PAD_INDEX = {ON | OFF}) 
 		| FILLFACTOR = fillfactor 
 		| SORT_IN_TEMPDB = {ON | OFF}
 		| IGNORE_DUP_KEY = {ON | OFF}
 		| STATISTICS_NORECOMPUTE = {ON | OFF}
 		| DROP_EXISTING = {ON | OFF}
 		| ONLINE = {ON | OFF}
 		| ALLOW_ROW_LOCKS = {ON | OFF}
 		| ALLOW_PAGE_LOCKS = {ON | OFF}
 		| MAX_DROP = max_degree_of_parallelism
 		| DATA_COMPRESSION = {NONE | ROW | PAGE} [,...,n]
]
ON {partition_schema_name (column_name) | filegroup_name | default}

下面简述以上各参数的含义:

  • UNIQUE: 唯一性索引,索引值不可重复.
  • CLUSTERED: 表示创建聚集索引.
  • NONCLUSTERED: 创建非聚集索引,这是CREATE INDEX的默认值.
  • ON: 指定创建索引的表/视图的列名称,可以指定排序优先级,默认是ASC.
  • INCLUDE: 包含到非聚集索引的页中的非键值(指定列).
  • PAD_INDEX: 指定索引的中间页级,为非叶级索引页指定填充,填充由FILLFACTOR指定.
  • SORT_IN_TEMPDB: 指定用于创建索引的中间结果的排序位置,选项为ON时,在TEMPDB中排序,为OFF时在当前数据库排序.
  • IGNORE_DUP_KEY: 指定唯一性索引键冗余数据的系统行为.选项为ON时,发出警告,只有违反数据唯一性的行插入失败,为OFF时,发出错误提示并取消所有插入操作.
  • STATISTICS_NORECOMPUTE: 指定是否重新计算分发统计信息.为ON时,不自动计算,为OFF时,启动自动计算功能.
  • DROP_EXISTING: 指定是否可以删除指定的索引并重建索引.为ON时,可以删除,为OFF时,不可删除.
  • ONLINE: 指定对索引操作期间是否可以查询表,为ON时,允许查询,为OFF时,不可查询.
  • ALLOW_ROW_LOCKS: 是否使用行锁,为ON时使用行锁,为OFF不使用行锁.
  • ALLOW_PAGE_LOCKS: 是否使用页锁,为ON时使用页锁,为OFF不使用页锁.
  • MAXDOP: 指定索引操作期间覆盖最大并行度的选项,主要用于限制执行并行计划过程中使用的处理器数量.
  • DATA_COMPRESSION: 为指定的索引、分区号或分区范围指定数据压缩选项.NONE为不压缩,ROW为按行压缩,PAGE为按页压缩.

下面使用一些简单的参数来演示索引的用法:

  1. 在学生表的学号列上建立唯一聚集性索引,这是非常符合常理的做法,这么做相当于主键:
USE Temp;
GO

CREATE UNIQUE CLUSTERED INDEX StudentID ON test.Student(studentNumber);	--在学生学号列创建唯一聚合索引;
GO

运行结果如下:

索引和查询优化
  1. 此时如果要在学生姓名列建立索引,则只能建立非聚集索引(因为已存在一个聚集索引),如下:
USE Temp;
GO

CREATE UNIQUE NONCLUSTERED INDEX StudentName ON test.Student(studentName);	--在姓名列创建唯一非聚集索引;
GO

运行结果如下:

索引和查询优化

从以上示例可以看出,虽然聚集索引索引至多只能有一个,但是可以有多个唯一性索引.


  1. 还可以将非(索引)键值作为附带项加入到索引中:
USE Temp;
GO

CREATE UNIQUE NONCLUSTERED INDEX ContactInfo ON test.Student(email)
INCLUDE (phoneNumber);		--将电话号码包含在索引中;
GO

运行结果如下:

索引和查询优化

这样做,就把phoneNumber列加入到了唯一非聚集索引的页级中,在查询时先会根据email属性查询,再依照phoneNumber属性进行查询.

如果在空表上创建索引,是否使用FILLFACTOR选项和PAD_INDEX选项都是一样的.因为这种指定填充度的行为仅在创建索引和重新生成索引时生效,从这一点来看,这两个选项是静态的.

修改/删除索引

同样的,使用ALTER语句可以对索引进行修改,使用ALTER INDEX语句可以重新生成索引、重新组织或禁止索引.

  • 重新生成索引: 表示删除索引并重新生成,可以根据指定的填充度压缩页来删除碎片、回收磁盘空间、重新排列索引.
  • 重新组织索引: 表示在不删除索引的情况下整理索引碎片,其整理程度和指定的选项参数有关.
  • 禁止索引: 表示禁止用户访问索引.

使用这三种操作的语句非常简单,如下:

ALTER INDEX index_name ON tb_name_or_view_name REBUILD;
--重新组织索引;
ALTER INDEX index_name ON tb_name_or_view_name REORGANIZE;
--禁止索引;
ALTER INDEX index_name ON tb_name_or_view_name DISABLE;

当索引不再需要的时候,使用DROP INDEX删除索引,其语法如下:

DROP INDEX index_name ON tb_name_or_view_name;

删除索引时应当注意如下问题:

  • 删除索引同时也会释放该索引占用的磁盘空间.
  • 不能使用DROP INDEX删除由主键/唯一性约束列创建的索引,要删除这些索引应该解除这些约束.
  • 删除表时,该表所有的索引也被删除.
  • 删除一个聚集索引时,该表上的所有非聚集索引全部自动重建(因为表的内部顺序发生了变动).
  • 不能在系统表上使用DROP INDEX语句.

查看索引信息

下表列出了一系列目录视图和系统函数,用于查看有关索引的信息:

目录视图和系统函数名 作用
sys.indexes 查看有关索引类型、文件组、分区方案、索引选项等信息
sys.index_columns 查看列ID、索引内的位置、类型、排列顺序等信息
sys.stats 查看与索引关联的统计信息
sys.stats_columns 查看与统计信息相关联的列ID
sys.xml_indexes 查看XML索引信息,包括类型、说明等
sys.dm_db_index_physical_stats 查看索引大小、碎片统计信息等
sys.dm_db_index_operational_stats 查看当前索引和表I/O统计信息
sys.dm_db_index_usage_stats 查看按查询类型排列的索引使用情况统计信息
INDEXKEY_PROPERTY 查看索引内的索引列位置及排列情况
INDEXPROPERTY 查看元数据中存储的索引类型、级别数量和索引选项的当前值
INDEX_COL 查看索引的键列名称

其中,关于几个函数的参数用法如下(点击函数名可以查看详细文档):

sys.dm_db_index_physical_stats:

sys.dm_db_index_physical_stats (   
    { database_id | NULL | 0 | DEFAULT }  
  , { object_id | NULL | 0 | DEFAULT }  
  , { index_id | NULL | 0 | -1 | DEFAULT }  
  , { partition_number | NULL | 0 | DEFAULT }  
  , { mode | NULL | DEFAULT }  
)

sys.dm_db_index_operational_stats:

sys.dm_db_index_operational_stats (    
    { database_id | NULL | 0 | DEFAULT }    
  , { object_id | NULL | 0 | DEFAULT }    
  , { index_id | 0 | NULL | -1 | DEFAULT }    
  , { partition_number | NULL | 0 | DEFAULT }    
)

INDEXKEY_PROPERTY:

INDEXKEY_PROPERTY ( object_ID ,index_ID ,key_ID ,property )

INDEXPROPERTY:

INDEXPROPERTY ( object_ID , index_or_statistics_name , property )

INDEX_COL:

INDEX_COL ( '[ database_name . [ schema_name ] .| schema_name ]  
    table_or_view_name', index_id , key_id )

如下是使用系统视图查看索引信息的语句:

USE Temp;
GO

SELECT * from sys.dm_db_index_usage_stats
SELECT * from sys.index_columns;

运行结果如下:

索引和查询优化

维护索引

索引创建后,由于数据的增加、删除、更新等操作使得索引页产生碎块,为了提高系统性能,必须对索引进行维护.这些维护操作包括查看碎块信息、维护统计信息、分析索引性能、删除重建索引等.

查看索引统计信息

索引统计信息是查询优化器用来分析和评估查询、确定最有查询计划的基础数据.用户可通过DBCC SHOW_STATISTICS命令访问:

DBCC SHOW_STATISTICS('tb_name_or_view_name',index_name);

通过一个实例说明(查看AdventureWorks2017Person.person表的PK_Person_BusinessEntityID索引统计信息):

USE AdventureWorks2017;
GO

DBCC SHOW_STATISTICS('Person.person',PK_Person_BusinessEntityID);
GO

运行结果如下:

索引和查询优化

以上统计信息包含三部分: 统计标题信息、统计密度信息、统计直方图信息,每一部分显示的信息如下:

  • 统计标题信息: 包括表中行数、统计的抽样行数、所有索引列平均长度.
  • 统计密度信息: 包括索引列前缀集的选择性、平均长度.
  • 统计直方图信息: 指定显示直方图时的信息.

查看索引碎片信息

可以使用DBCC SHOWCONTIG命令,但是…

索引和查询优化

因此我们还是使用sys.dm_db_index_physical_stats查看索引碎片信息吧!之前说过用法,现在就只举一例说明:

USE AdventureWorks2017;
GO

DECLARE @db_id INT;
DECLARE @object_id INT;
SET @db_id = DB_ID(N'AdventureWorks2017');
SET @object_id = OBJECT_ID(N'AdventureWorks2017');

SELECT * FROM sys.dm_db_index_physical_stats(@db_id,@object_id,NULL,NULL,DEFAULT);
GO

运行结果如下:

索引和查询优化

维护索引统计信息

统计信息是存储在SQL server中的列数据样本.这些数据一般用于索引列(也可以用于非索引列).SQL server通过维护索引统计信息,从而判断在某次查询中哪个索引是有用的,以此提高查询效率.索引统计信息越准确,查询优化就越理想.

当表中数据发生变化时,SQL server会自动修改统计信息.由于是一个抽样统计数据,因此只有当表中数据变化的量达到一定程度,才可能触发SQL server自动修改的阀值.50行数据变更则统计数据可能保持不变,而1000行数据变更就导致统计数据发生变化.

除了前面说的SQL server自动生成和维护的统计信息以外,用户也可以手动创建/维护统计信息,使用如下命令即可:

CREATE STATISTICS [statistics_name ON] tb_name_or_view_name [(column [ ,...,n ])];
--手动维护索引统计信息;
UPDATE STATISTICS tb_name_or_view_name [ (index_or_statistics_name) ];

手动创建/维护索引统计信息,都可以指定到某个具体的索引,不指定则默认对目标表中的所有索引都进行操作.看一个例子:

USE Temp;
GO
UPDATE STATISTICS test.Student (ContactInfo);
GO

更多关于手动创建/维护索引统计信息的文档,请移步:UPDATE STATISTICSCREATE STATISTICS.

查询优化

很多种情况下,为达到同样的效果,可以写出多种查询形式,但它们的效率却可能天差地别,这就好比O(nm)的方法匹配模式串和KMP算法匹配模式串的差距一样.为此,我们应该学习SQL server查询优化器和优化隐藏的特性.

在查询语句中,SQL server通过统计信息得到索引的选择性和索引类型,从而决定使用索引的优先顺序.一般的,选择性高的索引,指那些只有很少几行数据被选中(区分度高,查找效率高)的索引.下面通过一个例子说明系统如何选择这样的索引:

USE ElecTravelCom;
GO
SELECT * FROM books WHERE price > 20  AND publishDate > '2015-01-01';
GO

假设系统中有如下统计信息:

表中行数: 10000

对于books.Title列(非空值),估计的行数: 3000

对于books.publishDate列(非空值),估计的行数: 200

那么Title的选择性是: 3000/10000 = 30%,publishDate的选择性是: 200/10000 = 0.2%.publishDate的选择性远远高于Title的选择性(注意这里的"高"遵循的是越小越好的原则),那么查询时查询优化器将优先基于publishDate列生成的索引进行查询.


下面说一下优化隐藏的特点:

在SQL server中,连接是非常耗时间的,因此系统为我们提供了三种连接方式,即LOOPMERGEHASH.

LOOP连接中,外表中的每一行,都和内表进行比较,这样的连接是很耗时的,对外表的每一行,都要扫描一遍内表.只有当内表的连接列上有索引时,这种连接才有效果,其实现思路如下:

FOR 外表中的每一行
	把该行读入临时表A中
	FOR 内表中的每一行
		把该行读入临时表B中
		IF A.join_column = B.join_column
			该行满足条件,将其加入结果集中
		END IF
	END FOR
END FOR

如果连接中的外表和内表中的数据是按照列进行物理存储的,那么使用MERGE是一种很好的连接方式.这时查询优化器不需要索引.如果两个表中的数据都没有排序,则请不要使用MERGE连接方式,其实现思路如下:

按照连接列升序(降序)排列外表数据
按照连接列升序(降序)排列内表数据
FOR 外表中的每一行
	把该行读入临时表A中
	FOR内表中值小于等于连接列的每一行
		把该行读入临时表B中
		IF A.join_column = B.join_column
			该行满足条件,将其放入结果集中
		END IF
	END FOR
END FOR

如果连接中的两个表没有任何顺序,且都没有索引,那么可以考虑HASH连接方式.在这种方式中,系统使用散列函数对连接列计算出散列值,再按照散列值进行连接查询.

小结

在数据库原理中我们学习到: 索引是用于查询优化的物理结构,它的性质有:

  • 由用户直接/间接创建,是一个物理结构.
  • 自动维护,系统在查询时自动寻找合适的索引,除了创建和删除索引时,用户很少或根本无法接触到.

通过今天学习SQL server的索引,我们应该能够深刻体会到这两个性质,原因如下:

  • 索引创建时占用系统磁盘空间.
  • 虽然用户可以进行维护,但是无法指定查询时使用哪个索引,实际上用户干预的空间很小,索引基本都处于自我维护状态.

上一篇: 操纵数据
下一篇: 数据完整性