文章目录
概述
在SQL server中,一个页的最小单位占用8KB的物理空间.当一个页存满时,数据会自动存入下一个新建页.通常,数据之间都是杂乱无章的存放,并不是按照逻辑结构一一对应到物理存储上来,这样的方式称为堆放,对应的结构叫做堆.一个数据页存满而自动放入另一个数据页的行为,称为页分解.
索引是一种与表或视图关联的物理结构,可以加快检索数据的速度.创建索引的几大优势如下:
- 创建唯一索引,可以保证数据唯一性.
- 索引可以大大加快数据检索速度.
- 索引可以加速表和表之间的连接,实现参考完整性方面意义重大.
- 索引可以显著减少分组和排序的时间(执行GROUP BY或ORDER BY时).
- 索引可以调用优化隐藏器,提高系统性能.
相应的,索引也有其自身的局限性:
- 索引是物理结构,自身有空间开销.
- 创建和维护索引需要时间.
- 改变表/视图时,索引也需要动态维护,需要人力和时间.
因此,索引的建立是需要经过一定的考虑的,原则大概有以下几条(注意索引是建立在列上的):
- 经常被查询的列上,应该建立索引且应该在主键上建立索引.
- 经常用于连接的列上,应该建立索引且应该在外键上建立索引.
- 经常排序的列上建立索引,通过索引可以优化排序时间.
- 经常作为WHERE的条件列上建立索引.
- 不要在很少被查询到/经常被修改的列上建立索引.
-
不要在取值很少的列上建立索引,如
性别
这样的列. - 当某些列上UPDATE、INSERT、DELETE的性能远大于SELECT时,不应该建立索引.
说明: UPDATE、INSERT、DELETE的性能刚好和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中去.
- 索引查找
索引是一种树状结构,存储了关键字和包含关键字所在记录的数据页的指针.使用索引时,系统将顺着树的结构,找到符合条件的记录,并在最后全部显示出来.当系统沿着树形结构查找下去,使用搜索值和索引值依次比较,直到下面的两种情况之一发生:
- 搜索值索引值.
- 搜索值索引页的最后一个值.
在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为按页压缩.
下面使用一些简单的参数来演示索引的用法:
- 在学生表的学号列上建立唯一聚集性索引,这是非常符合常理的做法,这么做相当于主键:
USE Temp;
GO
CREATE UNIQUE CLUSTERED INDEX StudentID ON test.Student(studentNumber); --在学生学号列创建唯一聚合索引;
GO
运行结果如下:
- 此时如果要在学生姓名列建立索引,则只能建立非聚集索引(因为已存在一个聚集索引),如下:
USE Temp;
GO
CREATE UNIQUE NONCLUSTERED INDEX StudentName ON test.Student(studentName); --在姓名列创建唯一非聚集索引;
GO
运行结果如下:
从以上示例可以看出,虽然聚集索引索引至多只能有一个,但是可以有多个唯一性索引.
- 还可以将非(索引)键值作为附带项加入到索引中:
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 ( object_ID ,index_ID ,key_ID ,property )
INDEXPROPERTY ( object_ID , index_or_statistics_name , property )
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);
通过一个实例说明(查看AdventureWorks2017
的Person.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 STATISTICS、CREATE 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中,连接是非常耗时间的,因此系统为我们提供了三种连接方式,即LOOP、MERGE、HASH.
在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的索引,我们应该能够深刻体会到这两个性质,原因如下:
- 索引创建时占用系统磁盘空间.
- 虽然用户可以进行维护,但是无法指定查询时使用哪个索引,实际上用户干预的空间很小,索引基本都处于自我维护状态.