0

I'm trying to copy data from one table to another and add one column to the target table that isn't present in the source table using INSERT INTO and a stored procedure to generate the value for the additional column.

The target table has counter field (integer) that needs to be incremented for every row inserted (it is not an IDENTITY column, incrementing this counter is handled by other code).

The Counter table has counters for multiple tables and need to pass the Counter Name as a parameter to the stored procedure and use output as the additional column for the target table.

I have a stored procedure and can pass it a parameter (counter name, in example below counter name is "Counter_123") and it has as output value as the new counter value.

I can run the stored procedure like this and it works fine:

declare @value int
exec GetCounter 'Counter_123', @value output
PRINT @value

In this case if the counter was 3, the output is 4.

Here is what I'm trying to do in my INSERT INTO:

INSERT INTO Target_Table (col1, col2, col3, uspGetCounter 'Counter_123')
SELECT (val1, val2, val3) 
FROM Source_Table
WHERE ...

Error returned is syntax error near 'Counter_123'

I've also tried to put the stored procedure in the values list but that doesn't work either.

What is the syntax to call a stored procedure, pass it a parameter, and use the output in the INSERT INTO query as a value for a column?

Here is the stored procedure code for reference:

CREATE PROCEDURE [dbo].[uspGetCounter] 
    
    @CounterName varchar(30) = '', 
    @LastValue int OUTPUT
AS
BEGIN
    SET NOCOUNT ON
    
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

    BEGIN TRANSACTION

    if not exists (select * from Counter where COUNTER_NAME=@CounterName) 
       insert Counter (COUNTER_NAME, LAST_VALUE)
       values (@CounterName,0)

    select @LastValue=LAST_VALUE+1
       from Counter (holdlock)
       where COUNTER_NAME= @CounterName

    update Counter
       set LAST_VALUE=@LastValue,
           LAST_UPDATED=getdate(),
           from Counter
       where COUNTER_NAME=@CounterName

    COMMIT TRANSACTION
    
END
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Area51Resident
  • 71
  • 1
  • 2
  • 8
  • 1
    If you're going to use hints, AT LEAST use the current syntax and not the deprecated version. And it is time to understand with absolute precision what the hints you use actually do. Start [here](https://stackoverflow.com/questions/7843733/confused-about-updlock-holdlock). And then a discussion about this [inefficient upsert pattern](https://sqlperformance.com/2020/09/locking/upsert-anti-pattern). And better still to use a sequence or identity. – SMor Jun 26 '21 at 00:33

2 Answers2

1

Assuming you really need to roll your own sequence generator, because using a built in sequence is going to be much easier, then with your current logic you can only insert a single row at a time, because your stored procedure provides no mechanism to allocate a block of values in one go. You would therefore need a loop of some sort, maybe a cursor e.g.

DECLARE @Counter int, @Val1 int, @Val2 int, @Val3 int;

DECLARE MyCursor CURSOR LOCAL FOR
    SELECT val1, val2, val3 
    FROM Source_Table
    WHERE ...;

WHILE 1 =1 BEGIN
    FETCH NEXT FROM MyCursor INTO @Val1, @Val2, @Val3;
    IF @@FETCH_STATUS != 0 BREAK;

    EXEC uspGetCounter 'Counter_123', @Counter OUTPUT;

    INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
        VALUES (@Val1, @Val2, @Val3, @Counter);
END;

Where you run your SP for each row you need to insert and obtain a new sequence value.

If using this approach I would also modify your SP as follows for performance:

-- Attempt an update and assignment in a single statement
UPDATE dbo.[Counter] SET
  @LastValue = LAST_VALUE = LAST_VALUE + 1
  , LAST_UPDATED = GETDATE()
WHERE COUNTER_NAME = @CounterName;

-- If doesn't exist (less likely case as only the first time the counter is accessed)
IF @@ROWCOUNT = 0 BEGIN
    SET @LastValue = 1;
    -- Create a new record if none exists
    INSERT INTO dbo.[Counter] (COUNTER_NAME, LAST_VALUE, LAST_UPDATED)
        SELECT @CounterName, @LastValue, GETDATE();
