这个问题与此论坛主题。
在我的工作站上运行 SQL Server 2008 开发版,并在企业版双节点虚拟机群集上运行,我将其称为“alpha 群集”。
删除带有 varbinary(max) 列的行所需的时间与该列中数据的长度直接相关。乍一看,这可能听起来很直观,但经过调查,我发现这与我对 SQL Server 实际删除行和处理此类数据的理解相冲突。
该问题源于我们在 .NET Web 应用程序中看到的删除超时(> 30 秒)问题,但为了讨论起见,我对其进行了简化。
当一条记录被删除时,SQL Server 会将其标记为幽灵,以便在事务提交后稍后由幽灵清理任务进行清理(请参阅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 秒,而且我毫不怀疑,如果碎片严重且负载较低,时间会更长。
我试图确保它不只是在缓存中分配空间,从而占用了那段时间。在执行删除而不是方法之前,我从其他行读取了 2 GB 的数据checkpoint
,这比分配给 SQL Server 进程的数据要多。不确定这是否是一个有效的测试,因为我不知道 SQL Server 如何对数据进行重新排列。我假设它总是会淘汰旧的,而采用新的。
此外,它甚至不会修改页面。我可以用 看到这一点dm_os_buffer_descriptors
。删除后页面干净,而所有三个小型、中型和大型删除的修改页面数量均少于 20。我还比较了 的输出,DBCC PAGE
以对查找的页面进行抽样,没有任何变化(只有位ALLOCATED
从 PFS 中删除)。它只是释放它们。
为了进一步证明页面查找/释放是导致问题的原因,我尝试使用文件流列而不是 vanilla varbinary(max) 进行相同的测试。无论 LOB 大小如何,删除都是恒定时间。
首先,我的学术问题是:
- 为什么 SQL Server 需要查找所有 LOB 数据页才能对它们进行 X 锁定?这只是锁在内存中表示方式的一个细节(以某种方式与页面一起存储)吗?如果没有完全缓存,这会使 I/O 影响在很大程度上取决于数据大小。
- 为什么要使用 X 锁,只是为了释放它们?既然释放不需要修改页面本身,那么只锁定索引叶和行内部分还不够吗?还有其他方法可以获取锁所保护的 LOB 数据吗?
- 既然已经有一个专门用于此类工作的后台任务,为什么还要预先释放这些页面呢?
也许更重要的是我的实际问题是:
- 有什么方法可以让删除操作有所不同吗?我的目标是无论大小如何,都以恒定时间进行删除,类似于文件流,任何清理都在事后在后台进行。这是配置问题吗?我存储东西的方式奇怪吗?
以下是如何重现所描述的测试(通过 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
以下是在我的工作站上分析删除操作的一些结果:
| 列类型 | 删除大小 | 持续时间(毫秒) | 读取 | 写入 | CPU | -------------------------------------------------------------------------------- | 变量二进制 | 16 KB | 40 | 13 | 2 | 0 | | 变量二进制 | 4 MB | 952 | 2318 | 2 | 0 | | VarBinary | 50 MB | 2976 | 28594 | 1 | 62 | -------------------------------------------------------------------------------- | 文件流 | 16 KB | 1 | 12 | 1 | 0 | | 文件流 | 4 MB | 0 | 9 | 0 | 0 | | 文件流 | 50 MB | 1 | 9 | 0 | 0 |
我们不能只使用 filestream,因为:
- 我们的数据大小分布不能保证这一点。
- 实际上,我们会分多块添加数据,而 filestream 不支持部分更新。我们需要围绕这个问题进行设计。
更新 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'
对于大小超过 5 MB 的文件,将返回1651 | 171860
。
此外,如果将数据写入日志,我预计页面本身会很脏。似乎只有释放被记录下来,这与删除后的脏内容相符。
更新 2
我确实收到了 Paul Randal 的回复。他确认了必须读取所有页面才能遍历树并找到要释放的页面的事实,并表示没有其他方法可以查找哪些页面。这是对 1 和 2 的一半回答(虽然没有解释对行外数据进行锁定的必要性,但那只是小事一桩)。
问题 3 仍然悬而未决:如果已经有一个后台任务来清理删除的内容,为什么要预先释放页面?
当然,最重要的问题是:有没有办法直接缓解(即不解决)这种依赖于大小的删除行为?我认为这将是一个更常见的问题,除非我们真的是唯一在 SQL Server 中存储和删除 50 MB 行的人?其他人是否都使用某种形式的垃圾收集工作来解决这个问题?
答案1
我无法确切说明为什么删除 VARBINARY(MAX) 比删除文件流效率低得多,但如果您只是想避免 Web 应用程序在删除这些 LOBS 时超时,那么您可以考虑一个想法。您可以将 VARBINARY(MAX) 值存储在原始表(我们称之为 tblParent)引用的单独表(我们称之为 tblLOB)中。
从这里开始,当您删除一条记录时,您可以直接将其从父记录中删除,然后偶尔进行垃圾收集过程,清理 LOB 表中的记录。在此垃圾收集过程中可能会有额外的硬盘活动,但它至少与前端 Web 分开,并且可以在非高峰时间执行。