Есть ли способ перебрать табличную переменную в TSQL без использования курсора?

243

Допустим, у меня есть следующая простая табличная переменная:

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

Является ли объявление и использование курсора моим единственным вариантом, если я хочу перебирать строки? Есть ли другой способ?

луч
источник
3
Хотя я не уверен, что проблема, которую вы видите с вышеупомянутым подходом; Посмотрите, поможет ли это .. databasejournal.com/features/mssql/article.php/3111031
Гишу
5
Не могли бы вы сообщить нам причину, по которой вы хотите выполнять итерацию по строкам, возможно, существует другое решение, которое не требует итерации (и в большинстве случаев оно выполняется быстрее с большим запасом)
Pop Catalin
согласен с поп ... может не понадобиться курсор в зависимости от ситуации. но нет проблем с использованием курсоров, если вам нужно
Shawn
3
Вы не заявляете, почему вы хотите избежать курсора. Помните, что курсор может быть самым простым способом итерации. Возможно, вы слышали, что курсоры «плохие», но это действительно итерация по таблицам, которая является плохой по сравнению с операциями на основе множеств. Если вы не можете избежать итерации, лучше всего использовать курсор. Блокировка - это еще одна проблема с курсорами, но она не имеет значения при использовании табличной переменной.
JacquesB
1
Использование курсора - не единственный вариант, но если у вас нет способа избежать построчного подхода, тогда он будет вашим лучшим вариантом. CURSOR - это встроенная конструкция, которая более эффективна и менее подвержена ошибкам, чем ваш собственный глупый цикл WHILE. В большинстве случаев вам просто нужно использовать эту STATICопцию, чтобы убрать постоянную повторную проверку базовых таблиц и блокировку, которая существует по умолчанию и заставляет большинство людей ошибочно полагать, что CURSOR - это зло. @JacquesB очень близко: перепроверка, чтобы увидеть, существует ли еще строка результата + проблемы с блокировкой. И STATICобычно исправляет это :-).
Соломон Руцкий,

Ответы:

376

Прежде всего, вы должны быть абсолютно уверены, что вам нужно перебирать каждую строку - операции на основе набора будут выполняться быстрее в каждом случае, о котором я могу подумать, и, как правило, использовать более простой код.

В зависимости от ваших данных может быть возможно выполнить цикл, используя только SELECTоператоры, как показано ниже:

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

Другой альтернативой является использование временной таблицы:

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

Выбор, который вы должны выбрать, действительно зависит от структуры и объема ваших данных.

Примечание. Если вы используете SQL Server, вам лучше использовать:

WHILE EXISTS(SELECT * FROM #Temp)

Использование COUNTбудет касаться каждой строки в таблице, EXISTSединственной нужно касаться первой (см . Ответ Джозефа ниже).

Martynnw
источник
«Выберите Top 1 @Id = Id From ATable» должно быть «Выберите Top 1 @Id = Id From ATable, где Обработано = 0»
Amzath
10
Если вы используете SQL Server, см. Ответ Джозефа ниже для небольшого изменения выше.
Polshgiant
3
Можете ли вы объяснить, почему это лучше, чем использование курсора?
Марко-Фисет
5
Дали этому понижение. Почему он должен избегать использования курсора? Он говорит об итерации по табличной переменной , а не по традиционной таблице. Я не верю, что нормальные недостатки курсоров применимы здесь. Если построчная обработка действительно необходима (и, как вы заметили, он должен быть уверен в этом в первую очередь), тогда использование курсора является гораздо лучшим решением, чем те, которые вы описали здесь.
Петер
@peterh Вы правы. И на самом деле, вы обычно можете избежать этих «нормальных недостатков», используя STATICопцию, которая копирует результирующий набор во временную таблицу, и, следовательно, вы больше не блокируете и не перепроверяете базовые таблицы :-).
Соломон Руцки
132

Просто быстрое примечание, если вы используете SQL Server (2008 и выше), примеры, которые имеют:

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

Будет лучше обслужен с

While EXISTS(SELECT * From #Temp)

Счетчик должен будет коснуться каждой строки в таблице, EXISTSнужно только коснуться первой.

Josef
источник
9
Это не ответ, а комментарий / улучшение ответа Martynw.
Хаммад Хан
7
Содержание этой заметки обеспечивает лучшую функциональность форматирования, чем комментарий, который я бы предложил добавить в ответ.
Кастодио
2
В более поздних версиях SQL оптимизатор запросов достаточно умен, чтобы знать, что когда вы пишете первое, вы на самом деле имеете в виду второе и оптимизируете его как таковое, чтобы избежать сканирования таблицы.
Dan Def
39

Вот как я это делаю:

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

Нет курсоров, нет временных таблиц, нет лишних столбцов. Столбец USERID должен быть уникальным целым числом, как и большинство первичных ключей.

Тревор
источник
26

Определите свою временную таблицу следующим образом:

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

Тогда сделай это -

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
Seibar
источник
16

Вот как я бы это сделал:

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

[Править] Поскольку я, вероятно, пропустил слово «переменная» при первом прочтении вопроса, вот обновленный ответ ...


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
leoinfo
источник
4
так что в основном вы делаете курсор, но без всех преимуществ курсора
Shawn
1
... без блокировки таблиц, которые используются при обработке ... так как это одно из преимуществ курсора :)
leoinfo
3
Таблицы? Это таблица VARIABLE - одновременный доступ невозможен.
DenNukem
DenNukem, вы правы, я думаю, что я "пропустил" слово "переменная", когда я прочитал вопрос в то время ... Я добавлю некоторые примечания к своему первоначальному ответу
leoinfo
Я должен согласиться с DenNukem и Шоном. Почему, почему, почему вы идете на эти длины, чтобы избежать использования курсора? Опять же: он хочет перебрать переменную таблицы, а не традиционную таблицу !!!
Петер
10

