Чи є спосіб перенести змінну таблиці в 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
Gishu

5
Чи можете ви надати нам причину, чому ви хочете переглядати рядки, інше рішення, яке не потребує ітерації, може існувати (і яке у більшості випадків швидше з великим запасом)
Поп Каталін

згоден з попсом ... може не знадобиться курсор залежно від ситуації. але у вас немає проблем з використанням курсорів, якщо вам потрібно
Shawn

3
Ви не вказуєте, чому ви хочете уникати курсору. Майте на увазі, що курсор може бути найпростішим способом ітерації. Можливо, ви чули, що курсори «погані», але насправді ітерація над таблицями погана порівняно з операціями на основі встановлених даних. Якщо ви не можете уникнути ітерації, курсор може бути найкращим способом. Блокування - це ще одна проблема з курсорами, але це не актуально при використанні змінної таблиці.
ЖакБ

1
Використання курсору - не ваш єдиний варіант, але якщо у вас немає способу уникнути послідовного підходу, то це буде найкращим варіантом. CURSOR - це вбудована конструкція, яка є більш ефективною та менш схильною до помилок, ніж робити власний дурний цикл WHILE. Більшу частину часу вам просто потрібно скористатись STATICопцією, щоб зняти постійну повторну перевірку базових таблиць та блокування, які там за замовчуванням, і змусити більшість людей помилково вважати, що КУРСОРИ є злі. @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лише торкніться першого (див . Відповідь Йозефа нижче).


"Вибрати Топ 1 @Id = Ідентифікатор з ATable" має бути "Вибрати Топ 1 @Id = ІД з ATable Де обробляється = 0"
Amzath

10
Якщо ви використовуєте SQL Server, дивіться відповідь Йозефа нижче для невеликого перегляду вище.
Полшгіант

3
Чи можете ви пояснити, чому це краще, ніж використовувати курсор?
marco-fiset

5
Дав цю низку. Чому він повинен уникати використання курсору? Він говорить про повторення змінної таблиці , а не про традиційну таблицю. Я не вірю, що нормальні недоліки курсорів стосуються тут. Якщо обробка по рядках по-справжньому потрібна (і, як ви зазначаєте, він повинен бути впевнений у цьому спочатку), то використання курсору - це набагато краще рішення, ніж ті, які ви описали тут.
петер

@peterh Ви праві. Насправді, ви можете уникнути цих «нормальних недоліків», скориставшись STATICопцією, яка копіює набір результатів у тимчасову таблицю, а значить, ви більше не блокуєте та не перевіряєте базові таблиці :-).
Соломон Руцький

132

Лише коротка примітка, якщо ви використовуєте SQL Server (2008 і вище), приклади:

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

Було б краще подавати

While EXISTS(SELECT * From #Temp)

Графу доведеться торкнутися кожного ряду таблиці, EXISTSєдине потрібно торкнутися першого.


9
Це не відповідь, а коментар / удосконалення відповіді Мартінва.
Хаммад Хан

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

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

4
ви в основному робите курсор, але без усіх переваг курсору
Шон

1
... без блокування таблиць, які використовуються під час обробки ..., оскільки це одна з переваг курсора :)
leoinfo

3
Столи? Це таблиця ЗМІННА - одночасний доступ не можливий.
DenNukem

DenNukem, ти маєш рацію, я думаю, що я "пропустив" слово "змінна", коли я читав питання в той час ... Я додам кілька записок до моєї початкової відповіді
leoinfo

Я повинен погодитися з DenNukem і Shawn. Чому, чому, навіщо ви переходите на ці довжини, щоб не використовувати курсор? Знову ж таки: він хоче повторити табличну змінну, а не традиційну таблицю !!!
peterh

10

Якщо у вас немає іншого вибору, ніж пройти рядок за рядком, створивши курсор FAST_FORWARD. Це буде так само швидко, як накопичення циклу часу і набагато простіше в обслуговуванні протягом тривалого перевезення.

FAST_FORWARD Вказує курсор FORWARD_ONLY, READ_ONLY з увімкненою оптимізацією продуктивності. FAST_FORWARD не можна вказати, якщо вказано також SCROLL або FOR_UPDATE.


2
Так! Як я коментував в іншому місці, я ще не бачив жодних аргументів, чому НЕ використовувати курсор, коли справа полягає в перегляді змінної таблиці . FAST_FORWARDКурсор є прекрасним рішенням. (upvote)
peterh

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

4

Ви можете використовувати цикл час:

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

    Delete From #TempTable Where x = x
End

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 

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

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

2

Я збираюся надати рішення на основі набору.

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

Це набагато швидше, ніж будь-яка циклічна техніка, і її легше писати та підтримувати.


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

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

створити функцію [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. Виберіть верхній рядок 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: Таблиця темпів циклу на основі унікального номера рядка, створеного в темп

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

Я щойно зрозумів, що @ControlFreak рекомендував мені такий підхід; Я просто додав коментарі та більш детальний приклад.
Mass Dot Net

0

Це код, який я використовую 2008 R2. Цей код, який я використовую, полягає в побудові індексів на ключових полях (SSNO & EMPR_NO) 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

було б краще:

SET @pk += @pk

Уникайте використання SELECT, якщо ви не посилаєтесь на таблиці, вони просто присвоюють значення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.