sqlSERVER聚集索引与非聚集索引的再次研究(上)
上篇主要说聚集索引
下篇的地址:SQLSERVER聚集索引与非聚集索引的再次研究(下)
由于本人还是
sqlSERVER菜鸟一枚,
加上一些实验的逻辑严谨性,
单写《sqlSERVER聚集索引与非聚集索引的再次研究(上)》就用了12个小时,两篇文章加起来最起码写了20个小时,
本人非常非常用心的努力完成这两篇
文章,希望各位看官给点意见o(∩_∩)o
为了搞清楚索引内部工作原理和结构,真是千头万绪,这篇文章只是作为参考,里面的观点不一定正确
有一些问题,msdn里,网上的文章里,博客园里都有提到,但是这些问题的答案是正确的吗?其实有时候我自己都想知道答案
比如,画聚集索引的图,有一些人用表格来表示,但是他们正确吗?
以前知道聚集索引 非聚集索引是B树 二叉树结构,又知道执行计划图标很像二叉树很传神,但是还是觉得很抽象
这篇文章写完以后还是比较抽象但是最起码比以前清晰一些了


有很多问题不知道为什么,但是MSDN就是这样说的,既然说得这麽模糊不如自己做一下实验,验证一下MSDN的内容吧o(∩_∩)o
--------------------------------------------华丽的分割线---------------------------------------------
先来看一下索引的结构,文章里面的一些结构图都是自己画的一些草图,本人自认画得非常烂,希望各位看官谅解o(∩_∩)o



----------------------------------------------华丽的分割线---------------------------------------------------------
先创建一个表,保存DBCC IND的结果
1 CREATE TABLE DBCCResult (
2 PageFID NVARCHAR(200), 3 PagePID 4 IAMFID 5 IAMPID 6 ObjectID 7 IndexID 8 PartitionNumber 9 PartitionID 10 iam_chain_type 11 PageType 12 IndexLevel 13 NextPageFID 14 NextPagePID 15 PrevPageFID 16 PrevPagePID 200)
17 )
创建一个聚集索引表
1 --只有聚集索引
2 TABLE Department(
3 DepartmentID int IDENTITY(1,1) NOT NULL PRIMARY KEY,128)">4 Name 200) NULL,128)">5 GroupName 6 Company 300),128)">7 ModifiedDate datetime NULL DEFAULT (getdate())
8 )
插入10W条记录
INSERT INTO Department(name,[Company],groupname) VALUES('销售部',0)">中国你好有限公司XX分公司销售组')
GO 100000
将DBCC IND的结果放入DBCCRESULT表
INTO DBCCResult EXEC (DBCC IND(pratice,Department,-1) ')
查询Department表中的页面情况
先说明一下:
PageType 分页类型: 1:数据页面;2:索引页面;3:Lob_mixed_page;4:Lob_tree_page;10:IAM页面
IndexID 索引ID: 0 代表堆,1 代表聚集索引,2-250 代表非聚集索引 ,大于250就是text或image字段
红色框部分都是需要关注的

第一个:IAM页不是只有堆表才有也不只是维护堆表中的数据页的连续,有索引的表都有,所以IAM页不只维护数据页,也维护索引页的连续,在下篇说到非聚集索引的时候
我会给出MSDN的解释和IAM页在聚集索引表,非聚集索引表中的情况
第二个:每个数据页的IndexID都是1,不是说数据页变成了索引页,而是说现在数据页已经属于聚集索引的一部分,不在堆里了
第三个:每个数据页的IndexLevel都是0,就是说数据页在聚集索引的最下层
第四个:索引页和数据页,前一页和后一页是首尾相连的,但是数据页和索引页不是首尾相连的,也就是说没有一个数据页的[PrevPagePID]指向14464页或3528页
那么在上面的聚集索引图片中为什麽会说索引页指向数据页呢?叶子节点就是数据页呢?
数据页的index level是0,那么就是说聚集索引的叶子节点就是数据页


上面索引页的结构

现在来看一下索引页里都有什么,运行下面的sql语句
DBCC TRACEON(3604,128)">-1)
2 GO
3
4 DBCC PAGE(pratice3527,0)">3)
5 6
7
8 3528,128)"> 9 10
11 14464,128)">12 GO

您们应该看到ChildPageId,所以上面我的图为什麽会这样画的原因,索引页连接着数据页,而且一个索引页指向多个数据页


DepartmentID是主键列,从1开始自增,那么从下图可以看出主键列数据是从最左边的索引节点(不是叶子节点)开始排序

