Prosedur Penyimpanan Panggilan SQL untuk setiap Baris tanpa menggunakan kursor

163

Bagaimana seseorang dapat memanggil prosedur tersimpan untuk setiap baris dalam sebuah tabel, di mana kolom-kolom dari suatu baris adalah parameter input ke sp tanpa menggunakan Kursor?

Johannes Rudolph
sumber
3
Jadi, misalnya, Anda memiliki tabel Pelanggan dengan kolom customerId, dan Anda ingin memanggil SP satu kali untuk setiap baris dalam tabel, meneruskan customerId terkait sebagai parameter?
Gary McGill
2
Bisakah Anda menguraikan mengapa Anda tidak bisa menggunakan kursor?
Andomar
@Gary: Mungkin saya hanya ingin menyampaikan Nama Pelanggan, belum tentu ID. Tapi kamu benar.
Johannes Rudolph
2
@Andomar: Murni ilmiah :-)
Johannes Rudolph
1
Masalah ini sangat mengganggu saya.
Daniel

Jawaban:

200

Secara umum saya selalu mencari pendekatan berbasis set (kadang-kadang dengan mengorbankan mengubah skema).

Namun, cuplikan ini memang memiliki tempatnya ..

-- Declare & init (2008 syntax)
DECLARE @CustomerID INT = 0

-- Iterate over all customers
WHILE (1 = 1) 
BEGIN  

  -- Get next customerId
  SELECT TOP 1 @CustomerID = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @CustomerId 
  ORDER BY CustomerID

  -- Exit loop if no more customers
  IF @@ROWCOUNT = 0 BREAK;

  -- call your sproc
  EXEC dbo.YOURSPROC @CustomerId

END
Mark Powell
sumber
21
seperti dengan jawaban yang diterima GUNAKAN DENGAN Kation: Bergantung pada tabel dan struktur indeks Anda dapat berkinerja sangat buruk (O (n ^ 2)) karena Anda harus memesan dan mencari meja Anda setiap kali Anda menghitung.
csauve
3
Ini sepertinya tidak berhasil (break tidak pernah keluar dari loop untuk saya - pekerjaan sudah selesai tetapi query berputar dalam loop). Menginisialisasi id dan memeriksa null di kondisi while keluar dari loop.
dudeNumber4
8
@@ ROWCOUNT hanya dapat dibaca sekali. Bahkan pernyataan IF / PRINT akan menetapkannya ke 0. Tes untuk @@ ROWCOUNT harus dilakukan 'segera' setelah pemilihan. Saya akan memeriksa ulang kode / lingkungan Anda. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell
3
Sementara loop tidak lebih baik dari kursor, berhati-hatilah, mereka bisa lebih buruk: techrepublic.com/blog/the-enterprise-cloud/…
Jaime
1
@Brennan Paus Gunakan opsi LOKAL untuk CURSOR dan itu akan dimusnahkan setelah kegagalan. Gunakan LOCAL FAST_FORWARD dan hampir tidak ada alasan untuk tidak menggunakan CURSOR untuk loop semacam ini. Ini pasti akan mengungguli loop WHILE ini.
Martin
39

Anda dapat melakukan sesuatu seperti ini: pesanlah meja Anda dengan mis. CustomerID (menggunakan Sales.Customertabel sampel AdventureWorks ), dan lakukan iterasi pada pelanggan tersebut menggunakan loop WHILE:

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0

-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT

-- select the next customer to handle    
SELECT TOP 1 @CustomerIDToHandle = CustomerID
FROM Sales.Customer
WHERE CustomerID > @LastCustomerID
ORDER BY CustomerID

-- as long as we have customers......    
WHILE @CustomerIDToHandle IS NOT NULL
BEGIN
    -- call your sproc

    -- set the last customer handled to the one we just handled
    SET @LastCustomerID = @CustomerIDToHandle
    SET @CustomerIDToHandle = NULL

    -- select the next customer to handle    
    SELECT TOP 1 @CustomerIDToHandle = CustomerID
    FROM Sales.Customer
    WHERE CustomerID > @LastCustomerID
    ORDER BY CustomerID
END

