【问题标题】:SQL Server - Deleting/Updating LOB data in a HeapSQL Server - 删除/更新堆中的 LOB 数据
【发布时间】:2018-09-11 14:23:56
【问题描述】:

我有一个启用了 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 ON REORGANIZE WITH (LOB_CAMPACTION = ON)”语句,它会压缩 varbinary(max) LOB 列,即使它们不存在于任何非聚集索引且仅在底层堆中?这背后的基本原理是回收应用程序作业释放的任何空间,该作业将 LOB 列设置为 null 以用于符合条件的行。

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

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

【问题讨论】:

    标签: sql-server blob sql-server-2016 lob heap-table


    【解决方案1】:

    谁能确认如果我运行“ALTER INDEX ALL ON REORGANIZE WITH ( LOB_CAMPACTION = ON )” 声明,它将压缩 varbinary(max) LOB 列,即使它们不存在于任何 非聚集索引和仅在底层堆中?

    是的。您可以通过经验轻松地确认这一点,我们将在稍后完成。

    这背后的基本原理是回收由 将符合条件的行的 LOB 列设置为 null 的应用程序作业。

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

    让我证明一下。让我们用 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 中任何未使用的空间 列以及删除转发的记录并删除但不 完全解除分配的行?

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

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

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

    【讨论】:

    • 非常感谢您抽出时间发布该答案,这正是我所追求的,我无法要求更好的回应,非常感谢我的伙计!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 1970-01-01
    • 1970-01-01
    • 2011-01-09
    • 2011-04-10
    • 1970-01-01
    相关资源
    最近更新 更多