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 与它没有任何连接,但实际上并没有挂起:
我运行 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 点 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 分钟。两种方式不应该花费大约相同的时间吗?
编辑: 由于我无法评论您的原始帖子,我将在这里回答您在编辑 4 中提出的问题。您在 AX Index 上有 7 个索引是B 树,对该字段的每次更新都会导致 B 树重新平衡。从头开始重建这些索引比每次重新平衡要快。