是的,这听起来像是一个非常笼统的问题,但是我还不能将其范围缩小很多。
所以我在sql批处理文件中有一个UPDATE语句:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
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
现在,我可以看到它正在运行,并且带有ID降序的消息。从id = 40k到id = 13大约需要5分钟
然后在ID 13,由于某种原因,它似乎挂起了。除了SSMS之外,该数据库没有任何连接,但实际上并未挂起:
- 硬盘驱动器连续运行,因此肯定是在做某事(我在Process Explorer中检查了它确实是使用它的sqlserver.exe进程)
我运行了sp_who2,找到了SUSPENDED会话的SPID(70),然后运行了以下脚本:
从sys.dm_exec_requests中选择* r在r.session_id = t.session_id上加入sys.dm_os_tasks,其中r.session_id = 70
这给了我wait_type,在大多数情况下是PAGEIOLATCH_SH,但实际上有时更改为WRITE_COMPLETION,我猜这是在刷新日志时发生的
- 日志文件(当我还原数据库时(和ID为13时)为1.6GB)现在为3.5GB
其他可能有用的信息:
- 表A中B_ID 13的记录数不大(14)
- 我的同事在她的计算机上没有相同的问题,该数据库的副本(来自几个月前)具有相同的结构。
- 表A是迄今为止数据库中最大的表
- 它具有多个索引,并且几个索引视图都使用它。
- 数据库上没有其他用户,它是本地用户,没有应用程序在使用它。
- LDF文件的大小不受限制。
- 恢复模型为SIMPLE,兼容性级别为100
- Procmon没有给我太多信息:sqlserver.exe从MDF和LDF文件中读取和写入大量内容。
我仍在等待它完成(已经1h30了),但我希望也许有人会给我一些其他操作,我可以尝试解决此问题。
编辑:从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
通过使用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
(估计的)执行计划与任何B.ID相同,并且看起来相当简单。WHERE子句在B的非聚集索引上使用索引查找,JOIN在表的两个PK上使用聚集索引查找。A上的聚集索引查找使用并行度(x7),占CPU时间的90%。
更重要的是,实际上是立即执行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)
以前,消息日志如下所示:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Updating A for B_ID=13
这很奇怪,因为这意味着它甚至没有挂在WHILE
循环中的同一点。其余的看起来相同:sp_who2中有相同的UPDATE行等待,sqlserver.exe中有相同的PAGEIOLATCH_EX等待类型和相同的HD使用量。
我认为,下一步是删除所有索引和视图,然后重新创建它们。
编辑4:删除然后重建索引
因此,我删除了表上的所有索引视图(其中有7个,每个视图2个索引,包括聚簇的1个)。我运行了初始脚本(没有光标),并且实际上在5分钟内运行了。
所以我的问题源于这些索引的存在。
运行更新后,我重新创建了索引,这花了16分钟。
现在,我知道索引需要花费一些时间来重建,而完成20分钟的任务实际上是可以的。
我仍然不明白,为什么当我不首先删除索引而运行更新时需要花费几个小时,但是当我首先删除它们然后重新创建它们时却需要20分钟。两种方式都应该花费大约同一时间吗?
DBCC PAGE
来查看正在写入的内容。