¿Cómo copiar datos de migración a nuevas tablas con columna de identidad, mientras se preserva la relación FK?

8

Quiero migrar datos de una base de datos a otra. Los esquemas de la tabla son exactamente iguales:

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

Las dos bases de datos tienen datos diferentes, por lo que la nueva clave de identidad para la misma tabla sería diferente en las dos bases de datos. Eso no es problema; mi objetivo es agregar nuevos datos a los existentes, no completar el reemplazo de todos los datos de toda la tabla. Sin embargo, me gustaría mantener toda la relación padre-hijo de los datos insertados.

Si uso la función "Generar secuencia de comandos" de SSMS, la secuencia de comandos intentará insertar usando la misma ID, lo que entraría en conflicto con los datos existentes en la base de datos de destino. ¿Cómo puedo copiar datos usando scripts de base de datos solamente?

Quiero que la columna de identidad en el destino continúe normalmente desde su último valor.

Customersno tiene ninguna otra UNIQUE NOT NULLrestricción. Está bien tener datos duplicados en otras columnas (estoy usando Customersy Orderssolo como ejemplo aquí, así que no tengo que explicar toda la historia). La pregunta es sobre cualquier relación de uno a N.

Kevin
fuente

Respuestas:

11

Aquí hay una forma que se escala fácilmente a tres tablas relacionadas.

Use MERGE para insertar los datos en las tablas de copia para que pueda SALIR los valores de IDENTIDAD antiguos y nuevos en una tabla de control y usarlos para la asignación de tablas relacionadas.

La respuesta real es solo dos crear declaraciones de tabla y tres fusiones. El resto es configuración de datos de muestra y desmontaje.

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;
Señor magoo
fuente
Dios mío, me salvaste la vida. Podría agregar un filtro más, como 'solo copiar a la Base de datos 1 cuando Orders2 tiene más de 2 artículos'
Anh Por
2

Cuando hice esto en el pasado, hice algo como esto:

  • Copia de seguridad de ambas bases de datos.

  • Copie las filas que desea mover del primer DB al segundo en una nueva tabla, sin una IDENTITYcolumna.

  • Copie todas las filas secundarias de esas filas en nuevas tablas sin claves foráneas en la tabla primaria.

Nota: Nos referiremos al conjunto de tablas anterior como "temporal"; sin embargo, le recomiendo que los almacene en su propia base de datos y que los respalde también cuando haya terminado.

  • Determine cuántos valores de ID necesita de la segunda base de datos para las filas de la primera base de datos.
  • Use DBCC CHECKIDENTpara cambiar el siguiente IDENTITYvalor de la tabla de destino a 1 más allá de lo que necesita para el movimiento. Esto dejará un bloque abierto de IDENTITYvalores X que puede asignar a las filas que se traen de la primera base de datos.
  • Configure una tabla de mapeo, identificando el IDENTITYvalor anterior para las filas del primer DB y el nuevo valor que usarán en el segundo DB.
  • Ejemplo: está moviendo 473 filas que necesitarán un nuevo IDENTITYvalor de la primera base de datos a la segunda. Por DBCC CHECKIDENT, el siguiente valor de identidad para esa tabla en la segunda base de datos es 1128 en este momento. Use DBCC CHECKIDENTpara reiniciar el valor a 1601. Luego completará su tabla de mapeo con los valores actuales para la IDENTITYcolumna de su tabla principal como valores antiguos, y usará la ROW_NUMBER()función para asignar los números 1128 a 1600 como los nuevos valores.

  • Usando la tabla de mapeo, actualice los valores en lo que generalmente es la IDENTITYcolumna en la tabla primaria temporal.

  • Utilizando la tabla de asignación, actualice los valores que generalmente son claves foráneas para la tabla primaria, en todas las copias de las tablas secundarias.
  • Utilizando SET IDENTITY_INSERT <parent> ON, inserte las filas primarias actualizadas de la tabla primaria temporal en la segunda base de datos.
  • Inserte las filas secundarias actualizadas de las tablas secundarias temporales en la segunda base de datos.

