DBCC SHRINKFILE 实际上在做什么?

nob*_*ght 6 sql-server shrink

我有一个大小为 11TB 的数据库。我最近从这个数据库中截断了超过 5TB 的数据。

(我完全熟悉您通常不会收缩数据库的所有原因)

我很好奇 DBCC SHRINKFILE 命令实际上在做什么,因为当我运行该命令来缩小一个大小约为 650000MB 且可用空间为 45% 的文件时,它似乎实际上并没有移动任何页面。

有问题的代码是:

USE [CAF] 
GO 
DBCC SHRINKFILE (N'FILENAME' , 650239) 
GO
Run Code Online (Sandbox Code Playgroud)

当我在执行 DBCC SHRINKFILE 时监视服务器的性能指标时,我看到以下内容

  • % 磁盘的活动时间立即跳到 100%。
  • sqlservr.exe 和系统进程都以连续 2MB/s 的速度开始读取(有时以 5MB/s 达到峰值)。
  • 磁盘队列长度直接跳到 1
  • 磁盘 iops 跳到 250 左右(注意测试中的磁盘可以达到 6000 iops) CPU 保持空闲

即使我告诉 DBCC SHRINKFILE 将文件缩小 1 MB 并让它运行 30 分钟,它也不会完成。

我读过的所有内容都表明 DBCC SHRINKFILE 应该从文件末尾获取页面并将它们移动到开头附近的可用空间,但即使在最差的性能下,移动 1MB 的页面也不会超过几分钟

DBCC SHRINKFILE 实际上在做什么,对我来说没有意义?

Ran*_*gen 9

我读过的所有内容都表明 DBCC SHRINKFILE 应该从文件末尾获取页面并将它们移动到开头附近的可用空间,DBCC SHRINKFILE 实际上在做什么?

DBCC SHRINKFILE经历的步骤:

  • DbccSpaceReclaim 回收可用空间。EG 通过清除空盘区。
  • DbccFilesCompact 将非 LOB 页面移动到指定为第二个参数的点之前,尽可能靠近数据文件的开头
  • DbccLOBCompact 将 LOB 页移动到指定为第二个参数的点之前,尽可能靠近数据文件的开头

DbccFilesCompactDbccLOBCompact页从之后的参数传递给移动DBCC SHRINKFILE语句的参数之前,尽可能靠近数据页越好。 如果可能的话

所有这些页面移动之后,实际截断发生并且您会看到大小发生变化。

在此处输入图片说明

来源

监控DBCC SHRINKFILE过程可以为您提供更多信息,说明这三个中哪一个花费的时间最长/使用的资源最多(但我注意到 DbccFilesCompact 正在运行,而 Lob 页面在测试时被移动,必须在那里进行一些调查)。

根据这篇博文,每个事务以大约 32 页的批次完成收缩文件。至于在停止过程时获得恢复收缩的能力。

缩小数据库文件时的声明,您可以恢复大约。你离开的地方也在这个答案中得到证实。

取消命令时似乎什么也没发生,但页面会以这些较小的批次移动。由于在DbccCompact事件结束时发生截断,因此仅在结束时才注意到大小差异。

完全登录

在发生收缩时查看日志文件时,由于记录了许多事务,这也得到了确认:

LOP_BEGIN_XACT
LOP_MODIFY_ROW
LOP_MODIFY_ROW
... --many more LOP_MODIFY_ROW records
LOP_COMMIT_XACT
LOP_BEGIN_XACT
...
Run Code Online (Sandbox Code Playgroud)

我们看到所有这些LOP_MODIFY_ROW记录是因为收缩完全记录在事务日志中。这有额外的开销。

如果您不希望所有日志记录同时发生,或者您在收缩操作之间需要更多时间,您可以分批运行收缩,有或没有WAITFOR延迟。

多个事务的另一个副作用是在缩小或尝试缩小文件时可能会在多种情况下发生阻塞。

同样,为了减轻阻塞,您可以尝试小批量收缩,在收缩之间留出时间。

您应该尝试的第一个收缩是:TRUNCATEONLY不要移动页面,而只是尝试向操作系统释放空间。

DBCC SHRINKFILE (N'FILENAME' , TRUNCATEONLY)
Run Code Online (Sandbox Code Playgroud)

数据库在 MB 中包含大量二进制文件和 XML 数据,这将如何影响收缩?

由于额外的索引/表扫描来更新指针,收缩 Lob 数据可能需要很长时间。看看Paul Randal 的这篇文章

简而言之,您可以看到收缩需要很长时间处理DbccLobCompact

这是由于表页中的指针指向 ' OFF ROW' 值(Lob 数据),而不是相反。当由于收缩而移动 Lob 数据时,我们必须更新行页中的指针,从而导致大量额外开销。

非聚集索引的开销类似。


@TiborKaraszi 还提到:

还有一些注意事项(除了 LOB 方面)。Shrink 需要一个 X 锁,它将永远等待。移动堆页面意味着更新所有指向受此堆页面移动影响的所有行的 NC 索引。

这意味着您的收缩操作花费这么长时间的原因是您的收缩操作可能被查询阻塞并且它可以在运行时阻塞其他查询。

此外,由于 NC 索引更新,缩小堆可能会很昂贵。

TL; 博士

总而言之,由于扫描和更新底层聚集/非聚集索引中的指针,缩小具有较大对象的较大文件将花费更长的时间。

即使移动了 1mb 的 lob 数据,也必须相应地读取和更新对象。其速度也将基于您的 IO 子系统。

另一个有效的原因可能是您的查询由于排他锁定行为而被阻止。

当收缩运行时,您可以使用以下查询检查阻塞:

SELECT
est.TEXT,
er.blocking_session_id,
er.last_wait_type,
er.reads,
er.cpu_time,
er.total_elapsed_time
FROM sys.dm_exec_requests er
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS est
WHERE blocking_session_id != 0;
Run Code Online (Sandbox Code Playgroud)

缩小单个大吊球记录是全有还是全无操作?

我不完全确定的另一个原因是整个 Lob 对象及其页面将尽可能移到数据文件的前面,即使并非所有页面都超过DBCC SHRINKFILE.

编辑:对此进行了测试,并且此语句为假,DBCCLobCompact即使并非一条记录的所有页面都高于此阈值,也只会移动高于阈值的 Lob 页面。

使用 进行测试时DBCC IND,我们只能看到文件末尾的几个 lob 页面被移动,即使它们属于一个记录。


附加说明

  • 收缩文件是单线程的
  • Paul Randal 在他的一篇博文中指出的收缩的更好选择:
  • 创建一个新的文件组
  • 使用 CREATE INDEX … WITH (DROP_EXISTING = ON) ON 语法将所有受影响的表和索引移动到新文件组中,以同时移动表并从中删除碎片
  • 删除您无论如何要缩小的旧文件组(如果它是主要文件组,则将其缩小)
  • 收缩会导致严重碎片化