【问题标题】:Copy one column to another for over a billion rows in SQL Server database在 SQL Server 数据库中将一列复制到另一列超过十亿行
【发布时间】:2011-04-15 22:01:02
【问题描述】:

数据库:SQL Server 2005

问题:将值从一列复制到同一个表中的另一列,十亿以上 行。

test_table (int id, bigint bigid)

尝试1:更新查询

update test_table set bigid = id 

填满事务日志并由于事务日志空间不足而回滚。

尝试 2 - 以下几行的过程

set nocount on
set rowcount = 500000
while @rowcount > 0
begin
 update test_table set bigid = id where bigid is null
 set @rowcount = @@rowcount
 set @rowupdated = @rowsupdated + @rowcount
end
print @rowsupdated

上述过程在进行时开始变慢。

尝试 3 - 创建用于更新的游标。

在 SQL Server 文档中通常不鼓励这种方法,这种方法一次更新一行,这太耗时了。

是否有一种方法可以加快将值从一列复制到另一列的速度。基本上,我正在寻找一些“神奇”的关键字或逻辑,它们将允许更新查询按顺序一次遍历十亿行。

任何提示,指针将不胜感激。

【问题讨论】:

  • 为什么要表中的两列具有相同的值?也许还有另一种方法可以解决您的问题
  • 我猜他已经接近 INT 数据类型在升序人工键上的 21 亿个限制,他正试图将其更改为 BIGINT。他可能已经发现你不能轻易改变:)
  • 是的,迈克,布拉德的猜测是正确的。 @BradC 读心术 :-)

标签: sql sql-server sql-server-2005 tsql large-data-volumes


【解决方案1】:

我猜你正在接近 INT 数据类型在列的人工键上的 21 亿个限制。是的,这很痛苦。事前修复比实际达到该限制并且在您尝试修复它时关闭生产之后要容易得多:)

无论如何,这里的几个想法都会奏效。不过,让我们谈谈速度、效率、索引和日志大小。

对数增长

日志爆炸最初是因为它试图一次提交所有 2b 行。其他帖子中关于“分块”的建议会起作用,可能无法完全解决日志问题。

如果数据库处于 SIMPLE 模式,您会没事的(日志将在每批之后重新使用自己)。如果数据库处于 FULL 或 BULK_LOGGED 恢复模式,您必须在操作运行期间经常运行日志备份,以便 SQL 可以重用日志空间。这可能意味着在此期间增加备份频率,或者只是在运行时监控日志使用情况。

索引和速度

所有where bigid is null 答案都会随着表格的填充而变慢,因为(可能)新的 BIGID 字段上没有索引。您可以(当然)在 BIGID 上添加一个索引,但我不相信这是正确的答案。

键(双关语)是我假设原始 ID 字段可能是主键或聚集索引,或两者兼而有之。在这种情况下,让我们利用这一事实,对 Jess 的想法做一个变体:

set @counter = 1
while @counter < 2000000000 --or whatever
begin
  update test_table set bigid = id 
  where id between @counter and (@counter + 499999) --BETWEEN is inclusive
  set @counter = @counter + 500000
end

这应该非常快,因为 ID 上已有索引。

ISNULL 检查确实没有必要,我的 (-1) 也没有必要。如果我们在调用之间重复一些行,那没什么大不了的。

【讨论】:

  • 我启动了一个与您的脚本类似的脚本,并进行了一些修改,因为我的 id 从 -2B 到 +2B。我的期望是事务日志不会大规模增长,因为我正在批量更新最多 500K 行。事务日志设置为以 10G 递增到 100G。日志刚刚达到最大值,我看到查询回滚。我什至在 update 语句周围放置了一个 begin tran 和 commit 以确保事务是增量的。或者,开始 tran 和循环提交是否会导致事务日志增长?
  • 看起来我添加的 begin tran 和 commit 导致事务日志填满。从我的脚本中删除了这些,并且该过程已经在一夜之间清晰地翻阅了这些行,而没有填满事务日志:-)
  • @Adi:只要你有正确的语法(BEGIN TRANCOMMIT TRAN),那么把它们放在 inner 循环应该和我的脚本一样(默认情况下,SQL 使用隐式事务,这就是像这样的批处理工作的原因)。我曾经只放了COMMIT 而不是COMMIT TRAN,最后得到了疯狂的嵌套事务,就像你说的那样炸毁了日志。只是不要将事务放在循环的外部
  • 感谢@BradC 接受您的解决方案作为最佳答案,因为它具有最佳的恒定性能并很好地使用了现有索引。只需要稍微修改一下脚本,因为我的字段的值从 -2B 到 +2B 范围,并且还调整了最后一批的计数器值以避免溢出。
