Quy trình lưu trữ cuộc gọi SQL cho mỗi hàng mà không cần sử dụng con trỏ


163

Làm thế nào người ta có thể gọi một thủ tục được lưu trữ cho mỗi hàng trong một bảng, trong đó các cột của một hàng là các tham số đầu vào cho sp mà không cần sử dụng Con trỏ?


3
Vì vậy, ví dụ, bạn có một bảng Khách hàng với cột customerId và bạn muốn gọi SP một lần cho mỗi hàng trong bảng, truyền vào khách hàng tương ứng như một tham số?
Gary McGill

2
Bạn có thể giải thích lý do tại sao bạn không thể sử dụng một con trỏ?
Andomar

@Gary: Có lẽ tôi chỉ muốn thông qua Tên khách hàng, không nhất thiết phải là ID. Nhưng bạn đúng.
Julian Rudolph

2
@Andomar: Hoàn toàn khoa học :-)
Rudolph

1
Vấn đề này lỗi tôi quá nhiều.
Daniel

Câu trả lời:


200

Nói chung, tôi luôn tìm kiếm một cách tiếp cận dựa trên tập hợp (đôi khi phải trả giá cho việc thay đổi lược đồ).

Tuy nhiên, đoạn trích này có vị trí của nó ..

-- 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
như với câu trả lời được chấp nhận SỬ DỤNG VỚI CATION: Tùy thuộc vào bảng và cấu trúc chỉ mục của bạn, nó có thể hoạt động rất kém (O (n ^ 2)) vì bạn phải đặt hàng và tìm kiếm bảng của mình mỗi khi bạn liệt kê.
csauve

3
Điều này dường như không hoạt động (phá vỡ không bao giờ thoát khỏi vòng lặp đối với tôi - công việc được thực hiện nhưng truy vấn sẽ quay trong vòng lặp). Khởi tạo id và kiểm tra null trong điều kiện while thoát khỏi vòng lặp.
dudeNumber4

8
@@ ROWCOUNT chỉ có thể được đọc một lần. Ngay cả các câu lệnh IF / PRINT cũng sẽ đặt nó thành 0. Việc kiểm tra @@ ROWCOUNT phải được thực hiện 'ngay lập tức' sau khi chọn. Tôi sẽ kiểm tra lại mã / môi trường của bạn. technet.microsoft.com/en-us/l Library / ms187316.aspx
Mark Powell

3
Mặc dù các vòng lặp không tốt hơn con trỏ, hãy cẩn thận, chúng thậm chí còn tồi tệ hơn: techrepublic.com/blog/the-enterprise-cloud/ mẹo
Jaime

1
@Brennan Pope Sử dụng tùy chọn ĐỊA PHƯƠNG cho HIỆN TẠI và nó sẽ bị hủy khi thất bại. Sử dụng LOCAL FAST_FORWARD và gần như không có lý do nào để không sử dụng HIỆN TẠI cho các loại vòng lặp này. Nó chắc chắn sẽ tốt hơn vòng lặp WHILE này.
Martin

39

Bạn có thể làm một cái gì đó như thế này: đặt hàng bảng của bạn bằng cách ví dụ CustomerID (sử dụng Sales.Customerbảng mẫu AdventureWorks ) và lặp lại các khách hàng đó bằng vòng lặp 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

Điều đó sẽ làm việc với bất kỳ bảng nào miễn là bạn có thể định nghĩa một loại nào đó ORDER BYtrên một cột nào đó.


@Mitch: có, đúng - ít chi phí hơn một chút. Nhưng vẫn vậy - nó không thực sự nằm trong tâm lý dựa trên tập hợp của SQL
marc_s

6
Là một bộ thực hiện dựa trên thậm chí có thể?
Julian Rudolph

Tôi không biết cách nào để đạt được điều đó, thực sự - đó là một nhiệm vụ rất thủ tục để bắt đầu ....
marc_s

