web-dev-qa-db-fra.com

Comment copier migrer des données vers de nouvelles tables avec une colonne d'identité, tout en préservant la relation FK?

Je souhaite migrer les données d'une base de données vers une autre. Les schémas de table sont exactement les mêmes:

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

Les deux bases de données ont des données différentes, donc la nouvelle clé d'identité pour la même table serait différente dans les deux bases de données. Ce n'est pas un problème; mon objectif est d'ajouter de nouvelles données à celles existantes, et non de remplacer toutes les données de la table entière. Cependant, je voudrais conserver toutes les relations parent-enfant des données insérées.

Si j'utilise la fonction "Générer un script" de SSMS, le script tentera d'insérer en utilisant le même ID, ce qui entrerait en conflit avec les données existantes dans la base de données de destination. Comment puis-je copier des données à l'aide de scripts de base de données uniquement?

Je veux que la colonne d'identité à la destination continue normalement à partir de sa dernière valeur.

Customers n'a pas d'autre UNIQUE NOT NULL contrainte. Il est correct d'avoir des données en double dans d'autres colonnes (j'utilise Customers et Orders juste comme exemple ici, donc je n'ai pas à expliquer toute l'histoire). La question concerne toute relation un-à-N.

8
kevin

Voici un moyen qui s'adapte facilement à trois tableaux associés.

Utilisez MERGE pour insérer les données dans les tables de copie afin de pouvoir SORTIR les anciennes et nouvelles valeurs IDENTITY dans une table de contrôle et les utiliser pour le mappage des tables associées.

La réponse réelle est juste deux instructions de création de table et trois fusions. Le reste est un exemple de configuration et de démontage des données.

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

Quand je l'ai fait dans le passé, je l'ai fait quelque chose comme ça:

  • Sauvegardez les deux bases de données.

  • Copiez les lignes que vous souhaitez déplacer de la première base de données vers la seconde dans une nouvelle table, sans colonne IDENTITY.

  • Copiez toutes les lignes enfants de ces lignes dans de nouvelles tables sans clés étrangères dans la table parent.

Remarque: Nous désignerons l'ensemble de tableaux ci-dessus comme "temporaire"; cependant, je vous recommande fortement de les stocker dans leur propre base de données et de les sauvegarder également lorsque vous avez terminé.

  • Déterminez le nombre de valeurs d'ID dont vous avez besoin dans la deuxième base de données pour les lignes de la première base de données.
  • Utilisez DBCC CHECKIDENT Pour déplacer la prochaine valeur IDENTITY pour la table cible à 1 au-delà de ce dont vous avez besoin pour le déplacement. Cela laissera un bloc ouvert de valeurs X IDENTITY que vous pouvez affecter aux lignes en cours de transfert à partir de la première base de données.
  • Configurez une table de mappage, identifiant l'ancienne valeur IDENTITY pour les lignes du premier DB et la nouvelle valeur qu'elles utiliseront dans le deuxième DB.
  • Exemple: vous déplacez 473 lignes qui auront besoin d'une nouvelle valeur IDENTITY de la première base de données à la seconde. Par DBCC CHECKIDENT, La prochaine valeur d'identité pour cette table dans la deuxième base de données est actuellement 1128. Utilisez DBCC CHECKIDENT Pour redéfinir la valeur à 1601. Vous remplissez ensuite votre table de mappage avec les valeurs actuelles de la colonne IDENTITY de votre table parent en tant qu'anciennes valeurs, et utilisez ROW_NUMBER() pour attribuer les numéros 1128 à 1600 comme nouvelles valeurs.

  • À l'aide de la table de mappage, mettez à jour les valeurs dans ce qui est généralement la colonne IDENTITY de la table parent temporaire.

  • À l'aide de la table de mappage, mettez à jour les valeurs qui sont généralement des clés étrangères à la table parent, dans toutes les copies des tables enfants.
  • À l'aide de SET IDENTITY_INSERT <parent> ON, Insérez les lignes parent mises à jour de la table parent temporaire dans la deuxième base de données.
  • Insérez les lignes enfant mises à jour des tables enfant temporaires dans la deuxième base de données.

REMARQUE: si certaines des tables enfants ont leurs propres valeurs IDENTITY, cela devient assez compliqué. Mes scripts réels (partiellement développés par un fournisseur, donc je ne peux pas vraiment les partager) traitent des dizaines de tables et de colonnes de clé primaire, y compris certaines qui n'étaient pas des valeurs numériques à incrémentation automatique. Cependant, ce sont les étapes de base.

J'ai conservé les tables de mappage, post-migration, qui avaient l'avantage de nous permettre de trouver un "nouveau" enregistrement basé sur un ancien ID.

Ce n'est pas pour les faibles de cœur, et doit, doit, doit être testé (idéalement plusieurs fois) dans un environnement de test.

MISE À JOUR: Je dois également dire que, même avec cela, je ne me suis pas trop inquiété du "gaspillage" des valeurs d'identification. En fait, j'ai configuré mes blocs d'identification dans la deuxième base de données pour qu'ils soient 2-3 valeurs plus grandes que ce dont j'avais besoin, pour m'assurer que je ne heurterais pas accidentellement les valeurs existantes.

Je comprends certainement ne pas vouloir ignorer des centaines de milliers d'identifiants valides potentiels au cours de ce processus, surtout si le processus sera répété (le mien a finalement été exécuté un total d'environ 20 fois au cours des 30 mois). Cela dit, en général, on ne peut pas compter sur des valeurs d'ID à incrémentation automatique pour être séquentielles sans lacunes. Lorsqu'une ligne est créée et annulée, la valeur d'incrémentation automatique de cette ligne disparaît; la prochaine ligne ajoutée aura la valeur suivante, et celle de la ligne annulée sera ignorée.

2
RDFozz

J'utilise une table de la base de données WideWorldImporters qui est la nouvelle base de données exemple de Microsoft. De cette façon, vous pouvez exécuter mon script tel quel. Vous pouvez télécharger une sauvegarde de cette base de données depuis ici .

Table source (elle existe dans l'exemple avec les données).

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

Table de destination:

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

Exportation en cours sans valeur de colonne d'identité. Remarquez que je n'insère pas dans la colonne d'identité VehicleTemperatureID et que je ne sélectionne pas la même chose.

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

Pour répondre à la seconde de la question sur les contraintes FK, veuillez consulter this post. Surtout la section ci-dessous.

Ce que vous devez faire est d'enregistrer le package SSIS créé par l'assistant, puis de le modifier dans BIDS/SSDT. Lorsque vous modifiez le package, vous pourrez contrôler l'ordre dans lequel les tables sont traitées afin que vous puissiez traiter les tables parentes puis traiter les tables enfants lorsque toutes les tables parentes sont terminées.

0
SqlWorldWide