web-dev-qa-db-ja.com

FK関係を維持しながら、ID列を含む新しいテーブルにデータを移行する方法

データベース間でデータを移行したい。テーブルスキーマはまったく同じです。

CREATE TABLE Customers(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY,
    (some other columns ......)
);

CREATE TABLE Orders(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY,
    [CustomerId] INT NOT NULL,
    (some other columns ......),
    CONSTRAINT [FK_Customers_Orders] FOREIGN KEY ([CustomerId]) REFERENCES [Customers]([Id])
)

2つのデータベースには異なるデータがあるため、同じテーブルの新しいIDキーは2つのデータベースで異なります。それは問題ではない;私の目標は、既存のデータに新しいデータを追加することであり、テーブル全体のすべてのデータを完全に置き換えることではありません。ただし、挿入されたデータのすべての親子関係を維持したいと思います。

SSMSの「スクリプトの生成」機能を使用すると、スクリプトは同じIDを使用して挿入しようとし、宛先データベースの既存のデータと競合します。データベーススクリプトのみを使用してデータをコピーするにはどうすればよいですか?

宛先のID列を最後の値から通常どおりに継続させたい。

Customersには他のUNIQUE NOT NULL制約はありません。他の列にデータが重複していても問題ありません(ここでは例としてCustomersOrdersを使用しているので、全体を説明する必要はありません)。問題は、1対Nの関係についてです。

8
kevin

これは、3つの関連するテーブルに簡単にスケーリングする方法です。

MERGEを使用してデータをコピーテーブルに挿入し、古いIDENTITY値と新しいIDENTITY値をコントロールテーブルに出力して、それらを関連テーブルのマッピングに使用できるようにします。

実際の答えは、2つのcreate tableステートメントと3つのマージだけです。残りはサンプルデータのセットアップと破棄です。

USE tempdb;

--## Create test tables ##--

CREATE TABLE Customers(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [Name] NVARCHAR(200) NOT NULL
);

CREATE TABLE Orders(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [CustomerId] INT NOT NULL,
    [OrderDate] DATE NOT NULL,
    CONSTRAINT [FK_Customers_Orders] FOREIGN KEY ([CustomerId]) REFERENCES [Customers]([Id])
);

CREATE TABLE OrderItems(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [OrderId] INT NOT NULL,
    [ItemId] INT NOT NULL,
    CONSTRAINT [FK_Orders_OrderItems] FOREIGN KEY ([OrderId]) REFERENCES [Orders]([Id])
);

CREATE TABLE Customers2(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [Name] NVARCHAR(200) NOT NULL
);

CREATE TABLE Orders2(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [CustomerId] INT NOT NULL,
    [OrderDate] DATE NOT NULL,
    CONSTRAINT [FK_Customers2_Orders2] FOREIGN KEY ([CustomerId]) REFERENCES [Customers2]([Id])
);

CREATE TABLE OrderItems2(
    [Id] INT NOT NULL PRIMARY KEY IdENTITY,
    [OrderId] INT NOT NULL,
    [ItemId] INT NOT NULL,
    CONSTRAINT [FK_Orders2_OrderItems2] FOREIGN KEY ([OrderId]) REFERENCES [Orders2]([Id])
);

--== Populate some dummy data ==--

INSERT Customers(Name)
VALUES('Aaberg'),('Aalst'),('Aara'),('Aaren'),('Aarika'),('Aaron'),('Aaronson'),('Ab'),('Aba'),('Abad');

INSERT Orders(CustomerId, OrderDate)
SELECT Id, Id+GETDATE()
FROM Customers;

INSERT OrderItems(OrderId, ItemId)
SELECT Id, Id*1000
FROM Orders;

INSERT Customers2(Name)
VALUES('Zysk'),('Zwiebel'),('Zwick'),('Zweig'),('Zwart'),('Zuzana'),('Zusman'),('Zurn'),('Zurkow'),('ZurheIde');

INSERT Orders2(CustomerId, OrderDate)
SELECT Id, Id+GETDATE()+20
FROM Customers2;

INSERT OrderItems2(OrderId, ItemId)
SELECT Id, Id*1000+10000
FROM Orders2;

SELECT * FROM Customers JOIN Orders ON Orders.CustomerId = Customers.Id JOIN OrderItems ON OrderItems.OrderId = Orders.Id;

SELECT * FROM Customers2 JOIN Orders2 ON Orders2.CustomerId = Customers2.Id JOIN OrderItems2 ON OrderItems2.OrderId = Orders2.Id;