Если у вас нет выбора, кроме как идти строка за строкой, создавая курсор FAST_FORWARD. Это будет так же быстро, как создание цикла, и его будет гораздо легче поддерживать в течение длительного времени.

FAST_FORWARD Указывает курсор FORWARD_ONLY, READ_ONLY с включенной оптимизацией производительности. FAST_FORWARD не может быть указан, если также указаны SCROLL или FOR_UPDATE.


источник
2
Да! Как я уже говорил в другом месте, я еще не видел каких-либо аргументов относительно того, почему НЕ использовать курсор, когда дело касается итерации по табличной переменной . FAST_FORWARDКурсор является прекрасным решением. (upvote)
Петер
5

Другой подход без необходимости изменения схемы или использования временных таблиц:

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
SReiderB
источник
4

Вы можете использовать цикл while:

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

    Delete From #TempTable Where x = x
End
GateKiller
источник
4

Это будет работать в версии 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 
OrganicCoder
источник
4

Легкий, без необходимости создавать дополнительные таблицы, если у вас есть целое число IDна столе

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
Control Freak
источник
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

Я действительно не вижу смысла, почему вам нужно прибегать к использованию страшных cursor. Но вот еще один вариант, если вы используете SQL Server версии 2005/2008
Использовать рекурсию

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
dance2die
источник
2

Я собираюсь предоставить решение на основе множеств.

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

Это намного быстрее, чем любой метод зацикливания, и его легче писать и поддерживать.

HLGEM
источник
2

Я предпочитаю использовать Offset Fetch, если у вас есть уникальный идентификатор, который вы можете отсортировать по таблице:

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

Таким образом, мне не нужно добавлять поля в таблицу или использовать оконную функцию.

Ив А Мартин
источник
2

Для этого можно использовать курсор:

функция create [dbo] .f_teste_loop возвращает таблицу @tabela (cod int, nome varchar (10)) как начало

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;

конец

создайте процедуру [dbo]. [sp_teste_loop] как начало

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;

конец

Александр Пеццутто
источник
1
Разве не был оригинальный вопрос «Без использования курсора»?
Фернандо Гонсалес Санчес
1

Я согласен с предыдущим постом, что основанные на множестве операции, как правило, будут работать лучше, но если вам нужно перебирать строки, вот подход, который я бы выбрал:

  1. Добавить новое поле в табличную переменную (тип данных бит, по умолчанию 0)
  2. Вставьте ваши данные
  3. Выберите строку Top 1, где fUsed = 0 (Примечание: fUsed - это имя поля в шаге 1)
  4. Выполните любую обработку, которую вам нужно сделать
  5. Обновите запись в вашей табличной переменной, установив fUsed = 1 для записи
  6. Выберите следующую неиспользуемую запись из таблицы и повторите процесс

    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

Шаг 1: Оператор select ниже создает временную таблицу с уникальным номером строки для каждой записи.

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

Шаг 2: Объявите обязательные переменные

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Шаг 3: взять общее количество строк из временной таблицы

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

Шаг 4: Временная таблица петли, основанная на уникальном номере строки, создается в 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

Этот подход требует только одну переменную и не удаляет строки из @databases. Я знаю, что здесь есть много ответов, но я не вижу такого, который бы использовал MIN, чтобы получить следующий идентификатор, подобный этому.

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

Вот мое решение, которое использует бесконечный цикл, BREAKоператор и @@ROWCOUNTфункцию. Курсоры или временная таблица не нужны, и мне нужно написать только один запрос, чтобы получить следующую строку в @databasesтаблице:

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
Mass Dot Net
источник
Я только что понял, что @ControlFreak рекомендовал этот подход до меня; Я просто добавил комментарии и более подробный пример.
Mass Dot Net
0

Это код, который я использую 2008 R2. Этот код, который я использую, предназначен для построения индексов по ключевым полям (SSNO & EMPR_NO) во всех сказках.

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 
howmnsk
источник
0
SELECT @pk = @pk + 1

было бы лучше:

SET @pk += @pk

Избегайте использования SELECT, если вы не ссылаетесь на таблицы, которые просто присваивают значения.

Боб Элли
источник