这里有个问题:为什麽根节点只有两行???是不是根节点只作连接作用,所以只有两行 ,不过这个问题我也不清楚

聚集索引页里主键列DepartmentID上一行与下一行相差120条记录,一个数据页刚好容纳120条记录
KeyHashValue根据主键列的第一个字段而生成的,就算两个表完全一样,这个hash出来的KeyHashValue都不会一样
我创建了一个一模一样的表Department2,看到hash出来的值都不一样

而这个KeyHashValue我们就叫做键,也就是key-value中的key

------------------------------------------------------------华丽的分割线------------------------------------------------------
聚集索引怎麽找记录的???
这里要分两种情况:(1)聚集索引查找和(2)聚集索引扫描
(1)聚集索引查找

放大一下索引页

sqlSERVER首先把每个数据页的头一条记录里的DepartmentID的值加上一定范围值hash出一个key值,然后放在KeyHashValue列里
当我要找DepartmentID为110的那条记录里的GroupName和Company的值的时候,首先sqlSERVER根据where DepartmentID=110
将110加上一个范围值hash出一个值,这个值就是KeyHashValue的值,找到KeyHashValue=(f000ff86397c)的那条记录
然后到数据页13791里找出DepartmentID为110的那条记录里的GroupName和Company的值
其实这里的算法应该跟hash join是一样的,但是实际具体怎麽算的?本人就不清楚了,大家可以看一下hash join的原理
个人感觉在sqlSERVER里 key-value hash桶用途很广泛,执行计划、 hash join、 聚集索引都用到了
证明:这里我可以证明一下sqlSERVER聚集索引查找记录的流程
先到索引页里找到键值为KeyHashValue=XXX的那条记录,然后再到数据页里把实际数据读出来
运行下面的sql语句,看一下sqlSERVER申请的锁就知道了
下面实验我在Department2表里做的,表数据和表结构和Department1一模一样

View Code
下面这个证明代码在《sqlSERVER企业级平台管理实践》里找的





(2)聚集索引扫描
先drop掉Department2表,然后重新创建Department2表

证明:



上图“以下查询使用了聚集索引查找”,由于本人写sql代码的时候没有修改上面注释,大家可以不用理会
为什麽会有一个键锁,那么多的页锁,在徐海蔚老师的《sqlSERVER企业级平台管理实践》的书本里第361页说到
因为在有聚集索引的表格上,数据是直接存放在索引的最底层(叶子节点),所以要扫描整个表格里的数据,就要把整个聚集索引
扫描一遍。在这里,聚集索引扫描就相当于一个表扫描。所要用的时间和资源与表扫描没有什么差别

再看一下聚集索引查找的流程
sqlSERVER首先把每个数据页的头一条记录里的DepartmentID的值加上一定范围值hash出一个key值,然后放在KeyHashValue列里
当我要找DepartmentID为110的那条记录里的GroupName和Company的值的时候,首先sqlSERVER根据where DepartmentID=110
将110加上一个范围值hash出一个值,这个值就是KeyHashValue的值,找到KeyHashValue=(f000ff86397c)的那条记录
然后到数据页13791里找出DepartmentID为110的那条记录里的GroupName和Company的值
因为[GroupName]列不是索引列,所以根本找不到KeyHashValue值,所以这里只能使用扫描所有数据页的方法来找出记录,除非找到那条记录
不然sqlSERVER不会停止扫描数据页,所以才看到上图有那么多的页面上加了页锁,sqlSERVER需要逐个数据页逐个数据页去扫描就像堆表的全表扫描那样。
那个键锁我估计是当sqlSERVER找到那条记录之后,需要在
记录的所在页面(即是索引页指向那个记录的数据页的那一行)加上一个键锁,以防止别人删除索引页的那一行记录
但是聚集索引扫描是不是一定比聚集索引查找要差呢?这个不一定,要看实际情况o(∩_∩)o
那么非聚集索引扫描是不是跟聚集索引扫描一样,所要用的时间和资源与表扫描没有什么差别呢???
大家可以看一下《sqlSERVER聚集索引与非聚集索引的再次研究(下)》本人做的一个小实验
实验证明了《sqlSERVER企业级平台管理实践》里第363页说到的内容
索引扫描表明sqlSERVER正在扫描一个非聚集索引。由于非聚集索引上一般只会有一小部分字段,所以这里虽然也是扫描,但是
代价会比整表扫描要小很多

