İmleç kullanmadan her Satır için SQL Çağrı Saklı Yordamı


163

Arka arkaya sütunları sp giriş parametreleri nerede nasıl biri bir tablodaki her satır için saklı yordam çağırabilir olmadan bir imleç kullanarak?


3
Örneğin, customerId sütunu olan bir Müşteri tablonuz var ve ilgili customerId'yi parametre olarak ileterek, tablodaki her satır için SP'yi bir kez çağırmak mı istiyorsunuz?
Gary McGill

2
Neden imleci kullanamayacağınızı açıklayabilir misiniz?
Andomar

@Gary: Belki de sadece Müşteri Adını geçmek istiyorum, kimliği değil. Ama sen haklısın.
Johannes Rudolph

2
@Andomar: Tamamen bilimsel :-)
Johannes Rudolph

1
Bu sorun da beni çok rahatsız ediyor.
Daniel

Yanıtlar:


200

Genel olarak konuşursak her zaman kümeye dayalı bir yaklaşım ararım (bazen şemayı değiştirmek pahasına).

Ancak bu pasajın yeri var ..

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

21
kabul edilen cevapta olduğu gibi CATION İLE KULLANIN: Tablonuza ve dizin yapınıza bağlı olarak, her numaralandırışınızda masanızı sipariş etmek ve aramak zorunda olduğunuz için çok kötü performans gösterebilir (O ​​(n ^ 2)).
csauve

3
Bu işe yaramaz gibi görünüyor (break benim için döngüden asla çıkmaz - iş yapılır, ancak sorgu döngüde döner). Kimliğin başlatılması ve while durumunda null değerinin kontrol edilmesi döngüden çıkar.
dudeNumber4

8
@@ ROWCOUNT yalnızca bir kez okunabilir. IF / PRINT deyimleri bile 0 olarak ayarlanır. @@ ROWCOUNT için test, seçimden hemen sonra 'hemen' yapılmalıdır. Kodunuzu / ortamınızı tekrar kontrol ederim. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell

3
Döngüler imleçlerden daha iyi olmasa da, dikkatli olun, daha da kötü olabilirler: techrepublic.com/blog/the-enterprise-cloud/…
Jaime

1
@Brennan Pope Bir CURSOR için LOCAL seçeneğini kullanın; başarısızlık durumunda imha edilir. LOCAL FAST_FORWARD kullanın ve bu tür döngüler için CURSOR'ları kullanmamanın neredeyse sıfır nedeni vardır. Kesinlikle bu WHILE döngüsüne göre daha iyi performans gösterir.
Martin

39

Bunun gibi bir şey yapabilirsiniz: tablonuzu örneğin MüşteriNo (AdventureWorks Sales.Customerörnek tablosunu kullanarak) ile sipariş edin ve bir WHILE döngüsü kullanarak bu müşteriler üzerinde tekrarlayın:

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

ORDER BYBazı sütunlarda bir tür tanımlayabildiğiniz sürece bu herhangi bir tablo ile çalışmalıdır .


@Mitch: evet, doğru - biraz daha az ek yük. Ama yine de - gerçekten SQL'in set tabanlı zihniyetinde değil
marc_s

6
Kümeye dayalı bir uygulama bile mümkün müdür?
Johannes Rudolph

Bunu başarmak için herhangi bir yol bilmiyorum, gerçekten - başlamak çok prosedürel bir görev ....
marc_s

2
@marc_s, koleksiyondaki her öğe için set tabanlı işlemlerin ekmeği ve tereyağı gibi görünen bir işlev / mağaza işlemi yürütür. Sorun muhtemelen her birinden sonuç alamamaktan kaynaklanmaktadır. Çoğu işlevsel programlama dilinde "harita" ya bakınız.
Daniel

4
re: Daniel. Bir fonksiyon evet, saklı bir prosedür no. Tanımlı bir saklı yordamın yan etkileri olabilir ve sorgularda yan etkilere izin verilmez. Benzer şekilde, işlevsel bir dilde uygun bir "harita" yan etkileri yasaklar.
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)

Tamam, bu yüzden bu kodu asla üretime sokmam, ancak gereksinimlerinizi karşılar.


Prosedür satır değerini ayarlaması gereken bir değer döndürdüğünde aynı şey nasıl yapılır? ( işlev oluşturulmasına izin verilmediği için işlev yerine bir PROSEDÜR kullanılıyor )
user2284570

@ WeihuiGuo çünkü dizeleri kullanarak dinamik olarak oluşturulan Kod hataya ve popo hata ayıklamaya toplam ağrı KORKUNÇ eğilimli. Bir üretim ortamının rutin bir parçası olma şansı olmayan tek bir şey dışında kesinlikle böyle bir şey yapmamalısınız
Marie

11

Marc'ın cevabı iyidir (nasıl yapılacağını öğrenebilirsem yorum yapardım!)
Sadece döngüyü değiştirmenin daha iyi olabileceğini düşündüm, bu yüzden SELECTsadece bir kez var (gerektiğinde gerçek bir durumda) bunu yapmak SELECToldukça karmaşıktı ve iki kez yazmak riskli bir bakım sorunuydu).

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

UYGULA yalnızca işlevlerle kullanılabilir ... bu nedenle işlevlerle ilgili olmak istemiyorsanız bu yaklaşım çok daha iyidir.
Artur

