Wie kopiere ich Migrationsdaten in neue Tabellen mit Identitätsspalte, wobei die FK-Beziehung erhalten bleibt?

8

Ich möchte Daten von einer Datenbank in eine andere migrieren. Die Tabellenschemata sind genau gleich:

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])
)

Die beiden Datenbanken haben unterschiedliche Daten, sodass der neue Identitätsschlüssel für dieselbe Tabelle in den beiden Datenbanken unterschiedlich wäre. Das ist kein Problem; Mein Ziel ist es, neue Daten an die vorhandenen anzuhängen und nicht alle Daten der gesamten Tabelle vollständig zu ersetzen. Ich möchte jedoch die gesamte Eltern-Kind-Beziehung der eingefügten Daten beibehalten.

Wenn ich die Funktion "Skript generieren" von SSMS verwende, würde das Skript versuchen, mit derselben ID einzufügen, was zu Konflikten mit vorhandenen Daten in der Zieldatenbank führen würde. Wie kann ich Daten nur mit Datenbankskripten kopieren?

Ich möchte, dass die Identitätsspalte am Ziel normal von ihrem letzten Wert fortgesetzt wird.

Customershat keine andere UNIQUE NOT NULLEinschränkung. Es ist in Ordnung, doppelte Daten in anderen Spalten zu haben (ich verwende Customersund Ordersnur als Beispiel hier, damit ich nicht die ganze Geschichte erklären muss). Die Frage bezieht sich auf eine Eins-zu-N-Beziehung.

Kevin
quelle

Antworten:

11

Hier ist eine Möglichkeit, die sich leicht auf drei verwandte Tabellen skalieren lässt.

Verwenden Sie MERGE, um die Daten in die Kopiertabellen einzufügen, damit Sie die alten und neuen IDENTITY-Werte in eine Steuertabelle ausgeben und für die Zuordnung verwandter Tabellen verwenden können.

Die eigentliche Antwort ist nur zwei Anweisungen zum Erstellen von Tabellen und drei Zusammenführungen. Der Rest ist das Einrichten und Herunterfahren von Beispieldaten.

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.CustomerId) 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.OrderId) 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;
Herr Magoo
quelle
Oh mein Gott, du hast mein Leben gerettet. Könnten Sie bitte einen weiteren Filter hinzufügen, z. B. "Nur in Datenbank 1 kopieren, wenn Orders2 mehr als 2 Elemente enthält"
Anh Bảy
2

Wenn ich das in der Vergangenheit gemacht habe, habe ich es ungefähr so ​​gemacht:

  • Sichern Sie beide Datenbanken.

  • Kopieren Sie die Zeilen, die Sie von der ersten Datenbank in die zweite verschieben möchten, in eine neue Tabelle ohne IDENTITYSpalte.

  • Kopieren Sie alle untergeordneten Zeilen dieser Zeilen in neue Tabellen ohne Fremdschlüssel in die übergeordnete Tabelle.

Hinweis: Wir werden die obigen Tabellen als "temporär" bezeichnen. Ich empfehle Ihnen jedoch dringend, sie in ihrer eigenen Datenbank zu speichern und diese auch zu sichern, wenn Sie fertig sind.

  • Bestimmen Sie, wie viele ID-Werte Sie aus der zweiten Datenbank für Zeilen aus der ersten Datenbank benötigen.
  • Verwenden Sie DBCC CHECKIDENTdiese Option , um den nächsten IDENTITYWert für die Zieltabelle auf 1 zu verschieben, der über den für die Verschiebung erforderlichen Wert hinausgeht. Dadurch bleibt ein offener Block mit X- IDENTITYWerten übrig, den Sie den Zeilen zuweisen können, die aus der ersten Datenbank übernommen werden.
  • Richten Sie eine Zuordnungstabelle ein und identifizieren Sie den alten IDENTITYWert für die Zeilen aus der ersten Datenbank und den neuen Wert, den sie in der zweiten Datenbank verwenden.
  • Beispiel: Sie verschieben 473 Zeilen, die einen neuen IDENTITYWert benötigen , von der ersten Datenbank in die zweite. Per DBCC CHECKIDENTist der nächste Identitätswert für diese Tabelle in der zweiten Datenbank derzeit 1128. Verwenden Sie DBCC CHECKIDENTdiese Option, um den Wert auf 1601 zu setzen. Anschließend füllen Sie Ihre Zuordnungstabelle mit den aktuellen Werten für die IDENTITYSpalte aus Ihrer übergeordneten Tabelle als alte Werte und ROW_NUMBER()weisen mit der Funktion die Nummern 1128 bis 1600 als neue Werte zu.

  • Aktualisieren Sie mithilfe der Zuordnungstabelle die Werte in der IDENTITYSpalte, die normalerweise in der temporären übergeordneten Tabelle enthalten ist.

  • Aktualisieren Sie mithilfe der Zuordnungstabelle die Werte, die normalerweise Fremdschlüssel für die übergeordnete Tabelle sind, in allen Kopien der untergeordneten Tabellen.
  • Fügen Sie SET IDENTITY_INSERT <parent> ONmit die aktualisierten übergeordneten Zeilen aus der temporären übergeordneten Tabelle in die zweite Datenbank ein.
  • Fügen Sie die aktualisierten untergeordneten Zeilen aus den temporären untergeordneten Tabellen in die zweite Datenbank ein.