NOTA: Si algunas de las tablas secundarias tienen IDENTITYsus propios valores, esto se vuelve bastante complicado. Mis scripts reales (parcialmente desarrollados por un proveedor, por lo que realmente no puedo compartirlos) tratan con docenas de tablas y columnas de clave principal, incluidas algunas que no eran valores numéricos de incremento automático. Sin embargo, estos son los pasos básicos.

Conservé las tablas de mapeo, después de la migración, que tenían el beneficio de permitirnos encontrar un "nuevo" registro basado en una identificación anterior.

No es para los débiles de corazón, y debe, debe, debe probarse (idealmente varias veces) en un entorno de prueba.

ACTUALIZACIÓN: También debería decir que, incluso con esto, no me preocupé demasiado por "desperdiciar" los valores de ID. De hecho, configuré mis bloques de ID en la segunda base de datos para que sean 2-3 valores más grandes de lo que necesitaba, para intentar asegurarme de que no colisionaría accidentalmente con los valores existentes.

Ciertamente entiendo que no quiero omitir cientos de miles de identificaciones válidas potenciales durante este proceso, especialmente si el proceso se repetirá (el mío finalmente se ejecutó un total de alrededor de 20 veces en el transcurso de 30 meses). Dicho esto, en general, no se puede confiar en que los valores de ID de incremento automático sean secuenciales sin espacios. Cuando se crea y se revierte una fila, el valor de incremento automático para esa fila desaparece; la siguiente fila agregada tendrá el siguiente valor, y se saltará el de la fila revertida.

RDFozz
fuente
Gracias. Tengo la idea, básicamente preasignar un bloque de valores IDENTITY, luego alterar manualmente los valores en un conjunto de tablas temporales hasta que coincidan con el destino, luego insertar. Sin embargo, para mi escenario, la tabla secundaria tiene su columna IDENTIDAD (en realidad tengo que mover tres tablas, con dos relaciones 1-N entre ellas). Esto lo hace bastante complicado, pero aprecio la idea.
Kevin
1
¿El niño coloca a los padres en otras mesas? Ahí es cuando las cosas se complican.
RDFozz
Piensa como Customer-Order-OrderItemo Country-State-City. Las tres tablas, cuando se agrupan, son independientes.
Kevin
0

Estoy usando una tabla de la WideWorldImportersbase de datos que es la nueva base de datos de muestra de Microsoft. De esa manera puedes ejecutar mi script tal como está. Puede descargar una copia de seguridad de esta base de datos desde aquí .

Tabla fuente (esta existe en una muestra con datos).

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

Tabla de destino:

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

Ahora haciendo la exportación sin valor de columna de identidad. Tenga en cuenta que no estoy insertando en la columna de identidad VehicleTemperatureIDy tampoco seleccionando de la misma.

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

Para responder la segunda pregunta sobre las restricciones de FK, consulte esta publicación. Especialmente la sección a continuación.

Lo que debe hacer es guardar el paquete SSIS que crea el asistente y luego editarlo en BIDS / SSDT. Cuando edite el paquete, podrá controlar el orden en que se procesan las tablas para poder procesar las tablas primarias y luego procesar las tablas secundarias cuando todas las tablas primarias estén listas.

SqlWorldWide
fuente
Esto solo inserta datos en una tabla. No aborda la cuestión de cómo preservar la relación FK cuando la nueva PK no se conoce antes del tiempo de ejecución.
Kevin
1
Ya hay dos tablas en la pregunta, con una relación. Y sí, estoy exportando desde ambas tablas. (Sin ofender, pero no estoy seguro de cómo te lo perdiste ... )
Kevin
@SqlWorldWide esa pregunta parece algo relacionada pero no idéntica. ¿A cuál de las respuestas te refieres como una solución al problema aquí?
ypercubeᵀᴹ