【问题标题】:SQL Server query optimization for a example update query示例更新查询的 SQL Server 查询优化
【发布时间】:2018-03-12 00:39:02
【问题描述】:

在我的交易表中,我有大约 9000 万条记录。一列“时间”的格式如下所示。这种格式没有维护顺序,并且在“时间”列中非常随机。

 Time
----------
23:44:33
12:17 09
20 00 20
  :  :  
111913

我想将这个时间格式设置/更新为:

    Time
   --------
   23:44:33
   12:17:09
   20:00:20
   21:12:00  
   11:19:13

我已经写了下面的查询来更新“时间”以获得我想要的结果。

    Update [dbo].[table] 
   set [TIME] = '21:14:00'
   WHERE [TIME] = '  :  :  '


   Update [dbo].[table]
   set [time] = replace([TIME], ' ','')  
   WHERE [time]   like '[0-9][0-9] [0-9][0-9] [0-9][0-9]'

      Update [dbo].[table]
      set [time] = STUFF(STUFF([TIME],3,0,':'),6,0,':')
      WHERE [time] like '[0-9][0-9][0-9][0-9][0-9][0-9]'

Update [dbo].[table]
    set [time] = replace([time], ' ', ':')
    WHERE [time] like '[0-9][0-9]:[0-9][0-9] [0-9][0-9]'

上面的查询一直在执行,耗时很长。

有什么方法可以优化它以获得所需的结果?

请提供一个更好的主意。谢谢。