END;

Which reduces access and speeds up the SP.


However, again assuming you need to roll your own sequence, if you are using this to copy data frequently you would be much better off modifying your stored procedure to allocate a block of values in one go i.e. when you call your SP you pass a parameter @NumValuesRequired and then your return value is the first of a block that long. Then you can carry out your copy in a single go e.g.

DECLARE @RecordCount int, @CounterStart int;

SELECT @RecordCount = COUNT(*)
FROM Source_Table
WHERE ...;

EXEC uspGetCounter2 'Counter_123', @RecordCount, @CounterStart OUTPUT;

INSERT INTO Target_Table (Val1, Val2, Val3, CounterCol)
    SELECT val1, val2, val3, @CounterStart + ROW_NUMBER() OVER () - 1
    FROM Source_Table
    WHERE ...;
Dale K
  • 25,246
  • 15
  • 42
  • 71
  • I was hoping to avoid a cursor, but looks like that is my only option. I can't change the counter table or functionality, it is normally updated by the application when users insert new records, so the counter could be incremented outside my procedure at any time. This is being used to import some data so I have to check and increment the counter every INSERT to be sure it hasn't been updated. – Area51Resident Jun 26 '21 at 00:54
  • 1
    I am now. Thanks for you help. I've modified the cursor example with the additional fields I needed and it worked fine. Thank you. – Area51Resident Jun 29 '21 at 20:23
0

First of all, the the procedure is in the wrong place. That part of the INSERT statement expects column names, not column values. Anything you do with the procedure would go in the SELECT clause for the statement, if you could run it that way (which you can't).

That out of the way, the existing Counter table and stored procedure are obsolete. They exist to solve a problem that has since been rolled into SQL Server as a core feature. Therefore the BEST solution here is converting the counter (all of them) to use a SEQUENCE and NEXT VALUE FOR syntax.

Then, assuming you have primed the sequence to the correct value the INSERT would look like this:

INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT (val1, val2, val3, NEXT VALUE FOR Counter_123) 
FROM Source_Table
WHERE ...

If for some reason that's not an option, I would roll the counter logic into the query to run in a single transaction, rather many many transactions, like this:

BEGIN TRANSACTION

DECLARE @LastValue int;
SELECT @LastValue = LAST_VALUE
       FROM Counter (holdlock)
       WHERE COUNTER_NAME = 'Counter_123'

INSERT INTO Target_Table (col1, col2, col3, [Counter_123])
SELECT val1, val2, val3, @LastValue + ROW_NUMBER() OVER (ORDER BY val1, val2, val3) 
FROM Source_Table
WHERE ...

UPDATE Counter
SET LAST_VALUE = (SELECT MAX([Counter_123]) FROM [Target_Table])
     ,LAST_UPDATED=current_timestamp
WHERE COUNTER_NAME = 'Counter_123'

COMMIT
Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • I can't change the counter table or functionality, it is normally updated by the application when users insert new records, so the counter could be incremented outside my procedure at any time. The code base for the application is close to 15 years old, it isn't using any built-in database capabilities like ```SEQUENCE```. Looks like a cursor approach is the only way, this isn't for production use so doesn't need to be quick. – Area51Resident Jun 26 '21 at 00:58
  • Did you see the last part of my answer? The use of a TRANSACTION in that code sample makes it safe to go with the Counter table. – Joel Coehoorn Jun 26 '21 at 16:07
  • yes I saw that. No doubt it would work but the primary application that uses this database is ancient, undocumented code (written in a long-ago deprecated 4GL package) that I don't have access to or the ability to modify. I used the sproc to get one counter increment at a time to minimize locking of that table. A user could create a new record in that table at any time and how the application would handle a lock on that table in that case is unknown and I don't want to cause it to crash or risk data corruption. I hope that explains the situation. – Area51Resident Jun 29 '21 at 20:41
  • You'll have **MUCH** less overall locking doing this in a single set-based transaction group than requesting 1000 individual IDs, with a separate lock each. – Joel Coehoorn Jun 29 '21 at 20:47