SQL Call Stored Procedure สำหรับแต่ละแถวโดยไม่ใช้เคอร์เซอร์


163

เราจะเรียกโพรซีเดอร์ที่เก็บไว้สำหรับแต่ละแถวในตารางโดยที่คอลัมน์ของแถวเป็นพารามิเตอร์อินพุตไปยัง sp โดยไม่ต้องใช้เคอร์เซอร์ได้อย่างไร?


3
ตัวอย่างเช่นคุณมีตารางลูกค้าที่มีคอลัมน์ customerId และคุณต้องการเรียก SP หนึ่งครั้งสำหรับแต่ละแถวในตารางส่งผ่าน customerId ที่สอดคล้องกันเป็นพารามิเตอร์หรือไม่
Gary McGill

2
คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงใช้เคอร์เซอร์ไม่ได้?
Andomar

@ แกรี่: บางทีฉันแค่ต้องการส่งชื่อลูกค้าไม่จำเป็นต้องเป็น ID แต่คุณพูดถูก
โยฮันเนสรูดอล์ฟ

2
@ Andomar: วิทยาศาสตร์อย่างหมดจด :-)
Johannes Rudolph

1
ปัญหานี้ทำให้ฉันอย่างมากเช่นกัน
Daniel

คำตอบ:


200

โดยทั่วไปฉันมักจะมองหาวิธีการตั้งค่า (บางครั้งค่าใช้จ่ายในการเปลี่ยนสคีมา)

อย่างไรก็ตามตัวอย่างนี้ไม่มีที่ของมัน ..

-- 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
เช่นเดียวกับคำตอบที่ได้รับการยอมรับใช้กับ Cation: ขึ้นอยู่กับโครงสร้างตารางและดัชนีของคุณมันอาจมีประสิทธิภาพต่ำมาก (O (n ^ 2)) เนื่องจากคุณต้องสั่งซื้อและค้นหาตารางของคุณทุกครั้งที่ระบุ
csauve

3
สิ่งนี้ดูเหมือนจะไม่ทำงาน (ตัวแบ่งไม่เคยออกจากลูปสำหรับฉัน - งานเสร็จ แต่เคียวรีหมุนในลูป) การเริ่มต้น id และการตรวจสอบ null ในขณะที่เงื่อนไขออกจากลูป
dudeNumber4

8
@@ ROWCOUNT สามารถอ่านได้เพียงครั้งเดียว แม้ว่า IF / PRINT จะตั้งค่าเป็น 0 การทดสอบสำหรับ @@ ROWCOUNT จะต้องทำ 'ทันที' หลังจากเลือก ฉันจะตรวจสอบรหัส / สภาพแวดล้อมของคุณอีกครั้ง technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell

3
ในขณะที่ลูปไม่ได้ดีไปกว่าเคอร์เซอร์โปรดระวังพวกมันอาจแย่กว่านี้: techrepublic.com/blog/the-enterprise-cloud/ ......
Jaime

1
@Brennan Pope ใช้ตัวเลือก LOCAL สำหรับเคอร์เซอร์และมันจะถูกทำลายเมื่อล้มเหลว ใช้ LAST FAST_FORWARD และมีเหตุผลเกือบเป็นศูนย์ที่จะไม่ใช้ CURSOR สำหรับลูปประเภทนี้ มันจะมีประสิทธิภาพสูงกว่าแน่นอนในขณะที่วงนี้
มาร์ติน

39

คุณสามารถทำสิ่งนี้: สั่งซื้อตารางของคุณโดยเช่น CustomerID (ใช้Sales.Customerตารางตัวอย่างAdventureWorks ) และวนซ้ำกับลูกค้าเหล่านั้นโดยใช้ลูป 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

ที่ควรทำงานกับตารางใด ๆ ตราบเท่าที่คุณสามารถกำหนดชนิดของORDER BYบางอย่างในบางคอลัมน์


@ Mitch: ใช่จริง - ค่าใช้จ่ายน้อยลงเล็กน้อย แต่ถึงกระนั้น - มันไม่ได้อยู่ในความคิดที่ตั้งอยู่บน SQL
marc_s

6
การติดตั้งใช้งานเป็นไปได้หรือไม่
โยฮันเนสรูดอล์ฟ

ผมไม่ทราบวิธีที่จะประสบความสำเร็จที่ใด ๆ จริงๆ - มันเป็นงานที่ขั้นตอนมากที่จะเริ่มต้นด้วย ....
marc_s

2
@marc_s ดำเนินการฟังก์ชั่น / storeprocedure สำหรับแต่ละรายการในคอลเลกชันที่ดูเหมือนขนมปังและเนยของการดำเนินงานตามชุด ปัญหาอาจเกิดจากการไม่ได้ผลลัพธ์จากแต่ละข้อ ดู "แผนที่" ในภาษาโปรแกรมที่ใช้งานได้ดีที่สุด
Daniel

