Bir imleç kullanmadan TSQL'de bir tablo değişkeni arasında geçiş yapmanın bir yolu var mı?


243

Diyelim ki aşağıdaki basit tablo değişkenine sahibim:

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

Satırları yinelemek istersem imleci bildirmek ve kullanmak tek seçeneğim mi? Başka bir yol var mı?


3
Yukarıdaki yaklaşımda gördüğünüz problemden emin olmasam da; Bunun
işe yarayıp yaramadığını

5
Bize satırlar üzerinde tekrarlamak istemenizin nedenini verebilir misiniz, yineleme gerektirmeyen başka bir çözüm olabilir (ve çoğu durumda büyük bir farkla daha hızlıdır)
Pop Catalin

pop ile anlaşın ... duruma bağlı olarak imlece gerek olmayabilir. ama gerekiyorsa imleçleri kullanmada sorun yok
Shawn

3
Neden bir imleçten kaçınmak istediğinizi belirtmiyorsunuz. Bir imlecin yinelemenin en basit yolu olabileceğini unutmayın. İmleçlerin 'kötü' olduğunu duymuş olabilirsiniz, ancak küme tabanlı işlemlere kıyasla gerçekten kötü olan tablolar üzerinde yinelemedir. Yinelemeden kaçınamıyorsanız, imleç en iyi yol olabilir. Kilitleme imleçlerle ilgili başka bir sorundur, ancak bir tablo değişkeni kullanılırken bu geçerli değildir.
JacquesB

1
Bir imleç kullanmak tek seçeneğiniz değildir , ancak satır satır yaklaşımdan kaçınmanın bir yolu yoksa, o zaman en iyi seçenek olacaktır. CURSOR'lar, kendi aptal WHILE döngünüzü yapmaktan daha verimli ve daha az hataya açık olan yerleşik bir yapıdır. Çoğu zaman STATIC, temel tabloların sürekli olarak yeniden denetlenmesini ve varsayılan olarak orada bulunan ve çoğu kişinin yanlışlıkla CURSOR'ların kötü olduğuna inanmasına neden olan kilidi kaldırma seçeneğini kullanmanız gerekir . @ JacquesB çok yakın: sonuç satır hala var olup olmadığını görmek için yeniden kontrol + kilitleme sorunları vardır. Ve STATICgenellikle bunu düzeltir :-).
Solomon Rutzky

Yanıtlar:


376

Her şeyden önce, her satıra göre yinelemeniz gerektiğinden kesinlikle emin olmalısınız - set tabanlı işlemler, düşünebildiğim her durumda daha hızlı performans gösterecek ve normalde daha basit kod kullanacaktır.

Verilerinize bağlı olarak, SELECTaşağıda gösterildiği gibi yalnızca ifadeleri kullanarak döngü yapmak mümkün olabilir :

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

Başka bir alternatif geçici bir tablo kullanmaktır:

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

Seçmeniz gereken seçenek gerçekten verilerinizin yapısına ve hacmine bağlıdır.

Not: SQL Server kullanıyorsanız daha iyi hizmet sunabilirsiniz:

WHILE EXISTS(SELECT * FROM #Temp)

Kullanmak COUNT, tablodaki her satıra dokunmak zorunda kalacaktır, EXISTSsadece birincisine dokunmak gerekir ( aşağıda Josef'in cevabına bakınız).


"En İyi 1 @Id = ATable'dan Kimlik Seç" olmalıdır "En İyi 1 @Id = ATable'dan Kimlik İşlendiğinde = 0" olmalıdır
Amzath

10
SQL Server kullanıyorsanız, yukarıdaki küçük bir değişiklik için Josef'in cevabına bakınız.
Polshgiant

3
Bunun neden imleç kullanmaktan daha iyi olduğunu açıklayabilir misiniz?
marco-fiset

5
Bunu bir intikam verdi. Neden imleç kullanmaktan kaçınmalı? Geleneksel bir tablodan değil, tablo değişkeni üzerinden yineleme yapmaktan bahsediyor . İmleçlerin normal dezavantajlarının burada geçerli olduğuna inanmıyorum. Satır satır işlem gerçekten gerekliyse (ve önce işaret ettiğinden emin olursanız), imleci kullanmak burada tarif ettiğinizlerden çok daha iyi bir çözümdür.
peterh

@peterh Doğru. Ve aslında, STATICsonuç kümesini geçici tabloya kopyalayan seçeneği kullanarak genellikle bu "normal dezavantajları" önleyebilirsiniz ve bu nedenle artık temel tabloları kilitlemez veya yeniden denetlemezsiniz :-).
Solomon Rutzky

132

SQL Server (2008 ve üstü) kullanıyorsanız, aşağıdakilere sahip örnekler:

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

İle daha iyi hizmet olurdu

While EXISTS(SELECT * From #Temp)

Sayım'ın tablodaki her bir satıra dokunması gerekecek, EXISTSsadece ilkine dokunması gerekiyor.


9
Bu bir cevap değil, Martynw cevabı hakkında bir yorum / geliştirmedir.
Hammad Khan

7
Bu notun içeriği bir yorumdan daha iyi biçimlendirme işlevselliğini zorlar, Cevapta eklemenizi öneririm.
Custodio

2
SQL'in sonraki sürümlerinde, sorgu optimize edici, ilk şeyi yazdığınızda, aslında ikinciyi ifade ettiğinizi ve tablo taramasından kaçınmak için bunu optimize ettiğini bilecek kadar zekidir.
Dan Def

39

Bunu nasıl yaparım:

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

İmleç yok, geçici tablo yok, fazladan sütun yok. Birincil Anahtarların çoğu gibi USERID sütunu benzersiz bir tam sayı olmalıdır.


26

Geçici tablonuzu şu şekilde tanımlayın -

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

Sonra bunu yap -

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

İşte nasıl yaparım:

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

[Düzenle] Soruyu ilk kez okurken muhtemelen "değişken" kelimesini atladığım için, burada güncellenmiş bir yanıt var ...


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
temelde bir imleç yapıyorsunuz, ama bir imlecin tüm faydaları olmadan
Shawn

1
... işlem sırasında kullanılan tabloları kilitlemeden ... Bu bir imlecin avantajlarından biri olduğu için :)
leoinfo

3
Tablolar? Bu bir DEĞİŞKEN tablodur - eşzamanlı erişim mümkün değildir.
DenNukem

DenNukem, haklısın, sanırım o zaman soruyu okuduğumda "değişken" kelimesini "
atladım"

DenNukem ve Shawn ile hemfikirim. Neden, neden, neden imleç kullanmaktan kaçınıyorsunuz? Yine: geleneksel bir tablo değil, bir tablo değişkeni üzerinde yineleme yapmak istiyor !!!
peterh

10

FAST_FORWARD imleci oluşturmak için satır satır gitmek dışında bir seçeneğiniz yoksa. Bir süre döngü oluşturmak kadar hızlı olacak ve uzun yol boyunca bakımı çok daha kolay olacaktır.

FAST_FORWARD Performans optimizasyonlarının etkin olduğu bir FORWARD_ONLY, READ_ONLY imleci belirtir. SCROLL veya FOR_UPDATE de belirtilirse FAST_FORWARD belirtilemez.


2
Evet! Başka bir yerde yorumladığı gibi ben neden olarak herhangi argümanları henüz görmedik DEĞİL durum bir tekrarlatacak olduğunda imleç kullanmak tablo değişkeni . Bir FAST_FORWARDimleç iyi bir çözümdür. (upvote)
peterh

5

Şemanızı değiştirmek zorunda kalmadan veya geçici tablolar kullanmadan başka bir yaklaşım:

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 döngüsü kullanabilirsiniz:

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

    Delete From #TempTable Where x = x
End

4

Bu SQL SERVER 2012 sürümünde çalışacaktır.

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

IDMasada bir tamsayı varsa, fazladan tablo yapmak zorunda kalmadan hafif

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

Gerçekten neden korkunç kullanarak başvurmak gerekir noktası görmüyorum cursor. SQL Server sürüm 2005/2008 kullanıyorsanız Özyineleme
kullan

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

Set tabanlı bir çözüm sunacağım.

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

Bu, herhangi bir döngü tekniğinden çok daha hızlıdır ve yazması ve bakımı daha kolaydır.


2

Benzersiz bir kimliğiniz varsa Ofset Getirmeyi kullanmayı tercih ederim.

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

Bu şekilde tabloya alan eklemem veya pencere fonksiyonu kullanmam gerekmiyor.


2

Bunu yapmak için bir imleç kullanmak mümkündür:

create function [dbo] .f_teste_loop başlangıçta @tabela tablosunu (cod int, nome varchar (10)) döndürür

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;

son

[dbo] yordamı yarat. [sp_teste_loop] başlangıç ​​olarak

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;

son


1
Orijinal soru "Bir imleç kullanmadan" değil miydi?
Fernando Gonzalez Sanchez

1

Önceki yazıya, küme tabanlı işlemlerin genellikle daha iyi performans göstereceğini kabul ediyorum, ancak satırları yinelemeniz gerekiyorsa, alacağım yaklaşım:

  1. Tablo değişkeninize yeni bir alan ekleyin (Veri Türü Biti, varsayılan 0)
  2. Verilerinizi ekleyin
  3. FUsed = 0 olan İlk 1 Satırı seçin (Not: fUsed, 1. adımdaki alanın adıdır)
  4. Yapmanız gereken işlemleri yapın
  5. Kayıt için fUsed = 1 ayarlayarak tablo değişkeninizdeki kaydı güncelleyin
  6. Tablodan kullanılmayan bir sonraki kaydı seçin ve işlemi tekrarlayın

    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

Adım 1: select deyiminin altında, her kayıt için benzersiz satır numarasına sahip bir geçici tablo oluşturulur.

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

Adım 2: Gerekli değişkenleri beyan edin

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Adım3: Temp tablosundan toplam satır sayısını alın

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

Step4: Döngü temp tablosu benzersiz satır numarasına dayalı temp oluşturmak

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

Bu yaklaşım yalnızca bir değişken gerektirir ve @databases içindeki hiçbir satırı silmez. Burada çok cevap olduğunu biliyorum, ama böyle bir sonraki kimliğinizi almak için MIN kullanan birini görmüyorum.

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

Sonsuz bir döngü, BREAKdeyim ve @@ROWCOUNTişlev kullanan çözümüm . Hiçbir imleç veya geçici tablo gerekli ve @databasestablodaki sonraki satırı almak için sadece bir sorgu yazmak gerekir :

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

Ben sadece @ControlFreak benden önce bu yaklaşımı tavsiye fark ; Sadece yorum ve daha ayrıntılı bir örnek ekledim.
Mass Dot Net

0

Bu 2008 R2 kullandığım kod. Kullandığım bu kod anahtar alanlarda (SSNO ve EMPR_NO) n all tales dizin oluşturmaktır

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

Daha iyi olurdu:

SET @pk += @pk

Eğer referans tabloları sadece değerleri atamıyor iseniz SELECT kullanmaktan kaçının.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.