【问题标题】:How to write a foreach in SQL Server?如何在 SQL Server 中编写 foreach?
【发布时间】:2013-09-02 01:35:36
【问题描述】:

我正在尝试实现类似于 for-each 的目标,我想获取返回的 select 语句的 Id 并使用它们中的每一个。

DECLARE @i int
DECLARE @PractitionerId int
DECLARE @numrows int
DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)

INSERT @Practitioner
SELECT distinct PractitionerId FROM Practitioner

SET @i = 1
SET @numrows = (SELECT COUNT(*) FROM Practitioner)
IF @numrows > 0
    WHILE (@i <= (SELECT MAX(idx) FROM Practitioner))
    BEGIN

        SET @PractitionerId = (SELECT PractitionerId FROM @Practitioner WHERE idx = @i)

        --Do something with Id here
        PRINT @PractitionerId

        SET @i = @i + 1
    END

目前我有一些看起来像上面的东西,但我收到了错误:

列名“idx”无效。

【问题讨论】:

  • 如何在 SQL Server 中使用 Transact-SQL 遍历结果集:support.microsoft.com/kb/111401/nl
  • idx 位于 @Practitioner 而非 Practitioner。对于 for-each 方法,通常有更好的基于集合的替代方案,如果您展示了您对行值的处理方式,也许可以建议替代方案。
  • 请发布更多关于你想要完成的事情。避免像瘟疫一样的 RBAR(99% 的时间)。 simple-talk.com/sql/t-sql-programming/…
  • RBAR 坏,基于集合的好。
  • 如果您告诉我们--Do something with Id here 是什么,我们很可能可以向您展示如何在没有任何循环或游标的情况下解决此问题。在大多数情况下,您希望使用基于集合的解决方案,因为这是优化 SQL Server 工作的方式。一次循环和处理一行当然有它的位置,但我怀疑不是这样。

标签: sql-server tsql


【解决方案1】:

这通常(几乎总是)比游标执行得更好并且更简单:

DECLARE @PractitionerList TABLE(PracticionerID INT)
DECLARE @PracticionerID INT
    
INSERT @PractitionerList(PracticionerID)
SELECT PracticionerID
FROM Practitioner
    
WHILE(1 = 1)
BEGIN
            
    SET @PracticionerID = NULL
    SELECT TOP(1) @PracticionerID = PracticionerID
    FROM @PractitionerList
    
    IF @PracticionerID IS NULL
        BREAK
            
    PRINT 'DO STUFF'
    
    DELETE TOP(1) FROM @PractitionerList
    
END

