SQL Server 深刻解析索引存储(非汇集索引)

 标签:SQL SERVER/MSSQL SERVER/数据库/DBA/索引体系结构/非汇集索引html

概述  

非汇集索引与汇集索引具备相同的 B 树结构,它们之间的显著差异在于如下两点:数据库

  • 基础表的数据行不按非汇集键的顺序排序和存储。

  • 非汇集索引的叶层是由索引页而不是由数据页组成。

既可使用汇集索引来为表或视图定义非汇集索引,也能够根据堆来定义非汇集索引。非汇集索引中的每一个索引行都包含非汇集键值和行定位符。此定位符指向汇集索引或堆中包含该键值的数据行。session

非汇集索引行中的行定位器或是指向行的指针,或是行的汇集索引键,以下所述:测试

  • 若是表是堆(意味着该表没有汇集索引),则行定位器是指向行的指针。该指针由文件标识符 (ID)、页码和页上的行数生成。整个指针称为行 ID (RID)。

  • 若是表有汇集索引或索引视图上有汇集索引,则行定位器是行的汇集索引键。若是汇集索引不是惟一的索引,SQL Server 将添加在内部生成的值(称为惟一值)以使全部重复键惟一。此四字节的值对于用户不可见。仅当须要使汇集键惟一以用于非汇集索引中时,才添加该值。SQL Server 经过使用存储在非汇集索引的叶行内的汇集索引键搜索汇集索引来检索数据行。

对于索引使用的每一个分区,非汇集索引在 index_id >0 的 sys.partitions 中都有对应的一行。默认状况下,一个非汇集索引有单个分区。若是一个非汇集索引有多个分区,则每一个分区都有一个包含该特定分区的索引行的 B 树结构。例如,若是一个非汇集索引有四个分区,那么就有四个 B 树结构,每一个分区中一个。spa

根据非汇集索引中数据类型的不一样,每一个非汇集索引结构会有一个或多个分配单元,在其中存储和管理特定分区的数据。每一个非汇集索引至少有一个针对每一个分区的 IN_ROW_DATA 分配单元(存储索引 B 树页)。若是非汇集索引包含大型对象 (LOB) 列,则还有一个针对每一个分区的 LOB_DATA 分配单元。此外,若是非汇集索引包含的可变长度列超过 8,060 字节行大小限制,则还有一个针对每一个分区的 ROW_OVERFLOW_DATA 分配单元。有关分配单元的详细信息,请参阅表组织和索引组织。B 树的页集合由 sys.system_internals_allocation_units 系统视图中的 root_page 指针定位。3d

 要很好的理解这篇文章的内容以前须要先阅读我前面写的上中部分的两篇文章:指针

SQL Server 深刻解析索引存储(堆)code

SQL Server 深刻解析索引存储(汇集索引)htm

正文

非汇集索引结构

 

生成测试数据对象

 

CREATE TABLE Torder
(ID INT IDENTITY(1,1) NOT NULL,
NAME CHAR(100) NOT NULL,
pro VARCHAR(8000) NULL,
Statu INT NOT NULL,
IDATE DATETIME DEFAULT(GETDATE())
)
GO
---插入1000条测试数据
DECLARE @ID INT=1
WHILE(@ID<=1000)
BEGIN
INSERT INTO Torder(NAME,pro,Statu)VALUES('商品'+CONVERT(CHAR(20),@ID),REPLICATE(1,8000),LEFT(@ID,1))
SET @ID=@ID+1 
END
GO
---建立非汇集索引
CREATE INDEX IX_Torder ON Torder
(NAME,Statu
)
INCLUDE(IDATE)


SELECT DISTINCT so.name, so.object_id,sp.index_id,internals.type_desc,internals.total_pages, internals.used_pages, internals.data_pages,first_iam_page,  first_page, root_page
FROM sys.objects so
INNER JOIN sys.partitions sp ON so.object_id = sp.object_id
INNER JOIN sys.allocation_units sa ON sa.container_id = sp.hobt_id
INNER JOIN sys.system_internals_allocation_units internals ON internals.container_id = sa.container_id
WHERE so.object_id = object_id('Torder') 

 

