避免使用WHILE循环和CURSOR以获得更好的性能?

时间:2014-02-27 11:29:52

标签: sql-server tsql sql-server-2008-r2 cursor

我想知道是否有人可以帮助简化此程序 - 并提高性能......!

我们有关于拨款的数据。 “捐赠者”向“收件人”提供资金,我们希望在3个时间段内显示每位捐赠者的前15位收件人:CurrentYear-20,CurrentYear-10和CurrentYear。我们发布年度报告,显示每个捐赠者的World和GeoZone总数的百分比份额。

我已经“继承”了这个由我的前任之一编写的代码。在我们切换到使用视图之前,执行时间大约是15-30分钟。目前,它运行在四个小时(计划为服务器代理作业)之下!管理层不高兴。由于各种原因,必须继续使用该视图,并且目前具有来自20世纪50年代以后的数据的不到900,000行。我们目前为30个(大型)捐赠者运行此报告,每年增加更多。

为了帮助提高性能,我考虑过使用CTE或/使用SUM()OVER(Partition BY ...)或它们的组合,但我不知道如何去做。

有人能指出我正确的方向吗?

以下是流程:

  • 创建一个表(变量)来保存当前捐赠者的前15位收件人
  • 创建一个表(变量)来保存捐赠者列表
  • 按捐赠者在报告中出现的顺序填写捐赠者表格
  • 通过捐赠者表和每个捐助者循环:
    • 将此捐赠者的捐赠者ID放入临时表
    • 循环3次(对于CurrentYear-20,CurrentYear-10,CurrentYear)
    • 计算18个地区/区域中每个地区的份额
    • 打印报告中每个部分的值
  • 获取下一个捐赠者ID

正如您从上面所看到的,每个捐赠者的计算运行54次(18x3)!

这是代码(简化):

-- @LatestYear is passed as a parameter, hardcoded here for simplicity
DECLARE @LatestYear SMALLINT ,
    @CurrentYear SMALLINT ,
    @DonorID SMALLINT ,
    @totalWorld NUMERIC(10, 2) ,
    @LoopCounter TINYINT ,
    @DonorName VARCHAR(100)  
SELECT  @latestyear = 2012  

    -- create a table to hold list of top 15 recipients for each donor and their 'share' of ODA.  
DECLARE @Top15 TABLE
(
  Country VARCHAR(100) ,
  Percentage REAL
)  

    -- create a table to hold list of donors, ordered as they need to appear in the report.  
DECLARE @PageOrder TABLE
(
  DonorID SMALLINT ,
  DonorName VARCHAR(100) ,
  SortOrder SMALLINT IDENTITY(1, 1)
)

    -- create a table to store the "focus" donor.  
DECLARE @CurrentDonor TABLE ( DonorID SMALLINT )

INSERT  INTO @PageOrder
        SELECT  DonorID ,
                DonorName
        FROM    dbo.LookupDonor
        ORDER BY DonorName;  

    -- cursor to loop through the donors in SortOrder
DECLARE DonorCursor CURSOR
FOR
    SELECT  DonorID ,
            DonorName
    FROM    @PageOrder
    ORDER BY DonorName;
OPEN DonorCursor
FETCH NEXT FROM DonorCursor INTO @DonorID, @DonorName

