为什么 SQL 服务器上 varbinary(max) 的更新语句这么慢?
Why is this UPDATE statement on varbinary(max) on SQL Server so slow?
我有一个只有 500 行的 table S
和一个有 120000 行的 table F
。两者都使用 GUID 主键并且 table F
持有 table S
的外键。 Table F
包含一个 varbinary(max)
列 F.Data
,每行约 100 KB(数据库总大小约为 10 GB)。文件流已打开。我正在使用 SQL Server 2014 Express。
当我执行以下 UPDATE 语句时(在 SQL Server Management Studio 中),它会影响大约 100000 行
UPDATE F
SET F.Data = 0
FROM F
INNER JOIN S
ON S.SID = F.SID
WHERE S.BITFIELD = 1 AND S.Date < DATEADD(DAY,-90,GETDATE())
查询大约需要 30 分钟。那是 unacceptable 但我对 SQL 的了解还不够,无法知道为什么或如何使此查询更有效。有哪位大师可以提供帮助吗?
仅供参考,等效的 SELECT 语句只需要几秒钟。我在 Whosebug 和其他地方进行了搜索,但没有发现任何特别有用的东西(鉴于我对 SQL 的了解有限)。
我看到三件事可以在这里解决:
你没有提到 select 相当于 return 语句需要多少秒10) 您可能希望使用变量作为日期而不是 运行 DATEADD 函数 100k 次。其语法为:
DECLARE @MyDate as DATETIME = DATEADD(DAY,-90,GETDATE());
UPDATE F
SET F.Data = 0
FROM F
INNER JOIN S
ON S.SID = F.SID
WHERE S.BITFIELD = 1 AND S.Date < @MyDate
您可以选择分块进行更新,例如 10k 行;这不会锁定那么多,而且可能 return 更快。
- 我要检查的另一件事是 table F 上的索引数量。当你 selecting 时,优化器会决定使用哪个索引,你会完成,而在更新中,所有包含受影响字段的索引也需要更新。
评论:作为 PK 的 GUID 在这里对性能没有帮助。如果您有大量非聚集索引,则 GUID 问题会加剧。
您是否尝试过创建一个只有一个字段 (S.SID) 的临时文件 table 以及与 WHERE S.Date < DATEADD(DAY,-90,GETDATE) 匹配的所有记录())
然后在你的更新中加入它,而不是在更新期间在 where 子句中计算?
此外,GUID 上的索引可能不如 INT 上使用索引好。读这个 GUID vs INT IDENTITY
祝你好运。
像这样:
CREATE TABLE [#TEMPTBL1]([SID] uniqueidentifier);
CREATE CLUSTERED INDEX IDX_TEMPTBL1_SID ON [#TEMPTBL1]([SID]);
INSERT INTO [#TEMPTBL1]([SID])
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
UPDATE F
SET F.Data = 0
FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
DROP TABLE #TEMPTBL1;
------------带计数器的代码更新--------
DECLARE @updtCounter int = 0;
CREATE TABLE [#TEMPTBL1]([SID] uniqueidentifier);
CREATE CLUSTERED INDEX IDX_TEMPTBL1_SID ON [#TEMPTBL1]([SID]);
INSERT INTO [#TEMPTBL1]([SID])
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
SELECT @updtCounter = count(*) FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
UPDATE TOP (@updtCounter) F
SET F.Data = 0
FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
DROP TABLE #TEMPTBL1;
更多建议:
1. 这可能是一种罕见的情况,游标可以通过将 UPDATE 分成更小的块来提高性能。您提到 table S 有 500 行,table F 有 120K 行,因此如果它们大致均匀分布,则 S 中的每一行在 F 中有 240 行。
Declare @SID uniqueidentifier;
Declare c cursor forward_only for
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
Fetch next from c into @SID
While @@fetch_status = 0
Begin
UPDATE F
SET F.Data = 0
FROM F
WHERE F.SID = @SID
Fetch next from c into @SID
End
Deallocate c
此外,在 Update
.
周围使用 Begin Trans
和 Commit
可能会获得更好的性能
根据 table S 中的记录将 BitField 设置为 1 的频率,如果不是很频繁,您可以将更新放入 trigger
。
另一种方法可能是select仅当未设置 S 中的 BitField 时来自 F 的数据:
Select CASE WHEN S.BitField=1 THEN 0 ELSE F.Data END as Data
FROM F
INNER JOIN S
ON S.SID = F.SID
select 语句旨在使 F.Data 在 S 中的 BitField 设置为 1 时看起来包含 0。您可以将 Select 放入视图中,然后在其他查询中访问 F 时使用视图而不是 table。即使 F.Data 字段仍包含 100KB 值,任何时候您从视图中 select,它都会将 F.Data 显示为 0 或实际值,具体取决于 S.BitField。如果您需要减少正在使用的磁盘 space,您仍然需要执行更新,但您可以将其安排在系统未使用的时间。
我有一个只有 500 行的 table S
和一个有 120000 行的 table F
。两者都使用 GUID 主键并且 table F
持有 table S
的外键。 Table F
包含一个 varbinary(max)
列 F.Data
,每行约 100 KB(数据库总大小约为 10 GB)。文件流已打开。我正在使用 SQL Server 2014 Express。
当我执行以下 UPDATE 语句时(在 SQL Server Management Studio 中),它会影响大约 100000 行
UPDATE F
SET F.Data = 0
FROM F
INNER JOIN S
ON S.SID = F.SID
WHERE S.BITFIELD = 1 AND S.Date < DATEADD(DAY,-90,GETDATE())
查询大约需要 30 分钟。那是 unacceptable 但我对 SQL 的了解还不够,无法知道为什么或如何使此查询更有效。有哪位大师可以提供帮助吗?
仅供参考,等效的 SELECT 语句只需要几秒钟。我在 Whosebug 和其他地方进行了搜索,但没有发现任何特别有用的东西(鉴于我对 SQL 的了解有限)。
我看到三件事可以在这里解决:
你没有提到 select 相当于 return 语句需要多少秒10) 您可能希望使用变量作为日期而不是 运行 DATEADD 函数 100k 次。其语法为:
DECLARE @MyDate as DATETIME = DATEADD(DAY,-90,GETDATE()); UPDATE F SET F.Data = 0 FROM F INNER JOIN S ON S.SID = F.SID WHERE S.BITFIELD = 1 AND S.Date < @MyDate
您可以选择分块进行更新,例如 10k 行;这不会锁定那么多,而且可能 return 更快。
- 我要检查的另一件事是 table F 上的索引数量。当你 selecting 时,优化器会决定使用哪个索引,你会完成,而在更新中,所有包含受影响字段的索引也需要更新。
评论:作为 PK 的 GUID 在这里对性能没有帮助。如果您有大量非聚集索引,则 GUID 问题会加剧。
您是否尝试过创建一个只有一个字段 (S.SID) 的临时文件 table 以及与 WHERE S.Date < DATEADD(DAY,-90,GETDATE) 匹配的所有记录()) 然后在你的更新中加入它,而不是在更新期间在 where 子句中计算?
此外,GUID 上的索引可能不如 INT 上使用索引好。读这个 GUID vs INT IDENTITY 祝你好运。
像这样:
CREATE TABLE [#TEMPTBL1]([SID] uniqueidentifier);
CREATE CLUSTERED INDEX IDX_TEMPTBL1_SID ON [#TEMPTBL1]([SID]);
INSERT INTO [#TEMPTBL1]([SID])
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
UPDATE F
SET F.Data = 0
FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
DROP TABLE #TEMPTBL1;
------------带计数器的代码更新--------
DECLARE @updtCounter int = 0;
CREATE TABLE [#TEMPTBL1]([SID] uniqueidentifier);
CREATE CLUSTERED INDEX IDX_TEMPTBL1_SID ON [#TEMPTBL1]([SID]);
INSERT INTO [#TEMPTBL1]([SID])
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
SELECT @updtCounter = count(*) FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
UPDATE TOP (@updtCounter) F
SET F.Data = 0
FROM F
INNER JOIN #TEMPTBL1 TMP ON F.SID = TMP.SID
DROP TABLE #TEMPTBL1;
更多建议: 1. 这可能是一种罕见的情况,游标可以通过将 UPDATE 分成更小的块来提高性能。您提到 table S 有 500 行,table F 有 120K 行,因此如果它们大致均匀分布,则 S 中的每一行在 F 中有 240 行。
Declare @SID uniqueidentifier;
Declare c cursor forward_only for
SELECT ([SID]) FROM S
WHERE S.BITFIELD = 1
AND S.Date < DATEADD(DAY,-90,GETDATE());
Fetch next from c into @SID
While @@fetch_status = 0
Begin
UPDATE F
SET F.Data = 0
FROM F
WHERE F.SID = @SID
Fetch next from c into @SID
End
Deallocate c
此外,在
Update
. 周围使用 根据 table S 中的记录将 BitField 设置为 1 的频率,如果不是很频繁,您可以将更新放入
trigger
。另一种方法可能是select仅当未设置 S 中的 BitField 时来自 F 的数据:
Begin Trans
和 Commit
可能会获得更好的性能
Select CASE WHEN S.BitField=1 THEN 0 ELSE F.Data END as Data
FROM F
INNER JOIN S
ON S.SID = F.SID
select 语句旨在使 F.Data 在 S 中的 BitField 设置为 1 时看起来包含 0。您可以将 Select 放入视图中,然后在其他查询中访问 F 时使用视图而不是 table。即使 F.Data 字段仍包含 100KB 值,任何时候您从视图中 select,它都会将 F.Data 显示为 0 或实际值,具体取决于 S.BitField。如果您需要减少正在使用的磁盘 space,您仍然需要执行更新,但您可以将其安排在系统未使用的时间。