Itu harus bekerja dengan tabel apa saja selama Anda dapat mendefinisikan beberapa jenis ORDER BYpada beberapa kolom.

marc_s
sumber
@ Nyonya: ya, benar - overhead sedikit lebih sedikit. Tapi tetap saja - itu tidak benar-benar dalam mentalitas berbasis set SQL
marc_s
6
Apakah implementasi berbasis himpunan dimungkinkan?
Johannes Rudolph
Saya tidak tahu cara untuk mencapai itu, benar-benar - itu adalah tugas yang sangat prosedural untuk memulai dengan ....
marc_s
2
@marc_s menjalankan fungsi / prosedur penyimpanan untuk setiap item dalam koleksi, yang terdengar seperti roti dan mentega dari operasi berbasis set. Masalah muncul mungkin karena tidak memiliki hasil dari masing-masing. Lihat "peta" di sebagian besar bahasa pemrograman fungsional.
Daniel
4
re: Daniel. Sebuah fungsi ya, prosedur tersimpan no. Prosedur tersimpan menurut definisi dapat memiliki efek samping, dan efek samping tidak diperbolehkan dalam kueri. Demikian pula, "peta" yang tepat dalam bahasa fungsional melarang efek samping.
csauve
28
DECLARE @SQL varchar(max)=''

-- MyTable has fields fld1 & fld2

Select @SQL = @SQL + 'exec myproc ' + convert(varchar(10),fld1) + ',' 
                   + convert(varchar(10),fld2) + ';'
From MyTable

EXEC (@SQL)

Ok, jadi saya tidak akan pernah memasukkan kode seperti itu ke dalam produksi, tetapi memenuhi persyaratan Anda.

Thomas Gabriel
sumber
Bagaimana melakukan hal yang sama ketika prosedur mengembalikan nilai yang harus menetapkan nilai baris? (menggunakan PROSEDUR alih-alih fungsi karena pembuatan fungsi tidak diperbolehkan )
user2284570
@ WeihuiGuo karena kode yang dibangun secara dinamis menggunakan string sangat rentan terhadap kegagalan dan sangat menyakitkan di pantat untuk debug. Anda harus benar-benar tidak pernah melakukan hal seperti ini di luar hari libur yang tidak memiliki peluang untuk menjadi bagian rutin dari lingkungan produksi
Marie
11

Jawaban Marc baik (saya akan mengomentarinya jika saya bisa mengetahui caranya!)
Hanya berpikir saya akan menunjukkan bahwa mungkin lebih baik untuk mengubah loop jadi SELECThanya ada sekali (dalam kasus nyata di mana saya perlu lakukan ini, SELECTitu cukup rumit, dan menulisnya dua kali merupakan masalah pemeliharaan yang berisiko).

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0
-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT
SET @CustomerIDToHandle = 1

-- as long as we have customers......    
WHILE @LastCustomerID <> @CustomerIDToHandle
BEGIN  
  SET @LastCustomerId = @CustomerIDToHandle
  -- select the next customer to handle    
  SELECT TOP 1 @CustomerIDToHandle = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @LastCustomerId 
  ORDER BY CustomerID

  IF @CustomerIDToHandle <> @LastCustomerID
  BEGIN
      -- call your sproc
  END

END
Maks
sumber
APPLY hanya dapat digunakan dengan fungsi ... jadi pendekatan ini jauh lebih baik jika Anda tidak ingin melakukan fungsi.
Artur
Anda perlu 50 repetisi untuk berkomentar. Terus jawab pertanyaan-pertanyaan itu dan Anda akan mendapatkan lebih banyak kekuatan: D stackoverflow.com/help/privileges
SvendK
Saya pikir yang ini harus menjadi jawabannya, jelas dan lurus ke depan. Terima kasih banyak!
bom
7

Jika Anda bisa mengubah prosedur tersimpan menjadi fungsi yang mengembalikan tabel, maka Anda bisa menggunakan cross-apply.

Misalnya, Anda memiliki tabel pelanggan, dan Anda ingin menghitung jumlah pesanan mereka, Anda akan membuat fungsi yang mengambil CustomerID dan mengembalikan jumlahnya.

