เราจะเรียกโพรซีเดอร์ที่เก็บไว้สำหรับแต่ละแถวในตารางโดยที่คอลัมน์ของแถวเป็นพารามิเตอร์อินพุตไปยัง sp โดยไม่ต้องใช้เคอร์เซอร์ได้อย่างไร?
เราจะเรียกโพรซีเดอร์ที่เก็บไว้สำหรับแต่ละแถวในตารางโดยที่คอลัมน์ของแถวเป็นพารามิเตอร์อินพุตไปยัง sp โดยไม่ต้องใช้เคอร์เซอร์ได้อย่างไร?
คำตอบ:
โดยทั่วไปฉันมักจะมองหาวิธีการตั้งค่า (บางครั้งค่าใช้จ่ายในการเปลี่ยนสคีมา)
อย่างไรก็ตามตัวอย่างนี้ไม่มีที่ของมัน ..
-- 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
คุณสามารถทำสิ่งนี้: สั่งซื้อตารางของคุณโดยเช่น 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
บางอย่างในบางคอลัมน์
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)
ตกลงดังนั้นฉันจะไม่ใส่รหัสดังกล่าวในการผลิต แต่มันจะตอบสนองความต้องการของคุณ
คำตอบของ 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
หากคุณสามารถเปลี่ยนกระบวนงานที่เก็บไว้เป็นฟังก์ชั่นที่ส่งกลับตารางคุณสามารถใช้ 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
)
เห็นได้ชัดว่าตัวอย่างข้างต้นสามารถทำได้โดยไม่มีฟังก์ชั่นที่ผู้ใช้กำหนดในแบบสอบถามเดียว
ข้อเสียเปรียบก็คือฟังก์ชั่นนั้นมี จำกัด มาก - คุณสมบัติหลายอย่างของโพรซีเดอร์ที่เก็บไว้ไม่สามารถใช้งานได้ในฟังก์ชั่นที่ผู้ใช้กำหนดและการแปลงโพรซีเดอร์ที่เก็บไว้เป็นฟังก์ชั่นนั้นไม่ได้ผลเสมอไป
ฉันจะใช้คำตอบที่ยอมรับได้ แต่มีความเป็นไปได้อีกอย่างหนึ่งคือการใช้ตัวแปร 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
สำหรับ SQL Server 2005 เป็นต้นไปคุณสามารถทำได้ด้วยCROSS APPLYและฟังก์ชั่นที่ให้คุณค่ากับตาราง
เพื่อความชัดเจนฉันหมายถึงกรณีเหล่านี้ที่สามารถแปลงกระบวนงานที่เก็บไว้เป็นฟังก์ชันที่มีค่าในตาราง
นี่คือรูปแบบของโซลูชัน 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 แก่พวกเขา
หากคุณไม่ต้องการใช้เคอร์เซอร์ฉันคิดว่าคุณจะต้องทำจากภายนอก (รับตารางจากนั้นเรียกใช้สำหรับแต่ละคำสั่งและทุกครั้งที่เรียกใช้ sp) มันเหมือนกับการใช้เคอร์เซอร์ แต่ภายนอกเท่านั้น SQL ทำไมคุณไม่ใช้เคอร์เซอร์ล่ะ?
นี่เป็นรูปแบบของคำตอบที่ให้ไว้แล้ว แต่ควรจะทำงานได้ดีขึ้นเนื่องจากไม่ต้องการ 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
ฉันมักจะทำแบบนี้เมื่อมันค่อนข้างไม่กี่แถว:
(ในชุดข้อมูลขนาดใหญ่ฉันจะใช้วิธีแก้ปัญหาที่กล่าวถึงข้างต้น)
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;
ทางออกที่ดีกว่าสำหรับเรื่องนี้คือ
- คัดลอก / รหัสที่ผ่านมาของกระบวนงานที่เก็บไว้
- เข้าร่วมรหัสนั้นกับตารางที่คุณต้องการเรียกใช้อีกครั้ง (สำหรับแต่ละแถว)
นี่คือคุณได้รับผลลัพธ์การจัดรูปแบบตารางที่สะอาด แม้ว่าคุณจะเรียกใช้ SP สำหรับทุกแถวคุณจะได้รับผลลัพธ์แบบสอบถามแยกต่างหากสำหรับการทำซ้ำแต่ละครั้งซึ่งน่าเกลียด
ในกรณีที่คำสั่งซื้อมีความสำคัญ
--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
ฉันมีรหัสการผลิตบางอย่างที่สามารถรองรับพนักงานได้ครั้งละ 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
ฉันชอบทำสิ่งที่คล้ายกับสิ่งนี้ (แม้ว่ามันจะยังคล้ายกันมากกับการใช้เคอร์เซอร์)
[รหัส]
-- 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 ของคุณฉันเพียงต้องการทำเช่นนี้ดังนั้นฉันไม่ต้องลบระเบียนด้านบนออกจากคอลเลกชันขณะที่วนซ้ำผ่านลูป