4
re: แดเนียล ฟังก์ชั่นใช่เป็นกระบวนงานที่เก็บไว้ ขั้นตอนการจัดเก็บตามคำจำกัดความสามารถมีผลข้างเคียงและไม่อนุญาตให้มีผลข้างเคียงในแบบสอบถาม ในทำนองเดียวกัน "แผนที่" ที่เหมาะสมในภาษาที่ใช้งานได้ห้ามผลข้างเคียง
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)

ตกลงดังนั้นฉันจะไม่ใส่รหัสดังกล่าวในการผลิต แต่มันจะตอบสนองความต้องการของคุณ


จะทำสิ่งเดียวกันเมื่อโพรซีเดอร์ส่งคืนค่าที่ควรตั้งค่าแถวได้อย่างไร? (ใช้ PROCEDURE แทนฟังก์ชันเนื่องจากไม่อนุญาตให้สร้างฟังก์ชัน )
user2284570

@ WeihuiGuo เนื่องจาก Code ที่สร้างขึ้นแบบไดนามิกโดยใช้สตริงนั้น HORRIBLY มีแนวโน้มที่จะล้มเหลวและความเจ็บปวดโดยรวมในการแก้ไขข้อบกพร่อง คุณไม่ควรทำอะไรเช่นนี้นอกสถานที่ซึ่งไม่มีโอกาสได้เป็นส่วนหนึ่งของสภาพแวดล้อมการผลิต
Marie

11

คำตอบของ Marc เป็นสิ่งที่ดี (ฉันจะแสดงความเห็นถ้าฉันสามารถหาวิธีการ!)
แค่คิดว่าฉันชี้ให้เห็นว่ามันอาจจะดีกว่าที่จะเปลี่ยนวงเพื่อให้SELECTมีอยู่เพียงครั้งเดียว (ในกรณีจริงที่ฉันต้องการ ทำสิ่งนี้SELECTค่อนข้างซับซ้อนและการเขียนสองครั้งเป็นปัญหาการบำรุงรักษาที่มีความเสี่ยง)

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

สามารถใช้กับฟังก์ชั่นนี้ได้เท่านั้น ... ดังนั้นวิธีนี้จึงดีกว่ามากหากคุณไม่ต้องการใช้ฟังก์ชั่น
Artur

คุณต้องการ 50 reps เพื่อแสดงความคิดเห็น ตอบคำถามเหล่านั้นต่อไปและคุณจะได้รับพลังมากขึ้น: D stackoverflow.com/help/privileges
SvendK

ฉันคิดว่าอันนี้ควรเป็นคำตอบที่เรียบง่ายและตรงไปตรงมา ขอบคุณมาก!
ขยะแขยง

7

หากคุณสามารถเปลี่ยนกระบวนงานที่เก็บไว้เป็นฟังก์ชั่นที่ส่งกลับตารางคุณสามารถใช้ cross-Apply ได้

ตัวอย่างเช่นสมมติว่าคุณมีตารางลูกค้าและคุณต้องการคำนวณผลรวมของคำสั่งซื้อคุณจะสร้างฟังก์ชันที่ใช้ CustomerID และส่งคืนผลรวม

และคุณสามารถทำสิ่งนี้:

SELECT CustomerID, CustomerSum.Total

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

ฟังก์ชันจะมีลักษณะอย่างไร:

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

เห็นได้ชัดว่าตัวอย่างข้างต้นสามารถทำได้โดยไม่มีฟังก์ชั่นที่ผู้ใช้กำหนดในแบบสอบถามเดียว

ข้อเสียเปรียบก็คือฟังก์ชั่นนั้นมี จำกัด มาก - คุณสมบัติหลายอย่างของโพรซีเดอร์ที่เก็บไว้ไม่สามารถใช้งานได้ในฟังก์ชั่นที่ผู้ใช้กำหนดและการแปลงโพรซีเดอร์ที่เก็บไว้เป็นฟังก์ชั่นนั้นไม่ได้ผลเสมอไป


ในกรณีที่ไม่มีสิทธิ์ในการเขียนสำหรับการสร้างฟังก์ชั่น?
user2284570

7

ฉันจะใช้คำตอบที่ยอมรับได้ แต่มีความเป็นไปได้อีกอย่างหนึ่งคือการใช้ตัวแปร table เพื่อเก็บค่าตัวเลข (ในกรณีนี้เป็นเพียงฟิลด์ ID ของตาราง) และวนซ้ำตามค่าเหล่านั้นด้วย Row Number พร้อม JOIN ไปยังตารางเพื่อ เรียกสิ่งที่คุณต้องการสำหรับการกระทำภายในลูป

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

สิ่งนี้ดีกว่าเพราะไม่ถือว่าค่าที่คุณตามมาเป็นจำนวนเต็มหรือสามารถเปรียบเทียบได้อย่างสมเหตุสมผล
philw

สิ่งที่ฉันกำลังมองหา
Raithlin

6

สำหรับ SQL Server 2005 เป็นต้นไปคุณสามารถทำได้ด้วยCROSS APPLYและฟังก์ชั่นที่ให้คุณค่ากับตาราง

เพื่อความชัดเจนฉันหมายถึงกรณีเหล่านี้ที่สามารถแปลงกระบวนงานที่เก็บไว้เป็นฟังก์ชันที่มีค่าในตาราง