Dan Anda bisa melakukan ini:

SELECT CustomerID, CustomerSum.Total

FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum

Di mana fungsinya akan terlihat seperti:

CREATE FUNCTION ComputeCustomerTotal
(
    @CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)

Jelas, contoh di atas dapat dilakukan tanpa fungsi yang ditentukan pengguna dalam satu permintaan.

Kekurangannya adalah bahwa fungsi sangat terbatas - banyak fitur dari prosedur tersimpan tidak tersedia dalam fungsi yang ditentukan pengguna, dan mengubah prosedur tersimpan ke fungsi tidak selalu berfungsi.

David Griffiths
sumber
Dalam hal ini tidak ada izin tulis untuk membuat fungsi?
user2284570
7

Saya akan menggunakan jawaban yang diterima, tetapi kemungkinan lain adalah dengan menggunakan variabel tabel untuk menyimpan set nilai bernomor (dalam hal ini hanya bidang ID dari tabel) dan loop melalui mereka dengan Nomor Baris dengan GABUNG ke tabel untuk mengambil apa pun yang Anda butuhkan untuk tindakan di dalam loop.

DECLARE @RowCnt int; SET @RowCnt = 0 -- Loop Counter

-- Use a table variable to hold numbered rows containg MyTable's ID values
DECLARE @tblLoop TABLE (RowNum int IDENTITY (1, 1) Primary key NOT NULL,
     ID INT )
INSERT INTO @tblLoop (ID)  SELECT ID FROM MyTable

  -- Vars to use within the loop
  DECLARE @Code NVarChar(10); DECLARE @Name NVarChar(100);

WHILE @RowCnt < (SELECT COUNT(RowNum) FROM @tblLoop)
BEGIN
    SET @RowCnt = @RowCnt + 1
    -- Do what you want here with the data stored in tblLoop for the given RowNum
    SELECT @Code=Code, @Name=LongName
      FROM MyTable INNER JOIN @tblLoop tL on MyTable.ID=tL.ID
      WHERE tl.RowNum=@RowCnt
    PRINT Convert(NVarChar(10),@RowCnt) +' '+ @Code +' '+ @Name
END
Ajs Jsy
sumber
Ini lebih baik karena tidak menganggap nilai yang Anda cari adalah bilangan bulat atau dapat dibandingkan secara masuk akal.
philw
Persis apa yang saya cari.
Raithlin
6

Untuk SQL Server 2005 dan seterusnya, Anda dapat melakukan ini dengan CROSS APPLY dan fungsi bernilai tabel.

Hanya untuk kejelasan, saya merujuk pada kasus-kasus di mana prosedur tersimpan dapat dikonversi menjadi fungsi bernilai tabel.

Mitch Wheat
sumber
12
Ide bagus, tetapi suatu fungsi tidak dapat memanggil prosedur yang tersimpan
Andomar
3

Ini adalah variasi dari solusi n3rds di atas. Tidak diperlukan penyortiran dengan menggunakan ORDER BY, karena MIN () digunakan.

Ingat bahwa CustomerID (atau kolom numerik apa pun yang Anda gunakan untuk kemajuan) harus memiliki batasan unik. Selanjutnya, untuk membuatnya secepat mungkin, CustomerID harus diindeks.

-- Declare & init
DECLARE @CustomerID INT = (SELECT MIN(CustomerID) FROM Sales.Customer); -- First ID
DECLARE @Data1 VARCHAR(200);
DECLARE @Data2 VARCHAR(200);

-- Iterate over all customers
WHILE @CustomerID IS NOT NULL
BEGIN  

  -- Get data based on ID
  SELECT @Data1 = Data1, @Data2 = Data2
    FROM Sales.Customer
    WHERE [ID] = @CustomerID ;

  -- call your sproc
  EXEC dbo.YOURSPROC @Data1, @Data2

  -- Get next customerId
  SELECT @CustomerID = MIN(CustomerID)
    FROM Sales.Customer
    WHERE CustomerID > @CustomerId 

END

Saya menggunakan pendekatan ini pada beberapa varchars yang saya perlu melihat, dengan menempatkan mereka di tabel sementara pertama, untuk memberi mereka ID.

