3

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

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

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

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

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

4

6 回答 6

2

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

总体而言,尽管事务日志可以保护您,允许回滚等,但当您出于跟踪目的删除等时,它会变得更大。

注意:使用这种方法一定要先有一个很好的备份....

于 2010-03-05T16:15:22.910 回答
2

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

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

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

于 2010-03-05T16:28:35.270 回答
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)
于 2010-03-05T16:46:10.727 回答
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 空间,那么您可以用索引的物理临时表替换临时表/表变量。即使有几百万行,也不可能填满事务日志并崩溃。

于 2010-03-05T18:03:43.710 回答
0

如果您要从多对一(多对一 B)关系转变为一对一(一对一 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)
于 2010-03-05T16:14:50.340 回答
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
于 2010-03-05T16:59:11.490 回答