我有一个启用了RCSI的SQL Server 2016数据库,实际上是一堆堆。除了一个表之外,数据库中的每个其他表都是一个堆,最大的堆大约是200GB,占数据库总大小的50%以上。
这个特殊的大型堆有两个lob列,都有varbinary(max)数据类型。堆也有许多非聚簇索引,幸好varbinary(max)列不存在于任何这些非聚簇索引中,因此它们的大小相对较小。
供应商提供了一个清理脚本,该脚本从应用程序服务器运行并清除此大型堆中的数据。经过一番调查后,我发现这个清理脚本不会删除整行,而是根据某些条件将其中一个varbinary(max)列设置为null。
以下是有关堆的一些细节:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(N'<database>'), OBJECT_ID(N'GrimHeaper>'),0, null, 'DETAILED');
SELECT * FROM sys.dm_db_index_operational_stats(db_id('<database>'),object_id('GrimHeaper'),0,null);
我在这种情况下的理解是,通过将lob列中的值设置为null而释放的空间不会被自动重新声明,这是行为,无论表是堆还是聚簇,请纠正我,如果我错了。
在此Microsoft article以及此article中,它指出了以下关于索引重组操作:
REORGANIZE ALL对所有索引执行LOB_COMPACTION。对于每个索引,这将压缩聚簇索引,基础表或非聚簇索引中包含的列中的所有LOB列。
如果指定了ALL,则会重新组织与指定表或视图关联的所有索引,并且会压缩与聚簇索引,基础表或包含列的非聚簇索引关联的所有LOB列。
我觉得这些陈述含糊不清,不太清楚。任何人都可以确认,如果我运行“ALTER INDEX ALL WITH REORGANIZE WITH(LOB_CAMPACTION = ON)”语句它将压缩varbinary(max)LOB列,即使它们不在任何非聚集索引,只在底层堆中?这背后的基本原理是回收应用程序作业释放的任何空间,它将LOB列设置为null以用于符合条件的行。
此外,您还可以看到此堆具有多个转发记录。我还怀疑整个行已经从堆中删除了,但是由于已知的对堆的删除行为而未被解除分配,其中只有在通过表锁查询提示明确表锁时才对行进行解除分配或通过锁升级。考虑到这一点,我正在考虑禁用堆上的所有非聚集索引,重建堆然后重新启用非聚簇索引。此操作是否会重新声明/压缩lob列中任何未使用的空间,以及删除转发的记录和已删除但未完全取消分配的行?
免责声明 - 此数据库由供应商设计,不接受创建聚簇索引。使用这个数据库的应用程序在周末没有使用,因此我有大型维护窗口,因此重建堆可能是资源密集和痛苦的,这是可行的。
答案 0 :(得分:1)
任何人都可以确认,如果我运行“ALTER INDEX ALL ON REORGANIZE WITH(LOB_CAMPACTION = ON)“声明它会压缩 varbinary(max)LOB列,即使它们不存在 非聚集索引并且仅在底层堆中?
是。您可以根据经验轻松确认,我们将在一分钟内完成。
这背后的理由是回收任何被释放的空间 应用程序作业,它将LOB列设置为null以用于符合条件的行。
LOB压缩并不能完全回收所有释放的空间。即使重建整个表也不会收回LOB空间 - 重组是你能做的最好的,而且不会回收所有东西。如果它让你感觉更好:这不仅限于堆表,它实际上是一个功能,而不是一个bug。
让我证明一下。让我们用LOB数据创建一个堆表:
CREATE TABLE heap_of_trouble(ID INT IDENTITY, lobby VARBINARY(MAX));
-- SQL Server will store values <8K in the row by default; force the use of LOB pages
EXEC sp_tableoption 'heap_of_trouble', 'large value types out of row', 1;
SET NOCOUNT ON;
GO
BEGIN TRANSACTION;
GO
INSERT heap_of_trouble(lobby) VALUES (CONVERT(VARBINARY(MAX), REPLICATE(' ', 4000)));
GO 10000
COMMIT;
SELECT p.[rows], p.index_id, au.[type_desc], au.data_pages, au.total_pages, au.used_pages
FROM sys.partitions p
JOIN sys.allocation_units au ON au.container_id = p.hobt_id
JOIN sys.objects o ON o.[object_id] = p.[object_id]
WHERE o.[name] = 'heap_of_trouble'
+-------+----------+-------------+------------+-------------+------------+
| rows | index_id | type_desc | data_pages | total_pages | used_pages |
+-------+----------+-------------+------------+-------------+------------+
| 10000 | 0 | IN_ROW_DATA | 43 | 49 | 44 |
| 10000 | 0 | LOB_DATA | 0 | 5121 | 5118 |
+-------+----------+-------------+------------+-------------+------------+
让我们清楚一些专栏:
UPDATE heap_of_trouble SET lobby = NULL WHERE ID % 2 = 0;
让我们再次获得页数:
+-------+----------+-------------+------------+-------------+------------+
| rows | index_id | type_desc | data_pages | total_pages | used_pages |
+-------+----------+-------------+------------+-------------+------------+
| 10000 | 0 | IN_ROW_DATA | 43 | 49 | 44 |
| 10000 | 0 | LOB_DATA | 0 | 5121 | 5117 |
+-------+----------+-------------+------------+-------------+------------+
没有变化,除了最后一页。这是预期的。所以现在让我们重组并紧凑:
ALTER INDEX ALL ON heap_of_trouble REORGANIZE WITH (LOB_COMPACTION = ON);
+-------+----------+-------------+------------+-------------+------------+
| rows | index_id | type_desc | data_pages | total_pages | used_pages |
+-------+----------+-------------+------------+-------------+------------+
| 10000 | 0 | IN_ROW_DATA | 43 | 49 | 44 |
| 10000 | 0 | LOB_DATA | 0 | 3897 | 3897 |
+-------+----------+-------------+------------+-------------+------------+
您会注意到页数不是我们开始时的一半:LOB数据已经重新组织,但没有完全重建。
如果您尝试使用ALTER TABLE .. REBUILD
,您会注意到LOB数据根本没有压缩:
+-------+----------+-------------+------------+-------------+------------+
| rows | index_id | type_desc | data_pages | total_pages | used_pages |
+-------+----------+-------------+------------+-------------+------------+
| 10000 | 0 | IN_ROW_DATA | 29 | 33 | 30 |
| 10000 | 0 | LOB_DATA | 0 | 5121 | 5117 |
+-------+----------+-------------+------------+-------------+------------+
注意IN_ROW_DATA
是如何重建的,但LOB数据完全不受影响。您也可以使用聚簇索引尝试此操作(只需使ID
成为PRIMARY KEY
以隐式创建一个)。但是,对于非聚集索引,情况并非如此。重新开始,但这次添加另一个索引:
CREATE INDEX IX_heap_of_trouble_ID ON heap_of_trouble (ID) INCLUDE (lobby)
当然,在索引中包含LOB数据不是正常的设置;这只是为了说明。看看我们在ALTER TABLE REBUILD
之后得到的结果:
+-------+----------+-------------+------------+-------------+------------+
| rows | index_id | type_desc | data_pages | total_pages | used_pages |
+-------+----------+-------------+------------+-------------+------------+
| 10000 | 0 | IN_ROW_DATA | 29 | 33 | 30 |
| 10000 | 0 | LOB_DATA | 0 | 5121 | 5117 |
| 10000 | 2 | IN_ROW_DATA | 35 | 49 | 37 |
| 10000 | 2 | LOB_DATA | 0 | 2561 | 2560 |
+-------+----------+-------------+------------+-------------+------------+
惊喜(可能),非聚集索引的LOB数据被重建,而不仅仅是重新组织。 ALTER INDEX ALL .. REBUILD
将具有相同的效果,但会使堆完全不受影响。总结一下小桌子:
+----------------------+---------------+-------------------+----------------------+
| | TABLE REBUILD | INDEX ALL REBUILD | INDEX ALL REORGANIZE |
+----------------------+---------------+-------------------+----------------------+
| Heap in-row | Rebuild | - | - |
| Heap LOB | - | - | Reorganize |
| Clustered in-row | Rebuild | Rebuild | Reorganize |
| Clustered LOB | - | - | Reorganize |
| Non-clustered in-row | Rebuild | Rebuild | Reorganize |
| Non-clustered LOB | Rebuild | Rebuild | Reorganize |
+----------------------+---------------+-------------------+----------------------+
我正在考虑禁用所有非聚集索引 堆,重建堆,然后重新启用非群集 索引。
您无需单独重新启用非聚集索引; ALTER TABLE .. REBUILD
也会重建所有索引,并且重新启用已禁用的索引作为重建的一部分。
此操作是否会重新声明/压缩高架中任何未使用的空间 列以及删除转发的记录并删除但不是 完全取消分配行?
根据我们之前的结果,不,不完全是。如果您只是对LOB数据的压缩与表的其余部分重建感到满意,那么该过程将是:
ALTER INDEX ALL .. DISABLE
以禁用所有非聚集索引; ALTER INDEX ALL .. REORGANIZE WITH (LOB_COMPACTION = ON)
来压缩底层堆的LOB页面(这将使禁用的索引单独存在); ALTER TABLE .. REBUILD
以重建堆的行内数据以及索引的所有数据,并重新启用它们。如果你真的想将堆缩小到最小尺寸,你必须创建一个新表并在那里插入数据,但这需要更多脚本和明智地使用sp_rename
。它也非常昂贵,因为它需要复制所有LOB数据(REORGANIZE
避免的东西)。如果您在不关注文件组和使用的日志空间的情况下执行此操作,最终可能会占用比您要求回收更多的空间,并且它不太可能有助于提高性能。