MSSQL 的复合索引和包含索引有啥区别?

​一、背景

1. 讲故事

在 SQLSERVER 中有非常多的索引,比如:聚集索引,非聚集索引,唯一索引,复合索引,Include索引,交叉索引,连接索引,奇葩索引等等,当索引多了之后很容易傻傻的分不清,比如:​​复合索引​​​ 和 ​​Include索引​​,但又在真实场景中用的特别多,本篇我们就从底层数据页层面厘清一下。

二、到底有什么区别

1. 这些索引解决了什么问题

说区别之前,一定要知道它们大概解决了什么问题?这里我就从 ​​索引覆盖​​ 角度来展开吧,为了方便讲述,先上一个测试 sql:


IF(OBJECT_ID('t')ISNOTNULL)DROPTABLE t;

CREATETABLE t(a INT IDENTITY, b CHAR(6), c CHAR(10) DEFAULT 'aaaaaaaaaa')

SET NOCOUNT ON
DECLARE @num INT
SET @num =10000
WHILE (@num <90000)
BEGIN
INSERTINTO t(b)VALUES('b'+CAST(@num ASCHAR(5)))
SET @num=@num+1
END

CREATE CLUSTERED INDEX idx_a ON t(a)
CREATE INDEX idx_b ON t(b)

SELECT*FROM t;

代码非常简单,在 t 表中创建三个列,插入 8w 条数据,然后创建两个索引,接下来做一个查询获取 ​​b,c​​ 列。


SET STATISTICS IO ON
SET STATISTICS TIMEON
SELECT b,c FROM t WHERE b IN('b10000','b20000','b30000','b40000','b50000','b70000','b80000','b90000')
SET STATISTICS IO OFF
SET STATISTICS TIME OFF

输出如下:


表“t”。扫描计数 8,逻辑读取次数 30,物理读取次数 0,页面服务器读取次数 0,预读读取次数 0,页面服务器预读读取次数 0,LOb 逻辑读取次数 0,LOB 逻辑读取次数 0,LOB 页面服务器读取次数 0,LOB 预读读取次数 0,LOB 页面服务器预读读取次数 0

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =134 毫秒。

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =0 毫秒。

Completion time:2023-01-06T08:47:45.2364473+08:00

从执行计划看,这是一个经典的 书签查找​,这种查找返回的行数越多性能越差,在索引优化时一般都会规避掉这种情况,我们也看到了逻辑读取次数有 30 次,那能不能再小一点呢?

为了解决这个问题,干脆把 c 列也放到索引中去达到索引覆盖的效果,这就需要用到 复合索引 了,参考sql如下:


CREATE INDEX idx_complex ON t (b,c)

再次查询输出如下:


SQL Server 分析和编译时间:
CPU 时间 =0 毫秒,占用时间 =0 毫秒。
表“t”。扫描计数 8,逻辑读取次数 24,物理读取次数 0,页面服务器读取次数 0,预读读取次数 0,页面服务器预读读取次数 0,LOb 逻辑读取次数 0,LOB 逻辑读取次数 0,LOB 页面服务器读取次数 0,LOB 预读读取次数 0,LOB 页面服务器预读读取次数 0

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =96 毫秒。

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =0 毫秒。

Completion time:2023-01-06T08:53:56.9688921+08:00

从执行计划来看,这次没有走 书签查找​ 而是 索引查找​,并且逻辑读也降到了 24 次,这是一个好的优化。

相信有些朋友也知道用 Include索引 也能达到这个效果,接下来试着把复合索引给删了增加一个 Include索引,代码如下:


DROP INDEX idx_complex ON dbo.t;
CREATE INDEX idx_include ON t(b) INCLUDE (c)

再次查询输出如下:


表“t”。扫描计数 8,逻辑读取次数 16,物理读取次数 0,页面服务器读取次数 0,预读读取次数 0,页面服务器预读读取次数 0,LOb 逻辑读取次数 0,LOB 逻辑读取次数 0,LOB 页面服务器读取次数 0,LOB 预读读取次数 0,LOB 页面服务器预读读取次数 0

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =73 毫秒。

SQL Server 执行时间:
CPU 时间 =0 毫秒,占用时间 =0 毫秒。

Completion time:2023-01-06T08:58:18.1122561+08:00

从执行计划来看也是走的 非聚集索引​,而且逻辑读再次降到了 16​ 次,相比原始的书签查找已经优化了 50%,这是一个巨大的性能提升不是。

到这里其实有一个问题,两种优化走的都是 非聚集索引​,从逻辑读次数看貌似 Include索引 更好一些,为什么会这样呢?这就涉及到了底层存储,接下来一起扒一下。

