【问题标题】:How to convert Row by row execution in to SET based approach in SQL如何在 SQL 中将逐行执行转换为基于 SET 的方法
【发布时间】:2019-10-30 04:16:31
【问题描述】:

我正在编写一个庞大的 SQL 代码,不幸的是,它有一个 CURSOR,它处理其中的另外两个嵌套 CURSORS(存储过程中总共三个游标),它处理数百万个要删除、更新和插入的数据。由于逐行执行,这需要大量时间,我希望将其修改为基于 SET 的方法

许多文章显示不推荐使用 CURSOR,替代方法是使用 WHILE 循环,所以我尝试用三个 WHILE 循环替换三个 CUROSR,虽然我得到了相同的结果,但没有任何改进在性能方面,它与 CUROSR 所用的时间相同。

下面是我正在编写的代码的基本结构(我会尽量简单),并且我会将 cmets 放在他们应该做的事情上。

declare @projects table (
    ProjectID INT,
    fieldA int,
    fieldB int,
    fieldC int,
    fieldD int)

    INSERT INTO @projects
    SELECT ProjectID,fieldA,fieldB,fieldC, fieldD
    FROM ProjectTable

    DECLARE projects1 CURSOR LOCAL FOR /*First cursor - fetch the cursor from ProjectaTable*/
        Select ProjectID FROM @projects

    OPEN projects1
    FETCH NEXT FROM projects1 INTO @ProjectID
    WHILE @@FETCH_STATUS = 0
    BEGIN
    BEGIN TRY
        BEGIN TRAN

        DELETE FROM T_PROJECTGROUPSDATA td
        WHERE td.ID = @ProjectID

        DECLARE datasets CURSOR FOR /*Second cursor - this will get the 'collectionDate'field from datasetsTable for every project fetched in above cursor*/
            Select DataID, GroupID, CollectionDate
            FROM datasetsTable 
            WHERE datasetsTable.projectID = @ProjectID /*lets say this will fetch ten records for a single projectID*/

        OPEN datasets
        FETCH NEXT FROM datasets INTO @DataID, @GroupID, @CollectionDate
        WHILE @@FETCH_STATUS = 0
        BEGIN

            DECLARE period CURSOR FOR /*Third Cursor - this will process the records from another table called period with above fetched @collectionDate*/
            SELECT ID, dbo.fn_GetEndOfPeriod(ID) 
            FROM T_PERIODS
            WHERE DATEDIFF(dd,@CollectionDate,dbo.fn_GetEndOfPeriod(ID)) >= 0 /*lets say this will fetch 20 records for above fetched single @CollectionDate*/
            ORDER BY [YEAR],[Quarter]

                OPEN period
                FETCH NEXT FROM period INTO @PeriodID, @EndDate
                WHILE @@FETCH_STATUS = 0
                BEGIN
                    IF EXISTS (some conditions No - 1 )
                    BEGIN
                        BREAK
                    END
                    IF EXISTS (some conditions No - 2 )
                    BEGIN
                        FETCH NEXT FROM period INTO @PeriodID, @EndDate
                        CONTINUE
                    END

                    /*get the appropirate ID from T_uploads table for the current projectID and periodID fetched*/
                    SET @UploadID = (SELECT ID FROM T_UPLOADS u WHERE  u.project_savix_ID = @ProjectID AND u.PERIOD_ID = @PeriodID AND u.STATUS = 3)

                    /*Update some fields in T_uploads table for the current projectID and periodID fetched*/
                    UPDATE T_uploads
                    SET fieldA = mp.fieldA, fieldB = mp.fieldB
                    FROM @projects mp
                    WHERE T_UPLOADS.ID = @UploadID AND mp.ProjectID = @ProjectID

                    /*Insert some records in T_PROJECTGROUPSDATA table for the current projectID and periodID fetched*/
                    INSERT INTO T_PROJECTGROUPSDATA tpd ( fieldA,fieldB,fieldC,fieldD,uploadID)
                    SELECT fieldA,fieldB,fieldC,fieldD,@UploadID
                    FROM @projects
                    WHERE tpd.DataID = @DataID

                FETCH NEXT FROM period INTO @PeriodID, @EndDate
                END
                CLOSE period
                DEALLOCATE period

            FETCH NEXT FROM datasets INTO @DataID, @GroupID, @CollectionDate, @Status, @Createdate
        END

        CLOSE datasets
        DEALLOCATE datasets

        COMMIT
    END TRY

    BEGIN CATCH
        Error handling
        IF @@TRANCOUNT > 0
            ROLLBACK
    END CATCH
    FETCH NEXT FROM projects1 INTO @ProjectID, @FAID
END


CLOSE projects1
DEALLOCATE projects1

SELECT 1 as success

我请求您建议任何方法来重写此代码以遵循基于 SET 的方法。

【问题讨论】:

  • 样本数据最好使用DDL + DML。请edit您的问题包括它,您当前的尝试和您想要的结果。更多详情,read this.
  • 您混淆了太多,删除了太多,并引入了语法错误(例如,将语句插入 T_PROJECTSGROUPDATA),这些错误会妨碍您对逻辑的理解。我可以看到盲目应用的模式(无特殊原因将行加载到表变量中)。看起来您的前 2 个光标可以合并为一个。

