SQL Server2008存储结构之聚集索引

SQL Server 2008 连载之存储结构—— 聚集索引

聚集索引即基于数据行的键值在表内排序和存储这些数据行。每个表只能有一个聚集索引,因为数据行本身只能按一个顺序存储。

从某种程度上,聚集索引即数据,这句话是有道理的;但正如同其他索引一样,聚集索引也是按 B 树结构进行组织的。既然是 B 树组织,那么就有叶子结点和非叶子节点之分。聚集索引 B 树的顶端节点称为根节点;聚集索引中的底层节点称为叶节点。在根节点与叶节点之间的任何索引级别统称为中间级。在聚集索引中,叶节点包含基础表的数据页。根节点和中间级节点包含存有索引行的索引页。每个索引行包含一个键值和一个指针,该指针指向 B 树上的某一中间级页或叶级索引中的某个数据行。每级索引中的页均被链接在双向链接列表中。

因此可以这么说,聚集索引的叶子结点存储的是按聚集索引顺序排列的数据本身,而中间结点和根节点则在维护索引和其层级。

对于某个聚集索引, sys.system_internals_allocation_units 中的 root_page 列指向该聚集索引某个特定分区的顶部。 SQL Server 将从索引中向下移动以查找与某个聚集索引键对应的行。为了查找键的范围, SQL Server 将在索引中移动以查找该范围的起始键值,然后用向前或向后指针在数据页中进行扫描。为了查找数据页链的首页, SQL Server 将从索引的根节点沿最左边的指针进行扫描。

 

SQL Server2008存储结构之聚集索引_第1张图片

 

 

drop table testUniqueCluster

drop table testNonUniqueCluster

CREATE TABLE testUniqueCluster

(

  name     CHAR ( 900),

  remark   CHAR ( 1100)

)

CREATE UNIQUE CLUSTERED INDEX ix_testUniqueCluster

    ON testUniqueCluster ( name )

INSERT INTO testUniqueCluster VALUES ( 'B' , 'BBB1' )

INSERT INTO testUniqueCluster VALUES ( 'A' , 'AAA1' )

 

CREATE TABLE testNonUniqueCluster

(

  name     CHAR ( 900),

  remark   CHAR ( 1100)

)

CREATE CLUSTERED INDEX ix_testNonUniqueCluster

    ON testNonUniqueCluster ( name )

 

INSERT INTO testNonUniqueCluster VALUES ( 'B' , 'BBB2' )

INSERT INTO testNonUniqueCluster VALUES ( 'B' , 'BBB1' )     

INSERT INTO testNonUniqueCluster VALUES ( 'A' , 'AAA1' )

 

SELECT c . name , a . type_desc,

       total_pages , used_pages , data_pages ,

       testdb . dbo . f_get_page ( first_page ) first_page_address ,

       testdb . dbo . f_get_page ( root_page ) root_address ,

       testdb . dbo . f_get_page ( first_iam_page ) IAM_address

  FROM sys . system_internals_allocation_units a , sys . partitions b , sys . objects c

  WHERE a . container_id = b . partition_id and b . object_id = c . object_id

   AND c . name in ( 'testUniqueCluster' , 'testNonUniqueCluster' )    

 

TRUNCATE TABLE tablepage ;

INSERT INTO tablepage EXEC ( 'DBCC IND(testdb,testUniqueCluster,1)' );

INSERT INTO tablepage EXEC ( 'DBCC IND(testdb,testNonUniqueCluster,1)' );

SELECT

  b . name table_name ,

  CASE WHEN c . type = 0 THEN ' '

       WHEN c . type = 1 THEN ' 聚集 '

        WHEN c . type = 2 THEN ' 非聚集 '

       ELSE ' 其他 '

  END index_type ,  

  c . name index_name ,

  PagePID , IAMPID , ObjectID , IndexID , Pagetype , IndexLevel ,

  NextPagePID , PrevPagePID

  FROM tablepage a , sys . objects b , sys . indexes c

  WHERE A . ObjectID = b . object_id

   AND A . ObjectID = c . object_id

   AND a . IndexID = c . index_id

 

Name

Type_desc

Used_pages

Data_pages

First_page_address

Root_address

IAM_Address

testUniqueCluster

IN_ROW_DATA

2

1

1:233

1:233

1:234

testNonUniqueCluster

IN_ROW_DATA

2

1

1:235

1:235

1:236

 

下面我们用 dbcc 命令介绍一下聚集索引的构造。

