Có cách nào để lặp qua một biến bảng trong TSQL mà không cần sử dụng con trỏ không?


243

Giả sử tôi có biến bảng đơn giản sau:

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases

Là khai báo và sử dụng một con trỏ tùy chọn duy nhất của tôi nếu tôi muốn lặp qua các hàng? Có cách nào khác không?


3
Mặc dù tôi không chắc vấn đề bạn gặp phải với cách tiếp cận trên; Xem nếu điều này giúp .. cơ sở dữ
liệujournal.com/features/mssql/article.php/3111031

5
Bạn có thể cung cấp cho chúng tôi lý do tại sao bạn muốn lặp lại các hàng không, giải pháp khác không yêu cầu lặp lại có thể tồn tại (và nhanh hơn bởi một biên độ lớn trong hầu hết các trường hợp)
Pop Catalin

đồng ý với pop ... có thể không cần con trỏ tùy theo tình huống. nhưng không có vấn đề gì với việc sử dụng con trỏ nếu bạn cần
Shawn

3
Bạn không nói lý do tại sao bạn muốn tránh một con trỏ. Xin lưu ý rằng một con trỏ có thể là cách đơn giản nhất để lặp lại. Bạn có thể đã nghe nói rằng các con trỏ là 'xấu', nhưng nó thực sự lặp lại trên các bảng là xấu so với các hoạt động dựa trên thiết lập. Nếu bạn không thể tránh lặp lại, một con trỏ có thể là cách tốt nhất. Khóa là một vấn đề khác với con trỏ, nhưng điều đó không liên quan khi sử dụng biến bảng.
JacquesB

1
Sử dụng một con trỏ không phải là lựa chọn duy nhất của bạn , nhưng nếu bạn không có cách nào để tránh cách tiếp cận theo từng hàng thì đó sẽ là lựa chọn tốt nhất của bạn. HIỆN TẠI là một cấu trúc tích hợp có hiệu quả cao hơn và ít xảy ra lỗi hơn so với thực hiện vòng lặp WHILE ngớ ngẩn của riêng bạn. Hầu hết thời gian bạn chỉ cần sử dụng STATICtùy chọn để loại bỏ việc kiểm tra lại liên tục các bảng cơ sở và khóa theo mặc định và khiến hầu hết mọi người lầm tưởng rằng HIỆN TẠI là xấu. @JacquesB rất gần: kiểm tra lại xem hàng kết quả có còn tồn tại không + khóa là vấn đề. Và STATICthường sửa nó :-).
Solomon Rutzky

Câu trả lời:


376

Trước hết, bạn nên chắc chắn rằng bạn cần lặp lại qua từng hàng - các hoạt động dựa trên tập hợp sẽ thực hiện nhanh hơn trong mọi trường hợp tôi có thể nghĩ và thường sẽ sử dụng mã đơn giản hơn.

Tùy thuộc vào dữ liệu của bạn, có thể lặp lại bằng cách sử dụng chỉ các SELECTcâu lệnh như dưới đây:

Declare @Id int

While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
    Select Top 1 @Id = Id From ATable Where Processed = 0

    --Do some processing here

    Update ATable Set Processed = 1 Where Id = @Id 

End

Một cách khác là sử dụng bảng tạm thời:

Select *
Into   #Temp
From   ATable

Declare @Id int

