SQL Server 中 LOB 数据的删除性能

Jer*_*erg 17 sql-server

这个问题与此论坛主题有关

在我的工作站和企业版两节点虚拟机集群上运行 SQL Server 2008 开发版,我将其称为“alpha 集群”。

删除带有 varbinary(max) 列的行所需的时间与该列中数据的长度直接相关。起初这听起来可能很直观,但经过调查,它与我对 SQL Server 通常如何实际删除行和处理此类数据的理解相冲突。

该问题源于我们在 .NET Web 应用程序中看到的删除超时(> 30 秒)问题,但为了本次讨论,我已对其进行了简化。

当一条记录被删除时,SQL Server 将它标记为一个幽灵,以便在事务提交后稍后由 Ghost 清理任务清理(参见Paul Randal 的博客)。在分别删除 varbinary(max) 列中包含 16 KB、4 MB 和 50 MB 数据的三行的测试中,我看到这种情况发生在包含行内部分数据的页面以及事务中日志。

我觉得奇怪的是,在删除过程中所有 LOB 数据页上都放置了 X 锁,并且这些页在 PFS 中被释放。我在事务日志以及DMVsp_lock的结果中看到了这一点dm_db_index_operational_stats( page_lock_count)。

如果这些页面不在缓冲区缓存中,这会在我的工作站和我们的 alpha 集群上创建 I/O 瓶颈。事实上,page_io_latch_wait_in_ms来自同一个DMV的实际上是整个删除的持续时间,并且page_io_latch_wait_count与锁定页面的数量相对应。对于我工作站上的 50 MB 文件,当以空缓冲区缓存 ( checkpoint/ dbcc dropcleanbuffers)启动时,这会转化为超过 3 秒,而且我毫不怀疑,对于大量碎片和负载不足的情况,时间会更长。

我试图确保它不只是在缓存中分配空间占用了那段时间。在执行删除而不是checkpoint方法之前,我从其他行读取了 2 GB 的数据,这多于分配给 SQL Server 进程的数据。不确定这是否是一个有效的测试,因为我不知道 SQL Server 如何对数据进行混洗。我认为它总是会推出旧的以支持新的。

此外,它甚至不修改页面。这我可以看到dm_os_buffer_descriptors。删除后的页面是干净的,而小、中、大三个删除的修改页面数都小于20。我还比较DBCC PAGE了查找页面样本的输出,并且没有任何更改(仅从ALLOCATEDPFS 中删除了该位)。它只是释放它们。

为了进一步证明页面查找/释放是导致问题的原因,我尝试使用文件流列而不是 vanilla varbinary(max) 进行相同的测试。无论 LOB 大小如何,删除都是恒定时间。

所以,首先我的学术问题:

  1. 为什么 SQL Server 需要查找所有 LOB 数据页才能对它们进行 X 锁定?这只是关于锁在内存中如何表示的细节(以某种方式与页面一起存储)?如果没有完全缓存,这使得 I/O 影响强烈依赖于数据大小。
  2. 为什么 X 锁根本就只是为了释放它们?仅用行内部分锁定索引叶是不够的,因为释放不需要修改页面本身?有没有其他方法可以获取锁保护的 LOB 数据?
  3. 既然已经有专门用于此类工作的后台任务,为什么要预先取消分配页面?

也许更重要的是,我的实际问题:

  • 有什么方法可以使删除操作不同吗?我的目标是无论大小如何都进行恒定时间删除,类似于文件流,在此之后,任何清理都在后台进行。是配置问题吗?我存放东西很奇怪吗?

以下是如何重现所描述的测试(通过 SSMS 查询窗口执行):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK
Run Code Online (Sandbox Code Playgroud)

以下是在我的工作站上分析删除的一些结果:

| 列类型 | 删除大小 | 持续时间(毫秒)| 阅读 | 写 | 中央处理器 |
-------------------------------------------------- ------------------
| 变量二进制 | 16 KB | 40 | 13 | 2 | 0 |
| 变量二进制 | 4 MB | 952 | 2318 | 2 | 0 |
| 变量二进制 | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| 文件流 | 16 KB | 1 | 12 | 1 | 0 |
| 文件流 | 4 MB | 0 | 9 | 0 | 0 |
| 文件流 | 50 MB | 1 | 9 | 0 | 0 |

我们不一定只使用文件流,因为:

  1. 我们的数据大小分布并不能保证这一点。
  2. 在实践中,我们以多块的方式添加数据,文件流不支持部分更新。我们需要围绕这一点进行设计。

更新 1

测试了一个理论,即数据被写入事务日志作为删除的一部分,而事实似乎并非如此。我是否错误地对此进行了测试?见下文。

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'
Run Code Online (Sandbox Code Playgroud)

对于大小超过 5 MB 的文件,这将返回1651 | 171860.

此外,如果数据写入日志,我希望页面本身是脏的。似乎只记录了解除分配,这与删除后的脏内容相匹配。

更新 2

我确实收到了 Paul Randal 的回复。他肯定了这样一个事实,即它必须读取所有页面才能遍历树并找到要取消分配的页面,并表示没有其他方法可以查找哪些页面。这是对 1 和 2 的一半答案(虽然没有解释对行外数据锁定的必要性,但那是小土豆)。

问题 3 仍然悬而未决:如果已经有后台任务进行删除清理,为什么要预先释放页面?

当然,最重要的问题是:有没有办法直接减轻(即不解决)这种依赖大小的删除行为?我认为这将是一个更常见的问题,除非我们真的是唯一在 SQL Server 中存储和删除 50 MB 行的人?其他人是否通过某种形式的垃圾收集工作来解决这个问题?

小智 5

我不能说为什么与文件流相比,删除 VARBINARY(MAX) 的效率会低得多,但是如果您只是想在删除这些 LOBS 时避免 Web 应用程序超时,您可以考虑一个想法。您可以将 VARBINARY(MAX) 值存储在原始表引用的单独表(我们称之为 tblLOB)中(我们称之为 tblParent)。

从这里当您删除一条记录时,您可以将它从父记录中删除,然后偶尔进行垃圾收集过程并清理 LOB 表中的记录。在这个垃圾收集过程中可能会有额外的硬盘活动,但它至少会与前端 Web 分开,并且可以在非高峰时间执行。