Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to better duplicate a set of data in SQL Server

I have several related tables that I want to be able to duplicate some of the rows while updating the references.

I want to duplicate a row in Table1, and all of it's related rows from Table2 and Table3, and I'm trying to figure out an efficient way of doing it short of iterating through rows.

So for example, I have a table of baskets:

+----------+---------------+
| BasketId |  BasketName   |
+----------+---------------+
|        1 | Home Basket   |
|        2 | Office Basket |
+----------+---------------+

Each basket has fruit:

+---------+----------+-----------+
| FruitId | BasketId | FruitName |
+---------+----------+-----------+
|       1 |        1 | Apple     |
|       2 |        1 | Orange    |
|       3 |        2 | Mango     |
|       4 |        2 | Pear      |
+---------+----------+-----------+

And each fruit has some properties:

+------------+---------+--------------+
| PropertyId | FruitId | PropertyText |
+------------+---------+--------------+
|          1 |       2 | Is juicy     |
|          2 |       2 | Hard to peel |
|          3 |       1 | Is red       |
+------------+---------+--------------+

For this example, my properties are specific to the individual fruit row, these "apple" properties aren't properties of all apples in all baskets, just for that specific apple in that specific basket.

What I want to do is duplicate a basket. So given basket 1, I want to create a new basket, duplicate the fruit rows it contains, and duplicate the properties pointing to those fruits. In the end I'm hoping to have data like so:

+----------+---------------+
| BasketId |  BasketName   |
+----------+---------------+
|        1 | Home Basket   |
|        2 | Office Basket |
|        3 | Friends Basket|
+----------+---------------+

+---------+----------+-----------+
| FruitId | BasketId | FruitName |
+---------+----------+-----------+
|       1 |        1 | Apple     |
|       2 |        1 | Orange    |
|       3 |        2 | Mango     |
|       4 |        2 | Pear      |
|       5 |        3 | Apple     |
|       6 |        3 | Orange    |
+---------+----------+-----------+

+------------+---------+--------------+
| PropertyId | FruitId | PropertyText |
+------------+---------+--------------+
|          1 |       2 | Is juicy     |
|          2 |       2 | Hard to peel |
|          3 |       1 | Is red       |
|          4 |       6 | Is juicy     |
|          5 |       6 | Hard to peel |
|          6 |       5 | Is red       |
+------------+---------+--------------+

Duplicating the basket and it's fruit were pretty straightforward, but duplicating the properties of the fruit seems to me to lead to iterating over rows and I'm hoping there's a better solution in TSQL.

Any ideas?

like image 901
Mike Avatar asked Sep 27 '22 02:09

Mike


2 Answers

Why dont you join on the FruitName to get a table with old and new FruitId's? Considering information would be added at the same time.... it may not be the best option but you wont be using any cycles.

INSERT INTO BASKET(BASKETNAME)
VALUES ('COPY BASKET')

DECLARE @iBasketId int
SET @iBasketId = @@SCOPE_IDENTITY;


insert into Fruit (BasketId, FruitName)
select @iBasketId, FruitName
from Fruit 
where BasketId = @originalBasket

declare @tabFruit table (originalFruitId int, newFruitId int)

insert into @tabFruit (originalFruitId, newFruitId)
select o.FruitId, n.FruitId
from (SELECT FruitId, FruitName from Fruit where BasketId = @originalBasket) as o
join (SELECT FruitId, FruitName from Fruit where BasketId = @newBasket) as n
    on o.FruitName = n.FruitName


insert into Property (FruitId, PropertyText)
select NewFruitId, PropertyText
from Fruit f join @tabFruit t on t.originalFruitId = f.FruitId
like image 57
Tony O Avatar answered Oct 03 '22 06:10

Tony O


(ab)use MERGE with OUTPUT clause.

MERGE can INSERT, UPDATE and DELETE rows. In our case we need only to INSERT. 1=0 is always false, so the NOT MATCHED BY TARGET part is always executed. In general, there could be other branches, see docs. WHEN MATCHED is usually used to UPDATE; WHEN NOT MATCHED BY SOURCE is usually used to DELETE, but we don't need them here.