--== ** START ACTUAL ANSWER ** ==--

--== Create Linkage tables ==--

CREATE TABLE CustomerLinkage(old INT NOT NULL PRIMARY KEY, new INT NOT NULL);
CREATE TABLE OrderLinkage(old INT NOT NULL PRIMARY KEY, new INT NOT NULL);

--== Copy Header (Customers) rows and record the new key ==--

MERGE Customers2
USING Customers
ON 1=0 -- we just want an insert, so this forces every row as unmatched
WHEN NOT MATCHED THEN
INSERT (Name) VALUES(Customers.Name)
OUTPUT Customers.Id, INSERTED.Id INTO CustomerLinkage;

--== Copy Detail (Orders) rows using the new key from CustomerLinkage and record the new Order key ==--

MERGE Orders2
USING (SELECT Orders.Id, CustomerLinkage.new, Orders.OrderDate
FROM Orders 
JOIN CustomerLinkage
ON CustomerLinkage.old = Orders.Id) AS Orders
ON 1=0 -- we just want an insert, so this forces every row as unmatched
WHEN NOT MATCHED THEN
INSERT (CustomerId, OrderDate) VALUES(Orders.new, Orders.OrderDate)
OUTPUT Orders.Id, INSERTED.Id INTO OrderLinkage;

--== Copy Detail (OrderItems) rows using the new key from OrderLinkage ==--

MERGE OrderItems2
USING (SELECT OrderItems.Id, OrderLinkage.new, OrderItems.ItemId
FROM OrderItems 
JOIN OrderLinkage
ON OrderLinkage.old = OrderItems.Id) AS OrderItems
ON 1=0 -- we just want an insert, so this forces every row as unmatched
WHEN NOT MATCHED THEN
INSERT (OrderId, ItemId) VALUES(OrderItems.new, OrderItems.ItemId);

--== ** END ACTUAL ANSWER ** ==--

--== Display the results ==--

SELECT * FROM Customers2 JOIN Orders2 ON Orders2.CustomerId = Customers2.Id JOIN OrderItems2 ON OrderItems2.OrderId = Orders2.Id;

--== Drop test tables ==--

DROP TABLE OrderItems;
DROP TABLE OrderItems2;
DROP TABLE Orders;
DROP TABLE Orders2;
DROP TABLE Customers;
DROP TABLE Customers2;
DROP TABLE CustomerLinkage;
DROP TABLE OrderLinkage;
11
Mister Magoo

私が過去にこれを行ったとき、私は次のようなことをしました:

  • 両方のデータベースをバックアップします。

  • 最初のDBから2番目のDBに移動する行を、IDENTITY列なしで新しいテーブルにコピーします。

  • それらの行のすべての子行を、親テーブルへの外部キーなしで新しいテーブルにコピーします。

注:上記の表のセットを「一時的」と呼びます。ただし、それらを独自のデータベースに保存し、完了したらバックアップすることを強くお勧めします。

  • 最初のデータベースの行に対して2番目のデータベースから必要なID値の数を決定します。
  • DBCC CHECKIDENTを使用して、ターゲットテーブルの次のIDENTITY値を、移動に必要な値を超えて1にシフトします。これにより、X IDENTITY値のオープンブロックが残り、最初のデータベースから持ち込まれる行に割り当てることができます。
  • マッピングテーブルをセットアップし、最初のDBを形成する行の古いIDENTITY値と、2番目のDBで使用する新しい値を特定します。
  • 例:新しいIDENTITY値を必要とする473行を最初のデータベースから2番目のデータベースに移動しています。 DBCC CHECKIDENTによると、2番目のデータベースのそのテーブルの次のID値は現在1128です。 DBCC CHECKIDENTを使用して、値を1601に再シードします。次に、マッピングテーブルに親テーブルのIDENTITY列の現在の値を古い値として入力し、ROW_NUMBER()を使用します新しい値として1128から1600までの数値を割り当てる関数。

  • マッピングテーブルを使用して、一時的な親テーブルの通常IDENTITY列の値を更新します。

  • マッピングテーブルを使用して、子テーブルのすべてのコピーで、通常は親テーブルへの外部キーである値を更新します。
  • SET IDENTITY_INSERT <parent> ONを使用して、更新された親行を一時親テーブルから2番目のDBに挿入します。
  • 更新された子行を一時的な子テーブルから2番目のDBに挿入します。