------------------------------------------------华丽的分割线--------------------------------------------------------------------
这里有一个问题:没有主键但是有聚集索引,索引页的列数不一样,会多了一列,而这个列(uniquifier)的作用在下面会讲到
这里创建Department3表


可以看到只有聚集索引没有主键的表会比主键表多了一列uniquifier列,这个列的作用会在创建Department5表的时候讲到
-----------------------------------------------华丽的分割线-------------------------------------------------------
下面说一下,复合主键或者聚集索引建立在多个字段上,KeyHashValue只会根据第一个字段生成hash key
当你查询的时候where 后面的字段不包含创建聚集索引时的第一个字段或者复合主键的第一个字段就会聚集索引扫描而不是聚集索引查找
创建Department4表


SELECT
* FROM dbo].
Department4] WHERE name
=销售部6' 聚集索引扫描 因为name不是复合主键中的第一个字段
销售部241' AND DepartmentID]=241 聚集索引查找
3 WHERE 241 聚集索引查找


在建立聚集索引的时候在多个字段上建立聚集索引是没有任何意义的
因为聚集索引查找是根据建立索引的第一个字段来查找,索引扫描的时候会到数据页里扫描 ,而聚集索引的每一行只是一个数据页的范围值从而不能直接定位到要找的那条记录
所以只需要在数据表的一个字段上建立聚集索引就可以了,而究竟要在哪一个字段上建立聚集索引大家一定好好斟酌,本人建议那一个字段在order by中经常要排序的
因为数据页都已经按照聚集索引的第一个字段排好序的了
而不像非聚集索引的索引页跟数据表的记录一一对应,扫描的时候扫描索引页的每一行
大家可以对比一下聚集索引和非聚集索引页的结构
聚集索引页的结构

非聚集索引页的结构

非聚集索引页面的结构会在sqlSERVER聚集索引与非聚集索引的再次研究(下)里讲到
---------------------------------------------------------华丽的分割线-----------------------------------------------------
由于主键不允许重复值,那么就在表上创建一个不唯一的聚集索引,有人说在重复值很多的列上建立聚集索引没有意义
创建Department5表 在Company字段上建立聚集索引,Company字段的值全部都是"中国你好有限公司XX分公司"



在Department3表的时候讲到列(uniquifier),为什麽有主键的表没有这个列,而聚集索引的表有这个列,原因在于
主键列不能有重复值,必须是唯一的,而聚集索引允许有重复值,所以聚集索引需要增加列(uniquifier)来区分重复值
而且可以看到这里uniquifier列是没有规律的,不像Department表每隔120行记录在索引页里标记一行
看一下执行计划和执行结果
SET
STATISTICS TIME
ON
Department5241
4 sql Server 分析和编译时间:
5 cpu 时间
= 0 毫秒,占用时间
0 毫秒。
7 (
1 行受影响)
8
9 sql Server 执行时间:
10 cpu 时间
0 毫秒。

销售部106
106 聚集索引扫描
3 sql Server 执行时间:
4 cpu 时间

至于应不应该在重复值很多的列上建立聚集索引我这里也不敢妄下判断,因为实际环境和这里的测试环境不一样
在MSDN中的解释:http://msdn.microsoft.com/zh-cn/library/ms177484(v=SQL.105).aspx
如果聚集索引不是唯一的索引,sql Server 将添加在内部生成的值(称为唯一值)以使所有重复键唯一。此四字节的值对于用户不可见
还有一个,看一下叶子节点中的数据页,在每个数据页的每行记录中都有
Slot 101 Column 0 Offset 0x1d Length 4
UNIQUIFIER = 206
因为需要标记索引列中的唯一,所以需要在每行记录中增加一列UNIQUIFIER ,但是这一列在select * 表中数据的时候是select不出来的
还有人说UNIQUIFIER 是一个可变长度的字段,但是Length 4已经说明了是一个占用4字节的字段

还有增加了UNIQUIFIER 列之后,无论索引页和数据页都会有所增加,性能有所损耗
下面截图右边的是数据页pageid:14517中的数据,左边的是聚集索引页面

至于性能损耗多少,可以看一下宋大侠这篇文章:
从性能的角度谈SQL Server聚集索引键的选择
----------------------------------------------------华丽的分割线-------------------------------------------------------
堆表中的数据页之间[PrevPagePID],[NextPagePID]是否会首尾相连
堆表

聚集索引表