DBCC TRACEON ( 3604)  

DBCC PAGE ( testDB , 1, 233, 1)

m_type = 1

5E3BC060:   1000d407 42202020 20202020 20202020 ?....B      

....            

5E3BC3E0:   20202020 20202020 42424231 20202020 ?        BBB1            

...

5E3BC830:   20202020 0200fc 10 00d407 41 20202020 ?    .......A            

...

5E3BCBB0:   20202020 20202020 20202020 20202041 ?               A        

5E3BCBC0:   41413120 20202020 20202020 20202020 ?AA1                     

...

5E3BD000:   20202020 20202020 20202002 00fc 0000 ?            .....        

 

OFFSET TABLE:

Row - Offset                        

1 (0x1) - 96 (0x60)                 

0 (0x0) - 2103 (0x837)              

 

DBCC PAGE ( testDB , 1, 235, 1)

5E3BC060:   1000d407 42202020 20202020 20202020 ?....B     

...              

5E3BC3E0:   20202020 20202020 42424232 20202020 ?        BBB2            

...

5E3BC830:   20202020 0300f 8 30 00d407 42 20202020 ?    ...0...B            

...

5E3BCBB0:   20202020 20202020 20202020 20202042 ?               B        

5E3BCBC0:   42423120 20202020 20202020 20202020 ?BB1                     

...

5E3BD000:   20202020 20202020 20202003 00f80100 ?           .....        

5E3BD010:   df070100 0000 1000 d407 4120 20202020 ?..........A             

...

5E3BD390:   20202020 20202020 20202020 20204141 ?              AA        

5E3BD3A0:   41312020 20202020 20202020 20202020 ?A1                      

...

5E3BD7E0:   20202020 20202020 20200300 f 8 000021 ?          .....!        

 

OFFSET TABLE:

Row - Offset                        

2 (0x2) - 2103 (0x837)              

1 (0x1) - 96 (0x60)                 

0 (0x0) - 4118 (0x1016)             

其中红颜色的部分为每行的行头部分,蓝颜色部分为每行的结尾部分。

大家可以看到 m_type=1 即数据页面,大家应该很奇怪吧,为什么明明是聚集索引,却是数据页面呢?正如上面所提到,聚集索引的叶子页面即数据页面。因为这个表只有 2~3 条记录,所以 root 页面还达不到需要分为 B 树的程度,所以该 root 页面也是叶子页面。

我们首先来看一下 1000d407 的行头部如何解释

0

1-3

4

5

6-7

1 个字节

2 个字节

0

000

1

0

00

00

d407

10

00

2004

始终为 0

0 表示主记录
3 表示索引记录
5 表示幻影索引记录

存在NULL 位图

存在变长字段

保留

状态B 保留

字段长度

即该行为不存在变长字段的主记录,且字段长度为 2004 个字节。

30 00d407 该如何解释呢?即 00001100 即存在变长字段的主记录,我们的 testNonUniqueCluster 怎么会存在变长字段呢?

在该非唯一聚集索引表中,我们首先插入记录 B BBB2 记录,再插入 B BBB1 记录,这个时候对于非唯一索引如何去识别呢? SQL Server 在重复行的行尾增加了 8 个额外的字节,稍后我们再分析行尾。

testUniqueCluster 表中正常的行尾为 0200fc ,其解释如下 0200 表示该表有 2 个字段, fc 则为 1111 1100 ,即前 2 个字段不为空。

而对于 testNonUniqueCluster 表正常的行尾应为 0300 f8 ,其解释如下 0300 表示该表有 3 个字段, f8 则为 1111 1000 ,即前 3 个字段不为空;很显然 SQL Server 把非唯一索引的标识符也当做字段了;但的的确确因为 B BBB2 A AAA1 在插入的时候是唯一的,所以不需要这个字段。

我们接下来看看 B BBB1 行的尾部 03 00f8 0100 df070100 0000 0300f 8 解释同上, 0100 1 表示该表一共有 1 个变长字段, df07 2015 变长字段结束的位置,最后四个字节 0100 0000 为非唯一索引的标识符,换算成 10 进制即 1

从页面中记录的顺序我们其实可以看得出来,聚集索引的行的物理顺序与行的实际存储没有太大关系,而是与记录槽的顺序的有关。

既然我们再谈论聚集索引,那就不能不说聚集索引的中间节点和根节点了,

为了简化处理,我们使用 testUniqueCluster 来做进一步的研究。

