在SQL Server中处理大量数据的最佳方法是什么?

3
我们需要在数据库中执行以下操作:
表A有一个B_ID列,是指向表B的外键。表A中有许多行具有相同的B_ID值,我们想通过复制表B中对应的行并将A表中的行重定向到它们来解决这个问题。
这一切都相对简单,我们已经创建了一个脚本,通过迭代游标并调用存储过程来克隆表B中的行来解决这个问题。现在的问题是,A和B表都非常大,并且在指向B表中相同行的A表分组数量也非常大。
我们最终会遇到的问题是(在几分钟的执行后)填满事务日志并崩溃。我们甚至尝试将工作分成合理大小的批次,并逐个运行它们,但这最终也会填满日志。
除了某种方式清理日志之外,在SQL Server中处理数据的批量插入/更新是否有更快且不会完全填满日志的方法?

我们在这里谈论的是多大的规模?我真的怀疑问题出在您正在使用游标来处理可以用几个 INSERTUPDATE 语句完成的任务上。 - Aaronaught
@Chris:是的,你说得对,那正是我们想做的事情 ;) - Tomas Vana
@Aaronaught:Huge 意味着大约 1 百万行,甚至可能更多。 - Tomas Vana
除了创建重复记录外,B 还有其他的依赖关系吗?换句话说,B 还有其他的依赖关系吗?我试图弄清楚你为什么要使用游标。 - NotMe
好的,那么请看我下面的回答。 - NotMe
显示剩余2条评论
6个回答

2

我不确定这个方法在大量行上的效果如何,但可以试一下:

DECLARE @TableA table (RowID int, B_ID int)
INSERT INTO @TableA VALUES (1,1)
INSERT INTO @TableA VALUES (2,1) --need to copy
INSERT INTO @TableA VALUES (3,2)
INSERT INTO @TableA VALUES (4,2) --need to copy
INSERT INTO @TableA VALUES (5,2) --need to copy
INSERT INTO @TableA VALUES (6,1) --need to copy
INSERT INTO @TableA VALUES (7,3)
INSERT INTO @TableA VALUES (8,3) --need to copy
DECLARE @TableB table (B_ID int, BValues varchar(10))
INSERT INTO @TableB VALUES (1,'one')
INSERT INTO @TableB VALUES (2,'two')
INSERT INTO @TableB VALUES (3,'three')

DECLARE @Max_B_ID int
SELECT @Max_B_ID=MAX(B_ID) FROM @TableB

--if you are using IDENTITY, turn them off here
INSERT INTO @TableB 
        (B_ID, BValues)
        --possibly capture the data to eliminate duplication??
        --OUTPUT INSERTED.tableID, INSERTED.datavalue
        --INTO @y 
    SELECT
        dt.NewRowID, dt.BValues
        FROM (SELECT 
                  RowID, a.B_ID
                      ,@Max_B_ID+ROW_NUMBER() OVER(order by a.B_ID) AS NewRowID,b.BValues
                  FROM (SELECT
                            RowID, B_ID
                            FROM (SELECT 
                                      RowID, a.B_ID, ROW_NUMBER() OVER(PARTITION by a.B_ID order by a.B_ID) AS RowNumber
                                      FROM @TableA a
                                 ) dt
                            WHERE dt.RowNumber>1
                       )a
                      INNER JOIN @TableB  b ON a.B_ID=b.B_ID
             ) dt


UPDATE aa
    SET B_ID=NewRowID
    FROM @TableA   aa
        INNER JOIN (SELECT
                        dt.NewRowID, dt.BValues,dt.RowID
                        FROM (SELECT 
                                  RowID, a.B_ID
                                      ,@Max_B_ID+ROW_NUMBER() OVER(order by a.B_ID) AS NewRowID,b.BValues
                                  FROM (SELECT
                                            RowID, B_ID
                                            FROM (SELECT 
                                                      RowID, a.B_ID, ROW_NUMBER() OVER(PARTITION by a.B_ID order by a.B_ID) AS RowNumber
                                                      FROM @TableA a
                                                 ) dt
                                            WHERE dt.RowNumber>1
                                       )a
                                      INNER JOIN @TableB  b ON a.B_ID=b.B_ID
                             ) dt
                   ) dt2 ON aa.RowID=dt2.RowID

