How to pass procedure output to multi-row insert s

2019-09-14 23:05发布

问题:

Given the following procedure call which returns an application-managed unique integer:

DECLARE @id as integer
DECLARE @num_ids as integer
EXEC dbo.i13_get_ids 2, 1, @id output, @num_ids output
SELECT  @id as N'@base_id'

I'm passing the result id onto an insert statement

insert into dbo.TBL_LOCATIONS (objectid, X_Coord, Y_Coord, Loc_Name)
values (@id,296163,3961644, 'new test')

However, that can be a bit ominous when iserting 2 or more rows, as I've done here:

DECLARE @id as integer
DECLARE @num_ids as integer
EXEC dbo.i13_get_ids 2, 1, @id output, @num_ids output
SELECT  @id as N'@base_id';

insert into dbo.TBL_LOCATIONS (objectid, X_Coord, Y_Coord, Loc_Name)
values (@id,296163,3961644, 'new test');

EXEC dbo.i13_get_ids 2, 1, @id output, @num_ids output
SELECT  @id as N'@base_id';

insert into dbo.TBL_LOCATIONS (objectid, X_Coord, Y_Coord, Loc_Name)
values (@id,296163,3961644, 'new test2');

EXEC dbo.i13_get_ids 2, 1, @id output, @num_ids output
SELECT  @id as N'@base_id';

insert into dbo.TBL_LOCATIONS (objectid, X_Coord, Y_Coord, Loc_Name)
values (@id,296563,3961644, 'new test3');

Surely there has to be a more efficient way?

Here is the procedure that is retrieving the next ID:

GO

/****** Object:  StoredProcedure [dbo].[i13_get_ids]  ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[i13_get_ids]
@id_type integer,
@num_requested_ids integer,
@base_id integer OUTPUT,
@num_obtained_ids integer OUTPUT AS SET NOCOUNT ON
BEGIN
  IF (@num_requested_ids < 0)
  BEGIN 
    BEGIN TRAN id_tran
    /* We are resetting the generator. */
    /* Delete fragments and update the base value.*/
    UPDATE ATBI.DBO.i13 WITH  (tablockx, holdlock)
      SET base_id = base_id + @num_requested_ids
      WHERE num_ids = -1 AND id_type = @id_type
    DELETE FROM ATBI.DBO.i13 WHERE id_type = @id_type and num_ids <> -1
    COMMIT TRAN id_tran /* releases holdlock table lock */
  END
  ELSE
  BEGIN
    IF (@num_requested_ids > 0)
    BEGIN
      DECLARE I_cursor CURSOR FOR
        SELECT base_id, num_ids
        FROM ATBI.DBO.i13 WITH (tablockx, holdlock)
        WHERE id_type = @id_type
        ORDER BY num_ids DESC /* ensures that fragments come first */
        FOR UPDATE /* to get a lock */
    END
    ELSE
    BEGIN
      /* only interested in base id */
      DECLARE I_cursor CURSOR FOR
        SELECT base_id, num_ids
        FROM ATBI.DBO.i13 WITH (tablockx, holdlock)
        WHERE id_type = @id_type AND num_ids = -1
        FOR UPDATE /* to get a lock */
    END
    BEGIN TRAN id_tran
    OPEN I_cursor
    FETCH NEXT FROM I_cursor INTO @base_id, @num_obtained_ids
    IF (@num_requested_ids = 0)
    BEGIN
      /* Just getting current value */
      SET @num_obtained_ids = 0
    END
    ELSE
    BEGIN
      IF (@num_obtained_ids = -1)
      BEGIN
        /* user got the amount they wanted */
        SET @num_obtained_ids = @num_requested_ids
        /* update the last id and base id */
        UPDATE ATBI.DBO.i13
          SET base_id = base_id + @num_obtained_ids,
              last_id =  @base_id
          WHERE CURRENT OF I_cursor
      END
      ELSE
      BEGIN
        /* user got a fragment */
        IF (@num_requested_ids = 1) AND (@num_obtained_ids > 1)
        BEGIN
          /* they want one and exactly one id */
          SET @num_obtained_ids = 1
          UPDATE ATBI.DBO.i13 SET base_id = base_id + 1,
            num_ids =  num_ids - 1 WHERE CURRENT OF I_cursor
        END
        ELSE
        BEGIN
          /* Return the whole fragment, delete the the row */
          DELETE FROM ATBI.DBO.i13 WHERE CURRENT OF I_cursor
        END
      END
    END
    CLOSE I_cursor
    COMMIT TRAN id_tran /* releases holdlock table lock */
    DEALLOCATE I_cursor
  END
END 
GO

based on answer below, have implemented:

DECLARE @id INT, @num_ids INT;  
EXEC [dbo].[i13_get_ids] 2, 10, @id OUTPUT, @num_ids OUTPUT;
INSERT dbo.TBL_Locations(objectid, X_Coord, Y_Coord, Loc_name)
SELECT N, 296563, 3961644, 'new test'    
FROM dbo.oidNumbers WHERE n >= @id AND n < @id + @num_ids;

回答1:

First, you should have a numbers table (some background that I've written here and here). I'm picking 100,000 rows but you may need more or less depending on how much you're going to use it.

CREATE TABLE dbo.Numbers(n INT PRIMARY KEY);

INSERT dbo.Numbers(n) 
    SELECT TOP (100000) n = ROW_NUMBER()
     OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.objects AS s2;

Now, you should be able to simply say (I'm just picking 10 arbitrarily):

DECLARE @id INT, @num_ids INT;

EXEC [dbo].[i13_get_ids] 2, 10, @id OUTPUT, @num_ids OUTPUT;

--INSERT dbo.TBL_Locations(objectid, X_Coord, Y_Coord, Loc_name)
SELECT [GUID], 296563, 3961644, 'new test' + CONVERT(VARCHAR(12), n)
    FROM dbo.Numbers
    WHERE n >= @id AND n < @id + @num_ids;

When you believe it's returning the right set, uncomment the --INSERT line. Just don't go too crazy because you are exhausting their supply of ids.


Sorry, the stuff below this line was written with the assumption you were using unique identifiers, as your question originally stated, not integers. Leaving it in case it is useful to others.

If you create a stored procedure that can generate multiple GUIDs, imagine this procedure that returns a set of GUIDs:

CREATE PROCEDURE dbo.i13_get_multi_ids
AS
BEGIN
    SET NOCOUNT ON;

    -- application management magic here

    SELECT [GUID] = GuidValues FROM somewhere;
END
GO

Then you could do:

CREATE TABLE #f([GUID] UNIQUEIDENTIFIER);

INSERT #f EXEC dbo.i13_get_multi_ids;

INSERT dbo.TBL_LOCATIONS(objectid, X_Coord, Y_Coord, Loc_name)
SELECT [GUID], 296563, 3961644, 
   'new test' + CONVERT(VARCHAR(12), ROW_NUMBER() OVER (ORDER BY [GUID])) 
FROM #f ORDER BY [GUID];