优化具有 While 循环和交叉应用的 T-SQL 查询

Posted

技术标签:

【中文标题】优化具有 While 循环和交叉应用的 T-SQL 查询【英文标题】:Optimizing a T-SQL query featuring While loop and Cross Apply 【发布时间】:2020-01-22 05:00:22 【问题描述】:

寻求优化为计算滚动 12M 收入而编写的 T-SQL 查询。主要问题是我需要跨 6 个维度计算它,从而产生 45,000 多种组合,每个组合都需要自己的滚动收入。每个组合大约需要 5 分钟来计算。由于业务的性质,组合会随着时间的推移而增长。

所有操作都需要作为子查询完成。

DECLARE @NUMBER INT
DECLARE @ROWCOUNT INT = 0
DECLARE @REFERENCE VARCHAR(50)

SET @NUMBER =
    (SELECT MAX(r.Row)
        FROM (
            SELECT 
                a.GroupKey
                ,a.LevelKey
                ,a.StateKey
                ,a.ProductKey
                ,a.OptionKey
                ,a.LocKey
                ,ROW_NUMBER() OVER(ORDER BY a.GroupKey ASC) AS Row
            FROM Members AS a
            GROUP BY 
                a.GroupKey
                ,a.LevelKey
                ,a.StateKey
                ,a.ProductKey
                ,a.OptionKey
                ,a.LocKey ) r)
BEGIN
DECLARE @OUTPUT TABLE (
        RefKey VARCHAR(50)
        ,DateID INT
        ,GroupKey INT
        ,LevelKey INT
        ,StateKey INT
        ,ProductKey INT
        ,OptionKey INT
        ,LocKey INT
        ,Revenue DECIMAL(28,9)
        ,Rolling12Months DECIMAL(28,9)
         )

WHILE @ROWCOUNT < @NUMBER

BEGIN
SET @REFERENCE =
    (SELECT r.RefKey
        FROM (
            SELECT
                r.Refkey
                ,r.Row
                FROM (          
                    SELECT 
                        CONCAT( a.GroupKey, '-',a.LevelKey, '-',a.StateKey, '-',a.ProductKey, '-',a.OptionKey, '-',a.LocKey) AS RefKey
                        ,ROW_NUMBER() OVER(ORDER BY a.GroupKey ASC) AS Row
                    FROM Members AS a
                    GROUP BY 
                        a.GroupKey
                        ,a.LevelKey
                        ,a.StateKey
                        ,a.ProductKey
                        ,a.OptionKey
                        ,a.LocKey ) r) r WHERE r.Row = @ROWCOUNT + 1 )

INSERT INTO @OUTPUT         
SELECT
    a.RefKey
    ,a.DateID
    ,a.GroupKey
    ,a.LevelKey
    ,a.StateKey
    ,a.ProductKey
    ,a.OptionKey
    ,a.LocKey
    ,a.Revenue
    ,Rolling12Months=a.Revenue + b.RM 

FROM (

        SELECT
            CONVERT(datetime,STR(CI.DateID)) AS Date
            ,CI.DateID
            ,CI.GroupKey
            ,CI.LevelKey
            ,CI.StateKey
            ,CI.ProductKey
            ,CI.OptionKey
            ,CI.LocKey
            ,CONCAT( CI.GroupKey, '-',CI.LevelKey, '-',CI.StateKey, '-',CI.ProductKey, '-',CI.OptionKey, '-',CI.LocKey) AS RefKey
            ,SUM(CI.Revenue) AS Revenue
        FROM Members AS CI
        WHERE CONCAT( CI.GroupKey, '-',CI.LevelKey, '-',CI.StateKey, '-',CI.ProductKey, '-',CI.OptionKey, '-',CI.LocKey) = @REFERENCE
        GROUP BY CI.DateID,CI.StateKey,CI.GroupKey  ,CI.LevelKey,CI.StateKey,CI.ProductKey,CI.OptionKey ,CI.LocKey

        ) a
