SQL 更新语句需要很长时间/几个小时的高磁盘使用率

GFK*_*GFK 8 sql-server ssms sql-server-2008-r2

是的,这听起来像是一个非常普遍的问题,但我还没有能够缩小范围。

所以我在 sql 批处理文件中有一个 UPDATE 语句:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
Run Code Online (Sandbox Code Playgroud)

B 有 40k 条记录,A 有 4M 条记录,它们通过 A.B_ID 一对一关联,尽管两者之间没有 FK。

所以基本上我正在为数据挖掘目的预先计算一个字段。这个题虽然改了表名,但是语句没改,真的就是这么简单。

这需要几个小时才能运行,所以我决定取消一切。数据库损坏了,所以我删除了它,恢复了我在运行该语句之前所做的备份,并决定使用游标更深入地了解细节:

DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB 
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id

WHILE @@FETCH_STATUS = 0
BEGIN
    DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
    RAISERROR(@Msg, 10, 1) WITH NOWAIT

    UPDATE A
    SET A.X = B.X
    FROM A JOIN B ON A.B_ID = B.ID
    WHERE B.ID = @Id

    FETCH NEXT FROM CursorB INTO @Id
END
Run Code Online (Sandbox Code Playgroud)

现在我可以看到它运行时带有 id 降序的消息。发生的情况是从 id=40k 到 id=13 大约需要 5 分钟

然后在 id 13 处,出于某种原因,它似乎挂了。除了 SSMS 之外,DB 与它没有任何连接,但实际上并没有挂起:

  • 硬盘驱动器连续运行,所以它肯定在做一些事情(我在进程资源管理器中检查它确实是使用它的 sqlserver.exe 进程)
  • 我运行 sp_who2,找到 SUSPENDED 会话的 SPID (70),然后运行以下脚本:

    select * from sys.dm_exec_requests r join sys.dm_os_tasks t on r.session_id = t.session_id where r.session_id = 70

这给了我wait_type,大部分时间是PAGEIOLATCH_SH,但有时实际上会更改为WRITE_COMPLETION,我猜这是在刷新日志时发生的

  • 日志文件,当我恢复数据库时是 1.6GB(当它到达 id 13 时),现在是 3.5GB

其他可能有用的信息:

  • B_ID 13的A表记录数不大(14)
  • 我的同事在她的机器上没有同样的问题,这个数据库的副本(几个月前)具有相同的结构。
  • 表 A 是迄今为止数据库中最大的表
  • 它有几个索引,并且有几个索引视图使用它。
  • 数据库上没有其他用户,它是本地的,没有应用程序在使用它。
  • LDF 文件的大小没有限制。
  • 恢复模式为 SIMPLE,兼容级别为 100
  • Procmon 没有给我太多信息:sqlserver.exe 正在从 MDF 和 LDF 文件中读取和写入很多内容。

我仍在等待它完成(已经 1 点 30 分),但我希望也许有人会给我一些其他操作,我可以尝试解决此问题。

编辑:从 procmon 日志中添加提取物

15:24:02.0506105    sqlservr.exe    1760    ReadFile    C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF    SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal
Run Code Online (Sandbox Code Playgroud)

从使用 DBCC PAGE 来看,它似乎正在读取和写入看起来像表 A(或其索引之一)的字段,但对于不同的 B_ID 13。重建索引可能吗?

编辑2:执行计划

所以我取消了查询(实际上是删除了数据库及其文件然后恢复了它),并检查了执行计划:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13
Run Code Online (Sandbox Code Playgroud)

(估计的)执行计划与任何 B.ID 的执行计划相同,看起来相当简单。WHERE 子句在 B 的非聚集索引上使用索引查找,JOIN 在表的两个 PK 上使用聚集索引查找。A 上的聚集索引查找使用并行性 (x7) 并代表 90% 的 CPU 时间。

更重要的是,实际执行 ID 为 13 的查询是立即的。

编辑 3:索引碎片

索引的结构如下:

B 有一个聚集 PK(不是 ID 字段)和一个非聚集唯一索引,第一个字段是 B.ID - 这个第二个索引似乎总是被使用。

A 有一个聚集的 PK(字段不相关)。

A 上还有 7 个视图(都包括 AX 字段),每个视图都有自己的聚集 PK,另一个索引也包括 AX 字段

视图被过滤(带有不在这个等式中的字段),所以我怀疑 UPDATE A 是否会使用视图本身。但是他们确实有一个包含 AX 的索引,因此更改 AX 意味着编写包含该字段的 7 个视图和 7 个索引。

尽管预计 UPDATE 为此会更慢,但没有理由为什么特定 ID 会比其他 ID 长得多。

我检查了所有索引的碎片,所有索引都在 <0.1%,除了视图的二级索引,都在 25% 到 50% 之间。所有索引的填充因子似乎都在 90% 到 95% 之间。

我重新组织了所有二级索引,并重新运行了我的脚本。

它仍然被绞死,但在不同的点:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)
Run Code Online (Sandbox Code Playgroud)

而以前,消息日志如下所示:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

        Updating A for B_ID=13
Run Code Online (Sandbox Code Playgroud)

这很奇怪,因为这意味着它甚至没有挂在WHILE循环中的同一点。其余部分看起来相同:在 sp_who2 中等待的相同 UPDATE 行、相同的 PAGEIOLATCH_EX 等待类型以及 sqlserver.exe 中相同的大量 HD 使用。

下一步是删除所有索引和视图并重新创建我认为。

编辑 4:删除然后重建索引

因此,我删除了表上的所有索引视图(其中 7 个,每个视图 2 个索引,包括聚集的一个)。我运行了初始脚本(没有光标),它实际上运行了 5 分钟。

所以我的问题源于这些索引的存在。

运行更新后,我重新创建了索引,花了 16 分钟。

现在我明白索引需要时间来重建,我实际上对需要 20 分钟的完整任务没问题。

我仍然不明白的是,为什么当我在不先删除索引的情况下运行更新时,需要几个小时,但是当我先删除它们然后重新创建它们时,需要 20 分钟。两种方式不应该花费大约相同的时间吗?

boj*_*jan 0

  1. 坚持使用 UPDATE 命令。对于您尝试执行的操作,光标会变慢。
  2. 删除/禁用所有索引,包括索引视图的索引。如果 AX 上有外键,请将其删除。
  3. 创建仅包含 A.B_ID 的索引,并为 B.ID 创建另一个索引。
  4. 即使您使用简单恢复模型,最后一个事务在刷新到磁盘之前将始终位于事务日志中。这就是为什么您需要预先增加事务日志并将其设置为更大的数量(例如 100 MB)。
  5. 另外,将数据文件增长设置为更大的数量。
  6. 确保有足够的磁盘空间用于日志和数据文件的进一步增长。
  7. 更新完成后,重新创建/启用您在步骤 2 中删除/禁用的索引。
  8. 如果您不再需要它们,请删除在步骤 3 中创建的索引。

编辑: 由于我无法评论您的原始帖子,我将在这里回答您在编辑 4 中提出的问题。您在 AX Index 上有 7 个索引是B 树,对该字段的每次更新都会导致 B 树重新平衡。从头开始重建这些索引比每次重新平衡要快。