-----------------------------------------------------华丽的分割线-------------------------------------------------
聚集索引有一个特点,就是当表记录太少的时候不会生成任何索引页,当记录达到一定数量才生成索引页这个数量我现在还不清楚
但是就算没有索引页,sqlSERVER还会使用聚集索引查找,这个问题的确奇怪
创建Department6表,然后插入9条记录
USE
]
3 TABLE Department6
5 (
6 DepartmentID
INT 7 Name
8 GroupName
9 Company
10 ModifiedDate
DATETIME DEFAULT (
GETDATE() ),128)">11 CONSTRAINT PK_Department6_1KEY CLUSTERED
12 ( Name
ASC,DepartmentID
ASC )
13 WITH ( PAD_INDEX
= OFF,STATISTICS_
norECO
mpuTE
norE_DUP_KEY OFF,128)">14 ALLOW_ROW_LOCKS ON,ALLOW_PAGE_LOCKS ON ) ON PRIMARY15 ) 16
17
18 DECLARE @i INT
19 @i1
20 WHILE @i < 10
21 BEGIN
22 INSERT INTO Department6 ( name,0)">],groupname )
23 VALUES (
'+CAST(
AS VARCHAR(
200)),0)">'
)
24 + 25 END
只有一个数据页和一个IAM页



插入更多记录
100000
4 5 6 7 8 开始有索引页了

---------------------------------------------华丽的分割线---------------------------------------------------
大家再看一下Department2表的那部分,究竟数据页的排序顺序跟主键DepartmentID的排序顺序有没有关系呢?
先创建Department7表,插入1000条记录

View Code
T
runcATE TABLE [dbo].[DBCCResult]
3
4 DBCCResultORDER BY PageTypeDESC
根据数据页的首尾连接顺序,我画了一下草图


看一下索引页13791


再画一下草图

对比一下数据页的首尾连接顺序那张图,不知道大家看出规律没有
所以我把聚集索引结构图画成下面这个样子

为什麽聚集索引只能按照第一个字段生成key?为什麽数据页只能按照第一个字段来排序?
其实这个跟数据页排序有关的,大家再仔细看下面两张图


聚集索引页里根据第一个字段排列好这些数据页的第一个字段的范围值,数据页根据这个范围值首尾相连一一排序好
如果聚集索引按多个字段来排序,那么数据页根本排不了,多个字段又升序,又降序??那怎么排序啊?只能按照一个字段来排序
聚集索引查找的时候,使用order by为什麽这么快,因为数据已经根据索引第一个字段排好序了,例子中的字段就是DepartmentID
而只有非聚集索引的表order by的时候就需要排一下序了,因为表中没有聚集索引,数据页没有预先按照一定顺序来排序
详细可以看一下非聚集索引的结构:sqlSERVER聚集索引与非聚集索引的再次研究(下)
---------------------------------------------华丽的分割线--------------------------------------------------------
问题:为什么一个表只能建立一个聚集索引
其实大家看一下我上面画的聚集索引结构图和非聚集索引结构图就知道了

因为如果一个表有聚集索引,那么他的数据页跟索引页有非常强的联系,数据页跟主键第一个字段排好序了,例子中就是“DepartmentID”
如果你再建一个聚集索引,你叫sqlSERVER应该按哪个字段来排序?排序方式是按照你原来的那个聚集索引的DepartmentID列来排序还是
按照你新建的那个聚集索引的第一个字段来排序??
多个聚集索引,数据页都按不同的字段顺序排序,来建立双向链表,那数据表不就乱套了???
但是如果一个表中只有非聚集索引,非聚集索引里的索引页的每一行会有一个指针值指向数据页,数据页依然是堆,没有任何顺序可言
所以你可以在一个表上建立多个非聚集索引也没问题
至于表里面只有非聚集索引表结构是怎样的,大家可以看一下本系列的《sqlSERVER聚集索引与非聚集索引的再次研究(下)》
到时大家就会更加清楚了o(∩_∩)o
----------------------------------------------华丽的分割线----------------------------------------------------------
还有一个问题没有解决:
为什麽根节点只有两行???是不是根节点只作连接作用,所以只有两行 ?
聚集索引就说到这里了,有些地方有可能不对,希望大家强烈拍砖o(∩_∩)o
也希望给个推荐o(∩_∩)o
---------------------------------------------------------------------------
2013-7-21补充
为什麽根节点只有两行??其实根节点不只有两行的
由于出现二层索引节点需要插入大量数据,如果数据很少的话索引节点只有一层,并且不能用主键,只能创建聚集索引
根据宋大侠说的,当页拆分的时候,根节点就会增加记录,我这里提供一下脚本
------------------------------------------------------------------
DROP TABLE [dbo].[Department]
6 7 DepartmentID int 8 Name 9 GroupName 10 Company 11 ModifiedDate 12 )
13
14 CLUSTERED INDEX CL_DepartmentID ON Department(DepartmentID ASC)
15
16 17 300000
19 20 INTO Department(21 VALUES ( @i,0)">200)) )
3
23 END
24
25 DROP TABLE Department
26 FROM Department
27
28
29
30 31 32
33 大家可以测试一下
插入数据的时候最好是根节点前的记录

附上宋大侠的文章:
T-SQL查询高级—SQL Server索引中的碎片和填充因子
根节点的记录行数在下面两种情况下会有所变化
(1)向表中插入数据或更新表中数据并产生碎片的时候
(2)碎片很多然后alter index REORGANIZE 重组索引的时候
ALTER INDEX TableForTestCIXDepartment] REORGANIZE
------------------------------------------------------------------------------
2013-8-24 补充:
关于我在文中说的
至于应不应该在重复值很多的列上建立聚集索引我这里也不敢妄下判断,因为实际环境和这里的测试环境不一样