2
@marc_s thực thi một chức năng / storeprocedure cho từng mục trong bộ sưu tập, nghe có vẻ giống như bánh mì và bơ của các hoạt động dựa trên tập hợp. Vấn đề có thể từ việc không có kết quả từ mỗi người trong số họ. Xem "bản đồ" trong hầu hết các ngôn ngữ lập trình chức năng.
Daniel

4
lại: Daniel. Một chức năng có, một thủ tục lưu trữ không. Một thủ tục được lưu trữ theo định nghĩa có thể có tác dụng phụ và tác dụng phụ không được phép trong các truy vấn. Tương tự, một "bản đồ" thích hợp trong một ngôn ngữ chức năng cấm các tác dụng phụ.
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, vì vậy tôi sẽ không bao giờ đưa mã như vậy vào sản xuất, nhưng nó đáp ứng yêu cầu của bạn.


Làm thế nào để làm điều tương tự khi thủ tục trả về một giá trị sẽ đặt giá trị hàng? (sử dụng THỦ TỤC thay vì chức năng vì không cho phép tạo chức năng )
user2284570

@WeihuiGuo vì Mã được xây dựng một cách linh hoạt bằng cách sử dụng chuỗi rất HORRIBLY dễ bị lỗi và đau hoàn toàn ở mông để gỡ lỗi. Bạn tuyệt đối không bao giờ được làm bất cứ điều gì như thế này ngoài việc không có cơ hội trở thành một phần thường lệ của môi trường sản xuất
Marie

11

Câu trả lời của Marc là tốt (Tôi sẽ nhận xét về nó nếu tôi có thể tìm ra cách!)
Chỉ cần nghĩ rằng tôi chỉ ra rằng có thể tốt hơn để thay đổi vòng lặp để SELECTchỉ tồn tại một lần (trong trường hợp thực tế mà tôi cần làm điều này, SELECTkhá phức tạp và viết nó hai lần là một vấn đề bảo trì rủi ro).

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

ỨNG DỤNG chỉ có thể được sử dụng với các chức năng ... vì vậy phương pháp này tốt hơn nhiều nếu bạn không muốn thực hiện với các chức năng.
Artur

Bạn cần 50 đại diện để bình luận. Tiếp tục trả lời những câu hỏi đó và bạn sẽ nhận được nhiều sức mạnh hơn: D stackoverflow.com/help/priv đặc quyền
SvendK

Tôi nghĩ rằng đây nên là câu trả lời, rõ ràng và thẳng về phía trước. Cảm ơn rât nhiều!
bomblike

7

Nếu bạn có thể biến thủ tục được lưu trữ thành một hàm trả về một bảng, thì bạn có thể sử dụng áp dụng chéo.

Ví dụ: giả sử bạn có một bảng khách hàng và bạn muốn tính tổng số đơn đặt hàng của họ, bạn sẽ tạo một hàm lấy ID khách hàng và trả lại tổng.

Và bạn có thể làm điều này:

SELECT CustomerID, CustomerSum.Total

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

Chức năng sẽ như thế nào:

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

Rõ ràng, ví dụ trên có thể được thực hiện mà không có hàm do người dùng xác định trong một truy vấn duy nhất.

Hạn chế là các chức năng rất hạn chế - nhiều tính năng của một thủ tục được lưu trữ không có sẵn trong một hàm do người dùng xác định và việc chuyển đổi một thủ tục được lưu trữ thành một chức năng không phải lúc nào cũng hoạt động.


Trong trường hợp không có quyền ghi để tạo hàm?
dùng2284570

7

Tôi sẽ sử dụng câu trả lời được chấp nhận, nhưng một khả năng khác là sử dụng biến bảng để giữ một bộ giá trị được đánh số (trong trường hợp này chỉ là trường ID của bảng) và lặp qua các số đó bằng Row Number với THAM GIA vào bảng để lấy bất cứ thứ gì bạn cần cho hành động trong vòng lặp.

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

Điều này tốt hơn bởi vì nó không giả sử giá trị bạn theo sau là một số nguyên hoặc có thể được so sánh hợp lý.
philw

Chính xác những gì tôi đang tìm kiếm.
Raithlin

6

Đối với SQL Server 2005 trở đi, bạn có thể thực hiện việc này với CROSS ỨNG DỤNG và hàm có giá trị bảng.

