适用于:SQL Server
Azure SQL 数据库
Azure SQL 托管实例
Microsoft Fabric 预览版中的 SQL 数据库
设计高效的索引是实现良好的数据库和应用程序性能的关键。 缺少索引、过度索引或设计不佳的索引是数据库性能问题的主要来源。
本指南介绍索引体系结构和基础知识,并提供最佳做法来帮助设计有效的索引以满足应用程序的需求。
有关可用索引类型的详细信息,请参阅 索引。
本指南涉及以下类型的索引:
| 主存储格式 | 索引类型 |
|---|---|
| 基于磁盘的行存储 | |
| Clustered | |
| Nonclustered | |
| Unique | |
| Filtered | |
| Columnstore | |
| 聚集列存储 | |
| 非聚集列存储 | |
| Memory-optimized | |
| Hash | |
| 内存优化的非聚集 |
有关 XML 索引的信息,请参阅 XML INDEX (SQL Server) 和选择性 XML 索引 (SXI)。
有关空间索引的信息,请参阅空间索引概述。
有关全文索引的信息,请参阅填充全文索引。
索引基础知识
请考虑普通书籍:书本末尾有一个索引,可帮助快速查找书籍内的信息。 索引是按顺序排列的关键字列表,每个关键字旁边是一组页码,这些页码指向可在其中找到每个关键字的页面。
行存储索引类似:它是值的有序列表,对于每个值,都有指向这些值所在的数据 页 的指针。 索引本身也存储在页中,称为 索引页。 在普通书籍中,如果索引跨越多个页面,并且你必须找到包含单词SQL的所有页面的索引指针,那么你需要从索引的开头开始翻阅,直到找到包含关键字SQL的索引页。 在索引页面,你可以找到指向所有书籍页面的指针。 如果在索引的开头创建了一个页面,其中包含可以找到每个字母的字母顺序列表,则可以进一步优化此页面。 例如,“A 到 D - 第 121 页”,“E 到 G - 第 122 页”等等。 这个额外的页面将使你不必执行翻阅索引才能找到起始位置的步骤。 此类页面在常规书籍中不存在,但在行存储索引中确实存在。 此单个页面称为索引的根页。 根页是索引使用的树结构的起始页面。 按照树的类比,包含指向实际数据的指针的结束页面被称为树的“叶页”。
索引是与表或视图关联的磁盘上或内存中结构,可以加快从表或视图中检索行的速度。 行存储索引包含从表或视图中一列或多列的值生成的键。 对于行存储索引,这些键存储在树结构(B+ 树)中,使数据库引擎能够快速高效地查找与键值关联的行。
行存储索引以逻辑方式存储以具有行和列的表的形式组织的数据,并实际存储在名为 行存储1 的行数据格式中。 有一种替代方法,用于存储数据列,称为 列存储。
数据库及其工作负荷的正确索引的设计是查询速度、索引更新成本和存储成本之间的复杂平衡行为。 窄基于磁盘的行存储索引,或索引键中只有少量列的索引,需要较少的存储空间和较小的更新开销。 另一方面,宽索引可能会改进更多查询。 在找到最高效的索引集之前,可能需要尝试几种不同的设计。 随着应用程序的发展,索引可能需要更改才能保持最佳性能。 可以在不影响数据库架构或应用程序设计的情况下添加、修改和删除索引。 因此,应试验多个不同的索引而无需犹豫。
数据库引擎中的查询优化器通常选择执行查询的最有效索引。 若要查看查询优化器用于特定查询的索引,请在 SQL Server Management Studio 的 “查询 ”菜单上,选择“ 显示估计的执行计划 ”或 “包括实际执行计划”。
不要总是将索引的使用等同于良好的性能,或者将良好的性能等同于索引的高效使用。 如果只要使用索引就能获得最佳性能,那查询优化器的工作就简单了。 但事实上,不正确的索引选择并不能获得最佳性能。 因此,查询优化器的任务是选择索引或索引的组合,仅当索引提高性能时,并避免在阻碍性能时进行索引检索。
常见的设计错误是以推理方式创建许多索引来“提供优化器选择”。 生成的过度索引会降低数据修改速度,并可能导致并发问题。
1 行存储是存储关系表数据的传统方法。 行存储 是指基础数据存储格式为堆、B+ 树(聚集索引)或内存优化表的表。 “基于磁盘的行存储”排除了内存优化表。
索引设计任务
建议的索引设计策略包括以下任务:
了解数据库和应用程序的特征。
例如,在具有频繁数据修改并且必须保持高吞吐量的联机事务处理(OLTP)数据库中,一些针对最关键查询的窄行存储索引将是一个很好的初始索引设计。 对于极高的吞吐量,请考虑使用内存优化的表和索引,它们提供无锁定和无闩锁设计。 有关详细信息,请参阅本指南中的 内存优化非聚集索引设计指南 和 哈希索引设计指南 。
相反,对于必须快速处理非常大数据集的分析或数据仓库(OLAP)数据库,使用聚集列存储索引尤其合适。 有关详细信息,请参阅本指南中的 列存储索引:概述 或 列存储索引体系结构 。
了解最常用的查询的特征。
例如,知道常用查询联接两个或多个表有助于确定这些表的索引集。
了解查询谓词中使用的列中的数据分布。
例如,对于具有多个不同数据值的列,索引可能很有用,但对于具有多个重复值的列来说,索引可能不太有用。 对于具有许多 NUL 的列或具有明确定义的数据子集的列,可以使用筛选的索引。 有关详细信息,请参阅本指南中的筛选索引设计指南。
确定哪些索引选项可以提高性能。
例如,在现有大型表上创建聚集索引可能会受益于
ONLINE索引选项。ONLINE选项允许在创建索引或重新生成索引时继续对基础数据执行并发活动。 使用行或页面 数据压缩 可以通过减少索引的 I/O 和内存占用量来提高性能。 有关详细信息,请参阅 CREATE INDEX。检查表上的现有索引,以防止创建重复索引或非常相似的索引。
通常最好修改现有索引,而不是创建新的但大多重复的索引。 例如,请考虑将一两个额外包含的列添加到现有索引,而不是使用这些列创建新索引。 当你 使用缺少索引建议优化非聚集索引时,或者如果使用 数据库引擎优化顾问,你可能会在同一表和列上提供类似的索引变体时,这尤其相关。
常规索引设计指南
了解数据库、查询和表列的特征,可帮助你最初设计最佳索引,并在应用程序发展时修改设计。
数据库注意事项
设计索引时,应考虑以下数据库准则:
表上大量索引会影响
INSERT、UPDATE、DELETE和MERGE语句的性能,因为当表中的数据发生更改时,索引中的数据可能需要更新。 例如,如果列用于多个索引,并且您执行UPDATE修改该列数据的语句,则还必须更新包含该列的每个索引。避免对经常更新的表进行过多的索引,并且索引应保持较窄,就是说,列要尽可能少。
可以对只有少量数据修改但大量数据的表创建更多索引。 对于此类表,各种索引有助于查询性能,而索引更新开销仍可接受。 但是,不要以推理方式创建索引。 监视索引使用情况,并随着时间的推移删除未使用的索引。
为小型表编制索引可能不是最佳,因为数据库引擎可能需要更长的时间才能遍历搜索数据的索引,而不是执行基表扫描。 因此,可能永远不会使用小型表上的索引,但仍必须在更新表中的数据时进行更新。
当视图包含聚合和/或联接时,视图上的索引可提供显著的性能提升。 有关详细信息,请参阅 “创建索引视图”。
查询存储有助于识别性能欠佳的查询 ,并提供 查询执行计划的 历史记录,以便查看优化器选择的索引。 可以使用此数据,通过关注最频繁的查询和资源密集查询,使您的索引调整更具影响力。
查询注意事项
设计索引时,应考虑以下查询准则:
在查询中的 谓词 和联接表达式中经常使用的列上创建非聚集索引。 这些是 SARGable 列。 但是,应避免向索引添加不必要的列。 添加过多的索引列可能会对磁盘空间和索引更新性能产生不利影响。
关系数据库中的术语 SARGable 是指可以使用索引加快查询执行的 Search ARGument 谓词。 有关详细信息,请参阅 SQL Server 和 Azure SQL 索引体系结构和设计指南。
Tip
始终确保创建的索引实际上由查询工作负荷使用。 删除未使用的索引。
索引使用情况统计信息在 sys.dm_db_index_usage_stats 和 sys.dm_db_index_operational_stats中可用。
涵盖索引可以提高查询性能,因为符合查询要求的全部数据都存在于索引本身中。 也就是说,只需要索引页,而不需要表的数据页或聚集索引来检索所需数据,因此,减少了总体磁盘 I/O。 例如,对某一表(其中对列
A、B和A创建了组合索引)的列B和C的查询,仅仅从该索引本身就可以检索指定数据。Note
覆盖索引是一个非聚集索引,它直接满足查询的所有数据访问,而无需访问基表。
此类索引在索引键中具有所有必要的 SARGable 列,并且将非 SARGable 列作为包含的列。 这意味着查询所需的所有列都在索引中,无论是在
WHEREJOINGROUP BY子句中,还是在SELECT或UPDATE子句中。如果索引比表中的行和列更窄,那么执行查询所需的 I/O 可能会大大减少,因为索引涉及的列只是所有列的一小部分。
在从大型表中检索由固定谓词定义的小部分数据时,请考虑使用覆盖索引。
避免创建包含过多列的覆盖索引,因为这在扩充数据库存储、I/O 和内存占用时会降低其优势。
将插入或修改尽可能多的行的查询写入单个语句内,而不要使用多个查询更新相同的行。 这减少了索引更新开销。
列注意事项
设计索引时,应考虑以下列准则:
请保持索引键的长度较短,尤其是聚集索引的情况。
不能将 ntext、 text、 image、 varchar(max)、 nvarchar(max)、 varbinary(max)、 json 和 向量 数据类型的列指定为索引键列。 但是,可以将具有这些数据类型的列作为非键(包括)索引列添加到非聚集索引中。 有关详细信息,请参阅本指南中的“ 在非聚集索引中使用包含的列 ”部分。
检查列的唯一性。 同一键列上的唯一索引而不是非统一索引为查询优化器提供了其他信息,使索引更有用。 有关详细信息,请参阅本指南中的唯一索引设计指南。
在列中检查数据分布。 即使查询优化器使用索引,在包含许多行但只有少量不同值的列上创建索引可能无法提高查询性能。 作为类比,如果城市中的所有人都命名为史密斯或琼斯,按字母顺序排序的物理电话目录不会加快查找人员的速度。 有关数据分布的详细信息,请参阅 统计信息。
请考虑对具有明确定义的子集的列使用筛选索引,例如具有许多 NULL 的列、具有值类别的列以及具有不同值范围的列。 经过精心设计的筛选索引可以改善查询性能,降低索引更新成本,并在该子集与许多查询相关的情况下将一小部分所有行存储在表中,从而降低存储成本。
如果键包含多个列,请考虑索引键列的顺序。 在查询谓词中使用的列,如果用于相等(
=)、不等(>、>=、<、<=)表达式或BETWEEN中,或者参与联接,应该被优先放置。 其他列应该基于其非重复级别进行排序,就是说,从最不重复的列到最重复的列。例如,如果索引被定义为
LastName,FirstName则当子句中的WHERE查询谓词为WHERE LastName = 'Smith'或WHERE LastName = Smith AND FirstName LIKE 'J%'时,索引非常有用。 但是,查询优化器不会将索引用于仅对WHERE FirstName = 'Jane'进行搜索的查询,索引也不会提高此类查询的性能。如果计算列包含在查询谓词中,请考虑为计算列编制索引。 有关详细信息,请参阅计算列索引。
索引特征
在确定某一索引适合某一查询之后,可以选择最适合具体情况的索引类型。 索引特征包括:
- 聚集或非聚集
- 唯一或非唯一
- 单列或多列
- 索引中键列的升序或降序
- 非聚集索引的所有行或已筛选的行
- 列存储或行存储
- 内存优化表的哈希索引或非聚集索引
文件组或分区方案的索引设置
开发索引设计策略时,应该考虑在与数据库相关联的文件组上放置索引。
默认情况下,索引存储在创建索引的基表(聚集索引或堆)所在的同一文件组中。 其他配置可能,包括:
在基表的文件组以外的文件组上创建非聚集索引。
对要涵盖多个文件组的聚集和非聚集索引进行分区。
对于非分区表,最简单的方法是最好的方法:在同一文件组上创建所有表,并将尽可能多的数据文件添加到文件组,以利用所有可用的物理存储。
当分层存储可用时,可以考虑更高级的索引放置方法。 例如,可以为经常访问的表创建一个文件组,其中文件位于更快的磁盘上,以及一个文件组,用于在较慢的磁盘上存档表。
可以将具有聚集索引的表从一个文件组移到另一个文件组,方法是删除聚集索引,并在语句子句中MOVE TO指定新的文件组或分区方案,或者将DROP INDEX语句与子句结合使用CREATE INDEX。DROP_EXISTING
已分区索引
还可以考虑跨多个文件组对基于磁盘的堆、聚集索引和非聚集索引进行分区。 分区索引是根据分区函数水平分区(按行)的。 分区函数定义如何根据指定的特定列的值(称为分区列)将每一行映射到分区。 分区方案指定一组分区到文件组的映射。
对索引进行分区有以下优点:
使大型数据库更易于管理。 例如,OLAP 系统可以实现分区感知 ETL,从而大大简化了批量添加和删除数据的过程。
使某些类型的查询(例如长时间运行的分析查询)运行速度更快。 当查询使用分区索引时,数据库引擎可以同时处理多个分区,并跳过查询不需要的分区(消除)。
警告
分区很少提高 OLTP 系统中的查询性能,但如果事务查询必须访问多个分区,则可能会带来重大开销。
有关详细信息,请参阅已分区表和已分区索引。
索引排序顺序设计指南
定义索引时,请考虑每个索引键列是否应按升序或降序存储。 升序为默认值。
CREATE INDEX、CREATE TABLE 和 ALTER TABLE 语句的语法在索引和约束中的各列上支持关键字 ASC(升序)和 DESC(降序)。
当引用表的查询包含用以指定索引中键列的不同方向的 ORDER BY 子句时,指定键值存储在该索引中的顺序很有用。 在这些情况下,索引可以消除对查询计划中 排序运算符 的需求。
例如, Adventure Works Cycles 采购部门的买方不得不评估他们从供应商处购买的产品的质量。 买家最关心的是找到那些拒绝率很高的供应商所发送的产品。
检索数据以满足此条件需要将 表中的 RejectedQty 列按降序(由大到小)排序,并且将 Purchasing.PurchaseOrderDetail 列按升序(由小到大)排序,如下列针对 ProductID的查询所示。
SELECT RejectedQty,
((RejectedQty / OrderQty) * 100) AS RejectionRate,
ProductID,
DueDate
FROM Purchasing.PurchaseOrderDetail
ORDER BY RejectedQty DESC, ProductID ASC;
此查询的以下 执行计划 显示查询优化器使用 Sort 运算符按子句指定的 ORDER BY 顺序返回结果集。
如果使用与查询中的子句中的 ORDER BY 索引匹配的键列创建基于磁盘的行存储索引,则会消除查询计划中 的 Sort 运算符,从而提高查询计划的效率。
CREATE NONCLUSTERED INDEX IX_PurchaseOrderDetail_RejectedQty
ON Purchasing.PurchaseOrderDetail
(RejectedQty DESC, ProductID ASC, DueDate, OrderQty);
再次执行查询后,以下执行计划显示 Sort 运算符不再存在,并且使用了新建的非聚集索引。
数据库引擎可以按任一方向扫描索引。 定义为 RejectedQty DESC, ProductID ASC 的索引仍可用于一个查询,其中 ORDER BY 子句中列的排序方向是反向的。 例如,具有 ORDER BY 子句 ORDER BY RejectedQty ASC, ProductID DESC 的查询可以使用同一索引。
只能为索引中的键列指定排序顺序。 sys.index_columns目录视图报告索引列是按升序还是降序存储。
聚集索引设计指南
聚集索引存储表的所有行和所有列。 行按索引键值的顺序排序。 每个表只能有一个聚集索引。
术语 基表 可以指代聚集索引或堆。 堆是磁盘上的 未排序 数据结构,其中包含表的所有行和所有列。
除了一些例外情况,每个表都应具有聚集索引。 聚集索引的所需属性为:
| 资产 | Description |
|---|---|
| 窄 | 聚集索引键是同一基表上任何非聚集索引的一部分。 窄键,或键列的总长度较小,可减少表上所有索引的存储、I/O 和内存开销。 若要计算密钥长度,请为键列使用的数据类型添加存储大小。 有关详细信息,请参阅 数据类型类别。 |
| 独特 | 如果聚集索引不唯一,则会自动将 4 字节的内部唯一器列添加到索引键,以确保唯一性。 将现有的唯一列添加到聚集索引键可以避免表上所有索引中唯一化列的存储开销、I/O 读写开销和内存占用。 此外,当索引唯一时,查询优化器可以生成更高效的查询计划。 |
| 不断增加 | 在不断增加的索引中,数据始终添加到索引的最后一页上。 这样可避免索引中间的页面拆分,从而减少 页面密度 并降低性能。 |
| 不可变 | 聚集索引键是任何非聚集索引的一部分。 修改聚集索引的键列时,还必须在所有非聚集索引中进行更改,这会增加 CPU、日志记录、I/O 和内存开销。 如果聚集索引的键列不可变,则避免开销。 |
| 仅具有不可为空的列 | 如果行具有可为 null 的列,则它必须包含一个名为 NULL 块的内部结构,该结构在索引中添加每行 3-4 字节的存储。 使聚集索引的所有列不可为 null 可避免此开销。 |
| 仅具有固定宽度列 | 与固定宽度数据类型相比,使用可变宽度数据类型(如 varchar 或 nvarchar )的列使用每个值额外 2 个字节。 使用固定宽度数据类型(如 int )可避免表上所有索引中出现这种开销。 |
设计聚集索引时,尽可能多地满足这些属性不仅使聚集索引,而且使同一表上的所有非聚集索引更高效。 通过避免存储、I/O 和内存开销来提高性能。
例如,具有单个int或bigint且不可为 null 的列的聚集索引键,如果是通过IDENTITY或使用序列的默认约束进行填充,并且在插入行后未进行更新,则具有所有这些属性。
相反,具有单个 uniqueidentifier 列的聚集索引键更宽,因为它使用 16 字节的存储而不是 4 个字节的 int 和 8 个字节的 bigint,并且不会满足 不断增加 的属性,除非按顺序生成值。
Tip
创建 PRIMARY KEY 约束时,会自动创建支持该约束的唯一索引。 默认情况下,此索引聚集;但是,如果此索引不满足聚集索引的所需属性,则可以将约束创建为非聚集索引并改为创建不同的聚集索引。
如果不创建聚集索引,表将存储为堆,这通常不建议这样做。
聚集索引体系结构
行存储索引是按 B+ 树结构组织的。 索引 B+ 树中的每一页称为一个索引节点。 B+ 树的顶端节点称为根节点。 索引中的底层节点称为叶节点。 根节点与叶节点之间的任何索引级别统称为中间级。 在聚集索引中,叶节点包含基础表的数据页。 根节点和中间级节点包含存有索引行的索引页。 每个索引行包含一个键值和一个指针,该指针指向 B+ 树上的某一中间级页或叶级索引中的某个数据行。 每级索引中的页均被链接在双向链接列表中。
聚集索引在 sys.partitions 中为索引使用的每个分区有一行,其中 index_id = 1。 默认情况下,聚集索引有单个分区。 当聚集索引有多个分区时,每个分区都有一个单独的 B+ 树结构,其中包含该特定分区的数据。 例如,如果聚集索引有四个分区,则每个分区有四个 B+ 树结构。
根据聚集索引中的数据类型,每个聚集索引结构将有一个或多个分配单元,将在这些单元中存储和管理特定分区的相关数据。 每个聚集索引的每个分区中至少有一个 IN_ROW_DATA 分配单元。 如果聚集索引包含大型对象(LOB)列(例如 LOB_DATA则每个分区也有一个分配单元。 如果分区包含超过 8,060 字节行大小限制的可变长度列,则每个分区也有一个 ROW_OVERFLOW_DATA 分配单元。
B+ 树结构中的页面按聚集索引键的值排序。 所有插入均在插入行中的键值适合现有页面的排序顺序的页面上进行。 在页面中,行不一定以任何物理顺序存储。 但是,该页使用名为 槽数组的内部结构维护行的逻辑排序。 槽数组中的条目按索引键顺序进行维护。
下图显式了聚集索引单个分区中的结构。
非聚集索引设计指南
聚集索引和非聚集索引的主要区别在于,非聚集索引包含表中列的子集,通常排序方式与聚集索引不同。 (可选)可以筛选非聚集索引,这意味着它包含表中所有行的子集。
基于磁盘的行存储非聚集索引包含指向基表中行的存储位置的行定位符。 可以对表或索引视图创建多个非聚集索引。 通常,非聚集索引应设计为提高常用查询的性能,这些查询如果没有索引将需要扫描基表。
与使用书中索引的方式相似,查询优化器在搜索数据值时,先搜索非聚集索引以找到数据值在表中的位置,然后直接从该位置检索数据。 这使非聚集索引成为完全匹配查询的最佳选择,因为索引包含说明查询所搜索的数据值在表中的精确位置的项。
例如,若要查询 HumanResources.Employee 表中所有向特定经理报告的员工,查询优化器可能会使用非聚集索引 IX_Employee_ManagerID;其中 ManagerID 是其第一个键列。
ManagerID由于这些值在非聚集索引中排序,因此查询优化器可以快速查找与指定ManagerID值匹配的索引中的所有条目。 每个索引条目指向基表中的确切页和行,可以从该表中检索来自所有其他列的相应数据。 查询优化器找到索引中的所有条目后,可以直接转到确切的页面和行来检索数据,而不是扫描整个基表。
非聚集索引体系结构
基于磁盘的行存储非聚集索引与聚集索引具有相同的 B+ 树结构,但存在以下差异:
非聚集索引不一定包含表的所有列和行。
非聚集索引的叶级别是由索引页而不是由数据页组成。 非聚集索引的叶级别的索引页包含键列。 (可选)它们还可以包含表中其他列的子集(如包含的 列),以避免从基表检索它们。
非聚集索引行中的行定位符是指向行的指针,或者是行的聚集索引键,如下所示:
如果表有聚集索引或索引视图上有聚集索引,则行定位器是行的聚集索引键。
如果表是堆(意味着该表没有聚集索引),则行定位器是指向行的指针。 该指针由文件标识符 (ID)、页码和页上的行数生成。 整个指针称为行 ID (RID)。
行定位符还可确保非聚集索引行的唯一性。 下表介绍了数据库引擎如何将行定位符添加到非聚集索引:
| 基表类型 | 非聚集索引类型 | 行定位符 |
|---|---|---|
| Heap | ||
| Nonunique | 添加到键列的 RID | |
| Unique | 添加到包含列的 RID | |
| 唯一聚集索引 | ||
| Nonunique | 添加到键列的聚集索引键 | |
| Unique | 添加到包含列的聚集索引键 | |
| 非唯一聚集索引 | ||
| Nonunique | 添加到键列的聚集索引键和 uniqueifier(出现时) | |
| Unique | 添加到包含列的聚集索引键和 uniqueifier(出现时) |
数据库引擎永远不会多次将给定列存储在非聚集索引中。 用户在创建非聚集索引时指定的索引键顺序将始终被遵循:任何需要添加到非聚集索引键的行定位符列都将添加到键的末尾,之后是在索引定义中指定的列。 无论是在索引定义中显式指定还是隐式添加,非聚集索引中的聚集索引键行定位符都可用于查询处理。
以下示例显示如何在非聚集索引中实现行定位符:
| 聚集索引 | 非聚集索引定义 | 具有行定位符的非聚集索引定义 | Explanation |
|---|---|---|---|
具有键列(A、B 和 C)的唯一聚集索引 |
具有键列(B 和 A)以及包含列(E 和 G)的非唯一非聚集索引 |
键列(B、A 和 C)和包含列(E 和 G) |
非聚集索引是非唯一的,因此行定位符需要存在于索引键中。 行定位符中的列 B 和 A 已存在,因此仅添加列 C。 列 C 将添加到键列列表的末尾。 |
具有键列 (A) 的唯一聚集索引 |
具有键列(B 和 C)以及包含列 (A) 的非唯一非聚集索引 |
键列(B、C 和 A) |
非聚集索引是非唯一的,因此行定位符将添加到键中。 列 A 尚未被指定为键列,因此会添加到键列列表的末尾。 列 A 现在位于键中,因此无需将该列存储为包含列。 |
具有键列(A 和 B)的唯一聚集索引 |
具有键列 (C) 的唯一聚集索引 |
键列 (C) 和包含列(A 和 B) |
非聚集索引是唯一的,因此行定位符将添加到包含列。 |
对于索引使用的每个分区,非聚集索引在 sys.partitions 中有一行,其中 index_id > 1。 默认情况下,一个非聚集索引有单个分区。 如果一个非聚集索引有多个分区,则每个分区都有一个包含该特定分区的索引行的 B+ 树结构。 例如,如果非聚集索引有四个分区,则每个分区有四个 B+ 树结构。
根据非聚集索引中数据类型的不同,每个非聚集索引结构会有一个或多个分配单元,在其中存储和管理特定分区的数据。 每个非聚集索引至少每个分区都有一个 IN_ROW_DATA 分配单元,用于存储索引 B+ 树页。 如果非聚集索引包含大型对象(LOB)列(例如 LOB_DATA则每个分区也有一个分配单元。 此外,如果分区包含超过 8,060 字节行大小限制的可变长度列,则每个分区有一个 ROW_OVERFLOW_DATA 分配单元。
下图说明了单个分区中的非聚集索引结构。
在非聚集索引中使用包含的列
除了键列,非聚集索引还可以在叶级别存储非键列。 这些非键列称为包含列,CREATE INDEX 指定在语句的INCLUDE子句中。
包含非键列的索引在涵盖查询时可以显著提高查询性能,也就是说,当查询中使用的所有列都作为键列或非键列位于索引中时。 由于数据库引擎可以在索引中找到所有列值,因此可实现性能提升,从而减少磁盘 I/O 操作,因为基表无需访问。
如果列必须由查询检索,但不用于查询谓词、聚合和排序,请将其添加为包含列,而不是作为键列。 这具有以下优点:
被包含的列可以使用那些不允许用作索引键列的数据类型。
计算索引键列或索引键大小时,数据库引擎不会考虑包含的列。 包含的列不受 900 字节的最大键大小的限制。 可以创建涵盖更多查询的更广泛的索引。
将列从索引键移动到包含的列时,索引生成所需的时间更少,因为索引排序操作会更快。
如果表具有聚集索引,则聚集索引键中定义的列或列会自动添加到表上的每个非聚集索引中。 不必在非聚集索引键或包含列中指定它们。
包含列的索引指南
设计包含列的非聚集索引时,请考虑以下准则:
仅可在表或索引视图的非聚集索引中定义包含的列。
允许除 text、 ntext 和 image 之外的所有数据类型。
精确或不精确的确定性计算列都可以是包含列。 有关详细信息,请参阅计算列索引。
与键列一样,只要计算列的数据类型可以包含在列中,就可以包括从 图像、ntext 和 文本 数据类型派生的计算列。
不能同时在
INCLUDE列表和键列列表中指定列名。INCLUDE列表中的列名不能重复。必须在索引中定义至少一个键列。 包含的最大列数为 1,023。 也就是最大的表列数减 1。
无论是否存在包含的列,索引键列都必须遵循现有索引大小限制(最大 16 个键列),索引键总大小为 900 字节。
关于包含列的索引设计建议
请考虑重新设计那些索引键大小较大的非聚集索引,以使只有在查询谓词、聚合和排序中使用到的列成为键列。 将覆盖查询的所有其他列设置为包含性非键列。 这样,将具有覆盖查询所需的所有列,但索引键本身较小,而且效率高。
例如,假设要设计覆盖下列查询的索引。
SELECT AddressLine1,
AddressLine2,
City,
StateProvinceID,
PostalCode
FROM Person.Address
WHERE PostalCode BETWEEN N'98000' AND N'99999';
若要覆盖查询,必须在索引中定义每列。 尽管可以将所有列定义为键列,但键大小为 334 字节。 因为用作搜索条件的唯一列是 PostalCode 列(长度为 30 字节),所以更好的索引设计应该将 PostalCode 定义为键列,并且包含作为非键列的所有其他列。
下面的语句创建了一个覆盖查询的带有包含列的索引。
CREATE INDEX IX_Address_PostalCode
ON Person.Address (PostalCode)
INCLUDE (AddressLine1, AddressLine2, City, StateProvinceID);
若要验证索引是否包含查询,请创建索引,然后显示估计的执行计划。 如果执行计划中显示了索引的IX_Address_PostalCode运算符,则说明该索引涵盖了查询。
包含列的索引性能注意事项
避免创建包含大量列的索引。 尽管索引可能涵盖更多查询,但其性能优势也会减少,因为:
一页上能容纳的索引行将更少。 这会增加磁盘 I/O 并降低缓存效率。
需要更多的磁盘空间来存储索引。 具体而言,在包含的列中添加 varchar(max)、 nvarchar(max)、 varbinary(max)或 xml 数据类型可以显著增加磁盘空间需求。 这是因为列值被复制到了索引叶级别。 因此,它们既驻留在索引中,也驻留在基表中。
数据修改性能会降低,因为必须在基于表和非聚集索引中修改许多列。
必须确定查询性能的提高是否超过了数据修改性能的减少和磁盘空间需求的增加。
唯一索引设计指南
唯一索引保证索引键不包含重复值。 仅当唯一性是数据本身的特征时,才能创建唯一索引。 例如,如果要确保 NationalIDNumber 表中的 HumanResources.Employee 列的值是唯一的,当主键为 EmployeeID 时,应对 UNIQUE 列创建 NationalIDNumber 约束。 该约束拒绝引入具有重复国家 ID 号的行的任何尝试。
使用多列唯一索引,索引能够保证索引键中值的每个组合都是唯一的。 例如,如果在LastName、FirstName和MiddleName列的组合上创建了唯一索引,则表中的两行不能对这些列具有相同的值。
聚集索引和非聚集索引都可以是唯一的。 可以在同一个表上创建唯一聚集索引和多个唯一的非聚集索引。
唯一索引的优点包括:
- 强制执行需要数据唯一性的业务规则。
- 提供了对查询优化器有用的附加信息。
创建 PRIMARY KEY 或 UNIQUE 约束会自动为指定的列创建唯一索引。 创建 UNIQUE 约束和创建独立于约束的唯一索引没有明显的区别。 数据验证的方式是相同的,而且查询优化器不会区分唯一索引是由约束创建的还是手动创建的。 但是,当业务规则的强制实施是目标时,应该对列创建一个 UNIQUE 或 PRIMARY KEY 约束。 这样做才能使索引的目标明确。
唯一索引注意事项
如果数据中存在重复的键值,则不能创建唯一索引、
UNIQUE约束或PRIMARY KEY约束。如果数据是唯一的并且你希望强制实现唯一性,则为相同的列组合创建唯一索引而不是非唯一索引可以为查询优化器提供附加信息,从而生成更有效的执行计划。 在这种情况下,建议创建
UNIQUE约束或唯一索引。唯一非聚集索引可以包括包含性非键列。 有关详细信息,请参阅“在非聚集索引中使用包含的列”。
PRIMARY KEY约束不同,UNIQUE约束或唯一索引可以使用索引键中的可为 null 列来创建。 出于唯一性强制的目的,两个 NULL 被视为相等。 例如,这意味着在单列唯一索引中,该列只可以为表中的一行设置为 NULL。
筛选索引设计指南
筛选索引是优化的非聚集索引,尤其适用于需要表中少量数据的查询。 它使用索引定义中的筛选器谓词为表中的一部分行编制索引。 设计良好的筛选索引可以提高查询性能、降低索引更新成本,以及与全表索引相比,降低索引存储成本。
筛选索引与全表索引相比具有以下优点:
提高了查询性能和计划质量
设计良好的筛选索引可提高查询性能和执行计划质量,因为它小于全表非聚集索引。 过滤索引具有过滤后的统计信息,比全表统计信息更准确,因为它们仅涵盖过滤索引中的行。
降低索引更新成本
仅当数据作语言 (DML) 语句影响索引中的数据时,才会更新索引。 与全表非聚集索引相比,筛选索引可降低索引更新成本,因为它较小,并且仅在索引中的数据受到影响时更新。 筛选索引的数量可以非常多,特别是在其中包含很少受影响的数据时。 同样,如果筛选索引仅包含经常受影响的数据,则索引的较小大小会降低更新统计信息的成本。
减少了索引存储开销
在没必要创建全表索引时,创建筛选索引可以减少非聚集索引的磁盘存储开销。 你可能能够将全表非聚集索引替换为多个筛选索引,而不会显著增加存储要求。
当列包含定义良好的数据子集时,筛选索引非常有用。 示例包括:
包含多个 NULL 值的列。
包含多种类别的数据的异类列。
包含值范围(如数量、时间和日期)的列。
与全表索引相比,当索引中的行数较小时,筛选索引的更新成本降低最为明显。 如果筛选索引包含表中的大部分行,则与全表索引相比,其维护开销可能更高。 在这种情况下,应使用全表索引而不是筛选索引。
筛选索引是针对一个表定义的,仅支持简单比较运算符。 如果需要具有复杂逻辑或引用多个表的筛选器表达式,则应创建 索引计算列 或 索引视图。
筛选索引设计注意事项
为了设计有效的筛选索引,必须了解应用程序使用哪些查询以及这些查询与你的数据子集有何关联。 具有明确定义的子集的数据的一些示例包括具有许多 NUL 的列、具有不同值类别的列和具有不同值范围的列。
以下设计注意事项为筛选索引在全表索引上提供优势时,提供了多种方案。
数据子集的筛选索引
在列中只有一些相关值需要查询时,可以针对值的子集创建筛选索引。 例如,当列大多为 NULL 且查询只需要非 NULL 值时,可以创建包含非 NULL 行的筛选索引。
例如,AdventureWorks 示例数据库中有一个包含了 2,679 行的 Production.BillOfMaterials 表。 该 EndDate 列只有 199 行包含非 NULL 值,其他 2480 行包含 NULL。 以下筛选索引涵盖返回索引中定义的列的查询,并且仅需要具有非 NULL 值的 EndDate行。
CREATE NONCLUSTERED INDEX FIBillOfMaterialsWithEndDate
ON Production.BillOfMaterials (ComponentID, StartDate)
WHERE EndDate IS NOT NULL;
筛选索引 FIBillOfMaterialsWithEndDate 对下面的查询有效。
显示估计的执行计划,以确定查询优化器是否使用了该筛选索引。
SELECT ProductAssemblyID,
ComponentID,
StartDate
FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL
AND ComponentID = 5
AND StartDate > '20080101';
有关如何创建筛选索引以及如何定义筛选索引谓词表达式的详细信息,请参阅创建筛选索引。
异类数据的筛选索引
表中含有异类数据行时,可以为一种或多种类别的数据创建筛选索引。
例如,为 Production.Product 表中列出的每种产品均分配了一个 ProductSubcategoryID,后者又与 Bikes、Components、Clothing 或 Accessories 产品类别相关联。 这些类别是异类类别,因为它们在 Production.Product 表中的列值并不是紧密相关的。 例如,对于每种产品类别,列 Color、 ReorderPoint、 ListPrice、 Weight、 Class和 Style 均具有唯一特征。 假设会经常查询具有子类别 27-36(包含端点)的附件。 通过对 Accessories 子类别创建筛选索引,可以提高对 Accessories 的查询的性能,如下例所示。
CREATE NONCLUSTERED INDEX FIProductAccessories
ON Production.Product (ProductSubcategoryID, ListPrice)
INCLUDE (Name)
WHERE ProductSubcategoryID >= 27 AND ProductSubcategoryID <= 36;
筛选 FIProductAccessories 索引涵盖以下查询,因为查询结果包含在索引中,查询计划不需要访问基表。 例如,查询谓词表达式 ProductSubcategoryID = 33 是筛选索引谓词 ProductSubcategoryID >= 27 和 ProductSubcategoryID <= 36的子集,查询谓词中的 ProductSubcategoryID 和 ListPrice 列全都是索引中的键列,并且名称作为包含列存储在索引的叶级别。
SELECT Name,
ProductSubcategoryID,
ListPrice
FROM Production.Product
WHERE ProductSubcategoryID = 33
AND ListPrice > 25.00;
筛选索引中的键和包含列
最佳做法是在筛选索引定义中添加少量列,仅当查询优化器为查询执行计划选择筛选索引时,才有必要。 无论某一筛选索引是否涵盖了查询,查询优化器都可以为查询选择此筛选索引。 但是,如果某一筛选索引涵盖了查询,则查询优化器更有可能选择此筛选索引。
在某些情况下,筛选索引涵盖查询,但没有将筛选索引表达式中的列作为键或包含列包括在筛选索引定义中。 以下准则说明了筛选索引表达式中的列何时应为筛选索引定义中的键或包含列。 这些示例引用了此前创建的筛选索引 FIBillOfMaterialsWithEndDate 。
如果筛选索引表达式等效于查询谓词并且查询并未在查询结果中返回筛选索引表达式中的列,则筛选索引表达式中的列不需要作为筛选索引定义中的键或包含列。 例如, FIBillOfMaterialsWithEndDate 涵盖下面的查询,因为查询谓词等效于筛选表达式,并且查询结果中未返回 EndDate。 索引 FIBillOfMaterialsWithEndDate 不需要 EndDate 作为键或包含在筛选索引定义中的列。
SELECT ComponentID,
StartDate
FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL;
如果查询谓词在不与筛选索引表达式等效的比较中使用了筛选索引表达式中的某列,则该列应为筛选索引定义中的键或包含列。 例如, FIBillOfMaterialsWithEndDate 对下面的查询有效,因为它从筛选索引中选择了行的子集。 但是,它不涵盖下面的查询,因为在比较 EndDate 中使用了 EndDate > '20040101',此比较不与筛选索引表达式等效。 查询处理器在不检查EndDate的值的情况下,无法执行此查询。 因此, EndDate 应为筛选索引定义中的键或包含列。
SELECT ComponentID,
StartDate
FROM Production.BillOfMaterials
WHERE EndDate > '20040101';
如果筛选索引表达式中的某列在查询结果集中,则该列应为筛选索引定义中的键或包含列。 例如,FIBillOfMaterialsWithEndDate 不涵盖下面的查询,因为它在查询结果中返回了 EndDate 列。 因此, EndDate 应为筛选索引定义中的键或包含列。
SELECT ComponentID,
StartDate,
EndDate
FROM Production.BillOfMaterials
WHERE EndDate IS NOT NULL;
表的聚集索引键不需要是筛选索引定义中的键或包含列。 聚集索引键自动包含在所有非聚集索引(包括筛选索引)中。
筛选谓词中的数据转换运算符
如果筛选索引结果的筛选索引表达式中指定的比较运算符会导致隐式或显式数据转换,则转换发生在比较运算符的左边时,会出现错误。 解决方法是在比较运算符的右边编写包含数据转换运算符(CAST 或 CONVERT)的筛选索引表达式。
以下示例创建包含不同数据类型列的表。
CREATE TABLE dbo.TestTable
(
a INT,
b VARBINARY(4)
);
在以下筛选索引定义中,列 b 将隐式转换为整数数据类型,以便将其与常量 1 进行比较。 因为转换发生在筛选谓词中运算符的左边,所以这会生成错误消息 10611。
CREATE NONCLUSTERED INDEX TestTabIndex
ON dbo.TestTable (a, b)
WHERE b = 1;
解决方法是将右侧的常量转换为与列 b 的类型相同的类型,如下例所示:
CREATE INDEX TestTabIndex
ON dbo.TestTable (a, b)
WHERE b = CONVERT (VARBINARY(4), 1);
将数据转换从比较运算符的左边移动到右边可能会改变转换的含义。 在前面的示例中,当 CONVERT 运算符添加到右侧时,比较从 int 比较更改为 varbinary 比较。
列存储索引体系结构
列存储索引是一种使用列式数据格式(称为列存储)来存储、检索和管理数据的技术。 有关详细信息,请参阅 列存储索引:概述。
如果需要获取版本信息,了解新增功能,请访问列存储索引提供的新增功能。
了解这些基础知识可以更轻松地理解其他列存储文章,这些文章详细解释如何有效使用这一技术。
数据存储使用列存储和行存储
在提到列存储索引时,我们使用术语“行存储”和“列存储”来强调数据存储的格式。 列存储索引使用这两种类型的存储。
“列存储”是在逻辑上组织为包含行和列的表、在物理上以按列数据格式存储的数据。
列存储索引使用列存储格式以物理方式存储大部分数据。 使用列存储格式时,数据将以列的形式压缩和解压缩。 不需要解压缩每个行中未由查询请求的其他值。 这样,便可以快速扫描大型表的整个列。
“行存储”是在逻辑上组织为包含行和列的表、在物理上以按行数据格式存储的数据。 这是存储关系表数据(例如聚簇 B+ 树结构索引或堆)的传统方法。
列存储索引也将某些行以称为 增量存储 的行存储格式进行物理存储。 增量存储(也称为增量行组)是数量太少,不符合压缩到列存储中的条件的行的保存位置。 每个增量行组都作为聚集 B+ 树索引实现,该索引是行存储。
针对行组和列段执行操作
列存储索引将行分组成可管理的单元。 其中每个单元称为一个行组。 为了获得最佳性能,行组中的行数应足够大以提高压缩率,同时足够小以便于内存操作。
例如,列存储索引针对行组执行以下操作:
将行组压缩到列存储中。 针对行组中的每个列段执行压缩。
在
ALTER INDEX ... REORGANIZE操作期间合并行组,包括移除已删除的数据。在
ALTER INDEX ... REBUILD操作期间重新创建所有行分组。在动态管理视图 (DMV) 中报告行组运行状况和碎片。
增量存储由一个或多个名为“增量行组”的行组构成。 每个增量行组都是一个聚集 B+ 树索引,用于存储小批量加载和插入,直到行组包含 1,048,576 行,此时,一个称为 元组移动器 的进程会自动将已关闭的行组压缩到列存储中。
有关行组状态的详细信息,请参阅 sys.dm_db_column_store_row_group_physical_stats。
Tip
小行组过多会降低列存储索引的质量。 重新组织操作将遵循一个内部阈值策略(确定如何移除已删除行并合并已压缩行组)来合并较小的行组。 合并后,索引质量将得到改善。
在 SQL Server 2019(15.x)及更高版本中,元组移动器通过后台合并任务得到支持,该任务会自动压缩已经存在一段时间且由内部阈值确定的较小开启状态增量行组,或合并已删除大量行的压缩行组。
每个列在每个行组中都有自身的一些值。 这些值称为“列段”。 每个行组包含表中每个列的一个列段。 每个列在每个行组中有一个列段。
当列存储索引压缩行组时,会单独压缩每个列段。 若要解压缩整个列,列存储索引只需解压缩每个行组中的一个列段。
小规模加载和插入操作转到增量存储
列存储索引一次至少可将 102,400 个行压缩到列存储索引中,以此提高列存储的压缩率和性能。 若要批量压缩行,列存储索引可在增量存储中累积小规模的加载和插入操作。 增量数据存储操作在后台进行处理。 若要返回查询结果,聚集列存储索引将列存储和增量存储中的查询结果组合在一起。
在以下情况下,行将转到增量存储:
使用
INSERT INTO ... VALUES语句插入。行位于批量加载操作的末尾,并且编号小于 102,400。
Updated. 每个更新操作实现为删除并插入。
增量存储中还会存储标记为已删除、但实际并未从列存储中删除的已删除行的 ID 列表。
增量行组已满时将压缩到列存储中
聚集列存储索引最多收集每个增量行组中的 1,048,576 行,达到此数字后,会将行组压缩到列存储中。 这可以提高列存储索引的压缩率。 在增量行组达到最大行数后,它会从 OPEN 状态转换为 CLOSED 状态。 名为 tuple-mover 的后台进程会检查已关闭的行组。 如果进程找到已关闭的行组,就会压缩行组,并将它存储到列存储中。
压缩增量行组后,现有的增量行组会转换为 TOMBSTONE 状态,以便稍后由 tuple-mover 在没有引用该行组时将其删除,并将新的压缩行组标记为 COMPRESSED。
有关行组状态的详细信息,请参阅 sys.dm_db_column_store_row_group_physical_stats。
可以通过使用 ALTER INDEX 重新生成或重新组织索引,强制将增量行组压缩到列存储中。 如果在压缩期间遇到内存压力,列存储索引可能会减少压缩行组中的行数。
每个表分区具有自身的行组和增量行组
分区的概念在聚集索引、堆和列存储索引中是相同的。 将表分区会根据列值范围将表划分为较小的行组。 分区通常用于管理数据。 例如,可以为每年的数据创建一个分区,然后使用分区切换将旧数据存档到成本较低的存储。
行组始终在表分区中定义。 将某个列存储索引分区后,每个分区都有其自身的压缩行组和增量行组。 非分区表包含一个分区。
Tip
如果需要从列存储中删除数据,请考虑使用表分区。 切换和截断不再需要的分区是删除数据的高效策略,无需在列存储中引入碎片。
每个分区可以包含多个增量行组
每个分区可以包含多个增量行组。 当列存储索引需要将数据添加到增量行组并且增量行组被另一个事务锁定时,列存储索引会尝试获取对其他增量行组的锁定。 如果没有任何可用的增量行组,列存储索引会创建新的增量行组。 例如,具有 10 个分区的表可以轻松包含 20 个或更多的增量行组。
在同一个表中组合列存储索引和行存储索引
非聚集索引包含基础表中部分或全部行与列的副本。 索引将定义为表的一个或多个列,并具有一个用于筛选行的可选条件。
可以对行存储表创建可更新的非聚集列存储索引。 列存储索引将存储数据的副本,因此你需要提供额外的存储。 但是,列存储索引中的数据压缩后,其大小远小于行存储表所需的数据量。 通过这样操作,您可以同时在列存储索引上运行分析,并对行存储索引上的 OLTP 工作负荷进行处理。 当行存储表中的数据更改时,列存储将会更新,因此这两个索引适用于相同的数据。
一个行存储表可以有一个非聚集列存储索引。 有关详细信息,请参阅 列存储索引 - 设计指南。
可以在 聚集列存储表上具有一个或多个非聚集行存储索引。 这样,便可以针对基础列存储上执行有效的表查找。 其他选项也可供使用。 例如,可以通过对行存储表使用约束来强制实施唯一 UNIQUE 性。 当非统一值无法插入到行存储表中时,数据库引擎也不会将该值插入列存储中。
非聚集列存储性能注意事项
非聚集列存储索引定义支持使用筛选的条件。 若要最大程度地降低添加列存储索引的性能效果,请使用筛选器表达式仅针对分析所需的数据子集创建非聚集列存储索引。
内存优化表可以拥有一个列存储索引。 可以在创建表时创建它,或者稍后使用 ALTER TABLE 添加它。
有关详细信息,请参阅 列存储索引 - 查询性能。
内存优化哈希索引设计指南
使用 In-Memory OLTP 时,所有内存优化表必须至少有一个索引。 对于内存优化表,每个索引也是内存优化的。 哈希索引是内存优化表中可能存在的索引类型之一。 有关详细信息,请参阅内存优化表的索引。
内存优化哈希索引体系结构
哈希索引包含一个指针数组,该数组的每个元素被称为哈希桶。
- 每个桶为 8 个字节,用于存储键项的链接列表的内存地址。
- 每个条目是索引键的值,以及其在基础内存优化表中的对应行的地址。
- 每个条目指向条目的链接列表中的下一个条目,所有都链接到当前桶。
必须在创建索引时指定存储桶数:
- 桶与表行或非重复值的比例越低,桶链接列表的平均长度就越长。
- 较短的链接列表比较长的链接列表执行更快。
- 哈希索引中的最大哈希桶数目为 1,073,741,824。
Tip
要为你的数据确定合适的 BUCKET_COUNT,请参阅配置哈希索引桶计数。
哈希函数适用于索引键列,其结果用于确定键位于哪个哈希桶中。 每个哈希桶都包含一个指向行的指针,这些行的哈希键值映射到该哈希桶。
用于哈希索引的哈希函数具有以下特征:
- 数据库引擎拥有一个用于所有哈希索引的哈希函数。
- 哈希函数具有确定性。 相同的输入键值始终映射到哈希索引中的同一哈希桶。
- 多个索引键可能映射到同一个哈希 Bucket。
- 哈希函数经过均衡处理,这意味着索引键值在哈希桶上的分布通常符合泊松分布或钟型曲线分布,而不是平坦的线性分布。
- 泊松分布并非均匀分布。 索引键值并非均匀地分布在哈希桶中。
- 如果两个索引键映射到同一哈希桶,则产生哈希冲突。 大量哈希冲突可能会对读取作产生性能影响。 现实的目标是 30% 的存储桶包含两个不同的键值。
下图汇总了哈希索引和桶的交互作用。
配置哈希索引桶计数
哈希索引桶计数在索引创建时指定,可使用 ALTER TABLE...ALTER INDEX REBUILD 语法进行更改。
在大多数情况下,存储桶计数应为索引键中唯一值数量的 1 到 2 倍。
可能并不总是能够预测特定索引键具有的值数。 如果 BUCKET_COUNT 值在键值的实际数目的 10 倍以内,性能表现通常依然良好,并且高估通常比低估要好。
桶太少会带来以下缺点:
- 增多非重复键值的哈希冲突。
- 每个非重复值被迫与其他非重复值共享同一个桶。
- 每个桶的平均链长度将会增加。
- 桶链的长度越长,在索引中进行相等性查找的速度就越慢。
桶太多会带来以下缺点:
- 桶计数过高可能会导致空桶增多。
- 空桶会影响全文检索扫描的性能。 如果定期执行扫描,请考虑选择接近非重复索引键值数目的桶计数。
- 空桶会使用内存,尽管每个桶只用 8 个字节。
Note
添加更多的桶无益于减少共享重复值的、链接在一起的条目。 值重复率用于确定哈希索引还是非聚集索引是适当的索引类型,而不是计算存储桶计数。
哈希索引的性能注意事项
哈希索引的性能为:
- 当
WHERE子句中的谓词为哈希索引键的每一列指定确切值时表现极好。 如果有不等谓词,则哈希索引将恢复为扫描。 - 当
WHERE子句中的谓词查找索引键中的一系列值时表现不佳。 - 当
WHERE子句中的谓词为双列哈希索引键的第一列规定了一个特定值,但没有为该键的其他列指定值时表现不佳。
Tip
谓词必须包括哈希索引键 中的所有 列。 哈希索引需要整个键值来查找索引。
如果使用哈希索引,并且唯一索引键的数量比行数少超过100倍,请考虑增加桶数以避免出现大型行链表,或者改为使用非聚集索引。
创建哈希索引
创建哈希索引时,请考虑:
- 哈希索引只能存在于内存优化表中, 而不能存在于基于磁盘的表中。
- 默认情况下,哈希索引是非统一的,但可以声明为唯一。
以下示例创建唯一的哈希索引:
ALTER TABLE MyTable_memop ADD INDEX ix_hash_Column2
UNIQUE HASH (Column2) WITH (BUCKET_COUNT = 64);
内存优化表中的行版本和垃圾回收
在内存优化表中,当某行受 UPDATE 语句影响时,该表将创建行的更新版本。 在更新事务期间,其他会话也许能够读取较旧版本的行,从而避免与行锁相关的性能下降。
哈希索引可能也会提供不同的条目版本来适应更新。
以后,当不再需要旧版本时,垃圾回收 (GC) 线程将遍历桶及其链接列表,以清理旧条目。 如果链接列表链长度较短,GC 线程的执行效果会更佳。 有关详细信息,请参阅内存中 OLTP 垃圾回收。
内存优化非聚集索引设计指南
除了哈希索引,非聚集索引是内存优化表中其他可能的索引类型。 有关详细信息,请参阅内存优化表的索引。
内存优化的非聚集索引体系结构
内存优化表的非聚集索引是使用名为 Bw 树的数据结构实现的,最初由 Microsoft Research 在 2011 年设想和描述。 Bw 树是 B 树的无锁和无闩锁变体。 有关详细信息,请参阅 Bw 树:新硬件平台的 B 树。
从宏观上看,Bw树可以理解为由页面ID组织的页面地图(PidMap)、分配和重用页面ID的功能(PidAlloc),以及一组链接在页面地图和相互链接的页面。 这三个高级别子组件组成了 Bw 树的基本内部结构。
该结构一定程度上类似于常规 B 树,每页都有一组经过排序的键值,索引中的每个级别都指向更低的级别,并且叶级别指向数据行。 但也存在一些差异。
与哈希索引一样,可以将多个数据行链接在一起以支持版本控制。 级别之间的页指针是逻辑页 ID,这些逻辑页 ID 是页映射表中的偏移量,该表又具有每页的物理地址。
索引页没有就地更新。 因此引入了新的增量页。
- 页更新不需要闩锁获锁定。
- 索引页没有固定大小。
每个非叶节点页面的键值是它指向的子节点所包含的最高值,每行还包含页面的逻辑ID。 在叶级别页上,除键值外,还包含数据行的物理地址。
点查找类似于 B 树,不同之处在于,由于页面仅以一个方向链接,因此数据库引擎紧跟右页指针,其中每个非叶页的子页都具有其子级的最高值,而不是 B 树中的最低值。
如果叶级页面必须更改,数据库引擎不会修改页面本身。 相反,数据库引擎会创建描述更改的增量记录,并将其追加到上一页。 然后将先前页的页映射表地址更新为增量记录的地址,使该地址成为当前页的物理地址。
管理 Bw 树结构可能需要三个不同的操作:整合、拆分和合并。
增量合并
长增量记录链最终可能会降低搜索性能,因为它可能需要在搜索索引时进行长链遍历。 如果向已有 16 个元素的链添加了新的增量记录,该增量记录中的更改将整合到引用的索引页中,然后将重新生成该页,其中包括触发整合的新增量记录指示的更改。 重新生成的新页使用相同的页 ID,但使用新的内存地址。
拆分页
Bw 树中的索引页可按需增大,从存储单一行的大小开始,最多可存储 8 KB 的索引页。 索引页增大到 8 KB 后,插入一个新行将导致拆分索引页。 对于内部页,这表示不再有添加另一个键值和指针的空间,对于叶页,这表示所有增量记录整合在一起后,行会因为太大而不能容纳在页中。 叶页的页面页眉中的统计信息会持续跟踪整合增量记录所需的空间。 添加每个新的增量记录时,将调整此信息。
拆分操作通过两个原子步骤完成。 在下图中,假设因为正在插入值为 5 的键强制进行了叶页拆分,并存在一个指向当前叶级别页(键值 4)末尾的非叶页。
步骤 1:分配两个新页 P1 和 P2,将旧 P1 页中的行拆分到这些新页上,包括新插入的行。 使用页映射表中的新槽来存储 P2 页的物理地址。 页面 P1 和 P2 尚不能被任何并发操作访问。 此外,设置了从 P1 指向 P2 的逻辑指针。 然后,在一个原子步骤中更新页映射表,将指针从旧的 P1 更改为新的 P1。
步骤 2:非叶页指向 P1,但是没有指针从非叶页直接指向 P2。 只能通过 P2 到达 P1。 要创建从非叶页指向 P2 的指针,需要分配新的非叶页(内部索引页),复制旧的非叶页中的所有行,并添加一个指向 P2 的新行。 完成此操作后,在一个原子步骤中更新页映射表,将指针从旧的非叶页更改为新的非叶页。
合并页
DELETE当某个操作导致页面小于最大页面大小(8 KB)的 10%,或页面上仅包含一行时,该页面将与相邻的页面合并。
从某页中删除行时,会添加该删除操作的增量记录。 此外,还进行检查以确定索引页(非叶页)是否有资格合并。 此检查还会验证删除行之后,剩余空间是否小于最大页大小的 10%。 如果确实符合条件,则会在三个原子步骤中执行合并。
在下图中,假设 DELETE 操作删除键值 10。
步骤 1:创建表示键值 10(蓝色三角形)的增量页,并将该增量页在非叶页 Pp1 上的指针设置为新的增量页。 此外,创建一个特殊的合并增量页(绿色三角形),并链接该页,使其指向增量页。 在此阶段,这两个页(增量页和合并增量页)对任何并发事务都不可见。 在一个原子步骤中,页映射表中指向叶级别页 P1 的指针更新为指向合并增量页的指针。 执行此步骤之后,10 中键值 Pp1 对应的项现在指向合并增量页。
步骤 2:需要删除非叶页 7 中表示键值 Pp1 的行,然后将键值 10 对应的项更新为指向 P1。 为此,需要分配新的非叶页 Pp2,复制 Pp1 中除表示键值 7 的行之外的所有行,然后将键值 10 表示的行更新为指向页 P1。 完成此操作后,在一个原子步骤中将 Pp1 的页映射表入口点更新为指向 Pp2。 无法再访问 Pp1。
步骤 3:合并叶级别页 P2 和 P1,并删除增量页。 为此,分配新页 P3,合并 P2 和 P1 中的行,并在新的 P3 中包含增量页的更改。 然后,在一个原子步骤中将页 P1 的页映射表入口点更新为指向页 P3。
内存优化非聚集索引的性能注意事项
当查询具有不相等谓词的内存优化表时,非聚集索引的性能优于哈希索引。
内存优化的表中的列可以同时为哈希索引和非聚集索引的一部分。
非聚集索引中的键列包含许多重复值时,更新、插入和删除的性能可能会降低。 在这种情况下提高性能的一种方法是添加在索引键中具有更好的选择性的列。
索引元数据
若要检查索引元数据(如索引定义、属性和数据统计信息),请使用以下系统视图:
- sys.objects
- sys.indexes
- sys.index_columns
- sys.columns
- sys.types
- sys.partitions
- sys.internal_partitions
- sys.dm_db_index_usage_stats
- sys.dm_db_partition_stats
- sys.dm_db_index_operational_stats
以前的视图适用于所有索引类型。 对于列存储索引,还使用以下视图:
- sys.column_store_row_groups
- sys.column_store_segments
- sys.column_store_dictionaries
- sys.dm_column_store_object_pool
- sys.dm_db_column_store_row_group_operational_stats
- sys.dm_db_column_store_row_group_physical_stats
对于列存储索引,所有列在元数据中作为包含性列存储。 列存储索引中没有任何键列。
对于内存优化表上的索引,还使用下列视图:
- sys.hash_indexes
- sys.dm_db_xtp_hash_index_stats
- sys.dm_db_xtp_index_stats
- sys.dm_db_xtp_nonclustered_index_stats
- sys.dm_db_xtp_object_stats
- sys.dm_db_xtp_table_memory_stats
- sys.memory_optimized_tables_internal_attributes