3

我有一个启用了 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文章和这篇文章中,它就索引重组操作说明了以下内容:

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

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

我发现这些陈述模棱两可而且不是很清楚。谁能确认如果我运行“ALTER INDEX ALL ON REORGANIZE WITH (LOB_CAMPACTION = ON)”语句,即使它们不存在于任何非聚集索引中,它也会 压缩 varbinary(max) LOB 列并且只在底层堆中?这背后的基本原理是回收应用程序作业释放的任何空间,该作业将 LOB 列设置为 null 以用于符合条件的行。

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

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

4

1 回答 1

4

任何人都可以确认,如果我运行“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 数据完全保持不变。您也可以尝试使用聚集索引(只需使IDaPRIMARY 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避免的)。如果您在不注意文件组和使用的日志空间的情况下执行此操作,则最终消耗的空间可能比您寻求回收的空间多,并且不太可能有助于提高性能。

于 2018-04-03T14:20:38.597 回答