Để rõ ràng, tôi đang đề cập đến những trường hợp trong đó thủ tục được lưu trữ có thể được chuyển đổi thành một hàm có giá trị bảng.


12
Ý tưởng hay, nhưng một chức năng không thể gọi một thủ tục được lưu trữ
Andomar

3

Đây là một biến thể của giải pháp n3rd ở trên. Không cần sắp xếp bằng cách sử dụng ORDER BY, vì MIN () được sử dụng.

Hãy nhớ rằng CustomerID (hoặc bất kỳ cột số nào khác mà bạn sử dụng để tiến bộ) phải có một ràng buộc duy nhất. Hơn nữa, để làm cho nó càng nhanh càng tốt, ID khách hàng phải được lập chỉ mục.

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

Tôi sử dụng phương pháp này trên một số varchars tôi cần xem qua, bằng cách đặt chúng vào một bảng tạm thời trước, để cung cấp cho chúng một ID.


2

Nếu bạn không sử dụng con trỏ, tôi nghĩ bạn sẽ phải thực hiện bên ngoài (lấy bảng, sau đó chạy cho từng câu lệnh và mỗi lần gọi sp), nó giống như sử dụng con trỏ, nhưng chỉ ở bên ngoài SQL. Tại sao bạn không sử dụng con trỏ?


2

Đây là một biến thể của các câu trả lời đã được cung cấp, nhưng sẽ hoạt động tốt hơn vì nó không yêu cầu ĐẶT HÀNG B, NG, hoặc MIN / MAX. Nhược điểm duy nhất với cách tiếp cận này là bạn phải tạo một bảng tạm thời để chứa tất cả các Id (giả định là bạn có những khoảng trống trong danh sách ID khách hàng).

Điều đó nói rằng, tôi đồng ý với @Mark Powell mặc dù nói chung, cách tiếp cận dựa trên tập hợp vẫn sẽ tốt hơn.

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

Tôi thường làm theo cách này khi nó có một vài hàng:

  1. Chọn tất cả các tham số sproc trong bộ dữ liệu với SQL Management Studio
  2. Nhấp chuột phải -> Sao chép
  3. Dán vào excel
  4. Tạo các câu lệnh sql một hàng với công thức như '= "lược đồ EXEC.mysproc @ param =" & A2' trong một cột excel mới. (Trong đó A2 là cột excel của bạn chứa tham số)
  5. Sao chép danh sách các câu lệnh excel vào một truy vấn mới trong SQL Management Studio và thực thi.
  6. Làm xong.

(Trên các bộ dữ liệu lớn hơn, tôi sẽ sử dụng một trong những giải pháp được đề cập ở trên).


4
Không hữu ích lắm trong các tình huống lập trình, đó là một lần hack.
Warren P

1

GIAO HÀNG //

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

Một giải pháp tốt hơn cho việc này là

  1. Sao chép / mã quá khứ của Thủ tục lưu trữ
  2. Tham gia mã đó với bảng mà bạn muốn chạy lại nó (cho mỗi hàng)

Đây là bạn có được một đầu ra định dạng bảng sạch. Trong khi nếu bạn chạy SP cho mỗi hàng, bạn sẽ nhận được một kết quả truy vấn riêng cho mỗi lần lặp đó là xấu.


0

Trong trường hợp thứ tự là quan trọng

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

Tôi đã có một số mã sản xuất chỉ có thể xử lý 20 nhân viên một lúc, dưới đây là khung cho mã. Tôi chỉ sao chép mã sản xuất và loại bỏ những thứ bên dưới.

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

Tôi thích làm một cái gì đó tương tự như thế này (mặc dù nó vẫn rất giống với việc sử dụng một con trỏ)

[mã]

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

[/ mã]

Lưu ý rằng bạn không cần danh tính hoặc cột isIterated trên bảng temp / biến của bạn, tôi chỉ muốn làm theo cách này vì vậy tôi không phải xóa bản ghi hàng đầu khỏi bộ sưu tập khi tôi lặp qua vòng lặp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.