2010-03-05 184 views
3

我們需要在我們的數據庫中執行以下操作:在SQL Server中執行大量數據操作的最佳方式是什麼?

有一個表A具有列B_ID,它是表B的外鍵。表A中有許多行具有相同的值B_ID,我們想通過克隆B中相應的行並將A行重定向到它們來解決這個問題。

所有這些都相對簡單,我們已經創建了一個腳本,它通過迭代遊標並調用存儲過程來克隆表B中的行來解決此問題。現在問題是A和B表都很龐大,表A中也有大量的組指向B中的同一行。

我們最終得到的是(在幾分鐘的執行後)正在填充事務日誌和崩潰。我們甚至試圖將工作分成合理大小的批次並逐一運行,但這也最終填滿了日誌。

除了以某種方式清理日誌,是否有一些方法來處理SQL Server中的數據的批量插入/更新,這將更快並且不會炸燬日誌?

+0

只是爲了解:你想改變多對一的關係(許多A到B)成一對一的關係(一個A到一個B)而不填滿你的日誌? – NotMe 2010-03-05 16:13:10

+0

我們在這裏討論的有多大?我真的懷疑問題在於你正在使用一個遊標來完成一些可能通過一些'INSERT'和'UPDATE'語句完成的事情。 – Aaronaught 2010-03-05 16:13:56

+0

@Chris:是的,你是對的,這就是我們想要做的;) – 2010-03-05 16:14:37

回答

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:很多,我們希望使之成爲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空間,那麼可以用索引的物理臨時表替換臨時表/表變量。即使有數百萬行,這也不應該填滿事務日誌和崩潰。

+0

我會害怕做大#temp表,這取決於它們有多大,但OP從來沒有真正說過。我無法想象在單用戶模式以外的任何其他模式下運行,因此鎖定不會成爲問題。另外,我不認爲MERGE在SQL Server 2005中可用。 – 2010-03-05 18:20:29

+0

@KM:嗯,我錯過了這是SQL 2005。在這裏你不需要'MERGE'語義,它可以用'UPDATE FROM '。 OP說生產表是1M行,所以我想象臨時表會更小;如果情況並非如此,那麼我確實強調了使用物理臨時表的可能性。對於1M行來說,只要索引是好的,最好幾分鐘就可以運行查詢,這應該不是什麼大問題。無論如何,顯然我們沒有所有的信息,我只是提供了一個替代版本,可能會更有效率。 – Aaronaught 2010-03-05 22:16:20

0

如果您正在從一對多(一對一到多對一)的多對一(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) 
+0

以下看到我的答案哦,實際上這是不可能的,表B被引用許多其他表格。 – 2010-03-05 16:19:49

+0

@Thomas:好的,現在需要光標。 – NotMe 2010-03-05 16:21:38

+0

查看Mitchel的回答。 – NotMe 2010-03-05 16:23:17

2

如果你可以採取的操作下線,你可以更改數據庫的恢復模式,進行更改,然後更改恢復模型回來。

總的來說,雖然事務日誌是爲了保護你,允許回滾等,它會變得更大,因爲你刪除等用於跟蹤目的。

注:使用這​​種方法一定要有一個不錯的先備份....

+1

除非您可以將數據庫置於單用戶模式,否則不要這樣做。 – HLGEM 2010-03-05 16:19:06

+0

將此視爲解決問題的唯一合理方式,因爲光標顯然是必需的,因爲不僅複製了B,而且還複製了其他相關表。 – NotMe 2010-03-05 16:23:00

+0

這是一個合理的方式,但它不是*唯一的*合理的方式 - 完全可以使用一些小的臨時表而不是光標。 – Aaronaught 2010-03-05 16:38:11

2

我不能想象你爲什麼會想這樣做。目前的一對多關係有什麼問題?你現在不會有更大的表來執行你的所有工作嗎?

但是,鑑於您想要這樣做,首先您要進行事務日誌備份,頻率如何?如果不到每十五分鐘一次,那麼改變它。當您備份日誌時,日誌會被截斷,如果您沒有備份日誌,那麼日誌會增長,直到空間用完。也可能是您爲日誌指定的增長百分比太小。增加它,它也可能幫助你。

您可以嘗試在SSIS中完成這項工作,但我不知道這是否能夠真正幫助記錄問題。儘管如此,這將有助於提高執行任務的性能。

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 

OUTPUT:

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) 
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可以調整/截斷/重新加載提取的數據,因此您不需要每次運行查詢時都需要 。

然後設置恢復模式回滿你完成後:

ALTER DATABASE [database name] SET RECOVERY FULL 
相關問題