beruik
sumber
2

Jika Anda tidak harus menggunakan kursor, saya pikir Anda harus melakukannya secara eksternal (dapatkan tabel, lalu jalankan untuk setiap pernyataan dan setiap kali panggil sp) itu sama dengan menggunakan kursor, tetapi hanya di luar SQL Mengapa Anda tidak menggunakan kursor?

Dani
sumber
2

Ini adalah variasi pada jawaban yang sudah disediakan, tetapi harus berkinerja lebih baik karena tidak memerlukan ORDER OLEH, COUNT atau MIN / MAX. Satu-satunya kelemahan dengan pendekatan ini adalah Anda harus membuat tabel temp untuk menampung semua Id (asumsinya adalah Anda memiliki celah dalam daftar CustomerIDs Anda).

Yang mengatakan, saya setuju dengan @ Mark Powell meskipun, secara umum, pendekatan berbasis set masih harus lebih baik.

DECLARE @tmp table (Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL, CustomerID INT NOT NULL)
DECLARE @CustomerId INT 
DECLARE @Id INT = 0

INSERT INTO @tmp SELECT CustomerId FROM Sales.Customer

WHILE (1=1)
BEGIN
    SELECT @CustomerId = CustomerId, @Id = Id
    FROM @tmp
    WHERE Id = @Id + 1

    IF @@rowcount = 0 BREAK;

    -- call your sproc
    EXEC dbo.YOURSPROC @CustomerId;
END
Adriaan de Beer
sumber
1

Saya biasanya melakukannya dengan cara ini ketika beberapa baris:

  1. Pilih semua parameter sproc dalam dataset dengan SQL Management Studio
  2. Klik kanan -> Salin
  3. Tempel ke dalam untuk unggul
  4. Buat pernyataan sql baris tunggal dengan rumus seperti '= "EXEC schema.mysproc @ param =" & A2' di kolom excel baru. (Di mana A2 adalah kolom excel Anda yang berisi parameter)
  5. Salin daftar pernyataan excel ke kueri baru di SQL Management Studio dan jalankan.
  6. Selesai

(Pada dataset yang lebih besar saya akan menggunakan salah satu solusi yang disebutkan di atas).

Jonas Stensved
sumber
4
Tidak terlalu berguna dalam situasi pemrograman, itu adalah one-off-hack.
Warren P
1

DELIMITER //

CREATE PROCEDURE setFakeUsers (OUT output VARCHAR(100))
BEGIN

    -- define the last customer ID handled
    DECLARE LastGameID INT;
    DECLARE CurrentGameID INT;
    DECLARE userID INT;

    SET @LastGameID = 0; 

    -- define the customer ID to be handled now

    SET @userID = 0;

    -- select the next game to handle    
    SELECT @CurrentGameID = id
    FROM online_games
    WHERE id > LastGameID
    ORDER BY id LIMIT 0,1;

    -- as long as we have customers......    
    WHILE (@CurrentGameID IS NOT NULL) 
    DO
        -- call your sproc

        -- set the last customer handled to the one we just handled
        SET @LastGameID = @CurrentGameID;
        SET @CurrentGameID = NULL;

        -- select the random bot
        SELECT @userID = userID
        FROM users
        WHERE FIND_IN_SET('bot',baseInfo)
        ORDER BY RAND() LIMIT 0,1;

        -- update the game
        UPDATE online_games SET userID = @userID WHERE id = @CurrentGameID;

        -- select the next game to handle    
        SELECT @CurrentGameID = id
         FROM online_games
         WHERE id > LastGameID
         ORDER BY id LIMIT 0,1;
    END WHILE;
    SET output = "done";
END;//

CALL setFakeUsers(@status);
SELECT @status;
radixxko
sumber
1

Solusi yang lebih baik untuk ini adalah

  1. Salin / kode masa lalu dari Prosedur Tersimpan
  2. Gabungkan kode itu dengan tabel yang ingin Anda jalankan lagi (untuk setiap baris)

Ini adalah Anda mendapatkan output diformat tabel bersih. Sementara jika Anda menjalankan SP untuk setiap baris, Anda mendapatkan hasil kueri terpisah untuk setiap iterasi yang jelek.

