Come copiare i dati migrati su nuove tabelle con colonna Identity, preservando la relazione FK?


8

Voglio migrare i dati da un database all'altro. Gli schemi delle tabelle sono esattamente gli stessi:

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

I due database hanno dati diversi, quindi la nuova chiave di identità per la stessa tabella sarebbe diversa nei due database. Questo non è un problema; il mio obiettivo è aggiungere nuovi dati a quello esistente, non completare la sostituzione di tutti i dati dell'intera tabella. Tuttavia, vorrei mantenere tutte le relazioni genitore-figlio dei dati inseriti.

Se utilizzo la funzione "Genera script" di SSMS, lo script tenterebbe di inserire utilizzando lo stesso ID, in conflitto con i dati esistenti nel database di destinazione. Come posso copiare i dati usando solo gli script del database?

Voglio che la colonna identità nella destinazione continui normalmente dal suo ultimo valore.

Customersnon ha altri UNIQUE NOT NULLvincoli. Va bene avere dati duplicati in altre colonne (sto usando Customerse Orderssolo come esempio qui, quindi non devo spiegare l'intera storia). La domanda riguarda qualsiasi relazione uno-a-N.

Risposte:


11

Ecco un modo che si adatta facilmente a tre tabelle correlate.

Utilizzare MERGE per inserire i dati nelle tabelle di copia in modo da poter OUTPUT i valori di IDENTITÀ vecchi e nuovi in ​​una tabella di controllo e utilizzarli per il mapping delle tabelle correlate.

La risposta effettiva è solo due istruzioni create per la tabella e tre combinazioni. Il resto è la configurazione dei dati di esempio e la demolizione.

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;

Oh mio Dio, mi hai salvato la vita. Potresti aggiungere un altro filtro come 'copia solo nel Database 1 quando Order2 ha più di 2 articoli'
Anh Bảy

2

Quando l'ho fatto in passato, l'ho fatto in questo modo:

  • Eseguire il backup di entrambi i database.

  • Copia le righe che desideri spostare dal primo DB al secondo in una nuova tabella, senza una IDENTITYcolonna.

  • Copia tutte le righe figlio di tali righe in nuove tabelle senza chiavi esterne nella tabella padre.

Nota: faremo riferimento al set di tabelle sopra come "temporaneo"; tuttavia, consiglio vivamente di archiviarli nel proprio database e di eseguire il backup anche quando hai finito.

  • Determinare quanti valori ID sono necessari dal secondo database per le righe dal primo database.
  • Utilizzare DBCC CHECKIDENTper spostare il IDENTITYvalore successivo per la tabella di destinazione su 1 oltre a quello necessario per lo spostamento. Ciò lascerà un blocco aperto di IDENTITYvalori X che è possibile assegnare alle righe trasferite dal primo database.
  • Impostare una tabella di mappatura, identificando il vecchio IDENTITYvalore per le righe del primo DB e il nuovo valore che useranno nel secondo DB.
  • Esempio: stai spostando 473 righe che avranno bisogno di un nuovo IDENTITYvalore dal primo database al secondo. Per DBCC CHECKIDENT, il prossimo valore di identità per quella tabella nel secondo database è 1128 in questo momento. Utilizzare DBCC CHECKIDENTper ridimensionare il valore a 1601. Quindi si popoleranno la tabella di mappatura con i valori correnti per la IDENTITYcolonna dalla tabella principale come valori precedenti e si utilizzerà la ROW_NUMBER()funzione per assegnare i numeri da 1128 a 1600 come nuovi valori.

  • Utilizzando la tabella di mapping, aggiorna i valori in quella che di solito è la IDENTITYcolonna nella tabella padre temporanea.

  • Usando la tabella di mappatura, aggiorna i valori che di solito sono chiavi esterne alla tabella padre, in tutte le copie delle tabelle figlio.
  • Utilizzando SET IDENTITY_INSERT <parent> ON, inserire le righe padre aggiornate dalla tabella padre temporanea nel secondo DB.
  • Inserire le righe figlio aggiornate dalle tabelle figlio temporanee nel secondo DB.

NOTA: se alcune delle tabelle figlio hanno IDENTITYvalori propri, questo diventa piuttosto complicato. I miei script effettivi (parzialmente sviluppati da un fornitore, quindi non riesco davvero a condividerli) riguardano dozzine di tabelle e colonne di chiavi primarie, incluse alcune che non erano valori numerici con incremento automatico. Tuttavia, questi sono i passaggi di base.

Ho mantenuto le tabelle di mappatura, dopo la migrazione, che hanno avuto il vantaggio di permetterci di trovare un "nuovo" record basato su un vecchio ID.

Non è per i deboli di cuore e deve, deve, deve essere testato (idealmente più volte) in un ambiente di prova.

AGGIORNAMENTO: Dovrei anche dire che, anche con questo, non mi sono preoccupato troppo di "sprecare" i valori ID. In realtà ho impostato i miei blocchi ID nel secondo database in modo che siano 2-3 valori più grandi di quelli di cui avevo bisogno, per cercare di assicurarmi di non scontrarmi accidentalmente con i valori esistenti.

Sicuramente capisco di non voler saltare centinaia di migliaia di potenziali ID validi durante questo processo, specialmente se il processo verrà ripetuto (il mio alla fine è stato eseguito per un totale di circa 20 volte nel corso di 30 mesi). Detto questo, in generale, non si può fare affidamento sul fatto che i valori ID dell'incremento automatico siano sequenziali senza lacune. Quando viene creata e ripristinata una riga, il valore di incremento automatico per quella riga scompare; la riga successiva aggiunta avrà il valore successivo e quella della riga ripristinata verrà ignorata.


Grazie. Ho avuto l'idea, fondamentalmente pre-allocando un blocco di valori IDENTITY, quindi alterando manualmente i valori in un set di tabelle temporanee fino a quando non corrispondono alla destinazione, quindi inserisco. Per il mio scenario, la tabella figlio ha la sua colonna IDENTITY (in realtà devo spostare tre tabelle, con due relazioni 1-N tra di loro). Questo rende abbastanza complicato, ma apprezzo l'idea.
Kevin,

1
Le tabelle figlio sono genitori di altre tabelle? Questo è quando le cose si complicano.
RDFozz,

Pensa come Customer-Order-OrderItemo Country-State-City. Le tre tabelle, quando raggruppate insieme, sono autonome.
Kevin,

0

Sto usando una tabella dal WideWorldImportersdatabase che è il nuovo database di esempio di Microsoft. In questo modo puoi eseguire il mio script così com'è. È possibile scaricare un backup di questo database da qui .

Tabella di origine (esiste nel campione con dati).

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

Tabella di destinazione:

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

Ora eseguo l'esportazione senza il valore della colonna identità. Nota che non inserisco nella colonna identità VehicleTemperatureIDe non seleziono dalla stessa.

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

Per rispondere alla seconda domanda sui vincoli di FK, consulta questo post. Soprattutto la sezione seguente.

Quello che dovresti fare è salvare il pacchetto SSIS creato dalla procedura guidata, quindi modificarlo in BIDS / SSDT. Quando modifichi il pacchetto sarai in grado di controllare l'ordine in cui vengono elaborate le tabelle in modo da poter elaborare le tabelle padre quindi elaborare le tabelle figlio quando tutte le tabelle principali sono state completate.


Ciò inserisce solo i dati in una tabella. Non affronta la questione di come preservare la relazione FK quando il nuovo PK non è noto prima del runtime.
Kevin,

1
Ci sono già due tabelle nella domanda, con una relazione. E sì, sto esportando da entrambe le tabelle. (Senza offesa, ma non sono sicuro di come ti sia perso ... )
Kevin

@SqlWorld Questa domanda sembra in qualche modo correlata ma non identica. A quale delle risposte ti riferisci come soluzione al problema qui?
ypercubeᵀᴹ
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.