While (Select Count(*) From #Temp) > 0
Begin

    Select Top 1 @Id = Id From #Temp

    --Do some processing here

    Delete #Temp Where Id = @Id

End

Tùy chọn bạn nên chọn thực sự phụ thuộc vào cấu trúc và khối lượng dữ liệu của bạn.

Lưu ý: Nếu bạn đang sử dụng SQL Server, bạn sẽ được phục vụ tốt hơn bằng cách sử dụng:

WHILE EXISTS(SELECT * FROM #Temp)

Sử dụng COUNTsẽ phải chạm vào từng hàng trong bảng, EXISTSchỉ cần chạm vào hàng đầu tiên (xem câu trả lời của Josef bên dưới).


"Chọn Top 1 @Id = Id từ khả năng" nên là "Chọn Top 1 @Id = Id từ khả năng xử lý = 0"
Amzath

10
Nếu sử dụng SQL Server, hãy xem câu trả lời của Josef bên dưới để biết một điều chỉnh nhỏ ở trên.
Polshgiant

3
Bạn có thể giải thích tại sao điều này tốt hơn là sử dụng một con trỏ?
marco-fiset

5
Đã cho cái này một downvote. Tại sao anh ta nên tránh sử dụng một con trỏ? Anh ta đang nói về việc lặp qua biến bảng chứ không phải bảng truyền thống. Tôi không tin những nhược điểm thông thường của con trỏ áp dụng ở đây. Nếu việc xử lý theo từng hàng là thực sự cần thiết (và như bạn chỉ ra trước tiên anh ta nên chắc chắn về điều đó) thì sử dụng con trỏ là một giải pháp tốt hơn nhiều so với cách bạn mô tả ở đây.
peterh

@peterh Bạn nói đúng. Và trên thực tế, bạn thường có thể tránh những "nhược điểm thông thường" đó bằng cách sử dụng STATICtùy chọn sao chép kết quả được đặt vào bảng tạm thời và do đó bạn không còn khóa hoặc kiểm tra lại các bảng cơ sở :-).
Solomon Rutzky

132

Xin lưu ý, nếu bạn đang sử dụng SQL Server (2008 trở lên), các ví dụ có:

While (Select Count(*) From #Temp) > 0

Sẽ được phục vụ tốt hơn với

While EXISTS(SELECT * From #Temp)

Bá tước sẽ phải chạm vào từng hàng trong bảng, EXISTSchỉ cần chạm vào hàng đầu tiên.


9
Đây không phải là một câu trả lời mà là một nhận xét / nâng cao về câu trả lời của Martynw.
Hammad Khan

7
Nội dung của ghi chú này buộc chức năng định dạng tốt hơn bình luận, tôi sẽ đề nghị nối thêm vào Trả lời.
Custodio

2
Trong các phiên bản sau của SQL, trình tối ưu hóa truy vấn đủ thông minh để biết rằng khi bạn viết điều đầu tiên, bạn thực sự có nghĩa là điều thứ hai và tối ưu hóa nó như vậy để tránh quét bảng.
Dan Def

39

Đây là cách tôi làm điều đó:

declare @RowNum int, @CustId nchar(5), @Name1 nchar(25)

select @CustId=MAX(USERID) FROM UserIDs     --start with the highest ID
Select @RowNum = Count(*) From UserIDs      --get total number of records
WHILE @RowNum > 0                          --loop until no more records
BEGIN   
    select @Name1 = username1 from UserIDs where USERID= @CustID    --get other info from that row
    print cast(@RowNum as char(12)) + ' ' + @CustId + ' ' + @Name1  --do whatever

    select top 1 @CustId=USERID from UserIDs where USERID < @CustID order by USERID desc--get the next one
    set @RowNum = @RowNum - 1                               --decrease count
END

Không có con trỏ, không có bảng tạm thời, không có cột thêm. Cột USERID phải là một số nguyên duy nhất, vì hầu hết các Khóa chính là.


26

Xác định bảng tạm thời của bạn như thế này -

declare @databases table
(
    RowID int not null identity(1,1) primary key,
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

Sau đó làm điều này -

declare @i int
select @i = min(RowID) from @databases
declare @max int
select @max = max(RowID) from @databases

while @i <= @max begin
    select DatabaseID, Name, Server from @database where RowID = @i --do some stuff
    set @i = @i + 1
end

16

Đây là cách tôi sẽ làm điều đó:

Select Identity(int, 1,1) AS PK, DatabaseID
Into   #T
From   @databases

Declare @maxPK int;Select @maxPK = MAX(PK) From #T
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    -- Get one record
    Select DatabaseID, Name, Server
    From @databases
    Where DatabaseID = (Select DatabaseID From #T Where PK = @pk)

    --Do some processing here
    -- 

    Select @pk = @pk + 1
End

[Chỉnh sửa] Bởi vì tôi có thể bỏ qua từ "biến" khi lần đầu tiên đọc câu hỏi, đây là câu trả lời được cập nhật ...


declare @databases table
(
    PK            int IDENTITY(1,1), 
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases
--/*
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MyDB',   'MyServer2'
--*/

Declare @maxPK int;Select @maxPK = MAX(PK) From @databases
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    /* Get one record (you can read the values into some variables) */
    Select DatabaseID, Name, Server
    From @databases
    Where PK = @pk

    /* Do some processing here */
    /* ... */ 

    Select @pk = @pk + 1
End

4
về cơ bản, bạn đang thực hiện một con trỏ, nhưng không có tất cả các lợi ích của một con trỏ
Shawn

1
... mà không khóa các bảng được sử dụng trong khi xử lý ... vì đây là một trong những lợi ích của con trỏ :)
leoinfo

3
Những cái bàn? Đó là bảng BIẾN - không có quyền truy cập đồng thời.
DenNukem

DenNukem, bạn nói đúng, tôi nghĩ rằng tôi đã "bỏ qua" từ "biến" khi tôi đọc câu hỏi tại thời điểm đó ... Tôi sẽ thêm một số ghi chú vào câu trả lời ban đầu của mình
leoinfo

Tôi phải đồng ý với DenNukem và Shawn. Tại sao, tại sao, tại sao bạn đi đến những độ dài này để tránh sử dụng con trỏ? Một lần nữa: anh ta muốn lặp lại một biến bảng chứ không phải bảng truyền thống !!!
peterh

10

Nếu bạn không có lựa chọn nào khác ngoài việc đi từng hàng, tạo một con trỏ FAST_FORWARD. Nó sẽ nhanh như việc xây dựng một vòng lặp while và dễ dàng hơn nhiều để duy trì trong một quãng đường dài.

FAST_FORWARD Chỉ định con trỏ FORWARD_ONLY, READ_ONLY với tối ưu hóa hiệu suất được bật. FAST_FORWARD không thể được chỉ định nếu SCROLL hoặc FOR_UPDATE cũng được chỉ định.


2
Vâng! Như tôi đã nhận xét ở nơi khác, tôi vẫn chưa thấy bất kỳ đối số nào về lý do tại sao KHÔNG sử dụng con trỏ khi trường hợp lặp lại qua một biến bảng . Một FAST_FORWARDcon trỏ là một giải pháp tốt. (upvote)
peterh

5

Một cách tiếp cận khác mà không phải thay đổi lược đồ của bạn hoặc sử dụng bảng tạm thời:

DECLARE @rowCount int = 0
  ,@currentRow int = 1
  ,@databaseID int
  ,@name varchar(15)
  ,@server varchar(15);

SELECT @rowCount = COUNT(*)
FROM @databases;

WHILE (@currentRow <= @rowCount)
BEGIN
  SELECT TOP 1
     @databaseID = rt.[DatabaseID]
    ,@name = rt.[Name]
    ,@server = rt.[Server]
  FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY t.[DatabaseID], t.[Name], t.[Server]
       ) AS [RowNumber]
      ,t.[DatabaseID]
      ,t.[Name]
      ,t.[Server]
    FROM @databases t
  ) rt
  WHERE rt.[RowNumber] = @currentRow;

  EXEC [your_stored_procedure] @databaseID, @name, @server;

  SET @currentRow = @currentRow + 1;
END

4

Bạn có thể sử dụng một vòng lặp while:

While (Select Count(*) From #TempTable) > 0
Begin
    Insert Into @Databases...

    Delete From #TempTable Where x = x
End

4

Điều này sẽ hoạt động trong phiên bản SQL SERVER 2012.

declare @Rowcount int 
select @Rowcount=count(*) from AddressTable;

while( @Rowcount>0)
  begin 
 select @Rowcount=@Rowcount-1;
 SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end 

4

Nhẹ, không phải tạo thêm bảng, nếu bạn có số nguyên IDtrên bảng

Declare @id int = 0, @anything nvarchar(max)
WHILE(1=1) BEGIN
  Select Top 1 @anything=[Anything],@id=@id+1 FROM Table WHERE ID>@id
  if(@@ROWCOUNT=0) break;

  --Process @anything

END

3
-- [PO_RollBackOnReject]  'FININV10532'
alter procedure PO_RollBackOnReject
@CaseID nvarchar(100)

AS
Begin
SELECT  *
INTO    #tmpTable
FROM   PO_InvoiceItems where CaseID = @CaseID

Declare @Id int
Declare @PO_No int
Declare @Current_Balance Money


While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
        Select Top 1 @Id = PO_LineNo, @Current_Balance = Current_Balance,
        @PO_No = PO_No
        From #Temp
        update PO_Details
        Set  Current_Balance = Current_Balance + @Current_Balance,
            Previous_App_Amount= Previous_App_Amount + @Current_Balance,
            Is_Processed = 0
        Where PO_LineNumber = @Id
        AND PO_No = @PO_No
        update PO_InvoiceItems
        Set IsVisible = 0,
        Is_Processed= 0
        ,Is_InProgress = 0 , 
        Is_Active = 0
        Where PO_LineNo = @Id
        AND PO_No = @PO_No
End
End

2

Tôi thực sự không thấy được lý do tại sao bạn cần phải sử dụng sự sợ hãi cursor. Nhưng đây là một tùy chọn khác nếu bạn đang sử dụng SQL Server phiên bản 2005/2008
Sử dụng đệ quy

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

--; Insert records into @databases...

--; Recurse through @databases
;with DBs as (
    select * from @databases where DatabaseID = 1
    union all
    select A.* from @databases A 
        inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs

2

Tôi sẽ cung cấp giải pháp dựa trên tập hợp.

insert  @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server 
From ... (Use whatever query you would have used in the loop or cursor)

Điều này nhanh hơn nhiều so với bất kỳ kỹ thuật lặp và dễ viết và bảo trì hơn.


2

Tôi thích sử dụng Offset Fetch nếu bạn có một ID duy nhất, bạn có thể sắp xếp bảng của mình theo:

DECLARE @TableVariable (ID int, Name varchar(50));
DECLARE @RecordCount int;
SELECT @RecordCount = COUNT(*) FROM @TableVariable;

WHILE @RecordCount > 0
BEGIN
SELECT ID, Name FROM @TableVariable ORDER BY ID OFFSET @RecordCount - 1 FETCH NEXT 1 ROW;
SET @RecordCount = @RecordCount - 1;
END

Bằng cách này, tôi không cần thêm các trường vào bảng hoặc sử dụng chức năng cửa sổ.


2

Có thể sử dụng một con trỏ để làm điều này:

tạo hàm [dbo] .f_teste_loop trả về bảng @tabela (cod int, nome varchar (10)) khi bắt đầu

insert into @tabela values (1, 'verde');
insert into @tabela values (2, 'amarelo');
insert into @tabela values (3, 'azul');
insert into @tabela values (4, 'branco');

return;

kết thúc

tạo thủ tục [dbo]. [sp_teste_loop] khi bắt đầu

DECLARE @cod int, @nome varchar(10);

DECLARE curLoop CURSOR STATIC LOCAL 
FOR
SELECT  
    cod
   ,nome
FROM 
    dbo.f_teste_loop();

OPEN curLoop;

FETCH NEXT FROM curLoop
           INTO @cod, @nome;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    PRINT @nome;

    FETCH NEXT FROM curLoop
           INTO @cod, @nome;
END

CLOSE curLoop;
DEALLOCATE curLoop;

kết thúc


1
Không phải là câu hỏi ban đầu "Không sử dụng con trỏ"?
Fernando Gonzalez Sanchez

1

Tôi đồng ý với bài viết trước rằng các hoạt động dựa trên tập hợp thường sẽ hoạt động tốt hơn, nhưng nếu bạn cần lặp lại các hàng ở đây thì cách tiếp cận tôi sẽ thực hiện:

  1. Thêm một trường mới vào biến bảng của bạn (Bit loại dữ liệu, mặc định 0)
  2. Chèn dữ liệu của bạn
  3. Chọn Hàng 1 trên cùng trong đó fUsed = 0 (Lưu ý: fUsed là tên của trường trong bước 1)
  4. Thực hiện bất kỳ xử lý nào bạn cần làm
  5. Cập nhật bản ghi trong biến bảng của bạn bằng cách đặt fUsed = 1 cho bản ghi
  6. Chọn bản ghi không sử dụng tiếp theo từ bảng và lặp lại quy trình

    DECLARE @databases TABLE  
    (  
        DatabaseID  int,  
        Name        varchar(15),     
        Server      varchar(15),   
        fUsed       BIT DEFAULT 0  
    ) 
    
    -- insert a bunch rows into @databases
    
    DECLARE @DBID INT
    
    SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0 
    
    WHILE @@ROWCOUNT <> 0 and @DBID IS NOT NULL  
    BEGIN  
        -- Perform your processing here  
    
        --Update the record to "used" 
    
        UPDATE @databases SET fUsed = 1 WHERE DatabaseID = @DBID  
    
        --Get the next record  
        SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0   
    END

1

Bước 1: Dưới đây chọn câu lệnh tạo một bảng tạm thời với số hàng duy nhất cho mỗi bản ghi.

select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp 

Bước2: Khai báo các biến cần thiết

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Bước 3: Lấy tổng số hàng từ bảng tạm thời

SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int

Bước 4: Lặp lại bảng tạm thời dựa trên số hàng duy nhất tạo trong temp

while @rownumber>0
begin
  set @rno=@rownumber
  select @ename=ename from #tmp_sri where rno=@rno  **// You can take columns data from here as many as you want**
  set @rownumber=@rownumber-1
  print @ename **// instead of printing, you can write insert, update, delete statements**
end

1

Cách tiếp cận này chỉ yêu cầu một biến và không xóa bất kỳ hàng nào khỏi @database. Tôi biết có rất nhiều câu trả lời ở đây, nhưng tôi không thấy câu trả lời nào sử dụng MIN để lấy ID tiếp theo của bạn như thế này.

DECLARE @databases TABLE
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

DECLARE @CurrID INT

SELECT @CurrID = MIN(DatabaseID)
FROM @databases

WHILE @CurrID IS NOT NULL
BEGIN

    -- Do stuff for @CurrID

    SELECT @CurrID = MIN(DatabaseID)
    FROM @databases
    WHERE DatabaseID > @CurrID

END

1

Đây là giải pháp của tôi, sử dụng một vòng lặp vô hạn, BREAKcâu lệnh và @@ROWCOUNThàm. Không có con trỏ hoặc bảng tạm thời là cần thiết và tôi chỉ cần viết một truy vấn để có được hàng tiếp theo trong @databasesbảng:

declare @databases table
(
    DatabaseID    int,
    [Name]        varchar(15),   
    [Server]      varchar(15)
);


-- Populate the [@databases] table with test data.
insert into @databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values 
    (1, 'Roger', 'ServerA'),
    (5, 'Suzy', 'ServerB'),
    (8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])


-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare @databaseId int;

while (1=1)
begin
    -- Get the next database ID.
    select top(1) @databaseId = DatabaseId 
    from @databases 
    where DatabaseId > isnull(@databaseId, 0);

    -- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
    if (@@ROWCOUNT = 0) break;

    -- Otherwise, do whatever you need to do with the current [@databases] table row here.
    print 'Processing @databaseId #' + cast(@databaseId as varchar(50));
end

Tôi mới nhận ra rằng @ControlFreak đã đề xuất phương pháp này trước tôi; Tôi chỉ đơn giản là thêm ý kiến ​​và một ví dụ dài dòng hơn.
Mass Dot Net

0

Đây là mã mà tôi đang sử dụng 2008 R2. Mã này tôi đang sử dụng là để xây dựng các chỉ mục trên các trường chính (SSNO & EMPR_NO) n tất cả các câu chuyện

if object_ID('tempdb..#a')is not NULL drop table #a

select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')' 
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') '   'Field'
,ROW_NUMBER() over (order by table_NAMe) as  'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
    and TABLE_SCHEMA='dbo'

declare @loopcntr int
declare @ROW int
declare @String nvarchar(1000)
set @loopcntr=(select count(*)  from #a)
set @ROW=1  

while (@ROW <= @loopcntr)
    begin
        select top 1 @String=a.Field 
        from #A a
        where a.ROWNMBR = @ROW
        execute sp_executesql @String
        set @ROW = @ROW + 1
    end 

0
SELECT @pk = @pk + 1

sẽ tốt hơn:

SET @pk += @pk

Tránh sử dụng CHỌN nếu bạn không tham chiếu các bảng chỉ là gán giá trị.

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.