这个问题与此论坛主题有关。
在我的工作站和一个企业版两节点虚拟机群集上运行SQL Server 2008 Developer Edition,我将其称为“ alpha群集”。
删除带有varbinary(max)列的行所花费的时间与该列中数据的长度直接相关。起初听起来似乎很直观,但是经过调查,这与我对SQL Server通常如何实际删除行并处理此类数据的理解相矛盾。
该问题源于我们在.NET Web应用程序中看到的删除超时(> 30秒)问题,但是为了进行讨论,我将其简化了。
删除记录后,SQL Server会将其标记为要在事务提交后稍后由Ghost清除任务清除的虚影(请参阅Paul Randal的博客)。在删除varbinary(max)列中分别具有16 KB,4 MB和50 MB数据的三行的测试中,我看到这种情况发生在页面的数据行部分以及事务中日志。
在我看来,奇怪的是,在删除过程中,X锁被放置在所有LOB数据页上,而这些页被重新分配在PFS中。我在事务日志中以及DMV sp_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
了查找页面抽样的输出,并且没有变化(仅从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 | -------------------------------------------------- ------------------ | VarBinary | 16 KB | 40 | 13 | 2 | 0 | | VarBinary | 4 MB | 952 | 2318 | 2 | 0 | | VarBinary | 50 MB | 2976 | 28594 | 1 | 62 | -------------------------------------------------- ------------------ | FileStream | 16 KB | 1 | 12 | 1 | 0 | | FileStream | 4 MB | 0 | 9 | 0 | 0 | | FileStream | 50 MB | 1 | 9 | 0 | 0 |
我们不一定要使用文件流来代替,因为:
- 我们的数据大小分布不保证。
- 在实践中,我们以很多块添加数据,并且文件流不支持部分更新。我们将需要对此进行设计。
更新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行的行?是否其他所有人都可以通过某种形式的垃圾收集工作来解决此问题?