【讨论】:

    【解决方案2】:

    我创建了一个过程,可以为任何表执行FOREACHCURSOR

    使用示例:

    CREATE TABLE #A (I INT, J INT)
    INSERT INTO #A VALUES (1, 2), (2, 3)
    EXEC PRC_FOREACH
        #A --Table we want to do the FOREACH
        , 'SELECT @I, @J' --The execute command, each column becomes a variable in the same type, so DON'T USE SPACES IN NAMES
       --The third variable is the database, it's optional because a table in TEMPB or the DB of the proc will be discovered in code
    

    结果是每行有 2 次选择。 UPDATE和break FOREACH的语法写在提示里。

    这是过程代码:

    CREATE PROC [dbo].[PRC_FOREACH] (@TBL VARCHAR(100) = NULL, @EXECUTE NVARCHAR(MAX)=NULL, @DB VARCHAR(100) = NULL) AS BEGIN
    
        --LOOP BETWEEN EACH TABLE LINE            
    
    IF @TBL + @EXECUTE IS NULL BEGIN
        PRINT '@TBL: A TABLE TO MAKE OUT EACH LINE'
        PRINT '@EXECUTE: COMMAND TO BE PERFORMED ON EACH FOREACH TRANSACTION'
        PRINT '@DB: BANK WHERE THIS TABLE IS (IF NOT INFORMED IT WILL BE DB_NAME () OR TEMPDB)' + CHAR(13)
        PRINT 'ROW COLUMNS WILL VARIABLE WITH THE SAME NAME (COL_A = @COL_A)'
        PRINT 'THEREFORE THE COLUMNS CANT CONTAIN SPACES!' + CHAR(13)
        PRINT 'SYNTAX UPDATE:
    
    UPDATE TABLE
    SET COL = NEW_VALUE
    WHERE CURRENT OF MY_CURSOR
    
    CLOSE CURSOR (BEFORE ALL LINES):
    
    IF 1 = 1 GOTO FIM_CURSOR'
        RETURN
    END
    SET @DB = ISNULL(@DB, CASE WHEN LEFT(@TBL, 1) = '#' THEN 'TEMPDB' ELSE DB_NAME() END)
    
        --Identifies the columns for the variables (DECLARE and INTO (Next cursor line))
    
    DECLARE @Q NVARCHAR(MAX)
    SET @Q = '
    WITH X AS (
        SELECT
            A = '', @'' + NAME
            , B = '' '' + type_name(system_type_id)
            , C = CASE
                WHEN type_name(system_type_id) IN (''VARCHAR'', ''CHAR'', ''NCHAR'', ''NVARCHAR'') THEN ''('' + REPLACE(CONVERT(VARCHAR(10), max_length), ''-1'', ''MAX'') + '')''
                WHEN type_name(system_type_id) IN (''DECIMAL'', ''NUMERIC'') THEN ''('' + CONVERT(VARCHAR(10), precision) + '', '' + CONVERT(VARCHAR(10), scale) + '')''
                ELSE ''''
            END
        FROM [' + @DB + '].SYS.COLUMNS C WITH(NOLOCK)
        WHERE OBJECT_ID = OBJECT_ID(''[' + @DB + '].DBO.[' + @TBL + ']'')
        )
    SELECT
        @DECLARE = STUFF((SELECT A + B + C FROM X FOR XML PATH('''')), 1, 1, '''')
        , @INTO = ''--Read the next line
    FETCH NEXT FROM MY_CURSOR INTO '' + STUFF((SELECT A + '''' FROM X FOR XML PATH('''')), 1, 1, '''')'
    
    DECLARE @DECLARE NVARCHAR(MAX), @INTO NVARCHAR(MAX)
    EXEC SP_EXECUTESQL @Q, N'@DECLARE NVARCHAR(MAX) OUTPUT, @INTO NVARCHAR(MAX) OUTPUT', @DECLARE OUTPUT, @INTO OUTPUT
    
        --PREPARE TO QUERY
    
    SELECT
        @Q = '
    DECLARE ' + @DECLARE + '
    -- Cursor to scroll through object names
    DECLARE MY_CURSOR CURSOR FOR
        SELECT *
        FROM [' + @DB + '].DBO.[' + @TBL + ']
    
    -- Opening Cursor for Reading
    OPEN MY_CURSOR
    ' + @INTO + '
    
    -- Traversing Cursor Lines (While There)
    WHILE @@FETCH_STATUS = 0
    BEGIN
        ' + @EXECUTE + '
        -- Reading the next line
        ' + @INTO + '
    END
    FIM_CURSOR:
    -- Closing Cursor for Reading
    CLOSE MY_CURSOR
    
    DEALLOCATE MY_CURSOR'
    
    EXEC SP_EXECUTESQL @Q --MAGIA
    END
    

    【讨论】:

      【解决方案3】:

      这是更好的解决方案之一。

      DECLARE @i int
                  DECLARE @curren_val int
                  DECLARE @numrows int
                  create table #Practitioner (idx int IDENTITY(1,1), PractitionerId int)
                  INSERT INTO #Practitioner (PractitionerId) values (10),(20),(30)
                  SET @i = 1
                  SET @numrows = (SELECT COUNT(*) FROM #Practitioner)
                  IF @numrows > 0
                  WHILE (@i <= (SELECT MAX(idx) FROM #Practitioner))
                  BEGIN
      
                      SET @curren_val = (SELECT PractitionerId FROM #Practitioner WHERE idx = @i)
      
                      --Do something with Id here
                      PRINT @curren_val
                      SET @i = @i + 1
                  END
      

      这里我在表格中添加了一些值,因为最初它是空的。

      我们可以访问或者我们可以在循环体中做任何事情,我们可以通过在表定义中定义它来访问 idx。

                    BEGIN
                      SET @curren_val = (SELECT PractitionerId FROM #Practitioner WHERE idx = @i)
      
                      --Do something with Id here
      
                      PRINT @curren_val
                      SET @i = @i + 1
                  END
      

      【讨论】:

        【解决方案4】:

        我想出了一个非常有效、(我认为)可读的方法来做到这一点。

            1. create a temp table and put the records you want to iterate in there
            2. use WHILE @@ROWCOUNT <> 0 to do the iterating
            3. to get one row at a time do, SELECT TOP 1 <fieldnames>
                b. save the unique ID for that row in a variable
            4. Do Stuff, then delete the row from the temp table based on the ID saved at step 3b.
        

        这是代码。抱歉,它使用我的变量名而不是问题中的变量名。

                    declare @tempPFRunStops TABLE (ProformaRunStopsID int,ProformaRunMasterID int, CompanyLocationID int, StopSequence int );    
        
                INSERT @tempPFRunStops (ProformaRunStopsID,ProformaRunMasterID, CompanyLocationID, StopSequence) 
                SELECT ProformaRunStopsID, ProformaRunMasterID, CompanyLocationID, StopSequence from ProformaRunStops 
                WHERE ProformaRunMasterID IN ( SELECT ProformaRunMasterID FROM ProformaRunMaster WHERE ProformaId = 15 )
        
            -- SELECT * FROM @tempPFRunStops
        
            WHILE @@ROWCOUNT <> 0  -- << I dont know how this works
                BEGIN
                    SELECT TOP 1 * FROM @tempPFRunStops
                    -- I could have put the unique ID into a variable here
                    SELECT 'Ha'  -- Do Stuff
                    DELETE @tempPFRunStops WHERE ProformaRunStopsID = (SELECT TOP 1 ProformaRunStopsID FROM @tempPFRunStops)
                END
        

        【讨论】:

          【解决方案5】:

          假设PractitionerId列是唯一的,那么可以使用下面的循环

          DECLARE @PractitionerId int = 0
          WHILE(1 = 1)
          BEGIN
            SELECT @PractitionerId = MIN(PractitionerId)
            FROM dbo.Practitioner WHERE PractitionerId > @PractitionerId
            IF @PractitionerId IS NULL BREAK
            SELECT @PractitionerId
          END
          

          【讨论】:

          • 太简单了。您始终在循环内选择 MIN(PractitionerId) 。退出循环的条件是什么?对我来说似乎是一个无限循环。
          • @bluelabel 退出循环脚本具有以下条件 IF PractitionerId IS NULL BREAK
          • 多么有趣。在设置 @PractitionerId 的新值之前,在 WHERE 子句中使用 @PractitionerId 的旧值来限制对 MIN 函数的输入。有效,但对我们这些非 SQL 专家用户来说并不明显。
          【解决方案6】:

          您似乎想使用CURSOR。虽然大多数时候最好使用基于集合的解决方案,但有时CURSOR 是最好的解决方案。在不了解您的实际问题的情况下,我们无法为您提供更多帮助:

          DECLARE @PractitionerId int
          
          DECLARE MY_CURSOR CURSOR 
            LOCAL STATIC READ_ONLY FORWARD_ONLY
          FOR 
          SELECT DISTINCT PractitionerId 
          FROM Practitioner
          
          OPEN MY_CURSOR
          FETCH NEXT FROM MY_CURSOR INTO @PractitionerId
          WHILE @@FETCH_STATUS = 0
          BEGIN 
              --Do something with Id here
              PRINT @PractitionerId
              FETCH NEXT FROM MY_CURSOR INTO @PractitionerId
          END
          CLOSE MY_CURSOR
          DEALLOCATE MY_CURSOR
          

          【讨论】:

          • 请不要开始左右使用光标。
          • 当然,如果您想对循环中的每条记录实际一些事情(例如发送电子邮件或写入文件),而不是仅仅在 SQL 中随机播放内容,您需要使用游标或类似的东西(不是set 逻辑)。可能会使用 SQL 并尝试做一些 RBAR 的人,他们是作为一个需要它来做类似这样有用的事情的程序员来到它。
          【解决方案7】:

          虽然游标通常被认为是可怕的邪恶,但我相信这是 FAST_FORWARD 游标的一种情况 - 您可以在 TSQL 中获得最接近 FOREACH 的东西。

          【讨论】:

            【解决方案8】:

            您的版本中有以下行错误:

            WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))
            

            (缺少@)

            可能是一个改变命名约定的想法,以便表格更加不同。

            【讨论】:

              【解决方案9】:

              您的选择计数和选择最大值应该来自您的表变量而不是实际表

              DECLARE @i int
              DECLARE @PractitionerId int
              DECLARE @numrows int
              DECLARE @Practitioner TABLE (
                  idx smallint Primary Key IDENTITY(1,1)
                  , PractitionerId int
              )
              
              INSERT @Practitioner
              SELECT distinct PractitionerId FROM Practitioner
              
              SET @i = 1
              SET @numrows = (SELECT COUNT(*) FROM @Practitioner)
              IF @numrows > 0
                  WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))
                  BEGIN
              
                      SET @PractitionerId = (SELECT PractitionerId FROM @Practitioner WHERE idx = @i)
              
                      --Do something with Id here
                      PRINT @PractitionerId
              
                      SET @i = @i + 1
                  END
              

              【讨论】:

                【解决方案10】:

                除了idx 列实际上不存在于您从中选择的表中之外,我想说一切都可能有效。也许你的意思是从@Practitioner中选择:

                WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))
                

                因为在上面的代码中是这样定义的:

                DECLARE @Practitioner TABLE (
                    idx smallint Primary Key IDENTITY(1,1)
                    , PractitionerId int
                )
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2012-05-13
                  • 2017-12-12
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-04-03
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多