在SQL server中,一个页的最小单位占用8KB的物理空间.当一个页存满时,数据会自动存入下一个新建页.通常,数据之间都是杂乱无章的存放,并不是按照逻辑结构一一对应到物理存储上来,这样的方式称为堆放,对应的结构叫做堆.一个数据页存满而自动放入另一个数据页的行为,称为页分解.
索引是一种与表或视图关联的物理结构,可以加快检索数据的速度.创建索引的几大优势如下:
相应的,索引也有其自身的局限性:
因此,索引的建立是需要经过一定的考虑的,原则大概有以下几条(注意索引是建立在列上的):
性别
这样的列.说明: 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
维护,或使用一个内部标识符维护.这些标识符是系统使用的,用户无法访问.说明: 索引创建过程中,系统临时使用当前数据库的磁盘空间,聚集索引需要表大小的1.2倍空间,因此务必确保空间充足.
非聚集索引指的是数据表的物理结构和逻辑结构顺序不同的索引.非聚集索引可以建立在表/视图的聚集索引上,也可以建立在表/视图的堆上.由于非聚集索引表示行的逻辑结构,因此当需要以多种方式检索数据时,非聚集索引就会派上大用场.例如,某个咸鱼喜欢玩游戏,经常搜索有关游戏的书籍,并且希望能够按照游戏的原名和译名来检索.那么这时就可以以原名创建一个聚集索引,为译名创建一个非聚集索引.当创建非聚集索引时,应考虑如下情况:
除了聚集索引和非聚集索引,SQL server还有其他类型的索引,如:唯一性索引、包含性索引、索引视图、全文索引、XML索引.
创建聚集索引和非聚集索引时,索引键值既可以不同,也可以相同,若需要创建不重复的键值,则需要创建唯一性索引.当然,在创建聚集索引/非聚集索引时,都应该使用唯一性约束.这种唯一性约束和之前的主键唯一性约束是一样的,从某种意义上说,主键可以看作唯一性的聚集索引.索引中的索引列的数量和字节数是受到限制的.SQL server中,索引列的最大个数是16个,索引列字节总数不超过900字节.
一般的,把视图当作一个虚拟表看待,因为视图中没有存物理数据.但是如果希望提高视图的查询效率,则可以将视图的索引物理化,也就是将结果集永久存在索引中.视图索引的存储方法和表索引的存储方法是一样的.如果很少更新视图的基表数据,那么使用视图索引的效果更好.
全文索引是一种特殊类型的基于标记的索引.是通过SQL server的全文引擎服务创建、使用和维护,其目的是为用户提供在字符串数据中高效率地搜索复杂的词语.
表扫描是指系统将指针放在表头数据所在的数据页上,然后按照数据的排列方式,一页一页地扫描整个表所在的数据页,直到扫描完全部数据,在扫描时将符合查询条件的结果挑选出来,最后,将挑选出来的结果返回到select中去.
索引是一种树状结构,存储了关键字和包含关键字所在记录的数据页的指针.使用索引时,系统将顺着树的结构,找到符合条件的记录,并在最后全部显示出来.当系统沿着树形结构查找下去,使用搜索值和索引值依次比较,直到下面的两种情况之一发生:
在SQL server中,由系统自动确定索引的存在,若不存在索引,则自动按照表扫描的方式进行查找,显然索引将会提升查询效率.
创建索引分为直接创建和间接创建,直接法指的是直接用命令/GUI创建索引,间接法指的是在创建其它对象时附加创建了索引.
使用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}
下面简述以上各参数的含义:
下面使用一些简单的参数来演示索引的用法:
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;
删除索引时应当注意如下问题:
下表列出了一系列目录视图和系统函数,用于查看有关索引的信息:
目录视图和系统函数名 | 作用 |
---|---|
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);
通过一个实例说明(查看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的索引,我们应该能够深刻体会到这两个性质,原因如下:
上一篇: 操纵数据
下一篇: 数据完整性