CROSS APPLY
( 
    SELECT RM=SUM(b.Revenue)
    FROM
    (
        SELECT TOP 11 b.Date, b.Revenue 
        FROM (

                SELECT
                    CONVERT(datetime,STR(CI.DateID)) AS Date
                    ,CI.DateID
                    ,CI.GroupKey
                    ,CI.LevelKey
                    ,CI.StateKey
                    ,CI.ProductKey
                    ,CI.OptionKey
                    ,CI.LocKey
                    ,CONCAT( CI.GroupKey, '-',CI.LevelKey, '-',CI.StateKey, '-',CI.ProductKey, '-',CI.OptionKey, '-',CI.LocKey) AS RefKey
                    ,SUM(CI.Revenue) AS Revenue
                FROM Members AS CI
                WHERE CONCAT( CI.GroupKey, '-',CI.LevelKey, '-',CI.StateKey, '-',CI.ProductKey, '-',CI.OptionKey, '-',CI.LocKey) = @REFERENCE
                GROUP BY CI.DateID,CI.StateKey,CI.GroupKey  ,CI.LevelKey,CI.StateKey,CI.ProductKey,CI.OptionKey ,CI.LocKey

        ) b
        WHERE b.Date < a.Date AND b.RefKey = a.RefKey
        ORDER BY b.Date DESC
    ) b
) b

    SET @ROWCOUNT = @ROWCOUNT + 1
    END
    SELECT * FROM @OUTPUT
END

【问题讨论】:

您应该做的第一件事可能是摆脱循环。 SQL 不能很好地处理循环,如果可能的话,你应该找到一个基于集合的方法来替换它。我没有阅读您发布的代码,但这是 SQL 中的一条黄金法则。 谢谢佐哈尔。我不确定是否有任何不使用循环来对属性进行聚类的方法。 【参考方案1】:

我对你的代码有几点看法

尝试使用临时表而不是表变量 - 表变量可能存在多行性能问题。

通过连接很多列来动态生成一个键看起来很简洁,但是当你这样做时你会杀死任何索引或统计信息的使用。如果需要,加入单独的列,而不是在它们上使用函数。

您将日期列转换为日期时间,然后加入它 - 您应该再次使用未更改的列进行比较。

你没有解释你的日期列 - 你计算你的滚动 12 个月只是作为前 11 个最后日期小于当前日期,加上当前日期

,Rolling12Months=a.Revenue + b.RM 

 SELECT RM=SUM(b.Revenue)
    FROM
    (
        SELECT TOP 11 b.Date, b.Revenue 

您是否考虑过每个月可能有多个日期?在这种情况下,总和中您将获得少于 12 个月的数据。或者 ii,对于您按列分组的某种组合,有几个月没有数据,在这种情况下,您的总和将是更多个月!

但是,如果我们只需要按列对每个组合的最后 12 个日期求和,那么我会这样做:

/* When using many rows a temp table often performs better than a table variable */
DROP TABLE IF EXISTS #OUTPUT

CREATE TABLE #OUTPUT  (
        RefKey VARCHAR(50)
        ,DateID INT
        ,GroupKey INT
        ,LevelKey INT
        ,StateKey INT
        ,ProductKey INT
        ,OptionKey INT
        ,LocKey INT
        ,Revenue DECIMAL(28,9)
        ,Rolling12Months DECIMAL(28,9)
         )

/* I am doing a simple select and using the analytical sum, which can be set up with a window */
SELECT
    CONCAT(CI.GroupKey, '-', CI.LevelKey, '-', CI.StateKey, '-', CI.ProductKey, '-', CI.OptionKey, '-', CI.LocKey) RefKey
   ,CI.DateID
   ,CI.GroupKey
   ,CI.LevelKey
   ,CI.StateKey
   ,CI.ProductKey
   ,CI.OptionKey
   ,CI.LocKey
   ,CI.Revenue
   ,SUM(CI.Revenue) OVER (PARTITION BY CI.GroupKey, CI.LevelKey, CI.StateKey
    , CI.ProductKey, CI.OptionKey, CI.LocKey ORDER BY CI.DateID 
    ROWS BETWEEN 11 PRECEDING AND CURRENT ROW) Rolling12Months
FROM Members AS CI
GROUP BY
    CI.DateID
   ,CI.GroupKey
   ,CI.LevelKey
   ,CI.StateKey
   ,CI.ProductKey
   ,CI.OptionKey
   ,CI.LocKey

SELECT
    *
FROM #OUTPUT

这应该会给您相同的结果,但是我不会按原样使用它,因为它只返回最后 11 个日期的总和加上每个列组合的当前日期!但如果它适用于您的原始代码,这应该会复制您的结果

【讨论】:

以上是关于优化具有 While 循环和交叉应用的 T-SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

T-SQL While 循环和连接

T-SQL编程

SQL Server - where + TVF/SVF、交叉应用、T-SQL

循环结构while

优化我的 T-SQL 查询以提高性能

高级T-SQL第1级的阶梯:使用交叉连接来引入高级T-SQL