T-SQL: recorrer una matriz de valores conocidos

89

Aquí está mi escenario:

Digamos que tengo un procedimiento almacenado en el que necesito llamar a otro procedimiento almacenado en un conjunto de identificadores específicos; ¿Hay alguna forma de hacer esto?

es decir, en lugar de tener que hacer esto:

exec p_MyInnerProcedure 4
exec p_MyInnerProcedure 7
exec p_MyInnerProcedure 12
exec p_MyInnerProcedure 22
exec p_MyInnerProcedure 19

Haciendo algo como esto:

*magic where I specify my list contains 4,7,12,22,19*

DECLARE my_cursor CURSOR FAST_FORWARD FOR
*magic select*

OPEN my_cursor 
FETCH NEXT FROM my_cursor INTO @MyId
WHILE @@FETCH_STATUS = 0
BEGIN

exec p_MyInnerProcedure @MyId

FETCH NEXT FROM my_cursor INTO @MyId
END

Mi objetivo principal aquí es simplemente la capacidad de mantenimiento (fácil de eliminar / agregar identificaciones a medida que cambia el negocio), poder enumerar todas las identificaciones en una sola línea ... El rendimiento no debería ser un problema tan grande

Juan
fuente
relacionado, si necesita iterar en una lista no entera como varchars, solución con cursor: iterar a través de una lista-de-cadenas-en-sql-server
Pac0

Respuestas:

105
declare @ids table(idx int identity(1,1), id int)

insert into @ids (id)
    select 4 union
    select 7 union
    select 12 union
    select 22 union
    select 19

declare @i int
declare @cnt int

select @i = min(idx) - 1, @cnt = max(idx) from @ids

while @i < @cnt
begin
     select @i = @i + 1

     declare @id = select id from @ids where idx = @i

     exec p_MyInnerProcedure @id
end
Adam Robinson
fuente
Esperaba que hubiera una forma más elegante, pero creo que esto será lo más cercano que pueda: Terminé usando un híbrido entre el uso de las uniones select / aquí y el cursor del ejemplo. ¡Gracias!
John
13
@john: si está usando 2008, puede hacer algo como INSERTAR @ids VALORES (4), (7), (12), (22), (19)
Peter Radocchia
2
Solo para su información, las tablas de memoria como esta son generalmente más rápidas que los cursores (aunque para 5 valores apenas puedo ver que eso haga alguna diferencia), pero la razón principal por la que me gustan es que encuentro la sintaxis similar a la que encontrarías en el código de la aplicación , mientras que los cursores me parecen (a mí) relativamente diferentes.
Adam Robinson
aunque en la práctica dañará muy poco el rendimiento, quiero señalar que esto itera a través de todos los números dentro del espacio definido. la solución a continuación con Mientras existe (Seleccionar * De @Ids) ... es lógicamente más sólida (y más elegante).
Der U
41

Lo que hago en este escenario es crear una variable de tabla para contener los ID.

  Declare @Ids Table (id integer primary Key not null)
  Insert @Ids(id) values (4),(7),(12),(22),(19)

- (o llame a otra función con valor de tabla para generar esta tabla)

Luego, haz un bucle basado en las filas de esta tabla.

  Declare @Id Integer
  While exists (Select * From @Ids)
    Begin
      Select @Id = Min(id) from @Ids
      exec p_MyInnerProcedure @Id 
      Delete from @Ids Where id = @Id
    End

o...

  Declare @Id Integer = 0 -- assuming all Ids are > 0
  While exists (Select * From @Ids
                where id > @Id)
    Begin
      Select @Id = Min(id) 
      from @Ids Where id > @Id
      exec p_MyInnerProcedure @Id 
    End

Cualquiera de los enfoques anteriores es mucho más rápido que un cursor (declarado contra las tablas de usuario normales). Las variables con valores de tabla tienen una mala reputación porque cuando se usan incorrectamente (para tablas muy amplias con un gran número de filas) no funcionan. Pero si los está usando solo para contener un valor clave o un entero de 4 bytes, con un índice (como en este caso) son extremadamente rápidos.

