为什么 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 的了解有限)。

我看到三件事可以在这里解决:

  1. 你没有提到 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
    
  2. 您可以选择分块进行更新,例如 10k 行;这不会锁定那么多,而且可能 return 更快。

  3. 我要检查的另一件事是 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
  1. 此外,在 Update.

  2. 周围使用 Begin TransCommit 可能会获得更好的性能
  3. 根据 table S 中的记录将 BitField 设置为 1 的频率,如果不是很频繁,您可以将更新放入 trigger

  4. 另一种方法可能是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,您仍然需要执行更新,但您可以将其安排在系统未使用的时间。