Yorum yapmak için 50 temsilciye ihtiyacınız var. Bu soruları yanıtlamaya devam edin ve daha fazla güç elde edin: D stackoverflow.com/help/privileges
SvendK

Bence bu cevap basit ve düz bir cevap olmalı. Çok teşekkür ederim!
1717'de

7

Saklı yordamı bir tablo döndüren bir işleve dönüştürebilirseniz, çapraz uygulama kullanabilirsiniz.

Örneğin, bir müşteri tablonuz olduğunu ve siparişlerinin toplamını hesaplamak istediğinizi varsayarak, MüşteriNo'yu alan ve toplamı döndüren bir işlev yaratabilirsiniz.

Ve bunu yapabilirsiniz:

SELECT CustomerID, CustomerSum.Total

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

Fonksiyonun nerede görüneceği:

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

Açıkçası, yukarıdaki örnek tek bir sorguda kullanıcı tanımlı bir işlev olmadan yapılabilir.

Dezavantajı, fonksiyonların çok sınırlı olmasıdır - saklı bir prosedürün özelliklerinin çoğu kullanıcı tanımlı bir fonksiyonda mevcut değildir ve saklı bir prosedürü bir fonksiyona dönüştürmek her zaman işe yaramaz.


Bir işlev oluşturmak için yazma izinleri yoksa?
user2284570

7

Kabul edilen cevabı kullanırdım, ancak başka bir olasılık, sayılar kümesi (bu durumda sadece bir tablonun ID alanı) tutmak için bir tablo değişkeni kullanmak ve tabloya bir JOIN ile satır numarası ile döngü döngü içindeki eylem için ihtiyacınız olan her şeyi alın.

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

Bu daha iyidir çünkü peşinde olduğunuz değerin bir tamsayı olduğunu veya mantıklı olarak karşılaştırılabileceğini varsaymaz.
philw

Tam aradığım şey.
Raithlin


3

Bu yukarıdaki n3rds çözümünün bir varyasyonudur. MIN () kullanıldığından, ORDER BY kullanarak sıralama gerekmez.

CustomerID'nin (veya ilerleme için kullandığınız diğer sayısal sütunların) benzersiz bir kısıtlaması olması gerektiğini unutmayın. Ayrıca, olabildiğince hızlı hale getirmek için CustomerID endekslenmelidir.

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

Bu yaklaşımı, bir kimlik vermek için önce geçici bir tabloya koyarak göz atmam gereken bazı varcharlar üzerinde kullanıyorum.


2

Eğer bir imleci kullanmıyorsanız, bunu harici olarak yapmanız gerekeceğini düşünüyorum (tabloyu alın ve sonra her ifade için çalıştırın ve sp'yi her aradığınızda) bir imleç kullanmakla aynıdır, ancak sadece dışarıda SQL. Neden imleç kullanmayacaksın?


2

Bu zaten verilen cevapların bir varyasyonudur, ancak ORDER BY, COUNT veya MIN / MAX gerektirmediği için daha iyi performans göstermelidir. Bu yaklaşımın tek dezavantajı, tüm Kimlikleri tutmak için bir geçici tablo oluşturmanız gerektiğidir (varsayım, CustomerID'ler listenizde boşluklar olduğudur).

Bununla birlikte, @Mark Powell'a katılıyorum, ancak genel olarak konuşursak, set temelli bir yaklaşımın daha iyi olması gerekir.

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

1

Genellikle birkaç satır olduğunda bu şekilde yaparım:

  1. SQL Management Studio ile veri kümesindeki tüm sproc parametrelerini seçme
  2. Sağ tıklayın -> Kopyala
  3. Excel'e yapıştırın
  4. Yeni bir excel sütununda '= "EXEC schema.mysproc @ param =" & A2' gibi bir formülle tek satırlık sql ifadeleri oluşturun. (A2 parametresini içeren Excel sütununuz)
  5. Excel ifadeleri listesini SQL Management Studio'da yeni bir sorguya kopyalayın ve yürütün.
  6. Bitti.

(Daha büyük veri kümelerinde yukarıda belirtilen çözümlerden birini kullanırım).


4
Programlama durumlarında çok kullanışlı değil, bu bir kerelik bir 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;

1

Bunun için daha iyi bir çözüm

  1. Saklı Yordamın kodunu kopyala / geç
  2. Bu kodu, tekrar çalıştırmak istediğiniz tabloyla birleştirin (her satır için)

Bu tablo biçimlendirilmiş temiz bir çıktı almak oldu. Her satır için SP çalıştırırsanız, çirkin olan her yineleme için ayrı bir sorgu sonucu alırsınız.


0

Siparişin önemli olması durumunda

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

0

Bir seferde sadece 20 çalışanı işleyebilecek bazı üretim kodum vardı, kodun çerçevesi aşağıda. Sadece üretim kodunu kopyaladım ve aşağıdaki şeyleri kaldırdım.

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

-1

Buna benzer bir şey yapmayı seviyorum (yine de imleç kullanmaya çok benzer)

[Kod]

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

[/ Kod]

Eğer döngü veya temp / değişken tablonuzda isIterated sütun gerekmez , ben sadece bu şekilde yapmayı tercih böylece i döngü üzerinden yineleme olarak koleksiyonundan üst kayıt silmek zorunda değilsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.