Charles Bretana
fuente
El enfoque anterior es equivalente o más lento que un cursor declarado en una variable de tabla. Ciertamente no es más rápido. Sin embargo, sería más rápido que un cursor declarado con opciones predeterminadas en tablas de usuario normales.
Peter Radocchia
@Peter, ahhh, sí, tienes razón, asumo incorrectamente que usar un cursor implica una tabla de usuario normal, no una variable de tabla. He editado para dejar en claro la distinción
Charles Bretana
16

use una variable de cursor estática y una función de división :

declare @comma_delimited_list varchar(4000)
set @comma_delimited_list = '4,7,12,22,19'

declare @cursor cursor
set @cursor = cursor static for 
  select convert(int, Value) as Id from dbo.Split(@comma_delimited_list) a

declare @id int
open @cursor
while 1=1 begin
  fetch next from @cursor into @id
  if @@fetch_status <> 0 break
  ....do something....
end
-- not strictly necessary w/ cursor variables since they will go out of scope like a normal var
close @cursor
deallocate @cursor

Los cursores tienen una mala reputación, ya que las opciones predeterminadas cuando se declaran en las tablas de usuario pueden generar una gran sobrecarga.

Pero en este caso la sobrecarga es mínima, menor que cualquier otro método aquí. STATIC le dice a SQL Server que materialice los resultados en tempdb y luego repita sobre eso. Para listas pequeñas como esta, es la solución óptima.

Peter Radocchia
fuente
7

Puede intentar lo siguiente:

declare @list varchar(MAX), @i int
select @i=0, @list ='4,7,12,22,19,'

while( @i < LEN(@list))
begin
    declare @item varchar(MAX)
    SELECT  @item = SUBSTRING(@list,  @i,CHARINDEX(',',@list,@i)-@i)
    select @item

     --do your stuff here with @item 
     exec p_MyInnerProcedure @item 

    set @i = CHARINDEX(',',@list,@i)+1
    if(@i = 0) set @i = LEN(@list) 
end
Ramakrishna Talla
fuente
6
Haría esa declaración de lista como esta: @list ='4,7,12,22,19' + ','- así que está totalmente claro que la lista tiene que terminar con una coma (¡no funciona sin ella!).
AjV Jsy
5

Yo suelo usar el siguiente enfoque

DECLARE @calls TABLE (
    id INT IDENTITY(1,1)
    ,parameter INT
    )

INSERT INTO @calls
select parameter from some_table where some_condition -- here you populate your parameters

declare @i int
declare @n int
declare @myId int
select @i = min(id), @n = max(id) from @calls
while @i <= @n
begin
    select 
        @myId = parameter
    from 
        @calls
    where id = @i

        EXECUTE p_MyInnerProcedure @myId
    set @i = @i+1
end
kristof
fuente
2
CREATE TABLE #ListOfIDs (IDValue INT)

DECLARE @IDs VARCHAR(50), @ID VARCHAR(5)
SET @IDs = @OriginalListOfIDs + ','

WHILE LEN(@IDs) > 1
BEGIN
SET @ID = SUBSTRING(@IDs, 0, CHARINDEX(',', @IDs));
INSERT INTO #ListOfIDs (IDValue) VALUES(@ID);
SET @IDs = REPLACE(',' + @IDs, ',' + @ID + ',', '')
END

SELECT * 
FROM #ListOfIDs
Moshe
fuente
0

Haga una conexión a su base de datos usando un lenguaje de programación procedimental (aquí Python) y haga el ciclo allí. De esta manera, también puede hacer bucles complicados.

# make a connection to your db
import pyodbc
conn = pyodbc.connect('''
                        Driver={ODBC Driver 13 for SQL Server};
                        Server=serverName;
                        Database=DBname;
                        UID=userName;
                        PWD=password;
                      ''')
cursor = conn.cursor()

# run sql code
for id in [4, 7, 12, 22, 19]:
  cursor.execute('''
    exec p_MyInnerProcedure {}
  '''.format(id))
LoMaPh
fuente