索引键的唯一性(1/4):堆表上的唯一与非唯一非聚集索引的区别

本文涉及的产品
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
云数据库 RDS SQL Server,基础系列 2核4GB
简介: 原文:索引键的唯一性(1/4):堆表上的唯一与非唯一非聚集索引的区别在这篇文章里,我想详细介绍下SQL Server里唯一与非唯一非聚集索引的区别。看这个文章前,希望你已经理解了聚集和非聚集索引的概念,还有在SQL Server里是如何使用的。
原文: 索引键的唯一性(1/4):堆表上的唯一与非唯一非聚集索引的区别

在这篇文章里,我想详细介绍下SQL Server里唯一与非唯一非聚集索引的区别。看这个文章前,希望你已经理解了聚集和非聚集索引的概念,还有在SQL Server里是如何使用的。

很多人对唯一和非唯一索引非聚集索引的认识都不是很清晰。事实上,SQL Server在存储上这2类索引有着本质的区别,这些区别会影响到索引占用空间的大小和索引的使用效率。

今天我们从SQL Server里的堆表(Heap table) ,它是没有聚集索引定义的表,在它建立唯一和非唯一非聚集索引,来开始我们的分析。下列脚本会创建我们的测试表,并插入80000条记录。每条记录需要400 bytes,因此SQL Server在每页可以放20条记录。这就是说我们的堆表包括4000个数据页和1个IAM页。

 1 USE ALLOCATIONDB
 2 -- Create a table with 393 length + 7 bytes overhead = 400 bytes 
 3 -- Therefore 20 records can be stored on one page (8.096 / 400) = 20,24 
 4 CREATE TABLE CustomersHeap 
 5 ( 
 6     CustomerID INT NOT NULL, 
 7     CustomerName CHAR(100) NOT NULL, 
 8     CustomerAddress CHAR(100) NOT NULL, 
 9     Comments CHAR(189) NOT NULL 
10 ) 
11 GO
12 
13 -- Insert 80.000 records 
14 DECLARE @i INT = 1 
15 WHILE (@i <= 80000) 
16 BEGIN 
17     INSERT INTO CustomersHeap VALUES 
18     ( 
19         @i, 
20         'CustomerName' + CAST(@i AS CHAR), 
21         'CustomerAddress' + CAST(@i AS CHAR), 
22         'Comments' + CAST(@i AS CHAR) 
23     )     
24     SET @i += 1 
25 END 
26 GO
27 
28 -- Retrieve physical information about the heap table 
29 SELECT * FROM sys.dm_db_index_physical_stats 
30 ( 
31     DB_ID('ALLOCATIONDB'), 
32     OBJECT_ID('CustomersHeap'), 
33     NULL, 
34     NULL, 
35     'DETAILED'
36 ) 
37 GO

在堆表创建和数据插入后,你就可以在我们的堆表上CustomerID列定义唯一和非唯一非聚集索引。我们把2个索引都定义在同列,这样我们就可以分析唯一和非唯一聚集索引的区别。

1 -- Create a unique non clustered index 
2 CREATE UNIQUE NONCLUSTERED INDEX IDX_UniqueNCI_CustomerID 
3 ON CustomersHeap(CustomerID) 
4 GO  
5 
6 -- Create a non-unique non clustered index 
7 CREATE NONCLUSTERED INDEX IDX_NonUniqueNCI_CustomerID 
8 ON CustomersHeap(CustomerID) 
9 GO 

如果在非唯一数据的列上定义唯一非聚集索引,SQL Server会返回你一个错误信息。当你创建非聚集索引时,如果不指定UNIQUE,SQL Server会创建非唯一的非聚集索引,这点很重要!因此你创建的非聚集索引默认情况下都是非唯一的非聚集索引。

在2个索引创建后,我们可以分析它们的大小,索引深度,索引大小等。使用DMV sys.dm_db_index_physical_stats,第3个参数传入index-id值。所有非聚集索引的ID值都开始于2,因此第1个非聚集索引的ID值为2,第2个非聚集索引的ID值为3。

 1 -- Retrieve physical information about the unique non-clustered index 
 2 SELECT * FROM sys.dm_db_index_physical_stats 
 3 ( 
 4     DB_ID('ALLOCATIONDB'), 
 5     OBJECT_ID('CustomersHeap'), 
 6     2, 
 7     NULL, 
 8     'DETAILED'
 9 ) 
10 GO
11 
12 -- Retrieve physical information about the non-unique non-clustered index 
13 SELECT * FROM sys.dm_db_index_physical_stats 
14 ( 
15     DB_ID('ALLOCATIONDB'), 
16     OBJECT_ID('CustomersHeap'), 
17     3, 
18     NULL, 
19     'DETAILED'
20 ) 
21 GO

 