标签: sql-server tsql while-loop sql-server-2012 cursor


【解决方案1】:

在没有提供表结构和预期结果示例数据之前,我认为可以快速改进以下几点(其中一些已经被其他人在上面提到过):

  1. WHILE 循环也是一个游标。所以,改成while循环不是 让事情变得更快。
  2. 除非您需要回溯记录,否则请使用 LOCAL FAST_FORWARD 游标。这将使执行速度更快。
  3. 是的,我同意在大多数情况下使用基于 SET 的方法是最快的,但是如果您必须在某处存储中间结果集,我建议使用临时表而不是表变量。临时表在这两个选项之间是“较小的邪恶”。以下是您应该尽量避免使用表变量的几个原因:

    • 由于 SQL Server 在构建执行计划期间不会对表变量进行任何先前的统计,因此它始终认为在构建执行计划期间表变量将只返回一条记录。因此,存储引擎将只分配尽可能多的 RAM 内存来执行查询。但实际上,表变量在执行期间可能会保存数百万条记录。如果发生这种情况,SQL Server 将在执行期间强制将数据溢出到硬盘(您会在 sys.dm_os_wait_stats 中看到大量 PAGEIOLATCH),从而导致查询速度变慢。
    • 解决上述问题的一种方法是在使用表值的每个查询结束时提供语句级提示 OPTION (RECOMPILE)。这将迫使 SQL Server 在运行时每次都构建这些查询的执行计划,并且可以避免内存分配问题。但是这样做的缺点是:SQL Server 将不再能够利用该存储过程已经缓存的执行计划,并且每次都需要重新编译,这会在一定程度上降低性能。因此,除非您知道基础表中的数据经常更改或存储过程本身不经常执行,否则 Microsoft MVP 不建议使用此方法。

【讨论】:

    【解决方案2】:

    不建议盲目地将Cursor 替换为While,因此它不会影响您的性能,甚至可能对性能产生负面影响。

    当您使用 Declare C Cursor 定义游标时,实际上您将创建一个 SCROLL 游标,它指定所有获取选项 (FIRST, LAST, PRIOR, NEXT, RELATIVE, ABSOLUTE) 都可用。

    当您只需要Fetch Next 作为滚动选项时,您可以将光标声明为FAST_FORWARD

    这是 Microsoft 文档中关于 FAST_FORWARD 光标的引用:

    指定光标只能向前移动和滚动 第一行到最后一行。 FETCH NEXT 是唯一支持的 fetch 选项。当前执行的所有插入、更新和删除语句 影响结果集中行的用户(或由其他用户提交) 在获取行时可见。因为光标不能 向后滚动,但是,之后对数据库中的行所做的更改 获取的行通过游标不可见。只转发 默认情况下,游标是动态的,这意味着检测到所有更改 在处理当前行时。这提供了更快的光标打开 并使结果集能够显示对底层证券所做的更新 表。虽然只进游标不支持向后滚动, 应用程序可以通过关闭返回到结果集的开头 并重新打开光标。

    所以你可以使用DECLARE <CURSOR NAME> FAST_FORWARD FOR ... 声明你的游标,你会得到明显的改进

    【讨论】:

      【解决方案3】:

      我认为上面所有的游标代码都可以简化成这样:

      DROP TABLE IF EXISTS #Source;
      SELECT DISTINCT p.ProjectID,p.fieldA,p.fieldB,p.fieldC,p.fieldD,u.ID AS [UploadID]
      INTO #Source
      FROM ProjectTable p
      INNER JOIN DatasetsTable d ON d.ProjectID = p.ProjectID
      INNER JOIN T_PERIODS s ON DATEDIFF(DAY,d.CollectionDate,dbo.fn_GetEndOfPeriod(s.ID)) >= 0 
      INNER JOIN T_UPLOADS u ON u.roject_savix_ID = p.ProjectID AND u.PERIOD_ID = s.ID AND u.STATUS = 3
      WHERE NOT EXISTS (some conditions No - 1)
          AND NOT EXISTS (some conditions No - 2)
      ;
      
      UPDATE u SET u.fieldA = s.fieldA, u.fieldB = s.fieldB
      FROM T_UPLOADS u
      INNER JOIN #Source s ON s.UploadID = u.ID
      ;
      INSERT INTO T_PROJECTGROUPSDATA (fieldA,fieldB,fieldC,fieldD,uploadID)
      SELECT DISTINCT s.fieldA,s.fieldB,s.fieldC,s.fieldD,s.UploadID
      FROM #Source s
      ;
      
      DROP TABLE IF EXISTS #Source;
      

      此外,最好知道“某些条件否”的详细信息,因为查询可能会有所不同。

      【讨论】:

      • 感谢 Vitaly,这可能对我即兴创作我的代码有用。请原谅我没有提到“某些条件否”,因为这将需要很长的代码来表达。
      • 您好,Vitaly,我已在此处的另一篇文章中发布了与此问题相关的实际代码 - stackoverflow.com/questions/56712845/… 请看一下,您的想法对我很有价值
      猜你喜欢
      • 1970-01-01
      • 2020-08-18
      • 1970-01-01
      • 2019-08-30
      • 2021-08-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多