WHILE @@fetch_status = 0 
    BEGIN

        INSERT  INTO pubOutput
                ( XMLText )
                SELECT  @DonorName;

    -- Populate the DonorID table
        INSERT  INTO @CurrentDonor
        VALUES  ( @DonorID )

    /* The following loop is invoked 3 times. The first time through, the year will be 20 years before the latest year,
    the second time through, 10 years before. The last time through the year will be the latest year.
    */

        SET @LoopCounter = 1
        WHILE @LoopCounter <= 3 
            BEGIN
                SELECT  @CurrentYear = CASE @LoopCounter
                                         WHEN 1 THEN @LatestYear - 20
                                         WHEN 2 THEN @LatestYear - 10
                                         ELSE @LatestYear
                                       END

        -- calculate the world total for the current years (year,year-1) for all recipients
                SELECT  @totalWorld = SUM(Amount)
                FROM    dbo.vData2 d
                        INNER  JOIN ( SELECT    RecipientID
                                      FROM      dbo.RecipientGroup
                                      WHERE     GroupID = 160
                                    ) c ON d.RecipientID = c.RecipientID
                        INNER  JOIN @CurrentDonor z ON d.DonorID = z.DonorID
                WHERE   d.year IN ( @CurrentYear - 1, @CurrentYear )

        -- calculate the GeoZones total for the current years (year,year-1) 
                SELECT  @totalGeoZones = SUM(Amount)
                FROM    dbo.vDac2a d
                        INNER  JOIN ( SELECT    RecipientID
                                      FROM      dbo.GeoZones
                                      WHERE     GeoZoneID = 100
                                    ) x ON d.RecipientID = x.RecipientID
                        INNER  JOIN @CurrentDonor z ON d.DonorCode = z.DonorCode
                WHERE   d.year IN ( @CurrentYear - 1, @CurrentYear )

        -- Find the top 15 recipients for the current donor
                INSERT  INTO @Top15
                        SELECT TOP 15
                                r.RecipientName ,
                                ( ISNULL(SUM(Amount), 0) / @totalWorld ) * 100
                        FROM    dbo.vData2 d
                                INNER JOIN dbo.LookupRecipient r ON r.RecipientID = d.RecipientID
                                INNER JOIN @CurrentDonor z ON d.DonorID = z.DonorID
                        WHERE   d.year IN ( @CurrentYear - 1, @CurrentYear )
                        GROUP BY r.RecipientName
                        ORDER BY 2 DESC

        -- Print the top 15 recipients and total
                INSERT  INTO pubOutput
                        (
                          XMLText
                        )
                        SELECT  country + @Separator + CAST(percentage AS VARCHAR)
                        FROM    @Top15
                        ORDER BY percentage DESC
                INSERT  INTO pubOutput
                        (
                          XMLText
                        )
                        SELECT  @Heading1 + @Separator + CAST(SUM(Percentage) AS VARCHAR)
                        FROM    @Top15

    -- Breakdown by Regionas
        -- Region1
                IF @totalWorld IS NOT NULL 
                    INSERT  INTO pubOutput
                            (
                              XMLText
                            )
                            SELECT  'Region1' + @Separator
                                    + CAST(( ISNULL(SUM(Amount), 0) / @totalWorld ) * 100 AS VARCHAR)
                            FROM    dbo.vData2 d
                                    INNER JOIN ( SELECT RecipientID
                                                 FROM   dbo.RecipientGroup
                                                 WHERE  RegionID = 1
                                               ) c ON d.RecipientID = c.RecipientID
                                    INNER JOIN @CurrentDonor z ON d.DonorID = z.DonorID
                            WHERE   d.year IN ( @CurrentYear - 1, @CurrentYear )

                ELSE    -- force output of sub-total heading
                    INSERT  INTO pubOutput
                            (
                              XMLText
                            )
                            SELECT  @Heading2 + @Separator + '--'

        -- Region2-8
        /* similar syntax as Region1 above, for all Regions 2-8 */

        -- Total Regions
                INSERT  INTO pubOutput
                        (
                          XMLText
                        )
                        SELECT  @Heading2 + @Separator + CAST(@totalWorld AS VARCHAR)

    -- Breakdown by GeoZones 1-7
        -- GeoZone1
                INSERT  INTO pubOutput
                        (
                          XMLText
                        )
                        SELECT  'GeoZone1' + @Separator
                                + CAST(( ISNULL(SUM(Amount), 0) / @totalGeoZones ) * 100 AS VARCHAR)
                        FROM    dbo.vDac2a d
                                INNER JOIN ( SELECT RecipientID
                                             FROM   dbo.GeoZones
                                             WHERE  GeoZoneID = 1
                                           ) m ON d.RecipientID = m.RecipientID
                                INNER JOIN @CurrentDonor z ON d.DonorCode = z.DonorCode
                        WHERE   d.year IN ( @CurrentYear - 1, @CurrentYear )

        -- GeoZones2-8
        /* similar syntax as GeoZone1 above for GeoZones 2-7 */

        -- Total GeoZones - currently hard-coded as 100, due to minor rounding errors
                INSERT  INTO pubOutput
                        (
                          XMLText
                        )
                        SELECT  @Heading3 + @Separator + '100'

                SET @LoopCounter = @LoopCounter + 1

            END -- year loop

    -- Get the next donor from the cursor
        FETCH NEXT FROM DonorCursor 
    INTO @DonorID, @DonorName

    END
 -- donorcursor

    -- Cleanup
CLOSE DonorCursor
DEALLOCATE DonorCursor

非常感谢您提供的任何帮助。

1 个答案:

答案 0 :(得分:3)

必须避免光标。您可以使用'while'而不是光标。但是考虑到查询的复杂性,请在此时保持光标。

要以其他方式提高效果,请检查以下查询的记录数:

  1. SELECT RecipientCode FROM dbo.RecipientGroup WHERE GroupID = 160
  2. SELECT RecipientCode FROM dbo.GeoZones WHERE GeoZoneID = 100
  3. SELECT RecipientID FROM dbo.RecipientGroup WHERE RegionID = 1
  4. 我建议为上面的游标“outside”创建3个临时表,并在游标内部使用它们。

    希望这有帮助!