12
เป็นความคิดที่ดี แต่ฟังก์ชันไม่สามารถเรียกกระบวนงานที่เก็บไว้ได้
Andomar

3

นี่คือรูปแบบของโซลูชัน n3rds ด้านบน ไม่จำเป็นต้องเรียงลำดับโดยใช้ ORDER BY เนื่องจากมีการใช้ MIN ()

จำไว้ว่า CustomerID (หรือคอลัมน์ตัวเลขอื่น ๆ ที่คุณใช้เพื่อความคืบหน้า) จะต้องมีข้อ จำกัด ที่ไม่ซ้ำกัน นอกจากนี้เพื่อให้เร็วที่สุดเท่าที่เป็นไปได้ CustomerID จะต้องจัดทำดัชนีไว้

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

ฉันใช้วิธีนี้กับ varchars บางตัวที่ฉันต้องการตรวจสอบโดยวางลงในตารางชั่วคราวก่อนเพื่อให้ ID แก่พวกเขา


2

หากคุณไม่ต้องการใช้เคอร์เซอร์ฉันคิดว่าคุณจะต้องทำจากภายนอก (รับตารางจากนั้นเรียกใช้สำหรับแต่ละคำสั่งและทุกครั้งที่เรียกใช้ sp) มันเหมือนกับการใช้เคอร์เซอร์ แต่ภายนอกเท่านั้น SQL ทำไมคุณไม่ใช้เคอร์เซอร์ล่ะ?


2

นี่เป็นรูปแบบของคำตอบที่ให้ไว้แล้ว แต่ควรจะทำงานได้ดีขึ้นเนื่องจากไม่ต้องการ ORDER BY, COUNT หรือ MIN / MAX ข้อเสียเพียงอย่างเดียวของวิธีนี้คือคุณต้องสร้างตาราง temp เพื่อเก็บรหัสทั้งหมด (สมมุติว่าคุณมีช่องว่างในรายการรหัสลูกค้าของคุณ)

ที่กล่าวว่าฉันเห็นด้วยกับ @Mark Powell แม้ว่าโดยทั่วไปแล้ววิธีการตั้งค่าควรจะดีกว่า

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

ฉันมักจะทำแบบนี้เมื่อมันค่อนข้างไม่กี่แถว:

  1. เลือกพารามิเตอร์ sproc ทั้งหมดในชุดข้อมูลด้วย SQL Management Studio
  2. คลิกขวา -> คัดลอก
  3. วางลงใน excel
  4. สร้างคำสั่ง SQL แบบแถวเดี่ยวด้วยสูตรเช่น '= "EXEC schema.mysproc @ param =" & A2' ในคอลัมน์ excel ใหม่ (โดยที่ A2 คือคอลัมน์ excel ของคุณที่มีพารามิเตอร์)
  5. คัดลอกรายการของคำสั่ง excel ลงในแบบสอบถามใหม่ใน SQL Management Studio และดำเนินการ
  6. เสร็จสิ้น

(ในชุดข้อมูลขนาดใหญ่ฉันจะใช้วิธีแก้ปัญหาที่กล่าวถึงข้างต้น)


4
ไม่มีประโยชน์อย่างมากในสถานการณ์การเขียนโปรแกรมนั่นคือการแฮกแบบครั้งเดียว
วอร์เรน 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

ทางออกที่ดีกว่าสำหรับเรื่องนี้คือ

  1. คัดลอก / รหัสที่ผ่านมาของกระบวนงานที่เก็บไว้
  2. เข้าร่วมรหัสนั้นกับตารางที่คุณต้องการเรียกใช้อีกครั้ง (สำหรับแต่ละแถว)

นี่คือคุณได้รับผลลัพธ์การจัดรูปแบบตารางที่สะอาด แม้ว่าคุณจะเรียกใช้ SP สำหรับทุกแถวคุณจะได้รับผลลัพธ์แบบสอบถามแยกต่างหากสำหรับการทำซ้ำแต่ละครั้งซึ่งน่าเกลียด


0

ในกรณีที่คำสั่งซื้อมีความสำคัญ

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

ฉันมีรหัสการผลิตบางอย่างที่สามารถรองรับพนักงานได้ครั้งละ 20 คนด้านล่างนี้เป็นกรอบสำหรับรหัส ฉันเพิ่งคัดลอกรหัสการผลิตและลบสิ่งต่าง ๆ ด้านล่าง

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

ฉันชอบทำสิ่งที่คล้ายกับสิ่งนี้ (แม้ว่ามันจะยังคล้ายกันมากกับการใช้เคอร์เซอร์)

[รหัส]

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

[/รหัส]

โปรดทราบว่าคุณไม่จำเป็นต้องมีข้อมูลประจำตัวหรือคอลัมน์ที่มีการเปลี่ยนแปลงในตาราง temp / variable ของคุณฉันเพียงต้องการทำเช่นนี้ดังนั้นฉันไม่ต้องลบระเบียนด้านบนออกจากคอลเลกชันขณะที่วนซ้ำผ่านลูป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.