T-SQL: przechodzenie przez tablicę znanych wartości

90

Oto mój scenariusz:

Powiedzmy, że mam procedurę składowaną, w której muszę wywołać inną procedurę składowaną na zestawie określonych identyfikatorów; czy jest na to sposób?

tj. zamiast tego robić:

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

Robię coś takiego:

*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

Moim głównym celem jest tutaj po prostu łatwość utrzymania (łatwe do usunięcia / dodania identyfikatorów, gdy zmienia się biznes), możliwość wyszczególnienia wszystkich identyfikatorów w jednym wierszu ... Wydajność nie powinna być tak dużym problemem

Jan
źródło
powiązane, jeśli potrzebujesz iterować na liście niecałkowitej, takiej jak varchars, rozwiązanie z kursorem: iterate-through-a-list-of-strings-in-sql-server
Pac0

Odpowiedzi:

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
źródło
Miałem nadzieję, że będzie bardziej elegancki sposób, ale myślę, że będzie to tak bliskie, jak tylko mogę: Skończyło się na użyciu hybrydy między użyciem tutaj wyboru / związków a kursorem z przykładu. Dzięki!
Jan
13
@john: jeśli używasz 2008, możesz zrobić coś takiego jak INSERT @ids VALUES (4), (7), (12), (22), (19)
Peter Radocchia
2
Do Twojej wiadomości, tabele pamięci takie jak ta są generalnie szybsze niż kursory (chociaż dla 5 wartości prawie nie widzę, aby robiło to jakąkolwiek różnicę), ale największym powodem, dla którego je lubię, jest to, że uważam składnię podobną do tej, którą można znaleźć w kodzie aplikacji , podczas gdy kursory wydają się (mi) stosunkowo różne.
Adam Robinson
chociaż w praktyce tylko w niewielkim stopniu wpłynie to negatywnie na wydajność, chcę podkreślić, że to iteruje przez wszystkie liczby w określonej przestrzeni. poniższe rozwiązanie z opcją While istnieje (Select * From @Ids) ... jest logicznie bardziej rozsądne (i bardziej eleganckie).
Der U
41

W tym scenariuszu tworzę zmienną tabeli do przechowywania identyfikatorów.

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

- (lub wywołaj inną funkcję wycenioną w tabeli, aby wygenerować tę tabelę)

Następnie pętla oparta na wierszach w tej tabeli

  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

lub...

  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

Każde z powyższych podejść jest znacznie szybsze niż kursor (zadeklarowany względem zwykłych tabel użytkowników). Zmienne wyceniane w tabeli mają złą reprezentację, ponieważ niewłaściwie używane (w przypadku bardzo szerokich tabel z dużą liczbą wierszy) nie są wydajne. Ale jeśli używasz ich tylko do przechowywania wartości klucza lub 4-bajtowej liczby całkowitej, z indeksem (jak w tym przypadku), są one niezwykle szybkie.

Charles Bretana
źródło
Powyższe podejście jest równoważne lub wolniejsze niż kursor zadeklarowany w zmiennej tabeli. Z pewnością nie jest szybszy. Byłoby to jednak szybsze niż kursor zadeklarowany z domyślnymi opcjami w tabelach zwykłych użytkowników.
Peter Radocchia,
@Peter, ahhh, tak, masz rację, błędnie zakładam, że użycie kursora implikuje zwykłą tabelę użytkownika, a nie zmienną tabeli. Zostałem zredagowany, aby wyjaśnić rozróżnienie
Charles Bretana
16

użyj statycznej zmiennej kursora i funkcji podziału :

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

Kursory mają złego przedstawiciela, ponieważ domyślne opcje zadeklarowane w tabelach użytkownika mogą generować duże obciążenie.

Ale w tym przypadku narzut jest dość minimalny, mniejszy niż w przypadku innych metod tutaj. STATIC mówi SQL Server, aby zmaterializował wyniki w tempdb, a następnie wykonał iterację. W przypadku takich małych list jest to optymalne rozwiązanie.

Peter Radocchia
źródło
7

Możesz spróbować jak poniżej:

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
źródło
6
Zrobiłbym tę deklarację listy w następujący sposób: @list ='4,7,12,22,19' + ','- więc jest całkowicie jasne, że lista musi kończyć się przecinkiem (bez niego nie działa!).
AjV Jsy
5

Zwykle stosuję następujące podejście

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
źródło
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
Mosze
źródło
0

Nawiąż połączenie ze swoją bazą danych za pomocą proceduralnego języka programowania (tutaj Python) i wykonaj tam pętlę. W ten sposób możesz również wykonywać skomplikowane pętle.

# 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
źródło