更新 1.2 亿条记录的最快方法
Posted
技术标签:
【中文标题】更新 1.2 亿条记录的最快方法【英文标题】:Fastest way to update 120 Million records 【发布时间】:2010-09-14 17:08:42 【问题描述】:我需要在 1.2 亿条记录表中初始化一个值为 -1 的新字段。
Update table
set int_field = -1;
我让它运行了 5 个小时才取消它。
我尝试在事务级别设置为读取未提交的情况下运行它,结果相同。
Recovery Model = Simple.
MS SQL Server 2005
关于更快完成这项工作的任何建议?
【问题讨论】:
问题是基于 IO 的。 Remus 在这里解决了这个问题:***.com/questions/3523831/… 我担心它可能是基于 IO 的。可悲的是,我对此无能为力。较小的增量批次会运行得更快吗? 不。整体数据量将是相同的:更新页面和日志页面。它仍然是 120M 行,这些行实际上被写入了两次(一次写入日志,一次写入后备存储)。所以时间应该与你的桌子的整体大小成正比。如果它是一个具有单个 4 字节 int 的表,那么要写入的数据大约为 1G(120 * 4 = 480,*2 - 960)。那么,整张桌子有多大? @Will Hartung,我强烈反对。 你能发布表格的架构吗?我对每个列的数量、数据类型和大小感到非常好奇。 【参考方案1】:更新包含 120M 记录的表的唯一合理方法是使用填充 second 表的 SELECT
语句。执行此操作时必须小心。说明如下。
简单案例
对于没有聚集索引的表,在没有并发 DML 的时间内:
SELECT *, new_col = 1 INTO clone.BaseTable FROM dbo.BaseTable
在新表上重新创建索引、约束等
使用 ALTER SCHEMA ... TRANSFER 切换新旧版本。
删除旧表
如果您无法创建克隆架构,则可以使用同一架构中的不同表名。请记住在切换后重命名所有约束和触发器(如果适用)。
非简单案例
首先,在不同的架构下重新创建具有相同名称的BaseTable
,例如clone.BaseTable
。使用单独的架构将简化以后的重命名过程。
然后,用 1000 行测试您的插入:
-- assuming an IDENTITY column in BaseTable
SET IDENTITY_INSERT clone.BaseTable ON
GO
INSERT clone.BaseTable WITH (TABLOCK) (Col1, Col2, Col3)
SELECT TOP 1000 Col1, Col2, Col3 = -1
FROM dbo.BaseTable
GO
SET IDENTITY_INSERT clone.BaseTable OFF
检查结果。如果一切都按顺序显示:
截断克隆表 确保数据库采用批量记录或简单恢复模式 执行完整插入。这需要一段时间,但不会像更新那么长。完成后,检查克隆表中的数据,确保一切正确。
然后,重新创建所有非集群主键/唯一约束/索引和外键约束(按此顺序)。如果适用,重新创建默认和检查约束。重新创建所有触发器。在单独的批次中重新创建每个约束、索引或触发器。例如:
ALTER TABLE clone.BaseTable ADD CONSTRAINT UQ_BaseTable UNIQUE (Col2)
GO
-- next constraint/index/trigger definition here
最后,将 dbo.BaseTable
移动到备份架构,将 clone.BaseTable
移动到 dbo 架构(或您的表应该存在的任何位置)。
-- -- perform first true-up operation here, if necessary
-- EXEC clone.BaseTable_TrueUp
-- GO
-- -- create a backup schema, if necessary
-- CREATE SCHEMA backup_20100914
-- GO
BEGIN TRY
BEGIN TRANSACTION
ALTER SCHEMA backup_20100914 TRANSFER dbo.BaseTable
-- -- perform second true-up operation here, if necessary
-- EXEC clone.BaseTable_TrueUp
ALTER SCHEMA dbo TRANSFER clone.BaseTable
COMMIT TRANSACTION
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE() -- add more info here if necessary
ROLLBACK TRANSACTION
END CATCH
GO
如果您需要释放磁盘空间,此时您可能会删除原始表,但最好将其保留一段时间。
不用说,这是理想的离线操作。如果您在执行此操作时有人在修改数据,则您必须使用模式切换执行校正操作。我建议在dbo.BaseTable
上创建一个触发器,以将所有 DML 记录到单独的表中。在开始插入之前启用此触发器。然后在您执行模式传输的同一事务中,使用日志表执行校正。首先在数据子集上进行测试! Delta 很容易搞砸。
【讨论】:
【参考方案2】:如果您有磁盘空间,您可以使用 SELECT INTO 并创建一个新表。它是最低限度的日志记录,所以它会更快
select t.*, int_field = CAST(-1 as int)
into mytable_new
from mytable t
-- create your indexes and constraints
GO
exec sp_rename mytable, mytable_old
exec sp_rename mytable_new, mytable
drop table mytable_old
【讨论】:
是的,是的。更新非常需要大量日志。 @MikeForman 你好,请告诉我你将如何在上述解决方案中启用 nologging【参考方案3】:我将任务分解为更小的单元。为您的表使用不同的批量大小间隔进行测试,直到找到性能最佳的间隔。这是我过去使用的示例。
declare @counter int
declare @numOfRecords int
declare @batchsize int
set @numOfRecords = (SELECT COUNT(*) AS NumberOfRecords FROM <TABLE> with(nolock))
set @counter = 0
set @batchsize = 2500
set rowcount @batchsize
while @counter < (@numOfRecords/@batchsize) +1
begin
set @counter = @counter + 1
Update table set int_field = -1 where int_field <> -1;
end
set rowcount 0
【讨论】:
我想知道批量更新如何加快更新过程。我认为它只会减慢速度【参考方案4】:如果您的 int_field 已编入索引,请在运行更新之前删除该索引。然后再次创建索引...
5 小时对于 1.2 亿次录制来说似乎很多。
【讨论】:
您在应用数据更改时获得的收益,在重新应用索引时会失去(如果不是更糟的话)。 并且该字段没有被索引。【参考方案5】:set rowcount 1000000
Update table set int_field = -1 where int_field<>-1
看看需要多快,根据需要调整和重复
【讨论】:
【参考方案6】:我首先要尝试的是 在更新之前先删除所有约束、索引、触发器和全文索引。
如果上面的性能不够好,我的下一步将是 创建一个包含 1200 万条记录的 CSV 文件并使用 bcp 批量导入。
最后,我将在不同的文件组上创建一个没有索引的新堆表(即没有主键的表),并用 -1 填充它。对旧表进行分区,并使用“switch”添加新分区。
【讨论】:
你怎么没提到禁用触发器? 谢谢,丹尼斯。我还提到了禁用 FTS 索引【参考方案7】:当添加一个新列(“初始化一个新字段”)并为每个现有行设置一个值时,我使用以下策略:
ALTER TABLE MyTable
add NewColumn int not null
constraint MyTable_TemporaryDefault
default -1
ALTER TABLE MyTable
drop constraint MyTable_TemporaryDefault
如果该列可以为空并且您不包含“声明的”约束,则该列对于所有行都将设置为空。
【讨论】:
您知道这是否具有UPDATE
语句、批量记录的INSERT
语句或其他内容的性能概况?
我怀疑是“别的东西”。我从来没有在真正的大桌子上这样做过,但它往往会在中等大小的桌子上非常快速运行。无论如何,当您必须向现有表添加一列时,您必须支付“全有或全无”的价格,因为您不能一次将它添加到一批行中。 (另一面是 INSERT....SELECT... 一次 N 行,但如果你这样做,你可能不得不安排一些停机时间,此时锁定、阻塞和计时不应该非常关键。)
我已经在超过 3.8 亿行的表上进行了尝试,看起来它正在以相同的方式更新表并花费类似的时间。【参考方案8】:
declare @cnt bigint
set @cnt = 1
while @cnt*100<10000000
begin
UPDATE top(100) [Imp].[dbo].[tablename]
SET [col1] = xxxx
WHERE[col1] is null
print '@cnt: '+convert(varchar,@cnt)
set @cnt=@cnt+1
end
【讨论】:
【参考方案9】:听起来像是一个索引问题,就像 Pabla Santa Cruz 提到的那样。由于您的更新不是有条件的,因此您可以删除该列并使用 DEFAULT 值重新添加它。
【讨论】:
也许我做错了什么,但我对默认值的经验是,它们不是在创建列时设置的,而是在插入记录时设置的。 您可能是对的(我实际上从未这样做过)。也许如果您在列上不允许 NULLS,它会从默认值约束中获取值(因为它是内联添加的,它会有一个自动生成的名称)。 @TrickyNixon:事实上,如果您将字段重新创建为非空并在同一语句中声明默认约束:ALTER TABLE TableName ADD int_field INT NOT NULL CONSTRAINT DF_TableName_int_field DEFAULT (1)
,Brad 的建议将起作用。我不知道这是否具有 UPDATE 语句、INSERT 语句或其他内容的性能配置文件。如有疑问,请创建一个包含数据子集的测试表并在那里尝试。【参考方案10】:
一般来说,推荐是下一个:
-
删除或仅禁用表上的所有 INDEXES、TRIGGERS、CONSTRAINTS;
更频繁地执行 COMMIT(例如,在每更新 1000 条记录之后);
使用 select ... into。
但在特定情况下,您应该选择最合适的解决方案或它们的组合。
还请记住,有时索引可能很有用,例如当您通过某些条件执行非索引列的更新时。
【讨论】:
【参考方案11】:如果表有一个可以迭代的索引,我会将update top(10000)
语句放在一个循环中移动数据。这将使事务日志保持苗条,并且不会对磁盘系统产生如此巨大的影响。另外,我建议使用 maxdop
选项(将其设置为更接近 1)。
【讨论】:
以上是关于更新 1.2 亿条记录的最快方法的主要内容,如果未能解决你的问题,请参考以下文章