该表包含 2 个定长字段,合计 2000 字节,加上相应的头部的 4 个管理字节和尾部的 3 个管理字节,共计 2007 个字节,页头还需要 96 个字节,每行的偏移量需要 2 个字节,所以单页 8192 字节只能容纳大概 4 条记录。也就是说当我们完成第五条记录时就应该产生分页现象了。

INSERT INTO testUniqueCluster VALUES ( 'C' , 'CCC1' )

INSERT INTO testUniqueCluster VALUES ( 'D' , 'DDD1' )

INSERT INTO testUniqueCluster VALUES ( 'E' , 'EEE1' )

TRUNCATE TABLE tablepage ;

INSERT INTO tablepage EXEC ( 'DBCC IND(testdb,testUniqueCluster,1)' );

 

SELECT

  b . name table_name ,

  CASE WHEN c . type = 0 THEN ' '

       WHEN c . type = 1 THEN ' 聚集 '

       WHEN c . type = 2 THEN ' 非聚集 '

       ELSE ' 其他 '

  END index_type ,  

  c . name index_name ,

  PagePID , IAMPID , ObjectID , IndexID , Pagetype , IndexLevel ,

  NextPagePID , PrevPagePID

  FROM tablepage a , sys . objects b , sys . indexes c

  WHERE A . ObjectID = b . object_id

   AND A . ObjectID = c . object_id

   AND a . IndexID = c . index_id

以下为该表的详细页面分布

index_name

PagePID

IAMPID

IndexID

Pagetype

IndexLevel

NextPagePID

PrevPagePID

234

NULL

1

10

NULL

0

0

233

234

1

1

0

248

0

239

234

1

2

1

0

0

248

234

1

1

0

0

233

我们再用 sys.system_internals_allocation_units 来看一下该表的页面概要信息。

name

total_pages

used_pages

data_pages

first_address

root_address

IAM_address

testUniqueCluster

4

4

2

1:233

1:239

1:234

从以上两个表格,我们可以看出 IAM 页面未发生变化,仍旧是第 234 页面。

根节点页面发生了变化,现在是第 239 页面, pagetype=2 ,即索引页面,新增加了一个数据页面第 248 页面,第 233 页面仍继续存在;同时在第 248 233 个页面之间存在着互链的关系。

同时观察一下数据,发现在第 233 页中存在 A AAA1 B BBB1 C CCC1 D DDD1 4 条记录,而第 248 页中则存在 E EEE1 记录,也就是说对于 SQL Server 来说索引的分裂应该是以最小代价进行,而不是完全均衡策略。

再让我们用 DBCC PAGE(1,testDB,239,3) 观察一下根节点的内容。

FileId

PageId

Row

Level

ChildFileId

ChildPageId

name (key)

KeyHashValue

1

239

0

1

1

233

NULL

(6f4251ce1f81)

1

239

1

1

1

248

E                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      

(201c8aeace10)

因为这是个索引的非叶子节点,所以连表现形式都简化了。

FieldId 为当前页面的文件 ID

PageId 为当前页面的页面 ID

Row 表示为当前的 slot

Level 1 表示为当前为非叶子节点

ChildFieldId 表示为插槽号指向的页面的文件 ID

ChildPageId 表示为插槽号指向的页面的页面 ID

Name 表示为当前索引的键值

KeyHashValue SQL Server 键值的内部表示的 hash 值。

E 右侧的数据指向第 248 页面,而左侧的则指向第 233 页面。

那么再让我们插入 4 条记录看看根页面的变化。

INSERT INTO testUniqueCluster VALUES ( 'C' , 'CCC1' )

INSERT INTO testUniqueCluster VALUES ( 'D' , 'DDD1' )

INSERT INTO testUniqueCluster VALUES ( 'E' , 'EEE1' )

DBCC PAGE(1,testDB,239,3)

 

FileId

PageId

Row

Level

ChildFileId

ChildPageId

name (key)

KeyHashValue

1

239

0

1

1

233

NULL

(6f4251ce1f81)

1

239

1

1

1

248

E                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     

(201c8aeace10)

1

239

2

1

1

249

I                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      

(201cbd800c11)

现在我们可以看到在根节点上又增加了一个新的键值 I ,凡是大于等于 I 的记录均指向第 249 页;结合前面的描述,我们可以得到下面的索引结构变化示意图。


你可能感兴趣的:(sql,server,table,存储,insert,behavior,Allocation)