Hammad Khan
sumber
0

Dalam hal urutannya penting

--declare counter
DECLARE     @CurrentRowNum BIGINT = 0;
--Iterate over all rows in [DataTable]
WHILE (1 = 1)
    BEGIN
        --Get next row by number of row
        SELECT TOP 1 @CurrentRowNum = extendedData.RowNum
                    --here also you can store another values
                    --for following usage
                    --@MyVariable = extendedData.Value
        FROM    (
                    SELECT 
                        data.*
                        ,ROW_NUMBER() OVER(ORDER BY (SELECT 0)) RowNum
                    FROM [DataTable] data
                ) extendedData
        WHERE extendedData.RowNum > @CurrentRowNum
        ORDER BY extendedData.RowNum

        --Exit loop if no more rows
        IF @@ROWCOUNT = 0 BREAK;

        --call your sproc
        --EXEC dbo.YOURSPROC @MyVariable
    END
pembuat isx
sumber
0

Saya memiliki beberapa kode produksi yang hanya dapat menangani 20 karyawan sekaligus, di bawah ini adalah kerangka kerja untuk kode tersebut. Saya baru saja menyalin kode produksi dan menghapus hal-hal di bawah ini.

ALTER procedure GetEmployees
    @ClientId varchar(50)
as
begin
    declare @EEList table (employeeId varchar(50));
    declare @EE20 table (employeeId varchar(50));

    insert into @EEList select employeeId from Employee where (ClientId = @ClientId);

    -- Do 20 at a time
    while (select count(*) from @EEList) > 0
    BEGIN
      insert into @EE20 select top 20 employeeId from @EEList;

      -- Call sp here

      delete @EEList where employeeId in (select employeeId from @EE20)
      delete @EE20;
    END;

  RETURN
end
William Egge
sumber
-1

Saya suka melakukan sesuatu yang mirip dengan ini (meskipun masih sangat mirip dengan menggunakan kursor)

[kode]

-- Table variable to hold list of things that need looping
DECLARE @holdStuff TABLE ( 
    id INT IDENTITY(1,1) , 
    isIterated BIT DEFAULT 0 , 
    someInt INT ,
    someBool BIT ,
    otherStuff VARCHAR(200)
)

-- Populate your @holdStuff with... stuff
INSERT INTO @holdStuff ( 
    someInt ,
    someBool ,
    otherStuff
)
SELECT  
    1 , -- someInt - int
    1 , -- someBool - bit
    'I like turtles'  -- otherStuff - varchar(200)
UNION ALL
SELECT  
    42 , -- someInt - int
    0 , -- someBool - bit
    'something profound'  -- otherStuff - varchar(200)

-- Loop tracking variables
DECLARE @tableCount INT
SET     @tableCount = (SELECT COUNT(1) FROM [@holdStuff])

DECLARE @loopCount INT
SET     @loopCount = 1

-- While loop variables
DECLARE @id INT
DECLARE @someInt INT
DECLARE @someBool BIT
DECLARE @otherStuff VARCHAR(200)

-- Loop through item in @holdStuff
WHILE (@loopCount <= @tableCount)
    BEGIN

        -- Increment the loopCount variable
        SET @loopCount = @loopCount + 1

        -- Grab the top unprocessed record
        SELECT  TOP 1 
            @id = id ,
            @someInt = someInt ,
            @someBool = someBool ,
            @otherStuff = otherStuff
        FROM    @holdStuff
        WHERE   isIterated = 0

        -- Update the grabbed record to be iterated
        UPDATE  @holdAccounts
        SET     isIterated = 1
        WHERE   id = @id

        -- Execute your stored procedure
        EXEC someRandomSp @someInt, @someBool, @otherStuff

    END

[/kode]

Perhatikan bahwa Anda tidak perlu identitas atau kolom isIterated pada temp / tabel variabel Anda, saya hanya lebih suka melakukannya dengan cara ini jadi saya tidak perlu menghapus catatan teratas dari koleksi karena saya mengulangi melalui loop.

Kritner
sumber