今天看了一下《sqlSERVER企业级平台管理实践》,书里面第437页是这样说的

要慎重选择索引的第一个字段,最好选择一个重复记录最少的字段。这是因为索引上的统计信息只保存第一个字段的数据直方图。如果选择一个
重复数据很多的字段,这个索引的可选度就比较低了,会影响索引的价值
所以,为什麽建立聚集索引的时候,只能在一个字段上建立索引,并且这个字段最好不要重复,从数据直方图上也能解释这个原因
------------------------------------------------------------------------------------------
2013-9-15 补充:
如何查看聚集索引页面的内容,使用DBCC PAGE的时候使用1这个格式就可以了
聚集索引页中有三条记录,而且三条记录的Record Type = INDEX_RECORD
37397,255)">GO


DBCC 执行完毕。如果
DBCC 输出了错误信息,请与系统管理员联系。
2
3 PAGE: (
1:
37397)
4
5
6 BUFFER:
9 BUF
@0x03E5DC3C
11 bpage
0x1A8D8000 bhash
0x00000000 bpageno
= (
12 bdbid
5 breferences
0 bUse1
6487
13 bstat
0x3c0000b blog
0x212159bb bnext
0x00000000
14
15 PAGE HEADER:
18 Page
@0x1A8D8000
19
20 m_pageId
37397) m_headerVersion
1 m_type
2
21 m_typeFlagBits
0x0 m_level
1 m_flagBits
0x0
22 m_objId (AllocUnitId.i
dobj)
549 m_indexId (AllocUnitId.i
dind)
256
23 Metadata: AllocUnitId
72057594073907200
24 Metadata: PartitionId
72057594061717504 Metadata: IndexId
25 Metadata: ObjectId
1543676547 m_prevPage
0:
0) m_nextPage
0)
26 pminlen
11 m_slotCnt
3 m_freeCnt
8057
27 m_freeData
129 m_reservedCnt
0 m_lsn
3046:
261:
41)
28 m_xactReserved
0 m_xdesId
0) m_ghostRecCnt
0
29 m_tornBits
0
30
31 Allocation Status
33 GAM (
2)
= ALLOCATED SGAM (
3)
= ALLOCATED
34 PFS (
32352)
0x60 MIXED_EXT ALLOCATED 0_PCT_FULL DIFF (
6)
= CHANGED
35 ML (
7)
NOT MIN_LOGGED
36
37 DATA:
38
39
40 Slot
0,Offset
0x60,Length
11,DumpStyle BYTE
41
42 Record Type
= INDEX_RECORD Record Attributes
=
43 Memory
Dump @0x0A3BC060
44
45 00000000:
06010000 00283d00
000100†††††††††††††.....(
=....
46
47 Slot
0x6b,128)">48
49 Record Type
50 Memory
@0x0A3BC06B
51
52 06950100 00929100 000100†††††††††††††...........
53
54 Slot
2,0)">
0x76,128)">55
56 Record Type
57 Memory
@0x0A3BC076
58
59 06290300 002f0000
000100†††††††††††††.)...
/.....
60
61 OFFSET
TABLE:
62
63 Row
- Offset
64 2 (
0x2)
- 118 (
0x76)
65 1 (
0x1)
107 (
0x6b)
66 0 (
0x0)
96 (
0x60)
67
68
69 DBCC 输出了
错误信息,请与系统
管理员联系。

--本篇文章转自SQLSERVER聚集索引与非聚集索引的再次研究(上)
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。