Como copiar dados de migração para novas tabelas com coluna de identidade, preservando o relacionamento FK?

8

Eu quero migrar dados de um banco de dados para outro. Os esquemas da tabela são exatamente os mesmos:

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

Os dois bancos de dados têm dados diferentes; portanto, a nova chave de identidade para a mesma tabela seria diferente nos dois bancos de dados. Isso não é um problema; meu objetivo é acrescentar novos dados aos existentes, não concluindo a substituição de todos os dados da tabela inteira. No entanto, eu gostaria de manter toda a relação pai-filho dos dados inseridos.

Se eu usar o recurso "Gerar script" do SSMS, o script tentará inserir usando o mesmo ID, o que entraria em conflito com os dados existentes no banco de dados de destino. Como copiar dados usando apenas scripts de banco de dados?

Quero que a coluna de identidade no destino continue normalmente a partir do seu último valor.

Customersnão tem nenhuma outra UNIQUE NOT NULLrestrição. Não há problema em duplicar dados em outras colunas (estou usando Customerse Ordersapenas como exemplo aqui, para não precisar explicar a história toda). A questão é sobre qualquer relacionamento um-para-N.

Kevin
fonte

Respostas:

11

Aqui está uma maneira de escalar facilmente para três tabelas relacionadas.

Use MERGE para inserir os dados nas tabelas de cópia, para que você possa OUTPUT os valores antigos e novos de IDENTITY em uma tabela de controle e use-os para o mapeamento de tabelas relacionadas.

A resposta real é apenas duas instruções de criação de tabela e três mesclagens. O restante é exemplo de configuração de dados e desmontagem.

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;
Senhor Magoo
fonte
OMG você salvou minha vida. Poderia, por favor, adicionar um pouco mais de filtro como 'apenas copiar para o banco de dados 1 quando Orders2 tiver mais de 2 itens'
Anh Bảy
2

Quando eu fiz isso no passado, fiz algo assim:

  • Faça backup dos dois bancos de dados.

  • Copie as linhas que você deseja mover do primeiro banco de dados para o segundo em uma nova tabela, sem uma IDENTITYcoluna.

  • Copie todas as linhas filho dessas linhas em novas tabelas sem chaves estrangeiras para a tabela pai.

Nota: Vamos nos referir ao conjunto de tabelas acima como "temporário"; no entanto, eu recomendo que você os armazene em seu próprio banco de dados e faça o backup também quando terminar.

  • Determine quantos valores de ID são necessários no segundo banco de dados para linhas do primeiro banco de dados.
  • Use DBCC CHECKIDENTpara mudar o próximo IDENTITYvalor da tabela de destino para 1 além do necessário para a movimentação. Isso deixará um bloco aberto de IDENTITYvalores X que você pode atribuir às linhas que estão sendo trazidas do primeiro banco de dados.
  • Configure uma tabela de mapeamento, identificando o IDENTITYvalor antigo para as linhas do primeiro banco de dados e o novo valor que eles usarão no segundo banco de dados.
  • Exemplo: você está movendo 473 linhas que precisarão de um novo IDENTITYvalor do primeiro banco de dados para o segundo. Por DBCC CHECKIDENT, o próximo valor de identidade para essa tabela no segundo banco de dados é 1128 no momento. Use DBCC CHECKIDENTpara redefinir o valor para 1601. Você preencherá sua tabela de mapeamento com os valores atuais da IDENTITYcoluna da tabela pai como valores antigos e use a ROW_NUMBER()função para atribuir os números 1128 a 1600 como os novos valores.

  • Usando a tabela de mapeamento, atualize os valores que normalmente são a IDENTITYcoluna na tabela pai temporária.

  • Usando a tabela de mapeamento, atualize os valores que geralmente são chaves estrangeiras para a tabela pai, em todas as cópias das tabelas filho.
  • Usando SET IDENTITY_INSERT <parent> ON, insira as linhas pai atualizadas da tabela pai temporária no segundo banco de dados.
  • Insira as linhas filho atualizadas das tabelas filho temporárias no segundo banco de dados.

NOTA: Se algumas das tabelas filho tiverem IDENTITYvalores próprios, isso ficará bastante complicado. Meus scripts reais (parcialmente desenvolvidos por um fornecedor, portanto não posso compartilhá-los) lidam com dezenas de tabelas e colunas de chave primária, incluindo algumas que não eram valores numéricos de incremento automático. No entanto, estas são as etapas básicas.

Eu mantive as tabelas de mapeamento, pós-migração, que tinham o benefício de nos permitir encontrar um "novo" registro com base em um ID antigo.

Não é para os fracos de coração, e deve, deve, deve ser testado (idealmente várias vezes) em um ambiente de teste.

UPDATE: Também devo dizer que, mesmo com isso, não me preocupei demais com "desperdiçar" valores de ID. Na verdade, configurei meus blocos de ID no segundo banco de dados para serem 2-3 valores maiores que o necessário, para tentar garantir que não colidisse acidentalmente com os valores existentes.

Eu certamente entendo que não quero pular centenas de milhares de possíveis IDs válidos durante esse processo, especialmente se o processo for repetido (o meu acabou sendo executado um total geral de cerca de 20 vezes ao longo de 30 meses). Dito isto, em geral, não se pode confiar nos valores de ID de auto incremento para serem sequenciais sem lacunas. Quando uma linha é criada e revertida, o valor de incremento automático dessa linha desaparece; a próxima linha adicionada terá o próximo valor e a da linha revertida será ignorada.

RDFozz
fonte
Obrigado. Eu entendi a ideia, basicamente pré-alocando um bloco de valores de IDENTITY, depois alterei manualmente os valores em um conjunto de tabelas temporárias até que correspondam ao destino e insira. No entanto, para o meu cenário, a tabela filho tem sua coluna IDENTITY (na verdade, tenho que mover três tabelas, com duas relações 1-N entre elas). Isso torna bastante complicado, mas eu aprecio a ideia.
kevin
11
As tabelas filho são pais de outras tabelas? É quando as coisas ficam complicadas.
RDFozz
Pense como Customer-Order-OrderItemou Country-State-City. As três tabelas, quando agrupadas, são independentes.
kevin
0

Estou usando uma tabela do WideWorldImportersbanco de dados que é o novo banco de dados de exemplo da Microsoft. Dessa forma, você pode executar meu script como está. Você pode baixar um backup desse banco de dados aqui .

Tabela de origem (isso existe na amostra com dados).

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

Tabela 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

Agora, faça a exportação sem o valor da coluna de identidade. Observe que não estou inserindo na coluna de identidade VehicleTemperatureIDe também não selecionando a mesma.

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 à segunda pergunta sobre restrições de FK, consulte este post. Especialmente seção abaixo.

O que você deve fazer é salvar o pacote SSIS que o assistente cria e depois editá-lo em BIDS / SSDT. Ao editar o pacote, você poderá controlar a ordem em que as tabelas são processadas para poder processar as tabelas pai e, em seguida, processar as tabelas filho quando todas as tabelas pai estiverem concluídas.

SqlWorldWide
fonte
Isso insere apenas dados em uma tabela. Ele não aborda a questão de como preservar o relacionamento FK quando o novo PK não é conhecido antes do tempo de execução.
kevin
11
Já existem duas tabelas na pergunta, com um relacionamento. E sim, estou exportando das duas tabelas. (Sem ofensa, mas não sei como você perdeu ... )
kevin
@SqlWorldWide essa pergunta parece um pouco relacionada, mas não é idêntica. A quais das respostas você está se referindo como uma solução para o problema aqui?
ypercubeᵀᴹ