T-SQL: Durchlaufen eines Arrays bekannter Werte

87

Hier ist mein Szenario:

Angenommen, ich habe eine gespeicherte Prozedur, in der ich eine andere gespeicherte Prozedur für eine Reihe bestimmter IDs aufrufen muss. Gibt es eine Möglichkeit, dies zu tun?

dh anstatt dies tun zu müssen:

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

So etwas machen:

*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

Mein Hauptziel hier ist einfach die Wartbarkeit (einfach zu entfernen / IDs hinzuzufügen, wenn sich das Geschäft ändert), in der Lage zu sein, alle IDs in einer einzigen Zeile aufzulisten ... Die Leistung sollte kein so großes Problem sein

John
quelle
verwandt, wenn Sie auf einer nicht ganzzahligen Liste wie varchars iterieren
Pac0

Antworten:

103
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
quelle
Ich hatte gehofft, dass es einen eleganteren Weg geben würde, aber ich denke, dass dies so nah wie möglich sein wird: Am Ende wurde ein Hybrid zwischen der Verwendung von select / unions hier und dem Cursor aus dem Beispiel verwendet. Vielen Dank!
John
13
@ John: Wenn Sie 2008 verwenden, können Sie so etwas wie INSERT @ids VALUES (4), (7), (12), (22), (19)
Peter Radocchia
2
Nur zu Ihrer Information, Speichertabellen wie diese sind im Allgemeinen schneller als Cursor (obwohl ich bei 5 Werten kaum einen Unterschied sehe), aber der Hauptgrund, warum ich sie mag, ist, dass ich die Syntax ähnlich finde wie im Anwendungscode , während Cursor (für mich) relativ unterschiedlich erscheinen.
Adam Robinson
Obwohl es in der Praxis die Leistung nur sehr wenig beeinträchtigt, möchte ich darauf hinweisen, dass dies alle Zahlen innerhalb des definierten Raums durchläuft. Die folgende Lösung mit While existiert (Wählen Sie * From @Ids) ... ist logisch solider (und eleganter).
Der U
40

In diesem Szenario erstelle ich eine Tabellenvariable für die IDs.

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

- (oder rufen Sie eine andere Tabellenwertfunktion auf, um diese Tabelle zu generieren)

Dann Schleife basierend auf den Zeilen in dieser Tabelle

  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

oder...

  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

Jeder der oben genannten Ansätze ist viel schneller als ein Cursor (deklariert gegenüber regulären Benutzertabellen). Variablen mit Tabellenwerten haben eine schlechte Wiederholung, da sie bei unsachgemäßer Verwendung (für sehr breite Tabellen mit einer großen Anzahl von Zeilen) nicht performant sind. Wenn Sie sie jedoch nur verwenden, um einen Schlüsselwert oder eine 4-Byte-Ganzzahl mit einem Index (wie in diesem Fall) zu speichern, sind sie extrem schnell.

Charles Bretana
quelle
Der obige Ansatz entspricht oder ist langsamer als ein Cursor, der für eine Tabellenvariable deklariert ist. Es ist sicherlich nicht schneller. Es wäre jedoch schneller als ein Cursor, der in normalen Benutzertabellen mit Standardoptionen deklariert wurde.
Peter Radocchia
@ Peter, ahhh, ja, Sie haben Recht, ich gehe fälschlicherweise davon aus, dass die Verwendung eines Cursors eine reguläre Benutzertabelle impliziert, keine Tabellenvariable. Ich habe bearbeitet, um die Unterscheidung zu verdeutlichen
Charles Bretana
16

Verwenden Sie eine statische Cursor-Variable und eine Split-Funktion :

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

Cursor haben einen schlechten Repräsentanten, da die Standardoptionen, wenn sie für Benutzertabellen deklariert werden, viel Overhead verursachen können.

In diesem Fall ist der Overhead jedoch recht gering, weniger als bei allen anderen Methoden. STATIC weist SQL Server an, die Ergebnisse in tempdb zu materialisieren und dann darüber zu iterieren. Für kleine Listen wie diese ist es die optimale Lösung.

Peter Radocchia
quelle
7

Sie können wie folgt versuchen:

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
quelle
6
Ich würde diese Listendeklaration wie @list ='4,7,12,22,19' + ','folgt machen: - Das ist also völlig klar, dass die Liste mit einem Komma enden muss (ohne funktioniert es nicht!).
AjV Jsy
5

Normalerweise verwende ich den folgenden Ansatz

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
quelle
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
quelle
0

Stellen Sie mit einer prozeduralen Programmiersprache (hier Python) eine Verbindung zu Ihrer Datenbank her und führen Sie dort die Schleife aus. Auf diese Weise können Sie auch komplizierte Schleifen erstellen.

# 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
quelle