适用于:SQL Server
Azure SQL 数据库
Azure SQL 托管实例
Microsoft Fabric 预览版中的 SQL 数据库
堆是不含聚集索引的表。 可在存储为堆的表上创建一个或多个非聚集索引。 数据存储于堆中并且无需指定顺序。 通常,数据最初是按照插入行的顺序存储的。 但是,数据库引擎可以在堆中移动数据以高效地存储行。 在查询结果中,无法预测数据顺序。 若要确保从堆返回的行的顺序,使用 ORDER BY
子句。 若要指定用于存储行的永久逻辑顺序,请对表创建聚集索引,以便表不是堆。
Note
有时候可能有必要将表保留为堆,而不是创建聚集索引,但高效率地使用堆需要较高的技能。 大多数表应该具有仔细选择的聚集索引,除非有足够的理由将表保留为堆。
何时使用堆
堆非常适合经常被截断和重新加载的表。 数据库引擎通过填充最早的可用空间来优化堆中的空间。
考虑以下情况:
- 在堆中查找可用空间的成本可能很高,尤其是在有许多删除或更新的情况下。
- 聚集索引为不经常被截断的表提供稳定的性能。
对于经常被截断或重新创建的表(如临时表),使用堆通常更高效。
在使用堆和聚集索引之间选择可能会显著影响数据库的性能和效率。
在将某个表存储为堆时,通过引用由文件号、数据页码和页上的槽 (FileID:PageID:SlotID) 构成的 8 字节行标识符 (RID),标识单独行。 行 ID 是一个小且高效的结构。
可使用堆作为临时表来暂存大型无序插入操作。 由于插入数据时不应用严格的顺序,因此插入操作通常比插入到聚集索引的等效操作更快。 如果要读取堆的数据并将其处理成最终目标,创建一个包含查询所用搜索谓词的窄带非聚集索引可能会很有用。
Note
按数据页面的顺序从堆中检索数据,但这不一定是插入数据的顺序。
始终通过非聚集索引访问数据且 RID 小于聚集索引键时,有时数据专业人员也会使用堆。
如果某个表是堆并且不具有任何非聚集索引,则必须读取整个表(表扫描)以便找到任何行。 SQL Server 无法直接在堆上查找 RID。 当表较小时,此行为可以接受。
何时不使用堆
在经常以排序后的顺序返回数据时,不要使用堆。 排序列上的聚集索引可以避免排序操作。
在数据经常组合在一起时,不要使用堆。 数据必须首先进行排序,然后才能分组,并且排序列上的聚集索引可以避免排序操作。
在经常从表查询数据范围时,不要使用堆。 范围列上的聚集索引避免对整个堆进行排序。
在不存在非聚集索引并且表比较大时,不要使用堆。 此设计的唯一应用是在没有任何指定顺序的情况下返回整个表内容。 在堆中,数据库引擎会读取所有行以查找任何行。
如果数据经常更新,请不要使用堆。 如果更新记录,并且更新在数据页中使用的空间比当前使用的空间多,则必须将记录移动到具有足够可用空间的数据页。 这会创建指向数据的新位置的前推记录,并且前推指针必须在以前保存该数据的页中写入,以指示新的物理位置。 这会在堆中引入碎片。 当数据库引擎扫描堆时,它会遵循这些指针。 此操作会限制预读性能,并且可能会产生额外的 I/O,从而降低扫描性能。
Manage heaps
若要创建堆,请创建没有聚集索引的表。 如果表已具有某一聚集索引,则删除该聚集索引以便将该表返回到某一堆。
若要删除堆,请在该堆上创建聚集索引。
重新生成堆以回收浪费的空间:
- 在该堆上创建一个聚集索引,然后删除该聚集索引。
- 使用
ALTER TABLE ... REBUILD
命令重新生成堆。
Warning
创建或删除聚集索引要求重写整个表。 如果该表具有非聚集索引,则只要更改聚集索引,就必须全都重新创建所有非聚集索引。 因此,从堆更改为聚集索引结构或反之可能占用大量时间,并且要求足够的磁盘空间以便对 tempdb 中的数据重新进行排序。
Identify heaps
以下查询会返回当前数据库中的堆列表。 此列表包括:
- Table names
- Schema names
- 行数
- 表大小 (KB)
- 索引大小 (KB)
- Unused space
- 用于识别堆的列
SELECT t.name AS 'Your TableName',
s.name AS 'Your SchemaName',
p.rows AS 'Number of Rows in Your Table',
SUM(a.total_pages) * 8 AS 'Total Space of Your Table (KB)',
SUM(a.used_pages) * 8 AS 'Used Space of Your Table (KB)',
(SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS 'Unused Space of Your Table (KB)',
CASE
WHEN i.index_id = 0
THEN 'Yes'
ELSE 'No'
END AS 'Is Your Table a Heap?'
FROM sys.tables t
INNER JOIN sys.indexes i
ON t.object_id = i.object_id
INNER JOIN sys.partitions p
ON i.object_id = p.object_id
AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a
ON p.partition_id = a.container_id
LEFT JOIN sys.schemas s
ON t.schema_id = s.schema_id
WHERE i.index_id <= 1 -- 0 for Heap, 1 for Clustered Index
GROUP BY t.name,
s.name,
i.index_id,
p.rows
ORDER BY 'Your TableName';
Heap structures
堆是不含聚集索引的表。 堆的 sys.partitions中具有一行,对于堆使用的每个分区,都有 index_id = 0
。 默认情况下,一个堆有一个分区。 当堆有多个分区时,每个分区有一个堆结构,其中包含该特定分区的数据。 例如,如果一个堆有四个分区,则有四个堆结构;每个分区有一个堆结构。
根据堆中的数据类型,每个堆结构将有一个或多个分配单元来存储和管理特定分区的数据。 每个堆中每个分区至少有一个 IN_ROW_DATA
分配单元。 如果堆包含大型对象 (LOB) 列,则该堆的每个分区还将有一个 LOB_DATA
分配单元。 如果堆包含超过 8,060 字节的行大小限制的变量长度列,则它的每个分区中还会有一个 ROW_OVERFLOW_DATA
分配单元。
first_iam_page
系统视图中的列 sys.system_internals_allocation_units
指向 IAM 页链中的第一个 IAM 页,该 IAM 页链可管理分配给特定分区中的堆的空间。 SQL Server 使用 IAM 页在堆之间移动。 堆内的数据页和行没有任何特定的顺序,也不链接在一起。 数据页之间唯一的逻辑连接是记录在 IAM 页内的信息。
Important
sys.system_internals_allocation_units
系统视图保留为仅供 SQL Server 内部使用。 不保证以后的兼容性。
可以通过扫描 IAM 页对堆进行表扫描或串行读操作来找到容纳该堆的页的扩展盘区。 因为 IAM 按扩展盘区在数据文件内存在的顺序表示它们,所以这意味着串行堆扫描连续沿每个文件进行。 使用 IAM 页设置扫描顺序还意味着堆中的行一般不按照插入的顺序返回。
下图说明了 SQL Server 数据库引擎如何使用 IAM 页检索单个分区堆中的数据行。