注:子テーブルの一部に独自のIDENTITY値がある場合、これは非常に複雑になります。実際のスクリプト(一部はベンダーによって開発されたため、実際には共有できません)は、自動インクリメントされていない数値を含む数十のテーブルと主キー列を処理します。ただし、これらは基本的な手順です。

移行後にマッピングテーブルを保持しましたが、これには古いIDに基づいて「新しい」レコードを見つけることができるという利点がありました。

気の弱い人のためではなく、must、must、mustがテスト環境でテストされます(理想的にはmultiple回)。

更新:私はまた、これを使用しても、ID値の「浪費」についてあまり心配していなかったとも言うべきです。既存の値と誤って衝突しないようにするために、2番目のデータベースのIDブロックを実際に必要な値よりも2〜3大きい値に設定しました。

特にプロセスが繰り返される場合(特に、鉱山は30か月で合計で約20回実行された場合)、このプロセス中に数十万の潜在的な有効なIDをスキップしたくないことは確かに理解できます。とはいえ、一般的に、ギャップのない自動インクリメントID値をシーケンシャルにすることはできません。行が作成されてロールバックされると、その行の自動インクリメント値はなくなります。追加された次の行にはnext値が含まれ、ロールバックされた行からの行はスキップされます。

2
RDFozz

Microsoftの新しいサンプルデータベースであるWideWorldImportersデータベースのテーブルを使用しています。このようにして、スクリプトをそのまま実行できます。このデータベースのバックアップは here からダウンロードできます。

ソーステーブル(これはデータのサンプルに存在します)。

USE [WideWorldImporters]
GO


SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [Warehouse].[VehicleTemperatures]
(
    [VehicleTemperatureID] [bigint] IDENTITY(1,1) NOT NULL,
    [VehicleRegistration] [nvarchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [ChillerSensorNumber] [int] NOT NULL,
    [RecordedWhen] [datetime2](7) NOT NULL,
    [Temperature] [decimal](10, 2) NOT NULL,
    [FullSensorData] [nvarchar](1000) COLLATE Latin1_General_CI_AS NULL,
    [IsCompressed] [bit] NOT NULL,
    [CompressedSensorData] [varbinary](max) NULL,

 CONSTRAINT [PK_Warehouse_VehicleTemperatures]  PRIMARY KEY NONCLUSTERED 
(
    [VehicleTemperatureID] ASC
)
)
GO

宛先テーブル:

USE [WideWorldImporters]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [Warehouse].[VehicleTemperatures_dest]
(
    [VehicleTemperatureID] [bigint] IDENTITY(1,1) NOT NULL,
    [VehicleRegistration] [nvarchar](20) COLLATE Latin1_General_CI_AS NOT NULL,
    [ChillerSensorNumber] [int] NOT NULL,
    [RecordedWhen] [datetime2](7) NOT NULL,
    [Temperature] [decimal](10, 2) NOT NULL,
    [FullSensorData] [nvarchar](1000) COLLATE Latin1_General_CI_AS NULL,
    [IsCompressed] [bit] NOT NULL,
    [CompressedSensorData] [varbinary](max) NULL,

 CONSTRAINT [PK_Warehouse_VehicleTemperatures_dest]  PRIMARY KEY NONCLUSTERED 
(
    [VehicleTemperatureID] ASC
)
)
GO

ID列の値なしでエクスポートを実行しています。 ID列VehicleTemperatureIDに挿入しておらず、同じ列からも選択していないことに注意してください。

INSERT INTO [Warehouse].[vehicletemperatures_dest] 
            (
             [vehicleregistration], 
             [chillersensornumber], 
             [recordedwhen], 
             [temperature], 
             [fullsensordata], 
             [iscompressed], 
             [compressedsensordata]) 
SELECT  
       [vehicleregistration], 
       [chillersensornumber], 
       [recordedwhen], 
       [temperature], 
       [fullsensordata], 
       [iscompressed] [bit], 
       [compressedsensordata] 
FROM   [Warehouse].[vehicletemperatures] 

FK制約に関する2番目の質問に答えるには、 this の投稿を参照してください。特に以下のセクション。

ウィザードが作成したSSISパッケージを保存してから、BIDS/SSDTで編集する必要があります。パッケージを編集すると、テーブルが処理される順序を制御できるため、親テーブルを処理し、すべての親テーブルが完了したときに子テーブルを処理できます。

0
SqlWorldWide