This convoluted form of MERGE is equivalent to simple INSERT, but unlike simple INSERT its OUTPUT clause allows to refer to the columns that we need.

I will write down the definitions of table explicitly. Each primary key in the tables is IDENTITY. I've configured foreign keys as well.

Baskets

CREATE TABLE [dbo].[Baskets](
    [BasketId] [int] IDENTITY(1,1) NOT NULL,
    [BasketName] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED 
(
    [BasketId] ASC
)

Fruits

CREATE TABLE [dbo].[Fruits](
    [FruitId] [int] IDENTITY(1,1) NOT NULL,
    [BasketId] [int] NOT NULL,
    [FruitName] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Fruits] PRIMARY KEY CLUSTERED 
(
    [FruitId] ASC
)

ALTER TABLE [dbo].[Fruits]  WITH CHECK 
ADD CONSTRAINT [FK_Fruits_Baskets] FOREIGN KEY([BasketId])
REFERENCES [dbo].[Baskets] ([BasketId])

ALTER TABLE [dbo].[Fruits] CHECK CONSTRAINT [FK_Fruits_Baskets]

Properties

CREATE TABLE [dbo].[Properties](
    [PropertyId] [int] IDENTITY(1,1) NOT NULL,
    [FruitId] [int] NOT NULL,
    [PropertyText] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Properties] PRIMARY KEY CLUSTERED 
(
    [PropertyId] ASC
)

ALTER TABLE [dbo].[Properties]  WITH CHECK 
ADD CONSTRAINT [FK_Properties_Fruits] FOREIGN KEY([FruitId])
REFERENCES [dbo].[Fruits] ([FruitId])

ALTER TABLE [dbo].[Properties] CHECK CONSTRAINT [FK_Properties_Fruits]

Copy Basket

At first copy one row in Baskets table and use SCOPE_IDENTITY to get the generated ID.

BEGIN TRANSACTION;

-- Parameter of the procedure. What basket to copy.
DECLARE @VarOldBasketID int = 1;

-- Copy Basket, one row
DECLARE @VarNewBasketID int;

INSERT INTO [dbo].[Baskets] (BasketName) 
VALUES ('Friends Basket');

SET @VarNewBasketID = SCOPE_IDENTITY();

Copy Fruits

Then copy Fruits using MERGE and remember a mapping between old and new IDs in a table variable.

-- Copy Fruits, multiple rows
DECLARE @FruitIDs TABLE (OldFruitID int, NewFruitID int);

MERGE INTO [dbo].[Fruits]
USING
(
    SELECT
        [FruitId]
        ,[BasketId]
        ,[FruitName]
    FROM [dbo].[Fruits]
    WHERE [BasketId] = @VarOldBasketID
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
    ([BasketId]
    ,[FruitName])
VALUES
    (@VarNewBasketID
    ,Src.[FruitName])
OUTPUT Src.[FruitId] AS OldFruitID, inserted.[FruitId] AS NewFruitID
INTO @FruitIDs(OldFruitID, NewFruitID)
;

Copy Properties

Then copy Properties using remembered mapping between old and new Fruit IDs.

-- Copy Properties, many rows
INSERT INTO [dbo].[Properties] ([FruitId], [PropertyText])
SELECT
    F.NewFruitID
    ,[dbo].[Properties].PropertyText
FROM
    [dbo].[Properties]
    INNER JOIN @FruitIDs AS F ON F.OldFruitID = [dbo].[Properties].FruitId
;

Check results, change rollback to commit once you confirmed that the code works correctly.

SELECT * FROM [dbo].[Baskets];
SELECT * FROM [dbo].[Fruits];
SELECT * FROM [dbo].[Properties];

ROLLBACK TRANSACTION;
like image 34
Vladimir Baranov Avatar answered Oct 03 '22 07:10

Vladimir Baranov