views:

111

answers:

3

I am creating a SQL 2008 R2 stored procedure to duplicate a row and all it's children.

It's a 3-tiered setup with a Parent, Child and Sub-Child Given the ID of the parent I need to create a duplicate.

I have solved it using a fast_forward cursor.

I know I can also do it with a while loop through rows but I do not believe that will be faster than this cursor method. What are your thoughts?

Is there a better way to accomplish this task without using cursors?

EDIT: Another option I considered was creating a temp table holding the old / new PKID's of the TBLACStages records.

TBLACStages may have anywhere from 1 to 20 corresponding rows (and TBLACUpgrade will likely have 3 rows per TBLACStages row)

CREATE PROCEDURE [dbo].[spDuplicateACUnit]
@pACUnitID bigint = 0 
AS BEGIN
SET NOCOUNT ON;

DECLARE @NewACUnitID bigint = 0

INSERT INTO TBLACUnits ([col1] ,[col2] ,[...] ,[coln]) SELECT [col1] ,[col2] ,[...] ,[coln] FROM TBLACUnits WHERE ACUnitID = @pACUnitID

SELECT @NewACUnitID = SCOPE_IDENTITY()

DECLARE @ACStageID bigint = 0 
    DECLARE @NewACStageID bigint = 0

DECLARE @ACUnitCursor CURSOR

SET @ACUnitCursor = CURSOR LOCAL FAST_FORWARD FOR SELECT ACStageID FROM TBLACStages WHERE TBLACStages.ACUnitID = @pACUnitID

OPEN @ACUnitCursor

FETCH NEXT FROM @ACUnitCursor INTO @ACStageID

WHILE @@FETCH_STATUS = 0 
BEGIN

INSERT INTO TBLACStages ([ACUnitID] ,[col1] ,[col2] ,[...] ,[coln]) SELECT @NewACUnitID ,[col1] ,[col2] ,[...] ,[coln] FROM TBLACStages WHERE TBLACStages.ACStageID = @ACStageID

SELECT @NewACStageID = SCOPE_IDENTITY()

INSERT INTO TBLACUpgrade ([ACStageID] ,[col1] ,[col2] ,[...] ,[coln]) SELECT @NewACStageID ,[col1] ,[col2] ,[...] ,[coln] FROM TBLACUpgrade WHERE TBLACUpgrade.[ACStageID] = @ACStageID

FETCH NEXT FROM @ACUnitCursor INTO @ACStageID 
END

CLOSE @ACUnitCursor DEALLOCATE @ACUnitCursor

END

GO
A: 

To increase the speed of your SP you can add another statement FOR READ ONLY

So your SP will be like that:

    ...

SET @ACUnitCursor = CURSOR LOCAL FAST_FORWARD FOR 

SELECT ACStageID FROM TBLACStages WHERE TBLACStages.ACUnitID = @pACUnitID

FOR READ ONLY  -- add this to increase the speed

OPEN @ACUnitCursor

FETCH NEXT FROM @ACUnitCursor INTO @ACStageID

...
Luka
Maybe I do not understand your answer... `FAST_FORWARD` is, by definition, read-only
Matthew PK
Ops you are right, i type too fast and i didnt notice you set the cursor as `FAST_FORWARD`. Btw i suggest instead to insert your records in TBLACStages, TBLACUpgrade, ... directly, insert them in a `temp table as @TBLACStages`, and then when the loop is finish, with a only 1 SELECT INTO you can put all records from @TBLACStages > TBLACStages in one shot. This reduce a lot the work of SQL Server because the TEMP TABLE are in memory and are not written on the disk. I used this tecnique as in your case. This is just a tip.
Luka
+1  A: 

This should give you the idea:

CREATE TABLE t_parent (id INT NOT NULL PRIMARY KEY IDENTITY, value VARCHAR(100))
CREATE TABLE t_child (id INT NOT NULL PRIMARY KEY IDENTITY, parent INT NOT NULL, value VARCHAR(100))
CREATE TABLE t_grandchild (id INT NOT NULL PRIMARY KEY IDENTITY, child INT NOT NULL, value VARCHAR(100))

INSERT
INTO    t_parent (value)
VALUES  ('Parent 1')

INSERT
INTO    t_parent (value)
VALUES  ('Parent 2')

INSERT
INTO    t_child (parent, value)
VALUES  (1, 'Child 2')

INSERT
INTO    t_child (parent, value)
VALUES  (2, 'Child 2')

INSERT
INTO    t_grandchild (child, value)
VALUES  (1, 'Grandchild 1')

INSERT
INTO    t_grandchild (child, value)
VALUES  (1, 'Grandchild 2')

INSERT
INTO    t_grandchild (child, value)
VALUES  (2, 'Grandchild 3')

DECLARE @parent TABLE (oid INT, nid INT)
DECLARE @child TABLE (oid INT, nid INT)

MERGE
INTO    t_parent
USING   (
        SELECT  id, value
        FROM    t_parent
        ) p