2. 存储原理研究

研究它们的不同点,最彻底的方式就是从底层存储出发,首先我们观察下 复合索引​ 的底层存储是什么样的,可以用 DBCC 命令。


DBCC TRACEON(3604)
DBCC IND(MyTestDB,t,-1)

从 IndexLevel=2​ 来看这个复合索引​构成的B树已经达到了二层,接下来我们查一下 368 号数据页内容。


DBCC PAGE(MyTestDB,1,368,2)

输出如下:


PAGE:(1:368)

Memory Dump @0x000000F555578000

000000F555578000:01020002008000010000000000001b00 00000000 ....................
000000F555578014:000002003e010000601f9c00 7001000001000000 ....>...`...p.......
000000F555578028: f8000000 e0680000 f5010000 0000000000000000 .....h..............
000000F55557803C:0000000001000000000000000000000000000000 ....................
000000F555578050:0000000000000000000000000000000016623130 .................b10
000000F555578064:3030306161616161616161616101000000380500000aaaaaaaaaa....8..
000000F555578078:0001000400001662383336313661616161616161 .......b83616aaaaaaa
000000F55557808C:616161911f010070 050000010004000000006231 aaa....p..........b1

OFFSET TABLE:

Row - Offset
1(0x1)-126(0x7e)
0(0x0)-96(0x60)


DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。

根据下面的 Slot 个数可以知道这个分支节点数据页只有 2 条记录,分别为:(b10000,aaaaaaaaaa,0x01) , (b83616,aaaaaaaaaa,0x011f91)​,这里说明一下最后的 01 和 0x011f91 是主键key,接下来找个叶子节点,比如:1632 号索引页。


PAGE:(1:1632)


Memory Dump @0x000000F555578000

...
000000F555578050:0000000000000000000000000000000016623135 .................b15
000000F555578064:32383761616161616161616161a81400 00040000287aaaaaaaaaa.......
000000F555578078:1662313532383861616161616161616161a91400 .b15288aaaaaaaaaa...
000000F55557808C:0004000016623135323839616161616161616161 .....b15289aaaaaaaaa
000000F5555780A0:61aa1400 00040000166231353239306161616161 a........b15290aaaaa
000000F5555780B4:6161616161ab1400 000400001662313532393161 aaaaa........b15291a
000000F5555780C8:616161616161616161ac1400 0004000016623135 aaaaaaaaa........b15
000000F5555780DC:32393261616161616161616161ad1400 00040000292aaaaaaaaaa.......
000000F5555780F0:1662313532393361616161616161616161ae1400 .b15293aaaaaaaaaa...
000000F555578104:0004000016623135323934616161616161616161 .....b15294aaaaaaaaa
000000F555578118:61af1400 00040000166231353239356161616161 a........b15295aaaaa
000000F55557812C:6161616161b01400 000400001662313532393661 aaaaa........b15296a
000000F555578140:616161616161616161b11400 0004000016623135 aaaaaaaaa........b15
...

从叶子节点上看,也是 (b,c,key) 的布局模式,这时候脑子里就有了一张图。

用同样的方式观察下 Include索引​,发现 IndexLevel=1,说明只有一层。

再用 DBCC 观察下分支节点的布局。


PAGE:(1:1696)

Memory Dump @0x000000F554F78000

000000F554F78000:0102000100820001000000000000110000000000 ....................
000000F554F78014:00000601420100001c09d814 a0060000 01000000 ....B.... ..........
000000F554F78028:0f010000 78310000390100000000000000000000 ....x1..9...........
000000F554F7803C: f01efa04 00000000000000000000000000000000 ....................
000000F554F78050:0000000000000000000000000000000016623130 .................b10
000000F554F78064:3030300100000088030000010003000016623130000..............b10
000000F554F78078:33313138010000b0 0300000100030000166231303118.............b10
000000F554F7808C:3632326f 020000b1 030000010003000016623130622o.............b10
000000F554F780A0:393333a6 030000b2 030000010003000016623131933..............b11
...

从输出看并没有记录 列c​ 的值,就是那烦人的 aaaaaaaaaa,然后再抽个叶子节点看看,比如:1218号索引页。


PAGE:(1:1218)
Memory Dump @0x000000F554F78000

000000F554F78000:0102000004020001 c1040000 01001500 c3040000 ....................
000000F554F78014:01003701420100000a00881d c2040000 01000000 ..7.B...............
000000F554F78028:0f010000 00310000030000000000000000000000 .....1..............
000000F554F7803C: e7351886 00000000000000000000000000000000.5..................
000000F554F78050:0000000000000000000000000000000016623833 .................b83
000000F554F78064:313235a6 1d010061 616161616161616161040000125....aaaaaaaaaa...
000000F554F78078:16623833313236a7 1d010061 6161616161616161.b83126....aaaaaaaaa
000000F554F7808C:6104000016623833313237a8 1d010061 61616161 a....b83127....aaaaa
000000F554F780A0:616161616104000016623833313238a9 1d010061 aaaaa....b83128....a
000000F554F780B4:61616161616161616104000016623833313239aa aaaaaaaaa....b83129.
000000F554F780C8:1d010061 61616161616161616104000016623833 ...aaaaaaaaaa....b83
000000F554F780DC:313330ab 1d010061 616161616161616161040000130....aaaaaaaaaa...
...

在叶子节点中我们终于看到了 aaaaaaaaaa ,其实想一想肯定是有的,不然怎么做索引覆盖呢?有了这些信息,脑子中又有了一张图。

从图中可以看出,Include索引​ 的分支节点是不包含 c​ 列的,这个列只会保存在 叶子节点​ 中,再结合树的高度来看就能解释为什么 Include索引​ 的逻辑读要少于 复合索引。

三、总结

总的来说 复合索引​ 和 Include索引​ 各有利弊吧,前者会让索引页的行数据更大,导致索引页更多,也就会占用更多的存储空间,更多的逻辑读,索引维护开销也更大,而后者只会将 Include 列 保存在叶子节点,不参与索引计算,相对来说占用的索引页空间更小。

在查询方面,复合索引能达到的索引覆盖场景远大于单列索引,而且在过滤,排序场景下也能发挥奇效,所以还是根据你的读写比例做一个取舍吧。

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/262993.html<

(0)
管理的头像管理
上一篇2025-05-04 08:54
下一篇 2025-05-04 08:55

相关推荐

  • 站群服务器和普通服务器到底哪个更适合GEO,怎么选?

    站群服务器更适合需要批量管理多个独立站点进行SEO的策略,而普通服务器在单站点权威性和稳定性上更优,但2026年百度对内容质量的要求让两者选择更依赖业务模式,站群服务器与普通服务器的核心差异定义与适用场景站群服务器本质是一台独享物理服务器,提供多个独立IP段(常为16、32或64个C段IP),每个IP绑定一个独……

    2026-07-28
    0
  • 物理服务器和云服务器做站群到底选哪个,哪个更稳定?

    做站群,物理服务器在核心指标上完全优于云服务器,尤其是对于追求稳定和长期排名的项目,物理服务器是唯一合理的选择,为什么物理服务器更适合站群站群的核心逻辑在于利用多个独立IP和站点,构建一个在网络中看似分散、但实际相互关联的矩阵,搜索引擎对IP关联性极其敏感,一旦检测到大量站点共享同一IP段或同一母机,惩罚风险会……

    2026-07-28
    0
  • 国内高防服务器哪家防御真实靠谱,怎么选?

    国内高防服务器哪家防御真实靠谱?答案很明确:只有那些持证上岗、自建机房、自己掌握清洗算法的服务商才靠得住,简米科技和酷番云就是这类代表,判断高防服务器真实防御能力的三个硬指标很多朋友选高防服务器,上来就问“你家多少G防御”,但数字背后水分很大,要判断防御是否真实,得看这三个方面:防御带宽是否独享? 有些服务商宣……

    2026-07-28
    0
  • 裸金属服务器和物理服务器有什么区别?,怎么选?

    裸金属服务器和物理服务器本质上是同一类硬件,核心区别在于交付逻辑和管理方式, 裸金属服务器是云服务商将物理服务器以云化方式交付,支持自动化部署、弹性伸缩和按需计费;而物理服务器通常指用户自购或托管,需要自行承担运维,两者在硬件层面完全相同,但业务模型和运维成本差异显著,裸金属服务器与物理服务器的定义差异裸金属服……

    2026-07-28
    0
  • 做GEO站群选哪家服务器服务商靠谱,怎么选?

    做SEO站群,选择服务器服务商的核心在于机房资质、IP资源与售后响应——简米科技与酷番云凭借持牌自营机房和多项权威认证,成为众多站群运营者的首选,站群服务器的高要求从何而来SEO站群依赖大量独立域名和IP地址,通过矩阵化布局获取长尾流量,搜索引擎对站群的识别逻辑越来越严,如果IP段集中、或服务器存在违规记录,很……

    2026-07-28
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注