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

8

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.

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

Kevin
la source

Réponses:

11

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.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;
Monsieur Magoo
la source
OMG tu m'as sauvé la vie. Pourriez-vous s'il vous plaît ajouter un filtre supplémentaire comme «ne copier que dans la base de données 1 lorsque Orders2 a plus de 2 éléments»
Anh Bảy
2

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 IDENTITYcolonne.

  • 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 CHECKIDENTpour déplacer la IDENTITYvaleur suivante de la table cible à 1 au-delà de ce dont vous avez besoin pour le déplacement. Cela laissera un bloc ouvert de IDENTITYvaleurs X que vous pouvez attribuer aux lignes importées de la première base de données.
  • Configurez une table de mappage, identifiant l'ancienne IDENTITYvaleur des 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 IDENTITYvaleur 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 CHECKIDENTpour réaménager la valeur à 1601. Vous remplissez ensuite votre table de mappage avec les valeurs actuelles de la IDENTITYcolonne de votre table parent en tant qu'anciennes valeurs et utilisez la ROW_NUMBER()fonction pour attribuer les nombres 1128 à 1600 comme nouvelles valeurs.

  • À l'aide de la table de mappage, mettez à jour les valeurs dans ce qui est généralement la IDENTITYcolonne 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 IDENTITYleurs propres valeurs, 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 de ne pas vouloir ignorer des centaines de milliers d'ID 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 en 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 ligne suivante ajoutée aura la valeur suivante et celle de la ligne annulée sera ignorée.

RDFozz
la source
Merci. J'ai eu l'idée, essentiellement de pré-allouer un bloc de valeurs IDENTITY, puis de modifier manuellement les valeurs dans un ensemble de tables temporaires jusqu'à ce qu'elles correspondent à la destination, puis de les insérer. Pour mon scénario cependant, la table enfant a sa colonne IDENTITY (je dois en fait déplacer trois tables, avec deux relations 1-N entre elles). Cela rend les choses assez compliquées, mais j'apprécie l'idée.
kevin
1
Les enfants sont-ils des parents pour d'autres tables? C'est alors que les choses se compliquent.
RDFozz
Pensez comme Customer-Order-OrderItemou Country-State-City. Les trois tables, lorsqu'elles sont regroupées, sont autonomes.
kevin
0

J'utilise une table de WideWorldImportersbase de données 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 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é. Notez que je n'insère pas dans la colonne d'identité VehicleTemperatureIDet 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 cet article. 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.

SqlWorldWide
la source
Cela insère uniquement des données dans une table. Il ne répond pas à la question de savoir comment préserver la relation FK lorsque le nouveau PK n'est pas connu avant l'exécution.
Kevin
1
Il y a déjà deux tableaux dans la question, avec une relation. Et oui, j'exporte à partir des deux tables. (Aucune infraction, mais je ne sais pas comment vous l'avez manqué ... )
Kevin
@SqlWorldWide, cette question semble quelque peu liée mais pas identique. À laquelle des réponses faites-vous référence comme solution au problème ici?
ypercubeᵀᴹ