SQL Server - 删除/更新堆中的 LOB 数据

Fza*_*Fza 3 sql-server blob lob sql-server-2016 heap-table

我有一个启用了 RCSI 的 SQL Server 2016 数据库,它实际上是一堆堆。除了一张表之外,数据库中的所有其他表都是一个堆,最大的堆约为 200GB,占数据库总大小的 50% 以上。

\n\n

这个特定的大堆有两个 lob 列,均为 varbinary(max) 数据类型。堆还有许多非聚集索引,幸运的是 varbinary(max) 列不存在于任何这些非聚集索引中,因此它们的大小相对较小。

\n\n

供应商提供了一个清理脚本,该脚本从应用程序服务器运行并从这个大堆中清除数据。经过一番调查,我发现此清理脚本不会删除整行,而是根据某些条件将 varbinary(max) 列之一设置为 null。

\n\n

以下是有关堆的一些详细信息:

\n\n

在此输入图像描述

\n\n
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(N\'<database>\'), OBJECT_ID(N\'GrimHeaper>\'),0, null, \'DETAILED\');\n
Run Code Online (Sandbox Code Playgroud)\n\n

在此输入图像描述

\n\n

在此输入图像描述

\n\n
SELECT * FROM sys.dm_db_index_operational_stats(db_id(\'<database>\'),object_id(\'GrimHeaper\'),0,null);\n
Run Code Online (Sandbox Code Playgroud)\n\n

在此输入图像描述

\n\n

在此输入图像描述

\n\n

在此输入图像描述

\n\n

我对这种情况的理解是,通过将 lob 列中的值设置为 null 释放的空间不会自动重新声明,无论表是堆还是聚集表,都是这种行为,如果我错了,请纠正我。

\n\n

在这篇 Microsoft文章以及这篇文章中,它阐述了以下有关索引重组操作的内容:

\n\n
\n

REORGANIZE ALL 对所有索引执行 LOB_COMPACTION。对于每个索引,这会压缩聚集索引、基础表中的所有 LOB 列或非聚集索引中包含的列。

\n\n

指定 ALL 时,将重新组织与指定表或视图关联的所有索引,并压缩与聚集索引、基础表或包含列的非聚集索引关联的所有 LOB 列。

\n
\n\n

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

\n\n

另外,你还可以看到这个堆有很多转发的记录。我还怀疑整个行已从堆中删除,但尚未取消分配,因为针对堆的删除的已知行为仅在通过表锁查询提示显式获取表锁时才取消分配行或通过锁升级。考虑到这一点,我正在考虑禁用堆上的所有非聚集索引,重建堆,然后重新启用非聚集索引。此操作是否还会重新声明/压缩 lob 列中任何未使用的空间以及删除转发的记录和已删除但未完全取消分配的行?

\n\n

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

\n

Jer*_*ert 5

\n

任何人都可以确认,如果我运行 \xe2\x80\x9cALTER INDEX ALL ON REORGANISE\n WITH ( LOB_CAMPACTION = ON )\xe2\x80\x9d 语句,它甚至会压缩\n varbinary(max) LOB 列尽管它们不存在于任何非聚集索引中并且仅存在于底层堆中?

\n
\n\n

是的。您可以轻松地凭经验确认这一点,我们将在一分钟内完成。

\n\n
\n

其背后的基本原理是回收应用程序作业释放的任何空间,该作业将符合条件的行的 LOB 列设置为 null。

\n
\n\n

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

\n\n

让我证明一下。让我们创建一个包含 LOB 数据的堆表:

\n\n
CREATE TABLE heap_of_trouble(ID INT IDENTITY, lobby VARBINARY(MAX));\n\n-- SQL Server will store values <8K in the row by default; force the use of LOB pages\nEXEC sp_tableoption \'heap_of_trouble\', \'large value types out of row\', 1;\n\nSET NOCOUNT ON;\nGO\nBEGIN TRANSACTION;\nGO\nINSERT heap_of_trouble(lobby) VALUES (CONVERT(VARBINARY(MAX), REPLICATE(\' \', 4000)));\nGO 10000\nCOMMIT;\n\nSELECT p.[rows], p.index_id, au.[type_desc], au.data_pages, au.total_pages, au.used_pages\nFROM sys.partitions p \nJOIN sys.allocation_units au ON au.container_id = p.hobt_id\nJOIN sys.objects o ON o.[object_id] = p.[object_id]\nWHERE o.[name] = \'heap_of_trouble\'\n
Run Code Online (Sandbox Code Playgroud)\n\n
CREATE TABLE heap_of_trouble(ID INT IDENTITY, lobby VARBINARY(MAX));\n\n-- SQL Server will store values <8K in the row by default; force the use of LOB pages\nEXEC sp_tableoption \'heap_of_trouble\', \'large value types out of row\', 1;\n\nSET NOCOUNT ON;\nGO\nBEGIN TRANSACTION;\nGO\nINSERT heap_of_trouble(lobby) VALUES (CONVERT(VARBINARY(MAX), REPLICATE(\' \', 4000)));\nGO 10000\nCOMMIT;\n\nSELECT p.[rows], p.index_id, au.[type_desc], au.data_pages, au.total_pages, au.used_pages\nFROM sys.partitions p \nJOIN sys.allocation_units au ON au.container_id = p.hobt_id\nJOIN sys.objects o ON o.[object_id] = p.[object_id]\nWHERE o.[name] = \'heap_of_trouble\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

让我们清除一些列:

\n\n
UPDATE heap_of_trouble SET lobby = NULL WHERE ID % 2 = 0;\n
Run Code Online (Sandbox Code Playgroud)\n\n

让我们再次获取页数:

\n\n
+-------+----------+-------------+------------+-------------+------------+\n| rows  | index_id |  type_desc  | data_pages | total_pages | used_pages |\n+-------+----------+-------------+------------+-------------+------------+\n| 10000 |        0 | IN_ROW_DATA |         43 |          49 |         44 |\n| 10000 |        0 | LOB_DATA    |          0 |        5121 |       5118 |\n+-------+----------+-------------+------------+-------------+------------+\n
Run Code Online (Sandbox Code Playgroud)\n\n

除了最后一页之外没有任何变化。这是预料之中的。现在让我们重新组织和紧凑:

\n\n
ALTER INDEX ALL ON heap_of_trouble REORGANIZE WITH (LOB_COMPACTION = ON);\n
Run Code Online (Sandbox Code Playgroud)\n\n
UPDATE heap_of_trouble SET lobby = NULL WHERE ID % 2 = 0;\n
Run Code Online (Sandbox Code Playgroud)\n\n

您会注意到页数不是我们开始时的一半:LOB 数据已重新组织,但尚未完全重建。

\n\n

如果您尝试ALTER TABLE .. REBUILD相反,您会注意到 LOB 数据根本没有压缩:

\n\n
+-------+----------+-------------+------------+-------------+------------+\n| rows  | index_id |  type_desc  | data_pages | total_pages | used_pages |\n+-------+----------+-------------+------------+-------------+------------+\n| 10000 |        0 | IN_ROW_DATA |         43 |          49 |         44 |\n| 10000 |        0 | LOB_DATA    |          0 |        5121 |       5117 |\n+-------+----------+-------------+------------+-------------+------------+\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意如何IN_ROW_DATA重建,但 LOB 数据完全保持不变。您也可以尝试使用聚集索引(只需使用 aIDPRIMARY KEY式创建一个)。但是,对于非聚集索引则不然。重新开始,但这次添加另一个索引:

\n\n
CREATE INDEX IX_heap_of_trouble_ID ON heap_of_trouble (ID) INCLUDE (lobby)\n
Run Code Online (Sandbox Code Playgroud)\n\n

当然,在索引中包含 LOB 数据并不是正常设置;这只是为了说明。看看我们得到了什么ALTER TABLE REBUILD

\n\n
+-------+----------+-------------+------------+-------------+------------+\n| rows  | index_id |  type_desc  | data_pages | total_pages | used_pages |\n+-------+----------+-------------+------------+-------------+------------+\n| 10000 |        0 | IN_ROW_DATA |         29 |          33 |         30 |\n| 10000 |        0 | LOB_DATA    |          0 |        5121 |       5117 |\n| 10000 |        2 | IN_ROW_DATA |         35 |          49 |         37 |\n| 10000 |        2 | LOB_DATA    |          0 |        2561 |       2560 |\n+-------+----------+-------------+------------+-------------+------------+\n
Run Code Online (Sandbox Code Playgroud)\n\n

令人惊讶的是(也许),非聚集索引的 LOB 数据被重建,而不仅仅是重组。ALTER INDEX ALL .. REBUILD将具有相同的效果,但将使堆完全保持不变。用一张小表总结一下:

\n\n
ALTER INDEX ALL ON heap_of_trouble REORGANIZE WITH (LOB_COMPACTION = ON);\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n

我正在考虑禁用堆上的所有非聚集索引,重建堆,然后重新启用非聚集索引。

\n
\n\n

您不需要单独重新启用非聚集索引;ALTER TABLE .. REBUILD也会重建所有索引,并且禁用的索引将作为重建的一部分重新启用。

\n\n
\n

此操作是否还会重新声明/压缩 lob 列中任何未使用的空间以及删除转发的记录和已删除但未完全取消分配的行?

\n
\n\n

根据我们之前的结果,不,不完全是。如果您只满足于将 LOB 数据与重建表的其余部分进行压缩,则过程如下:

\n\n
    \n
  1. 执行ALTER INDEX ALL .. DISABLE禁用所有非聚集索引;
  2. \n
  3. 执行ALTER INDEX ALL .. REORGANIZE WITH (LOB_COMPACTION = ON)压缩底层堆的 LOB 页(这将保留禁用的索引);
  4. \n
  5. 执行ALTER TABLE .. REBUILD重建堆的行内数据以及索引的所有数据,并重新启用它们。
  6. \n
\n\n

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

\n