ON      1 = 0
WHEN NOT MATCHED THEN
INSERT  (value)
VALUES  (value)
OUTPUT  p.id, INSERTED.id
INTO    @parent;
SELECT  *
FROM    @parent
MERGE
INTO    t_child
USING   (
        SELECT  c.id, p.nid, c.value
        FROM    @parent p
        JOIN    t_child c
        ON      c.parent = p.oid
        ) c
ON      1 = 0
WHEN NOT MATCHED THEN
INSERT  (parent, value)
VALUES  (nid, value)
OUTPUT  c.id, INSERTED.id
INTO    @child;
SELECT  *
FROM    @child;
INSERT
INTO    t_grandchild (child, value)
SELECT  c.nid, gc.value
FROM    @child c
JOIN    t_grandchild gc
ON      gc.child = c.oid
SELECT  *
FROM    t_grandchild
Quassnoi
Do you think three nested `MERGE` statements will be faster than a `CURSOR` over a max of 20 rows?Espescially now given the overhead of two read-in temporary tables (Parent and Child) instead of the one in my solution (Child) ?
Matthew PK
@Matthew: The `MERGE` statements are in fact mere `INSERTS` (`INSERT` doesn't allow returning old `id`, so you have to use `MERGE`).
Quassnoi
@Matthew: you can reuse the temporary table in my solution too. One query against a `20`-record table will be definitely faster than `20` queries against separate records. You should always use set-based solutions if possible.
Quassnoi
Yes, I see that you have created `INSERT` s using the `MERGE` only because you want them to `OUTPUT` something.However, your solution using `MERGE` declared and reads-in (using `OUTPUT`) two temporary tables which you then `JOIN` to facilitate the next merge. I am not convinced this is a faster solution over a `CURSOR` when the recordsets are as small as mine.
Matthew PK
@Quassnoi: I know that set-based solutions are almost always better solutions than cursors. However, when you consider that each `MERGE` is technically an `EXISTS` and *two* `INSERTS` I wasn't sure it would be faster
Matthew PK
@Matthew: the `CURSOR` solution (which, by the way, creates a temporary table behind the scenes) will have to switch execution context between `SQL` and cursor operation (and hence copy the record data between different queries), while the set-based solution will do it within the query plan (which is optimized for such things). For only `20` records this of course will hardly be noticeable (in absolute times which in both cases will be measured in milliseconds), but set-based operations will be significantly faster in relative terms.
Quassnoi
@Matthew: there will be `1` (one) `INSERT` and `0` (zero) `EXISTS` per `MERGE` statement. `1 = 0` will be optimized away.
Quassnoi
@Quassnoi: I know that the `CURSOR` solution creates a temp table too, but only via a single `INSERT`I guess I am trying to compare the number of queries required to fulfill your `MERGE` operations in comparison to the number of queries potentially executed using my `CURSOR` solution.
Matthew PK
@Quassnoi: Ah yes, I see that the `1 = 0` will be cleared, but each `MERGE` still processes two inserts: one into the real table and another into the temp table.
Matthew PK
@Matthew: the same with the `CURSOR` solution. `OPEN CURSOR` will create and fill the hidden temporary table, `INSERT` will read from the temp table and populate the real table.
Quassnoi
@Quassnoi: I'm probably going to tweak the `MERGE` a bit but thank you for the basic framework. I've pasted my adaptation of your answer below and marked your response.
Matthew PK
A: 

Ok, this is the MERGE I've come up with based on Quassnoi's solution. I should work appropriately without the CURSOR

DECLARE @parent TABLE (oid BIGINT, nid BIGINT)
DECLARE @child TABLE (oid BIGINT, nid BIGINT)

MERGE
INTO    TBLACUnits T
USING   (SELECT [col1], [...], [coln] FROM TBLACUnits WHERE ID = @pID) S

ON      1 = 0
WHEN NOT MATCHED THEN
INSERT  ([ACUnitID]
   ,[col1]
   ,[...]
   ,[coln])
VALUES  (S.[ACUnitID]
   ,S.[col1]
   ,S.[...]
   ,S.[coln]])
OUTPUT  S.ACUnitID, INSERTED.ACUnitID
INTO    @parent;

MERGE
INTO    TBLACStages T
USING   (
  SELECT  tt.[nid] 
                       ,TBLACStages.[col1]
                       ,TBLACStages.[...]
                       ,TBLACStages.[coln]
  FROM TBLACStages
  JOIN @parent tt ON tt.oid = TBLACStages.ACUnitID
  ) S
ON      1 = 0
WHEN NOT MATCHED THEN
INSERT  ([ACUnitID]
   ,[col1]
   ,[...]
   ,[coln])
VALUES  ([nid]
   ,[col1]
   ,[...]
   ,[coln])
OUTPUT  S.[ACStageID], INSERTED.[ACStageID]
INTO    @child;

INSERT INTO TBLACUpgrade 
([ACStageID]
   ,[col1]
   ,[...]
   ,[coln])
SELECT  c.[nid]
   ,TBLACUpgrade.[col1]
   ,TBLACUpgrade.[...]
   ,TBLACUpgrade.[coln]
FROM    @child c
JOIN    TBLACUpgrade
 ON      TBLACUpgrade.ACStageID  = c.oid
Matthew PK