从输出结果你可以看到,唯一非聚集索引的索引根页占用约24%,非唯一非聚集索引的索引根页占用约39%,因此在堆表上,唯一/非唯一非聚集索引的存储格式肯定不一样!下一步我们用一个帮助表来存储DBCC IND命令的输出,用来做进一步分析。

 1 -- Create a helper table 
 2 CREATE TABLE sp_table_pages 
 3 ( 
 4    PageFID TINYINT, 
 5    PagePID INT, 
 6    IAMFID TINYINT, 
 7    IAMPID INT, 
 8    ObjectID INT, 
 9    IndexID TINYINT, 
10    PartitionNumber TINYINT, 
11    PartitionID BIGINT, 
12    iam_chain_type VARCHAR(30), 
13    PageType TINYINT, 
14    IndexLevel TINYINT, 
15    NextPageFID TINYINT, 
16    NextPagePID INT, 
17    PrevPageFID TINYINT, 
18    PrevPagePID INT, 
19    PRIMARY KEY (PageFID, PagePID) 
20 ) 
21 GO
22 
23 -- Write everything in a table for further analysis 
24 INSERT INTO sp_table_pages 
25 EXEC('DBCC IND(ALLOCATIONDB, CustomersHeap, 2)') 
26 GO
27 
28 -- Write everything in a table for further analysis 
29 INSERT INTO sp_table_pages 
30 EXEC('DBCC IND(ALLOCATIONDB, CustomersHeap, 3)') 
31 GO

现在我们可以用DBCC PAGE命令分下聚集索引页,使用这个命令前我们需要运行 DBCC TRACEON(3604)。在此之前,我们先找下根页。

1 SELECT * FROM dbo.sp_table_pages ORDER BY IndexLevel DESC,IndexID

可以看到,唯一非聚集索引的根页是14624;非唯一非聚集的根页是14608。

1 DBCC TRACEON(3604) 
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 14624, 3) 
4 GO

从输出结果我们可以看到,SQL Server存储者B树的子页信息,即非聚集索引最小键值位置。例如,14537页包含最小键值540到1078值的记录。当你使用参数1的DBCC PAGE时,你就获得了索引根页上,所有索引记录字节显示内容:

1 DBCC TRACEON(3604) 
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 14624, 1) 
4 GO

SQL Server这里需要11个字节来存储索引行,这些11个字节存储下列信息:

  • 1 byte:状态位
  • 4 bytes:索引键值(CustomerID),例如 540
  • 4 bytes:子页ID值(ChildPageId),例如 14537
  • 2 bytes: 字段ID值(FileId),例如 1

索引行的长度取决于非聚集键的长度。这就是说如果你选择更短的非聚集键,SQL Server就可以保存更多的索引行。如果你选择了CHAR(100)类型字段作为非聚集索引键,SQL Server就需要更多的索引页来保存你的非聚集索引,因此使用长度短的索引键更高效。
最后我们看看子页14537的内容:

1 DBCC TRACEON(3604) 
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 14537, 3) 
4 GO

从图中,我们可以看到,SQL Server保存了数据页的索引键(CustomerID (key))和用于定位对应记录的槽号(HEAP RID)。因为我们在表上没有定义聚集索引,SQL Server这里使用RID来指向数据页的记录。在堆表上的叶子层的索引页和聚集表上叶子层的索引页是不一样的。如果你用参数1来使用DBCC PAGE时,你就得到如下显示:

1 DBCC TRACEON(3604) 
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 14537, 1) 
4 GO