因为建立的表只有非汇集索引,因此整个表的页存储中有三部分数据:堆页面、溢出页面、索引页面;

堆中共有20个数据页和一个IAM页;

溢出单元有1001个页面包括一个IAM页;

索引中共有20个页其中18个数据页一个ROOT页和一个IAM页.

一个堆页对应多个溢出页,由于Pro有8000个字节因此一行占一页,而表的其它字段只有116个字节一个堆页能够存50条记录,因此并非一个溢出页就惟一对应一个堆页

 分析页的存储信息

---开启跟踪标志
DBCC TRACEON(3604,2588)
--DBCC TRACEOFF(3604,2588)
---获取对象的数据页,结构:数据库、对象、显示
DBCC IND(Ixdata,Torder,-1)

上一章中已经讲过了堆页面和溢出页面,因此如今就讲非汇集索引页

 看过前面的文章应该一眼就能看出1281页是ROOT页,如今就分析1281页

分析非汇集索引根页

DBCC page(Ixdata,1,1281,3)

如今来分析行定位指针是怎样的:0x6801000001002F00

除去开头的16进制标示,剩下总共8个字节,从右往左其中行号2个字节,文件标示ID2个字节,剩下的4个字节就是页号了,因此

行号(002f)=47

文件页(0001)=1

页号(00000168)=360页

如今查看360页的信息

DBCC page(Ixdata,1,360,3)

47行的记录正好是“商品150”

 分析非汇集索引索引页

 

经过对比会发现索引页比根页多出了索引包含列值和键的哈希值,这个里面的keyhashvalue应该是NAME和statu字段的值经过某种方法算出来的,这里只是猜想经过平时的查询你会产生这样的猜想。

测试简单的查询

这里的'商品150'和'商品153'都是1280页中的记录,1280页是索引页,其中'商品150'是该页的第一条记录
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN  
SELECT [ID]
      ,[NAME]
      ,[pro]
      ,[Statu]
      ,[IDATE]
  FROM [Ixdata].[dbo].[Torder]
  WHERE NAME='商品153'

--COMMIT 

另开一个窗口
SELECT
[request_session_id],
c.[program_name],
DB_NAME(c.[dbid]) AS dbname,
[resource_type],
[request_status],
[request_mode],
[resource_description],OBJECT_NAME(p.[object_id]) AS objectname,
p.[index_id]
FROM sys.[dm_tran_locks] AS a LEFT JOIN sys.[partitions] AS p
ON a.[resource_associated_entity_id]=p.[hobt_id]
LEFT JOIN sys.[sysprocesses] AS c ON a.[request_session_id]=c.[spid]
 WHERE c.[dbid]=DB_ID('Ixdata') AND a.[request_session_id]=58  ----要查询申请锁的数据库
ORDER BY [request_session_id],[resource_type]

 

 

从上面的查询过程能够知道页面总共读取了三次(索引叶一次堆页一次溢出页一次)。

 

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO        
BEGIN TRAN  
 SELECT 
       [ID]
      ,[NAME]
      ,[pro]
      ,[Statu]
      ,[IDATE]
  FROM [Ixdata].[dbo].[Torder]
  WHERE NAME='商品150'                                                                                     
  

 

 

经过对比查询'商品150'和'商品153'能够看到若是查找的页面的第一条记录,它须要再读取该索引页的前一个页面,若是该索引页是第一页就无需再读除自己的其余索引页了,文章写到后面反过来思考才知道为何非汇集索引还须要多查找一个页面了。由于非汇集索引是容许存在重复值因此才须要再往前查找,若是前面一个页查找不到则结束,若是前面一个页还没查完会再往前一个页进行查,固然查询商品153的时候就已经判断了前一条记录的键值是不同的不然也是要再查询前一个页,这也是非汇集索引的一个特殊状况吧!

索引扫描

update Torder
set statu=100
where id=1

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO        
BEGIN TRAN  
 SELECT 
       [ID]
      ,[NAME]
      ,[pro]
      ,[Statu]
      ,[IDATE]
  FROM [Ixdata].[dbo].[Torder]
  WHERE [Statu]=100      