【问题讨论】:

    标签: sql-server tsql sql-server-2008-r2 sql-server-2016


    【解决方案1】:

    不要试图找出所有不同的变化...只需取出所有不符合所需模式的值并去掉所有非数字字符,留下 6 个数字,所有数字都在正确的位置。在那里形成,只需使用 STUFF 函数将冒号放在它们所属的位置。

    SET NOCOUNT ON;
    
    IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL 
    DROP TABLE #TestData;
    
    CREATE TABLE #TestData (
        TimeVal VARCHAR(20)
        );
    INSERT #TestData (TimeVal) VALUES
        ('23:44:33'),
        ('12:17 09'),
        ('20 00 20'),
        ('  :  :  '),
        ('111913'),
        ('12:17'),
        ('12:   09'),
        ('  :17 09');
    
    -- before values...
    SELECT [Before] = td.TimeVal FROM #TestData td;
    
    -- update problem values...
    UPDATE td SET  
        td.TimeVal = CASE LEN(rr.TimeVal)
                        WHEN 6 THEN STUFF(STUFF(rr.TimeVal, 5, 0, ':'), 3, 0, ':')
                        ELSE '21:12:00'
                    END
    FROM
        #TestData td
        CROSS APPLY ( VALUES (REPLACE(REPLACE(td.TimeVal, ' ', ''), ':', '')) ) rr (TimeVal)
    WHERE 
        td.TimeVal NOT LIKE '[0-9][0-9]:[0-9][0-9]:[0-9][0-9]';
    
    -- after values...
    SELECT [After] = td.TimeVal FROM #TestData td;
    

    结果前后...

    Before
    --------------------
    23:44:33
    12:17 09
    20 00 20
      :  :  
    111913
    12:17
    12:   09
      :17 09
    
    After
    --------------------
    23:44:33
    12:17:09
    20:00:20
    21:12:00
    11:19:13
    21:12:00
    21:12:00
    21:12:00
    

    至于如何最好地针对 90M 行执行此操作...很难说如果不了解您的环境的更多信息。什么是恢复模式?你的服务器规格是什么?非聚集索引如何包含此列? 和你一起去 DBA,他/她会比任何人都知道数据库将如何处理更新......另外,如果你通过填充日志文件和/或 tempdb 锁定实例,他们会为你而来驱动器。你希望他们参与进来。

    也就是说,我会同意其他人建议将其分成几块的观点。当我必须对大型表进行大规模更新时,我使用类似的循环过程并将进度记录到一个简单的日志表中,以便 1)我可以在进度进行时跟踪进度,并且 2)我知道最后提交的事务集如果我需要关闭它,稍后再关闭。

    类似下面的...

    -- create a log table to make it easy to know where you arw in the update process.
    CREATE TABLE dbo.TimeValUpdate_LOG (
        BegID INT,
        EndID INT,
        RowsUpdated INT,
        BegTime DATETIME,
        EndTime DATETIME,
        SecsToComplete AS DATEDIFF(SECOND, BegTime, EndTime)
        );
    
    -- update script...
    DECLARE 
        @BegID INT = 0,
        @EndID INT = 500000,
        @BegTime DATETIME,
        @EndTime DATETIME;
    
    
    WHILE EXISTS (SELECT 1 FROM dbo.RealTable rt WHERE rt.PrimaryKey  > @BegID)
    BEGIN
        BEGIN TRY
            BEGIN TRANSACTION; 
            --===================================
            SET @BegTime = CURRENT_TIMESTAMP;
            INSERT dbo.TimeValUpdate_LOG (BegID, EndID, BegTime) VALUES (@BegID, @EndID, @BegTime);
            --=============================================================================
    
                -- update problem values...
                UPDATE rt SET  
                    rt.TimeVal = CASE LEN(rr.TimeVal)
                                    WHEN 6 THEN STUFF(STUFF(rr.TimeVal, 5, 0, ':'), 3, 0, ':')
                                    ELSE '21:12:00'
                                END
                FROM
                    dbo.RealTable rt
                    CROSS APPLY ( VALUES (REPLACE(REPLACE(rt.TimeVal, ' ', ''), ':', '')) ) rr (TimeVal)
                WHERE 
                    rt.PrimaryKey >= @BegID
                    AND rt.PrimaryKey < @EndID
                    AND rt.TimeVal NOT LIKE '[0-9][0-9]:[0-9][0-9]:[0-9][0-9]';
    
            --=============================================================================
            UPDATE tul SET
                tul.RowsUpdated = @@ROWCOUNT,
                tul.EndTime = CURRENT_TIMESTAMP
            FROM 
                dbo.TimeValUpdate_LOG tul
            WHERE 
                tul.BegID = @BegID
                AND tul.EndID = @EndID;
            --===================================
            SET @BegID = @EndID + 1;
            SET @EndID = @BegID + 500000;
            --===================================
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
    
                DECLARE @ErrorNumber INT = ERROR_NUMBER();
                DECLARE @ErrorLine INT = ERROR_LINE();
                DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
                DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
                DECLARE @ErrorState INT = ERROR_STATE();
    
                PRINT 'Actual error number: ' + CAST(@ErrorNumber AS VARCHAR(10));
                PRINT 'Actual line number: ' + CAST(@ErrorLine AS VARCHAR(10));
    
                RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
        END CATCH;
    END;
    

    【讨论】:

    • 哪里有 : : 我想将默认值更新为 21:12:00(不是空白)。另外,您能否对交叉应用做一个简单的解释 - 它是如何工作的?
    • 交叉应用获取时间值,从中去除空格和冒号,并将其与#TestData 表中的每一行相匹配。所以你有两列,一个是未修改的,一个是被剥离的。我只建议您将 ELSE '' 更改为 ELSE '21:12:00' 并使用默认值。如果您要在超过 9000 万行中执行此操作,我会分块执行,以避免表锁定和日志文件爆炸。 @lad2025 上面有一个很好的模型。
    • @AskMe - 您是在问 CROSS APPLY 正在做什么或 CROSS APPLY 操作员一般如何工作?如果是前者,JM_7 是完全正确的。它正在从字符串中删除空格和分号。如果您的其余数据具有其他非 int 字符,则应将其扩展以去除这些字符。如果再晚一点,对于几百个字符来说,这个话题就太大了……这里有一些来自 Itzik Ben-Gan 的非常好的视频。真的很值得花时间看他们youtu.be/-m426WYclz8 & youtu.be/goyWzAu-AA0
    • @AskMe - Mt 答案已更新为允许使用默认值,并且我添加了与大型表的大规模更新相关的 cmets 和代码。
    【解决方案2】:

    我不建议一次性完成,而是:

    DECLARE @r INT;
    
    WHILE @r > 0
    BEGIN
       Update TOP (50000) [dbo].[table]
       set [time] = replace([TIME], ' ','')  
       WHERE [time]   like '[0-9][0-9] [0-9][0-9] [0-9][0-9]';
    
       SET @r = @@ROWCOUNT;
       -- when there is no rows left @@ROWCOUNT will be 0
    END;
    

    我还会检查数据库recovery model 并控制事务日志的增长。


    在更新前添加索引可能会有所帮助:

    CREATE NONCLUSTERED INDEX IDX_dbo_table_time
      ON [dbo].[table] ([time] ASC) 
      WHERE [time] like '[0-9][0-9] [0-9][0-9] [0-9][0-9]';
    

    【讨论】:

    • 你只考虑了时间格式:[time] like '[0-9][0-9] [0-9][0-9] [0-9][0-9 ]' 。其他时间格式呢?我应该单独做每个时间格式吗?此外,当我们执行 SET @r = @@ROWCOUNT; 时,看起来 while 循环会一次又一次地运行。一旦每一行都使用该条件更新,@@ROWCOUNT 是否会为 0?您对此有何看法?
    • @AskMe 当然,您必须为所需的每种时间格式都这样做。所以你最终会得到 3 个循环和 3 个支持索引。至于 ROWCOUNT 之后将没有要更新的行,它将为 0 并且循环将结束。 Demo
    【解决方案3】:

    您提到查询正在持续执行,因此我了解您正在更正从另一个应用程序插入的数据,您无法更改(如果它只是一次性工作,那么请遵循任何其他答案,他们有很好的建议为此)。

    在这种情况下,您可以使用触发器,以便以正确的格式插入记录,我想该表有一个主键/唯一键,而仅记录事务的表可能不是这种情况:

    create trigger trCorrectTime on [table] after insert
    as
        update [table] set [time] = '21:14:00'
        from [table]
        join inserted on inserted.id=[table].id
        where inserted.[TIME] = '  :  :  '
    
        update [table] set [time] = STUFF(STUFF([table].[time],3,0,':'),6,0,':')
        from [table]
        join inserted on inserted.id=[table].id
        where inserted.[time] like '[0-9][0-9][0-9][0-9][0-9][0-9]'
    
        update [table] set [time] = replace([table].[time], ' ',':')  
        from [table]
        join inserted on inserted.id=[table].id
        where inserted.[time] like '[0-9][0-9]_[0-9][0-9]_[0-9][0-9]'
    go
    

    id 上有一个适当的索引(并假设键是一个简单的索引,例如int 值),这应该几乎不会为插入操作增加任何时间,因为您只使用插入的记录,但当然你应该测试它,特别是如果插入事务是时间紧迫的。

    【讨论】:

      猜你喜欢
      • 2022-01-24
      • 1970-01-01
      • 2013-08-11
      • 1970-01-01
      • 1970-01-01
      • 2013-10-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多