【问题标题】:What is the best way to perform a manipulation with huge amounts of data in SQL Server?在 SQL Server 中对大量数据执行操作的最佳方法是什么?
【发布时间】:2010-03-05 16:10:33
【问题描述】:

我们需要在我们的数据库中执行以下操作:

表 A 的 B_ID 列是表 B 的外键。表 A 中有许多行具有相同的 B_ID 值,我们希望通过克隆 B 中的相应行来解决此问题并将行从 A 重定向到它们。

所有这一切都相对简单,我们已经创建了一个脚本,通过迭代游标并调用存储过程来克隆表 B 中的行来解决这个问题。现在的问题是 A 和 B 表都很大并且存在也是表 A 中的大量组指向 B 中的同一行。

我们最终得到的是(在执行几分钟后)填满事务日志并崩溃。我们甚至尝试将工作分成合理大小的批次并逐个运行,但这最终也会填满日志。

除了以某种方式清理日志之外,还有什么方法可以更快地处理 SQL Server 中数据的批量插入/更新,而且根本不会破坏日志吗?

【问题讨论】:

  • 只是为了理解:您想在不填满日志的情况下将多对一关系(多 A 对一 B)更改为一对一关系(一对 A 对一 B)?
  • 我们在这里谈论的规模有多大?我真的怀疑问题在于您正在使用光标来完成一些可能通过 INSERTUPDATE 语句完成的事情。
  • @Chris:是的,你是对的,这就是我们想要做的 ;)
  • @Aaronaught :巨大意味着约 1M 行,可能更多
  • 除了创建重复记录之外,B 是否还有更多?换句话说,B 是否存在其他依赖关系?我想弄清楚为什么你有一个光标。

标签: sql-server database sql-server-2005 tsql bulkinsert


【解决方案1】:

这是批量执行此操作的另一种方法(无游标)。 @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。为此,首先将更新列表排队(我们将在一些 other 组唯一列上对其进行索引以加快稍后的合并):

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 从未真正说过。除了单用户模式之外,我无法想象在其他任何模式下运行它,因此锁定不会成为问题。另外,我认为 MERGE 在 SQL Server 2005 中不可用。
  • @KM:啊,我错过了这是 SQL 2005。这里你不需要MERGE 语义,可以用UPDATE FROM 替换它。 OP 说生产表是 1M 行,所以我想临时表会小得多;如果不是这样,那么我确实强调了使用物理临时表的可能性。对于 1M 行,只要索引良好,这应该没什么大不了的,最多可能需要几分钟来运行查询。无论如何,很明显我们没有所有信息,我只是提供了一个可能更有效的替代版本。
【解决方案2】:

如果您可以使操作脱机,则可以更改数据库的恢复模式,进行更改,然后再更改恢复模式。

总体而言,虽然事务日志是为了保护您,允许回滚等,但当您出于跟踪目的进行删除等操作时,它会变得更大。

注意:使用此方法时,请务必先进行良好的备份....

【讨论】:

  • 除非您可以将数据库置于单用户模式,否则不要这样做。
  • 赞成这是解决问题的唯一合理方法,因为光标显然是必要的,因为不仅复制 B,还复制其他相关表。
  • 这是一种合理的方式,但不是唯一合理的方式——完全可以使用一些小的临时表来代替游标。
【解决方案3】:

我无法想象你为什么要这样做。当前的一对多关系有什么问题?您现在不是要拥有更大的表来执行您的所有工作吗?

但是,如果您想这样做,首先您是否进行事务日志备份,多久备份一次?如果频率低于每十五分钟一次,则更改它。当你备份日志时,日志会被截断,如果你不备份日志,那么它会增长,直到你用完空间。此外,您为日志指定的增长百分比也可能太小。增加它,它也可能对你有所帮助。

您可以尝试在 SSIS 中进行这项工作,但我不知道这是否真的有助于解决日志记录问题。不过,这将有助于提高执行任务的性能。

【讨论】:

    【解决方案4】:

    我不确定这将如何在很多行上运行,但请试一试:

    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)
    

    【讨论】:

      【解决方案5】:

      如果您要从多对一(多 A 到一 B)关系转变为一对一(一对 A 到一 B),那么在我看来,最简单的方法是在 A 中创建字段为了支持这一点,然后对 A 进行简单更新,将 B 中的值复制到其中。

      这样您就可以完全摆脱 B,并且可以在一个更新查询中执行更改。比如:

      update tableA SET
        col1 = B.col1,
        col2 = B.col2
      from tableA A
      inner join tableB on (B.ID = A.B_ID)
      

      【讨论】:

      • 哦,这实际上不可能,表B也被许多其他表引用..
      • @Thomas:好的,现在对光标的需求是有意义的。
      【解决方案6】:

      这就是我的工作:

      创建一个查询,返回两个表(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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-10
        • 2018-10-01
        • 2012-10-02
        • 1970-01-01
        • 2011-06-04
        • 1970-01-01
        相关资源
        最近更新 更多