【解决方案2】:

我支持 UPDATE TOP(X) 语句

另外建议,如果您处于循环中,请在两者之间添加一些 WAITFOR 延迟或 COMMIT,以允许其他进程有时间在需要时使用该表,而不是在所有更新完成之前永久阻塞

【讨论】:

    【解决方案3】:

    如果有的话,第一步是在操作之前删除索引。这可能是导致速度随时间下降的原因。

    另一个选项,有点跳出框框思考......你能以一种可以在选择中实现列值的方式表达更新吗?如果你能做到这一点,那么你可以使用 SELECT INTO 创建一个新表,这是一个最少记录的操作(假设在 2005 年你设置为 SIMPLE 或 BULK LOGGED 的恢复模式)。这会非常快,然后您可以删除旧表,将此表重命名为旧表名并重新创建任何索引。

    select id, CAST(id as bigint) bigid into test_table_temp from test_table
    drop table test_table
    exec sp_rename 'test_table_temp', 'test_table'
    

    【讨论】:

      【解决方案4】:

      UPDATE statement中使用TOP:

      UPDATE TOP (@row_limit) dbo.test_table
         SET bigid = id 
       WHERE bigid IS NULL
      

      【讨论】:

        【解决方案5】:

        这是一次性的吗?如果是这样,只需按范围进行:

        set counter = 500000
        while @counter < 2000000000 --or whatever your max id
        begin
         update test_table set bigid = id where id between (@counter - 500000) and @counter and bigid is null
         set counter = @counter + 500000
        end
        

        【讨论】:

        • 也将确保这一点。 'bigid 不为 null' 或 'x 和 y 之间的 id' 哪个有效。
        • 对此+1(并查看我的类似答案)。这利用了 ID 上的现有 PK,因此不应随着进程的进展而减慢。 (只要你去掉BIGID IS NULL
        【解决方案6】:

        我没有运行它来尝试它,但如果你能让它一次更新 500k,我认为你正朝着正确的方向前进。

        set rowcount 500000
        update test_table tt1
        set bigid = (SELECT tt2.id FROM test_table tt2 WHERE tt1.id = tt2.id)
        where bigid IS NULL
        

        您也可以尝试更改恢复模式,以便不记录事务

        ALTER DATABASE db1
        SET RECOVERY SIMPLE
        GO
        
        update test_table
        set bigid = id
        GO
        
        ALTER DATABASE db1
        SET RECOVERY FULL
        GO
        

        【讨论】:

        • 不推荐使用连接的子查询。当替代方案 (bigid = id) 如此简单时,在这个巨大的桌子上做太多不必要的工作。
        • “对于针对远程表以及本地和远程分区视图的 INSERT、UPDATE 和 DELETE 语句,将忽略 SET ROWCOUNT 选项的设置。”来自 MSDN。我不认为 rowcount 将与 update 命令一起使用,因此需要 select 才能使 rowcount 起作用。
        • 我们不处理远程表或分区视图,因此该警告不适用。但是,是的,UPDATE TOP x 可能比 SET ROWCOUNT 更受欢迎
        【解决方案7】:

        您可以尝试使用 SET ROWCOUNT 之类的东西并进行批量更新:

        SET ROWCOUNT 5000;
        
        UPDATE dbo.test_table 
        SET bigid = id 
        WHERE bigid IS NULL
        GO
        

        然后根据需要重复多次。

        这样,您可以避免游标和 while 循环的 RBAR(逐行处理)症状,而且不会不必要地填满事务日志。

        当然,在两次运行之间,您必须进行备份(尤其是日志)以将其大小保持在合理的范围内。

        【讨论】:

        • 行计数是否适用于 UPDATE 命令? - msdn.microsoft.com/en-us/library/ms188774.aspx
        • 是的,它在 SQL 2008 之前(包括 SQL 2008)都可以工作,在 SQL vNext 中不起作用。但你是对的,UPDATE TOP 5000 ... 是 SQL 2005 及更高版本的首选语法。见msdn.microsoft.com/en-us/library/ms177523.aspx
        • 喜欢 RBAR 的首字母缩略词。我得偷那个。
        • @marc_s 是的,我将 R2 包含在“2008”的保护伞下。他们不会在主要编号版本之间弃用功能。
        • 为什么这在 SQL vNext (2011/2012/whenever) 中不起作用???加上 vNext 将是一个主要编号的版本......这意味着,根据你后来的评论,这个“功能”不会被弃用。
        猜你喜欢
        • 2018-01-13
        • 1970-01-01
        • 1970-01-01
        • 2010-09-16
        • 2012-05-29
        • 1970-01-01
        • 2016-04-16
        • 1970-01-01
        相关资源
        最近更新 更多