SQL Server需要13字节来保存每个索引行:

  • 1 byte 状态位
  • 4 bytes 索引键ID(CustomerID),例如540
  • 4 bytes 页ID(PageID
  • 2 bytes 文件ID(FileID
  • 2 bytes 槽号(Slot number

手头有了这些信息,我们就很容易定位页上的记录,因为知道了页号,文件号,还有槽号,页上的记录就可以很容易定位到。

我们再来看看非唯一的非聚集索引。根页号是14608,index id是3。我们来看下14608页的内容。

1 DBCC TRACEON(3604) 
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 14608, 3) 
4 GO

我们看到了不同的东西!!用DBCC PAGE输出非唯一非聚集索引的根页内容是不一样的。这里SQL Server额外增加了“ HEAP RID(key) ”列。这列的值是为了让你的非唯一非聚集索引唯一。在你索引行里HEAP RID列使用8个额外字节来存储下列信息,用来保证堆表索引键的唯一:

  • 4 bytes: 页号(PageID)
  • 2 bytes:文件号(FileID)
  • 2 bytes:槽号(Slot number)

在堆表上非唯一非聚集索引上,所有索引层的每个索引行都会增加8个额外字节占用,不包括叶子层,因为叶子层都会保存HEAP RID。因此在你创建非唯一非聚集索引时,请记住索引行的8字节的额外占用。因为我已经说过,默认创建的非聚集索引都是非唯一的。

这个例子,我们的非唯一索引非聚集索引占用空间是唯一非聚集索引的2倍,因为唯一索引需要11 bytes,而非唯一索引需要19 bytes(包括8 bytes的HEAP RID)。我们回头看下DMV  sys.dm_db_index_physical_stats的信息输出,唯一非聚集索引的根页,页面空间使用率约24%,而非唯一非聚集索引的根页,页面空间使用率是39%。在大的非聚集索引上会更加明显。

因此当你用下列脚本定义非聚集索引时:

1 CREATE NONCLUSTERED INDEX ...

如果不考虑下你数据的唯一性,你的非聚集索引就在浪费大量的存储空间,降低你的索引性能,并增加日后索引维护难度。

这个系列的下篇文章我们会看下唯一和非唯一非聚集索引之间的区别,请继续关注! 

相关实践学习
使用SQL语句管理索引
本次实验主要介绍如何在RDS-SQLServer数据库中,使用SQL语句管理索引。
SQL Server on Linux入门教程
SQL Server数据库一直只提供Windows下的版本。2016年微软宣布推出可运行在Linux系统下的SQL Server数据库,该版本目前还是早期预览版本。本课程主要介绍SQLServer On Linux的基本知识。 相关的阿里云产品:云数据库RDS&nbsp;SQL Server版 RDS SQL Server不仅拥有高可用架构和任意时间点的数据恢复功能,强力支撑各种企业应用,同时也包含了微软的License费用,减少额外支出。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/sqlserver
目录
相关文章
|
JavaScript 数据可视化
基于fabric.js的图片编辑器, 画布背景实现原理
基于vue3 + fabric.js + vite + element-plus + typescript等技术,画布背景原理分析
基于fabric.js的图片编辑器, 画布背景实现原理
|
消息中间件 数据采集 域名解析
数据采集-Lua集成kafka流程跑通|学习笔记
快速学习数据采集-Lua集成kafka流程跑通
数据采集-Lua集成kafka流程跑通|学习笔记
|
6月前
|
存储 监控 安全
日志审计是什么?为什么企业需要日志审计?
日志审计是对信息系统中产生的日志进行收集、分析和存储的过程,记录系统活动如用户登录、操作行为等。企业通过日志审计可满足合规要求(如金融、医疗行业的法规),及时发现安全威胁(如异常登录、数据泄露),并为事故调查提供依据。工具如EventLog Analyzer能帮助企业生成合规报表,确保符合PCI DSS、HIPAA等标准,并支持实时监控与存档分析,保障网络安全与数据隐私。
391 2
|
定位技术
Echarts实战案例代码(10):echarts结合世界所有国家地图数据集geojson的(英文翻译映射)解决方案
Echarts实战案例代码(10):echarts结合世界所有国家地图数据集geojson的(英文翻译映射)解决方案
1004 0
Echarts实战案例代码(10):echarts结合世界所有国家地图数据集geojson的(英文翻译映射)解决方案
|
6月前
|
人工智能 安全 搜索推荐
OA办公的未来:从工具到流程的深度整合
在企业数字化转型中,“效率”至关重要。多人协同编辑(如板栗看板)突破传统文档处理方式,解决了信息孤岛、重复劳动和信息不透明等协作瓶颈,提供即时同步、可追溯、灵活及安全的编辑体验。它作为现代化OA系统的核心模块,通过优化文档协作、流程和知识管理,以及组织间的协作,显著提升多部门工作效率,缩短定稿周期,并支持跨区域无缝对接。未来,该技术将向智能化发展,集成AI辅助、跨平台融合和个性化配置等功能,为企业创造更大价值。
|
7月前
|
机器学习/深度学习 传感器 人工智能
《智启工业新篇:人工智能驱动的故障预测性维护》
在工业生产中,机器设备的稳定运行至关重要。传统维护模式存在滞后性和不确定性,导致高昂成本和风险。随着人工智能技术兴起,故障预测性维护成为可能。通过传感器采集数据,利用机器学习和深度学习算法,AI能提前预判故障,提高生产效率和安全性。工业物联网(IIoT)进一步增强了实时监控与远程管理能力,使得维护更加智能化、精准化。尽管面临数据安全和模型解释性等挑战,AI驱动的预测性维护正逐步改变传统模式,引领工业生产迈向更智能、高效的新时代。
618 18
|
10月前
|
机器学习/深度学习 数据采集 自然语言处理
使用Python实现深度学习模型:智能社交媒体内容分析
使用Python实现深度学习模型:智能社交媒体内容分析
971 69
|
11月前
|
安全 JavaScript 前端开发
自动化测试的魔法:如何用Python编写你的第一个测试脚本
【8月更文挑战第31天】 在软件的世界里,质量是王道。而自动化测试,就像是维护这个王国的骑士,确保我们的软件产品坚不可摧。本文将引导你进入自动化测试的奇妙世界,教你如何使用Python这把强大的魔法杖,编写出能够守护你代码安全的第一道防护咒语。让我们一起开启这场魔法之旅吧!
|
NoSQL JavaScript 算法
Higress 全新 Wasm 运行时,性能大幅提升
本文介绍 Higress 将 Wasm 插件的运行时从 V8 切换到 WebAssembly Micro Runtime (WAMR) 的最新进展。
55525 65
|
Java 调度 流计算
基于多线程的方式优化 FLink 程序
这篇内容介绍了线程的基本概念和重要性。线程是程序执行的最小单位,比进程更细粒度,常用于提高程序响应性和性能。多线程可以实现并发处理,利用多核处理器,实现资源共享和复杂逻辑。文章还讨论了线程的五种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED)以及如何在Java中创建和停止线程。最后提到了两种停止线程的方法:使用标识和中断机制。
375 5