该查询总共扫描了18个索引页+1个堆页+1个溢出页.

建立汇集索引

 

ALTER TABLE dbo.Torder ADD CONSTRAINT
    PK_Torder PRIMARY KEY CLUSTERED 
    (
    ID
    ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]


SELECT DISTINCT so.name, so.object_id,sp.index_id,internals.type_desc,internals.total_pages, internals.used_pages, internals.data_pages,first_iam_page,  first_page, root_page
FROM sys.objects so
INNER JOIN sys.partitions sp ON so.object_id = sp.object_id
INNER JOIN sys.allocation_units sa ON sa.container_id = sp.hobt_id
INNER JOIN sys.system_internals_allocation_units internals ON internals.container_id = sa.container_id
WHERE so.object_id = object_id('Torder') 

非汇集索引数据页比以前少了一页

因为如今的指针比以前的16进制指针要所占有的字节要少,因此只须要17个页面就能够存下。

分析索引页148

 

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN  
SELECT [ID]
      ,[NAME]
      ,[pro]
      ,[Statu]
      ,[IDATE]
  FROM [Ixdata].[dbo].[Torder]
  WHERE NAME='商品152'


在另外一个窗口打开
SELECT
[request_session_id],
c.[program_name],
DB_NAME(c.[dbid]) AS dbname,
[resource_type],
[request_status],
[request_mode],
[resource_description],OBJECT_NAME(p.[object_id]) AS objectname,
p.[index_id]
FROM sys.[dm_tran_locks] AS a LEFT JOIN sys.[partitions] AS p
ON a.[resource_associated_entity_id]=p.[hobt_id]
LEFT JOIN sys.[sysprocesses] AS c ON a.[request_session_id]=c.[spid]
 WHERE c.[dbid]=DB_ID('Ixdata') AND a.[request_session_id]=58  ----要查询申请锁的数据库
ORDER BY [request_session_id],[resource_type]

从上面的逻辑读取和查询步骤能够证明前面的猜想,应该是隐藏了一张行定位表。

若是表有汇集索引或索引视图上有汇集索引,则行定位器是行的汇集索引键。若是汇集索引不是惟一的索引,SQL Server 将添加在内部生成的值(称为惟一值)以使全部重复键惟一。此四字节的值对于用户不可见。仅当须要使汇集键惟一以用于非汇集索引中时,才添加该值。SQL Server 经过使用存储在非汇集索引的叶行内的汇集索引键搜索汇集索引来检索数据行。

查看索引统计信息

DBCC SHOW_STATISTICS ("dbo.Torder", IX_Torder);

 

前面建的包含索引有这三种组合方式,因此组合索引的第二个字段不被用来单独作查找。

总结

  非汇集索引和汇集索引不同,汇集索引索引页的键值指向数据页的具体行;而非汇集索引不存在数据页,非汇集索引的索引页中的记录行指向汇集索引或者堆的具体数据页的数据行而后来获取记录,若是堆或汇集索引还存在溢出的话,从堆或者汇集索引的数据记录还有指向溢出页面的指针。

补充一下在非汇集索引中存在汇集索引与堆的优势,看完上文你会发现非汇集索引的数据页记录的行定位指针分别指向汇集索引或堆的行,可是指向汇集索引的行定位是逻辑值而指向堆的是实际的rid值,逻辑值的好处就是在汇集索引起生分页的状况下,逻辑值不用改变也就无需更新非汇集索引的指针。

花了四天时间终于把这个系列的写完了,从新去理解一遍把之前的一些不理解的知识点给弄明白了,仍是收获不少。

 

若是文章对你们有帮助,但愿你们能给个推荐,谢谢!!!

 

备注:

    做者:pursuer.chen

    博客:http://www.cnblogs.com/chenmh

本站点全部随笔都是原创,欢迎你们转载;但转载时必须注明文章来源,且在文章开头明显处给明连接,不然保留追究责任的权利。

《欢迎交流讨论》

相关文章
相关标签/搜索