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))
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.