HINWEIS: Wenn einige der untergeordneten Tabellen IDENTITYeigene Werte haben, wird dies ziemlich kompliziert. Meine eigentlichen Skripte (teilweise von einem Anbieter entwickelt, sodass ich sie nicht wirklich freigeben kann) behandeln Dutzende von Tabellen und Primärschlüsselspalten, einschließlich einiger, die numerische Werte nicht automatisch inkrementieren. Dies sind jedoch die grundlegenden Schritte.

Ich habe die Zuordnungstabellen nach der Migration beibehalten, was den Vorteil hatte, dass wir einen "neuen" Datensatz basierend auf einer alten ID finden konnten.

Es ist nicht für schwache Nerven, und muss, muss, muss ( im Idealfall getestet werden mehrere Male) in einer Testumgebung.

UPDATE: Ich sollte auch sagen, dass ich mir trotzdem keine großen Sorgen darüber gemacht habe, ID-Werte zu "verschwenden". Ich habe meine ID-Blöcke in der zweiten Datenbank so eingerichtet, dass sie 2-3 Werte größer sind als erforderlich, um sicherzustellen, dass ich nicht versehentlich mit vorhandenen Werten kollidiere.

Ich verstehe sicherlich, dass ich während dieses Prozesses nicht Hunderttausende potenziell gültiger IDs überspringen möchte, insbesondere wenn der Prozess wiederholt wird (meiner wurde letztendlich über einen Zeitraum von 30 Monaten insgesamt etwa 20 Mal ausgeführt). Im Allgemeinen kann man sich jedoch nicht darauf verlassen, dass ID-Werte automatisch inkrementiert werden, um ohne Lücken sequentiell zu sein. Wenn eine Zeile erstellt und zurückgesetzt wird, verschwindet der Wert für die automatische Inkrementierung für diese Zeile. Die nächste hinzugefügte Zeile hat den nächsten Wert, und die aus der zurückgerollten Zeile wird übersprungen.

RDFozz
quelle
Vielen Dank. Ich habe die Idee, im Grunde genommen einen Block von IDENTITY-Werten vorab zuzuweisen, dann die Werte in einer Reihe von temporären Tabellen manuell zu ändern, bis sie mit dem Ziel übereinstimmen, und dann einzufügen. In meinem Szenario hat die untergeordnete Tabelle jedoch die Spalte IDENTITY (ich muss tatsächlich drei Tabellen mit zwei 1-N-Beziehungen zwischen ihnen verschieben). Das macht es ziemlich kompliziert, aber ich schätze die Idee.
Kevin
1
Sind die Kindertabellen Eltern zu noch anderen Tischen? Dann wird es kompliziert.
RDFozz
Denken Sie wie Customer-Order-OrderItemoder Country-State-City. Wenn die drei Tabellen zusammen gruppiert sind, sind sie in sich geschlossen.
Kevin
0

Ich verwende eine Tabelle aus der WideWorldImportersDatenbank, bei der es sich um die neue Beispieldatenbank von Microsoft handelt. Auf diese Weise können Sie mein Skript so ausführen, wie es ist. Sie können aus einer Sicherung dieser Datenbank herunterladen hier .

Quelltabelle (diese existiert im Beispiel mit Daten).

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

Zieltabelle:

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

Führen Sie nun den Export ohne Identitätsspaltenwert durch. Beachten Sie, dass ich nicht in die Identitätsspalte einfüge VehicleTemperatureIDund auch nicht aus derselben auswähle.

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

Um die zweite Frage zu FK-Einschränkungen zu beantworten, lesen Sie bitte diesen Beitrag. Besonders Abschnitt unten.

Speichern Sie das vom Assistenten erstellte SSIS-Paket und bearbeiten Sie es in BIDS / SSDT. Wenn Sie das Paket bearbeiten, können Sie die Reihenfolge steuern, in der die Tabellen verarbeitet werden, sodass Sie die übergeordneten Tabellen verarbeiten und dann die untergeordneten Tabellen verarbeiten können, wenn alle übergeordneten Tabellen fertig sind.

SqlWorldWide
quelle
Dadurch werden nur Daten in eine Tabelle eingefügt. Es wird nicht die Frage behandelt, wie die FK-Beziehung beibehalten werden kann, wenn die neue PK vor der Laufzeit nicht bekannt ist.
Kevin
1
Die Frage enthält bereits zwei Tabellen mit einer Beziehung. Und ja, ich exportiere aus beiden Tabellen. (Nichts für ungut, aber nicht sicher, wie Sie es verpasst haben ... )
Kevin
@ SQLWorldWide diese Frage scheint etwas verwandt, aber nicht identisch. Auf welche der Antworten beziehen Sie sich hier als Lösung für das Problem?
Ypercubeᵀᴹ