SQL Server 中 LOB 数据的慢速 DELETE

Aba*_*cus 8 performance sql-server delete sql-server-2012 blob

我有一个用于记录的表和一个用于清除DELETE 性能非常慢的旧数据的存储过程,这超出了我的理解。我正在寻找如何修改表或 DELETE 语句以在 LOB 数据上表现得相当好。或者,如果微软确认了这个问题——比如“我们已经用 SQL 服务器版本 x 解决了这个问题”,或者甚至“我们看到这性能很差,但它不是优先事项”——这也可以.

这是在 Microsoft SQL Server 2012 (SP3) 上运行的。下面基本上是我的实际表格和代码,只是稍微简化了:

CREATE TABLE [LOG_VALUE](
 [ID] [int] IDENTITY(1,1) NOT NULL,
 [VALUE] [varchar](max) NOT NULL,
 [CHECKSUM] [int] NOT NULL,
 [VALUE_LEN] [int] NOT NULL,
 CONSTRAINT [PK_LOG_REQUEST] PRIMARY KEY CLUSTERED ([ID] ASC)
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

有问题的删除是通过以下方式完成的:

WHILE (@@ROWCOUNT > 0)
  DELETE TOP (100) [LOG_VALUE]
  OUTPUT DELETED.[VALUE_LEN] INTO @DELETED_ROWS
  WHERE [ID] IN (SELECT [ID] FROM @DELETE_IDS);
Run Code Online (Sandbox Code Playgroud)

基本的存储过程流程是:

  1. 删除引用要清除的行的值
  2. 从上述日志表中不存在的 LOB 表中选择 [ID] 值,放入表变量 @DELETE_IDS
  3. 一次删除前 100 行(以减少/防止争用/锁定)

显然,LOB 数据必须加载到服务器的内存中才能删除。支持:1)当我在表中插入 12000 行时,它几乎可以是瞬间 2)检索 12000 行几乎是瞬间 3)一旦它们不再在缓存中,选择和删除相同的 12000 行都需要大约 40秒。

我能找到的最相关的问题是:https : //serverfault.com/questions/241893/delete-performance-for-lob-data-in-sql-server 从 2011 年开始,但遗憾的是没有令人满意的答案。我只是希望在 dba.stackexchange.com 上提出这个问题会找到更多知识渊博的观众:)。该问题的作者肯定比我更了解这一点,但当我阅读它时,中心点是:

  1. 删除行时,SQL Server 通常只是将它们标记为已删除,然后实际上会被“Ghost Cleanup Task”清除
  2. 删除 LOB 数据时,在删除过程中对所有 LOB 数据页放置排他锁,并释放这些页
  3. 当页面不在缓冲区缓存中时,这些排他锁等待页面加载到内存中
  4. 这些被释放的页面“预先”发生,而不是在清理任务中。这意味着删除操作总是在语句完成之前等待要加载的数据,解锁相关行

似乎这种释放应该在清理任务中发生,而不是预先发生。由于在谓词(或 OUTPUT)中没有引用 LOB 数据,因此根本不需要将其加载到缓存中。

更多可能相关的信息,但我认为不是:

  • 这些值通常超过 100KB,但大小差异很大
  • 指向 [LOG_VALUE].[ID] 的另一个表未强制执行引用完整性
  • @DELETED_ROWS 表的累积是为了注意,在存储过程结束时,删除了多少行和字节
  • 当前有 110 万行,根据 IDENTITY 的值,只有 160 万行
  • 没有触发器
  • 目前,Ghost Record Count = 0
  • READ_COMMITTED_SNAPSHOT并且ALLOW_SNAPSHOT_ISOLATION都关闭。

除了删除/更新 LOB 数据外,整个数据库(以及同一服务器上的其他数据库)在所有情况下的表现都符合我的预期。

Pau*_*ite 9

...一旦它们不再在缓存中,选择和删除相同的 12000 行都需要大约 40 秒。

这似乎表明存储子系统不足。如果这是原因,SQL Server 可能会使用其中一种PAGEIOLATCH_XX等待类型进行等待。

显然,LOB 数据必须加载到服务器的内存中才能删除。

正如 Paul Randal 所说(更新 2,在您链接的问题中),SQL Server必须遍历 LOB 树才能找到必须删除的页面。为此,SQL Server 需要将这些 LOB 树页放入内存中。真的没有办法解决这个问题。该信息无法以任何其他方式获得。

似乎这种释放应该在清理任务中发生,而不是预先发生。

这种工作方式没有很好的记录。

在我的测试中,如果没有特殊原因保留该行,SQL Server 会立即删除 LOB 数据。这似乎是一种优化:毕竟,如果我们已经接触到 LOB 数据,我们不妨趁着还手把它删除掉。将它推迟到幽灵清理 (GC) 只会增加开销,因为 GC 还需要遍历链。此外,日志记录也减少了,因为 SQL Server 只需要记录解除分配,而不是完整的 LOB 内容。

如果(例如)表上有触发器,或者如果启用了行版本控制隔离级别,则LOB 删除操作将推迟到GC。这两个功能都使用行版本,必须在 LOB 行上维护行版本。在这些情况下,LOB 删除被完全记录。当 GC 稍后运行时,它会执行 LOB 页的释放。可以使用(记录在案的)全局跟踪标志 661暂时禁用 GC ,如果您想自己测试,可以查看这些幻影 LOB 记录。

鉴于所有这些,很难看出(完全)记录删除并推迟重复已经完成的大部分相同工作到 GC 会对您有所帮助。在删除期间记录更少并删除 LOB 数据可能会更快。


除了存储系统不堪重负/指定不足的可能性之外,问题中没有足够的内容来推测您删除性能不佳的确切原因,但我可以说,总的来说,最好是:

  1. 避免在读未提交隔离下访问 LOB 数据
  2. 执行删除时避免在包含 LOB 数据的表上使用触发器
  3. 注意行版本控制的副作用
  4. 确保存储子系统足以进行日志记录和常规 I/O
  5. 运行最新版本的 SQL Server(SQL Server 2012 当前为 11.00.6523)

有关第 1 点的更多信息,请参阅Paul Randal 的性能错误:涉及行外 LOB 数据NOLOCK 扫描。有关第 2 点和第 3 点的更多信息,请参阅删除拆分页面和转发的重影(由我撰写)。

同样可能是简化的示例遗漏了一个关键细节,或者表变量产生了低效的执行计划,或者您的表已经有数十亿条幽灵 LOB 记录。详细的分析可能需要访问系统本身。