T-SQL: цикл по массиву известных значений

90

Вот мой сценарий:

Допустим, у меня есть хранимая процедура, в которой мне нужно вызвать другую хранимую процедуру для набора определенных идентификаторов; Есть ли способ сделать это?

т.е. вместо этого:

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

Делаем что-то вроде этого:

*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

Моя главная цель здесь - простая ремонтопригодность (легко удалить / добавить идентификаторы по мере изменения бизнеса), возможность перечислить все идентификаторы в одной строке ... Производительность не должна быть такой большой проблемой

Джон
источник
связанные, если вам нужно выполнить итерацию в нецелочисленном списке, таком как varchars, решение с курсором: итерация-через-список-строк-в-sql-сервере
Pac0

Ответы:

106
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
Адам Робинсон
источник
Я надеялся, что будет более элегантный способ, но я думаю, что он будет настолько близок, насколько я могу: закончился использованием гибрида между использованием здесь select / union и курсора из примера. Благодаря!
Джон
13
@john: если вы используете 2008, вы можете сделать что-то вроде INSERT @ids VALUES (4), (7), (12), (22), (19)
Питер Радоккья
2
К вашему сведению, такие таблицы памяти, как правило, быстрее курсоров (хотя для 5 значений я почти не вижу, чтобы это имело какое-либо значение), но самая большая причина, по которой они мне нравятся, заключается в том, что я нахожу синтаксис похожим на то, что вы найдете в коде приложения , тогда как курсоры кажутся (мне) относительно разными.
Адам Робинсон
хотя на практике это очень мало повредит производительности, я хочу отметить, что это повторяет все числа в определенном пространстве. приведенное ниже решение с использованием While exists (Select * From @Ids) ... логически более надежно (и более элегантно).
Der U
41

Что я делаю в этом сценарии, так это создаю табличную переменную для хранения идентификаторов.

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

- (или вызовите другую функцию с табличным значением для создания этой таблицы)

Затем выполните цикл на основе строк в этой таблице

  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

или же...

  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

Любой из вышеперечисленных подходов намного быстрее, чем курсор (объявленный для обычных пользовательских таблиц). Переменные с табличным значением имеют плохую репутацию, потому что при неправильном использовании (для очень широких таблиц с большим количеством строк) они неэффективны. Но если вы используете их только для хранения ключевого значения или 4-байтового целого числа с индексом (как в этом случае), они работают очень быстро.

Чарльз Бретана
источник
Вышеупомянутый подход эквивалентен или медленнее, чем курсор, объявленный для табличной переменной. Это конечно не быстрее. Однако это будет быстрее, чем курсор, объявленный с параметрами по умолчанию для обычных пользовательских таблиц.
Питер Радоккья
@Peter, аааа, да, вы правы, я ошибочно предполагаю, что использование курсора подразумевает обычную пользовательскую таблицу, а не табличную переменную. Я отредактировал, чтобы прояснить различие
Чарльз Бретана
16

используйте статическую курсорную переменную и функцию разделения :

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

Курсоры имеют плохую репутацию, поскольку параметры по умолчанию, объявленные для пользовательских таблиц, могут вызвать много накладных расходов.

Но в этом случае накладные расходы минимальны, меньше, чем любые другие методы здесь. STATIC сообщает SQL Server материализовать результаты в tempdb, а затем перебирать их. Для таких небольших списков это оптимальное решение.

Питер Радоккья
источник
7

Вы можете попробовать, как показано ниже:

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
Рамакришна Талла
источник
6
Я бы сделал это объявление списка следующим образом: @list ='4,7,12,22,19' + ','- чтобы было совершенно ясно, что список должен заканчиваться запятой (без нее он не работает!).
AjV Jsy
5

Обычно я использую следующий подход

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
Кристоф
источник
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
Моше
источник
0

Подключитесь к своей БД с помощью процедурного языка программирования (здесь Python) и выполните цикл там. Таким образом, вы также можете делать сложные петли.

# 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
источник