SQL 服务器 - Deleting/Updating 堆中的 LOB 数据

SQL Server - Deleting/Updating LOB data in a Heap

我有一个启用了 RCSI 的 SQL Server 2016 数据库,它实际上是一堆堆。除了一个 table,数据库中的其他 table 都是一个堆,最大的堆是 ~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 释放的 space 不会自动重新声明,这是与 table 是否无关的行为是堆还是簇,如有错误请指正

在此 Microsoft article and also this article 中,它说明了以下关于索引重组操作的内容:

REORGANIZE ALL performs LOB_COMPACTION on all indexes. For each index, this compacts all LOB columns in the clustered index, underlying table, or included columns in a nonclustered index.

When ALL is specified, all indexes associated with the specified table or view are reorganized and all LOB columns associated with the clustered index, underlying table, or nonclustered index with included columns are compacted.

我觉得这些说法模棱两可,不是很清楚。任何人都可以确认如果我 运行 “ALTER INDEX ALL ON REORGANIZE WITH ( LOB_CAMPACTION = ON )” 声明 它会 压缩 varbinary(max) LOB列,即使它们不存在于任何非聚集索引中并且仅存在于底层堆中?这背后的基本原理是回收应用程序作业释放的任何 space,该作业将 LOB 列设置为 null 用于符合条件的行。

另外,你也可以看到这个堆有很多转发的记录。我还怀疑整行已从堆中删除,但由于针对堆的删除的已知行为而未取消分配,其中行仅在通过 table 锁显式获取时才取消分配table 锁定查询提示或通过锁定升级。考虑到这一点,我正在考虑禁用堆上的所有非聚集索引,重建堆然后重新启用非聚集索引。此操作还会 re-claim/compact lob 列中任何未使用的 space 以及删除转发的记录和已删除但未完全取消分配的行吗?

免责声明 - 此数据库由供应商设计,不接受创建聚簇索引table。使用此数据库的应用程序在周末不使用,因此我需要进行大量维护 windows 因此,虽然重新构建堆可能会占用大量资源并且很痛苦,但这是可行的。

Can anyone confirm that if I ran the “ALTER INDEX ALL ON REORGANISE WITH ( LOB_CAMPACTION = ON )” statement that it would compact the varbinary(max) LOB column(s) even though they are not present in any of the non-clustered indexes and only in the underlying heap?

是的。您可以根据经验轻松地确认这一点,我们将在一分钟内这样做。

The rationale behind this would be to reclaim any space freed by the application job which sets the LOB column to null for qualifying rows.

LOB 压缩并不会真正回收所有 space 释放的内容。即使重建整个 table 也不会回收 LOB space -- 重组是您所能做的最好的事情,但这并不能回收所有内容。如果它让您感觉更好:这不仅限于堆 tables,它实际上是一个功能,而不是错误。

让我证明一下。让我们用 LOB 数据创建一个堆 table:

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 即可隐式创建一个)。但是,non-clustered 索引并非如此。重新开始,但这次添加另一个索引:

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 |
+-------+----------+-------------+------------+-------------+------------+

令人惊讶(也许),non-clustered 索引的 LOB 数据被重建,而不仅仅是重组。 ALTER INDEX ALL .. REBUILD 将具有相同的效果,但将完全不影响堆。总结一下table:

+----------------------+---------------+-------------------+----------------------+
|                      | 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           |
+----------------------+---------------+-------------------+----------------------+

I am thinking about disabling all the non-clustered indexes on the heap, rebuilding the heap and then re-enabling the non-clustered indexes.

不需要单独re-enable non-clustered索引; ALTER TABLE .. REBUILD 也会重建所有索引,禁用的索引将作为重建的一部分 re-enabled。

Would this operation also re-claim/compact any unused space in the lob column as well as removing the forwarded records and deleted but not fully de-allocated rows?

根据我们之前的结果,不,不完全是。如果您对仅将 LOB 数据与 table 的其余部分进行重建感到满意,则该过程将是:

  1. 执行ALTER INDEX ALL .. DISABLE禁用所有non-clustered索引;
  2. 执行 ALTER INDEX ALL .. REORGANIZE WITH (LOB_COMPACTION = ON) 压缩底层堆的 LOB 页面(这将保留禁用的索引);
  3. 执行ALTER TABLE .. REBUILD重建堆的in-row数据,以及索引的所有数据,re-enable它们。

如果您真的想将堆缩小到最小大小,则必须创建一个新的 table 并在其中插入数据,但这涉及更多的脚本编写和明智地使用 sp_rename。它也非常昂贵,因为它需要复制所有 LOB 数据(REORGANIZE 避免的事情)。如果您在不注意文件组和日志 space 使用的情况下执行此操作,您最终可能会消耗 space 比您寻求回收的更多,并且不太可能对性能有帮助。