SELECT * FROM @TableA
SELECT * FROM @TableB

输出:

RowID       B_ID
----------- -------
1           1
2           4
3           2
4           6
5           7
6           5
7           3
8           8

(8 row(s) affected)

B_ID        BValues
----------- -------
1           one
2           two
3           three
4           one
5           one
6           two
7           two
8           three

(8 row(s) affected)

2

以下是一种批处理的方法(无游标)来完成此操作。@KM的方法看起来应该可以工作,但涉及大量的锁定和扫描,这让我感觉有些慢/可怕。如果将工作集限制为仅包含新行,则速度应该很快。

以下是测试数据的设置脚本:

CREATE TABLE Colors
(
    ColorID int NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    ColorName varchar(50) NOT NULL
)

CREATE TABLE Markers
(
    MarkerID int NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    MarkerName varchar(50) NOT NULL,
    ColorID int NOT NULL,
    CONSTRAINT FK_Markers_Colors FOREIGN KEY (ColorID)
        REFERENCES Colors (ColorID)
)

INSERT Colors (ColorName) VALUES ('Red')
INSERT Colors (ColorName) VALUES ('Green')
INSERT Colors (ColorName) VALUES ('Blue')

INSERT Markers (MarkerName, ColorID) VALUES ('Test1', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test2', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test3', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test4', 2)
INSERT Markers (MarkerName, ColorID) VALUES ('Test5', 2)
INSERT Markers (MarkerName, ColorID) VALUES ('Test6', 3)
INSERT Markers (MarkerName, ColorID) VALUES ('Test7', 3)

我们有一个1:Many的关系,现在想要将其变为1:1。为了实现这个目标,首先需要列出一份更新列表(我们将对其进行索引,使用一些其他唯一列来加速后续合并操作):

CREATE TABLE #NewColors
(
    MarkerID int NOT NULL,
    ColorName varchar(50) NOT NULL,
    Seq int NOT NULL,
    CONSTRAINT PK_#NewColors PRIMARY KEY (MarkerID)
)

CREATE INDEX IX_#NewColors
ON #NewColors (ColorName, Seq);

WITH Refs AS
(
    SELECT
        MarkerID,
        ColorID,
    ROW_NUMBER() OVER (PARTITION BY ColorID ORDER BY (SELECT 1)) AS Seq
    FROM Markers
)
INSERT #NewColors (MarkerID, ColorName, Seq)
SELECT r.MarkerID, c.ColorName, r.Seq - 1
FROM Refs r
INNER JOIN Colors c
    ON c.ColorID = r.ColorID
WHERE r.Seq > 1

结果将为每个需要获得新颜色的标记生成一行。然后插入新颜色并捕获完整输出:
DECLARE @InsertedColors TABLE
(
    ColorID int NOT NULL PRIMARY KEY,
    ColorName varchar(50) NOT NULL
)

INSERT Colors (ColorName)
OUTPUT inserted.ColorID, inserted.ColorName
INTO @InsertedColors
    SELECT ColorName
    FROM #NewColors nc;

最后将其合并(这就是临时表上额外索引的好处所在):
WITH InsertedColorSeq AS
(
    SELECT
        ColorID, ColorName,
        ROW_NUMBER() OVER (PARTITION BY ColorName ORDER BY ColorID) AS Seq
    FROM @InsertedColors
),
Updates AS
(
    SELECT nc.MarkerID, ic.ColorID AS NewColorID
    FROM #NewColors nc
    INNER JOIN InsertedColorSeq ic
    ON ic.ColorName = nc.ColorName
    AND ic.Seq = nc.Seq
)
MERGE Markers m
USING Updates u
    ON m.MarkerID = u.MarkerID
WHEN MATCHED THEN
    UPDATE SET m.ColorID = u.NewColorID;

DROP TABLE #NewColors

这应该非常高效,因为它只需要查询生产表一次。其他所有操作都将在相对较小的临时表中进行。

测试结果:

SELECT m.MarkerID, m.MarkerName, c.ColorID, c.ColorName
FROM Markers m
INNER JOIN Colors c
    ON c.ColorID = m.ColorID

这是我们的输出结果:
MarkerID     MarkerName   ColorID   ColorName
1            Test1        1         Red
2            Test2        6         Red
3            Test3        7         Red
4            Test4        2         Green
5            Test5        5         Green
6            Test6        3         Blue
7            Test7        4         Blue

这应该是您想要的,对吧?没有游标,没有严重的丑陋。如果它消耗太多内存或tempdb空间,则可以将临时表/表变量替换为索引物理暂存表。即使有数百万行,也不可能填满事务日志并崩溃。


我会害怕创建大型的 #temp 表,这取决于它们有多大,但 OP 从未真正说明。我无法想象在除了单用户模式之外的任何模式下运行此操作,因此锁定不会成为问题。此外,我认为 SQL Server 2005 中并没有可用的 MERGE 功能。 - KM.
@KM:啊,我错过了这是SQL 2005。在这里,您实际上不需要MERGE语义,它可以被替换为UPDATE FROM。OP说生产表有100万行,所以我想临时表会小得多;如果不是这种情况,那么我确实强调了使用物理分段表的可能性。对于100万行,只要索引良好,运行查询最多可能需要几分钟时间,这不应该是什么大问题。无论如何,很明显我们没有所有信息,我只提供了一个可能更有效的替代版本。 - Aaronaught

2
如果您可以将操作离线,您可以更改数据库的恢复模式,进行更改,然后将恢复模式改回来。总的来说,事务日志是为了保护您,允许回滚等操作,并且随着删除等操作进行,它会变得越来越大以用于跟踪目的。
注意:使用此方法之前,请确保有一个非常好的备份...

1
不要操作,除非你能将数据库置于单用户模式。 - HLGEM
点赞此方法是解决问题的唯一合理方式,因为光标显然是必要的,由于不仅复制了B表,还复制了其他相关表。 - NotMe
这是一种合理的方式,但并不是唯一合理的方式 - 完全可以使用一些小的临时表来替代游标。 - Aaronaught

2
我无法理解为什么你想这样做。当前的一对多关系有什么问题吗?现在你不得不使用更大的表来执行所有的工作,对吗?
但是既然你想这么做,首先你是否正在备份事务日志,并且备份频率如何?如果少于每十五分钟备份一次,则应更改。当您备份日志时,日志会被截断,如果您不备份日志,则它会继续增长,直到耗尽空间。也许你指定的日志增长百分比太小了。增加它可能也会有所帮助。
您可以尝试在SSIS中完成工作,但我不知道这是否真正有助于日志记录问题。不过,这将有助于提高执行任务的性能。

0
如果您从多对一(多个A对应一个B)关系转移到一对一(一个A对应一个B),那么最简单的方法似乎是创建A中的字段以支持此操作,然后在A上执行简单的更新,将B中的值复制到A中。
这样,您就可以完全摆脱B,并且可以在一个更新查询中执行更改。类似于:
update tableA SET
  col1 = B.col1,
  col2 = B.col2
from tableA A
inner join tableB on (B.ID = A.B_ID)

哦,实际上这是不可能的,表B也被许多其他表所引用。 - Tomas Vana
@Thomas:好的,现在光标的需求有意义了。 - NotMe

0
这是我的工作内容:
创建一个查询,从两个表(A、B)中返回数据,确保数据在最终表(C)中的格式正确,并将其放入ExtractData.sql文件中:
select
    A.id,
    A.xxx,
    A.yyy,
    B.*
from
   A

   JOIN B
     on B.id = A.id

然后在cmd窗口中,执行以下命令将数据提取到文件中:

sqlcmd.exe -S [Server] -U [user] -P [pass] -d [dbname] -i DataExtract.sql -s "|" -h -1 -W -o ExtractData.dat

为了避免填满日志,请在插入前尝试将数据库恢复模式设置为简单模式:
ALTER DATABASE [database name] SET RECOVERY SIMPLE

如果您需要清除旧数据(它不像删除一样添加到日志中),则执行TRUNCATE TABLE C

然后在cmd窗口中执行此命令以批量加载数据到表C中:

bcp.exe dbname.dbo.C in ExtractData.dat -S [Server] -U [user] -P [pass] -t "|" -e ExtractData.err -r \n -c

错误记录将显示在ExtractData.err文件中,因此如果您需要调整C表的模式,您可以调整/截断/重新加载提取的数据,以便您不需要每次运行查询。

完成后,请将恢复模式设置回FULL:

ALTER DATABASE [database name] SET RECOVERY FULL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接