Как скопировать данные переноса в новые таблицы со столбцом идентификаторов, сохранив при этом связь FK?

8

Я хочу перенести данные из одной базы данных в другую. Схемы таблиц точно такие же:

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

Две базы данных имеют разные данные, поэтому новый ключ идентификации для одной и той же таблицы будет отличаться в двух базах данных. Это не проблема; Моя цель - добавить новые данные к существующим, а не завершить замену всех данных всей таблицы. Однако я хотел бы сохранить все родительско-дочерние отношения вставленных данных.

Если я использую функцию «Создать сценарий» в SSMS, сценарий попытается вставить с использованием того же идентификатора, что приведет к конфликту существующих данных в целевой базе данных. Как я могу копировать данные, используя только сценарии базы данных?

Я хочу, чтобы столбец идентификатора в месте назначения продолжал нормально с его последнего значения.

Customersне имеет никаких других UNIQUE NOT NULLограничений. Вполне допустимо иметь дубликаты данных в других столбцах (я использую Customersи Ordersпросто в качестве примера здесь, поэтому мне не нужно объяснять всю историю). Вопрос о каких-либо однозначных отношениях.

Кевин
источник

Ответы:

11

Вот способ, который легко масштабируется до трех связанных таблиц.

Используйте MERGE для вставки данных в таблицы копирования, чтобы вы могли ВЫХОДИТЬ старые и новые значения IDENTITY в управляющую таблицу и использовать их для сопоставления связанных таблиц.

Фактический ответ - просто два оператора создания таблицы и три слияния. Остальное - пример настройки данных и демонтажа.

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;
Мистер магу
источник
О боже, ты спас мне жизнь. Не могли бы вы добавить еще один фильтр, например, «копировать только в базу данных 1, когда у Orders2 более 2 предметов»
Anh Bảy
2

Когда я делал это в прошлом, я делал это примерно так:

  • Резервное копирование обеих баз данных.

  • Скопируйте строки, которые вы хотите переместить из первой БД во вторую таблицу, без IDENTITYстолбца.

  • Скопируйте все дочерние строки этих строк в новые таблицы без внешних ключей в родительскую таблицу.

Примечание. Мы будем называть приведенный выше набор таблиц «временным»; тем не менее, я настоятельно рекомендую вам сохранить их в своей собственной базе данных, а также сделать резервную копию, когда вы закончите.

  • Определите, сколько значений идентификаторов вам нужно из второй базы данных для строк из первой базы данных.
  • Используйте DBCC CHECKIDENTдля смещения следующего IDENTITYзначения для целевой таблицы на 1 сверх того, что вам нужно для перемещения. Это оставит открытый блок IDENTITYзначений X, который вы можете назначить строкам, перенесенным из первой базы данных.
  • Настройте таблицу сопоставления, указав старое IDENTITYзначение для строк из первой БД и новое значение, которое они будут использовать во второй БД.
  • Пример: вы перемещаете 473 строки, которым потребуется новое IDENTITYзначение из первой базы данных во вторую. Во-первых DBCC CHECKIDENT, следующее значение идентификатора для этой таблицы во второй базе данных сейчас - 1128. Используйте DBCC CHECKIDENTдля повторного заполнения значения до 1601. Затем вы заполните таблицу сопоставления текущими значениями для IDENTITYстолбца из родительской таблицы как старые значения и воспользуйтесь ROW_NUMBER()функцией, чтобы назначить числа от 1128 до 1600 в качестве новых значений.

  • Используя таблицу сопоставления, обновите значения в том, что обычно является IDENTITYстолбцом во временной родительской таблице.

  • Используя таблицу сопоставления, обновите значения, которые обычно являются внешними ключами, для родительской таблицы во всех копиях дочерних таблиц.
  • Используя SET IDENTITY_INSERT <parent> ON, вставьте обновленные родительские строки из временной родительской таблицы во вторую БД.
  • Вставьте обновленные дочерние строки из временных дочерних таблиц во вторую БД.

ПРИМЕЧАНИЕ. Если некоторые дочерние таблицы имеют IDENTITYсобственные значения, это становится довольно сложным. Мои настоящие сценарии (частично разработанные поставщиком, поэтому я не могу ими поделиться) имеют дело с десятками таблиц и столбцов первичного ключа, включая те, которые не были автоматически увеличены числовыми значениями. Тем не менее, это основные шаги.

После переноса я сохранил таблицы сопоставления, что позволило нам найти «новую» запись на основе старого идентификатора.

Это не для слабонервных, и должны, должны, должны быть проверены (в идеале несколько раз) в тестовой среде.

ОБНОВЛЕНИЕ: Я должен также сказать, что, даже с этим, я не слишком беспокоился о «потере» значений ID. Я фактически установил свои блоки идентификаторов во второй базе данных так, чтобы они были на 2-3 значения больше, чем мне было нужно, чтобы убедиться, что я случайно не столкнусь с существующими значениями.

Я, конечно, понимаю, что не хочу пропустить сотни тысяч потенциальных действительных идентификаторов во время этого процесса, особенно если этот процесс будет повторен (в конечном итоге мой был запущен всего около 20 раз в течение 30 месяцев). При этом, как правило, нельзя полагаться на то, что значения идентификатора автоматического увеличения будут последовательными без пропусков. Когда строка создается и откатывается, значение автоинкремента для этой строки исчезает; следующая добавленная строка будет иметь следующее значение, а та из откатной строки будет пропущена.

RDFozz
источник
Спасибо. Я получил идею, в основном предварительно выделив блок значений IDENTITY, затем вручную изменил значения в наборе временных таблиц, пока они не соответствуют назначению, а затем вставил. Однако для моего сценария дочерняя таблица имеет столбец IDENTITY (на самом деле мне нужно переместить три таблицы с двумя отношениями 1-N между ними). Это делает его довольно сложным, но я ценю эту идею.
Кевин
1
Являются ли дочерние столы родителями для других столов? Вот когда все усложняется.
RDFozz
Думай как Customer-Order-OrderItemили Country-State-City. Три таблицы, сгруппированные вместе, являются автономными.
Кевин
0

Я использую таблицу из WideWorldImportersбазы данных, которая представляет собой новый образец базы данных от Microsoft. Таким образом, вы можете запустить мой скрипт как есть. Вы можете скачать резервную копию этой базы данных здесь .

Исходная таблица (существует в образце с данными).

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

Таблица назначения:

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

Сейчас делаем экспорт без значения идентификатора столбца. Обратите внимание, что я не вставляю в столбец идентификаторов, VehicleTemperatureIDа также не выбираю из этого же.

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

Чтобы ответить на второй вопрос об ограничениях ФК, см. Этот пост. Особенно раздел ниже.

Вам нужно сохранить пакет служб SSIS, созданный мастером, а затем отредактировать его в BIDS / SSDT. Когда вы редактируете пакет, вы сможете контролировать порядок обработки таблиц, чтобы вы могли обрабатывать родительские таблицы, а затем обрабатывать дочерние таблицы, когда все родительские таблицы готовы.

SqlWorldWide
источник
Это только вставляет данные в одну таблицу. В нем не рассматривается вопрос о том, как сохранить отношения FK, когда новый PK неизвестен до времени выполнения.
Кевин
1
В вопросе уже есть две таблицы со связями. И да, я экспортирую из обеих таблиц. (Без обид, но не знаю , как вы пропустили это ... )
Кевином
@SqlWorldWide этот вопрос кажется несколько связанным, но не идентичным. Какой из ответов вы называете решением проблемы здесь?
ypercubeᵀᴹ