【问题标题】:SQL Server - conditional aggregation with correlationSQL Server - 具有相关性的条件聚合
【发布时间】:2016-07-28 15:54:01
【问题描述】:

背景:

original case 非常简单。从最高到最低计算每位用户的总收入:

CREATE TABLE t(Customer INTEGER  NOT NULL PRIMARY KEY 
              ,"User"   VARCHAR(5) NOT NULL
              ,Revenue  INTEGER  NOT NULL);

INSERT INTO t(Customer,"User",Revenue) VALUES
(001,'James',500),(002,'James',750),(003,'James',450),
(004,'Sarah',100),(005,'Sarah',500),(006,'Sarah',150),
(007,'Sarah',600),(008,'James',150),(009,'James',100);

查询:

SELECT *,
    1.0 * Revenue/SUM(Revenue) OVER(PARTITION BY "User") AS percentage,
    1.0 * SUM(Revenue) OVER(PARTITION BY "User" ORDER BY Revenue DESC)
         /SUM(Revenue) OVER(PARTITION BY "User") AS running_percentage
FROM t;

LiveDemo

输出:

╔════╦═══════╦═════════╦════════════╦════════════════════╗
║ ID ║ User  ║ Revenue ║ percentage ║ running_percentage ║
╠════╬═══════╬═════════╬════════════╬════════════════════╣
║  2 ║ James ║     750 ║ 0.38       ║ 0.38               ║
║  1 ║ James ║     500 ║ 0.26       ║ 0.64               ║
║  3 ║ James ║     450 ║ 0.23       ║ 0.87               ║
║  8 ║ James ║     150 ║ 0.08       ║ 0.95               ║
║  9 ║ James ║     100 ║ 0.05       ║ 1                  ║
║  7 ║ Sarah ║     600 ║ 0.44       ║ 0.44               ║
║  5 ║ Sarah ║     500 ║ 0.37       ║ 0.81               ║
║  6 ║ Sarah ║     150 ║ 0.11       ║ 0.93               ║
║  4 ║ Sarah ║     100 ║ 0.07       ║ 1                  ║
╚════╩═══════╩═════════╩════════════╩════════════════════╝

可以使用特定的窗口函数进行不同的计算。


现在让我们假设我们不能使用窗口化的SUM 并重写它:

SELECT c.Customer, c."User", c."Revenue"
    ,1.0 * Revenue / NULLIF(c3.s,0) AS percentage
    ,1.0 * c2.s    / NULLIF(c3.s,0) AS running_percentage
FROM t c
CROSS APPLY
        (SELECT SUM(Revenue) AS s
        FROM t c2
        WHERE c."User" = c2."User"
            AND c2.Revenue >= c.Revenue) AS c2
CROSS APPLY
        (SELECT SUM(Revenue) AS s
        FROM t c2
        WHERE c."User" = c2."User") AS c3
ORDER BY "User", Revenue DESC;

LiveDemo

我使用了CROSS APPLY,因为我不喜欢SELECT 列列表中的相关子查询,并且c3 被使用了两次。

一切正常。但是当我们仔细观察时,c2c3 非常相似。那么为什么不将它们结合起来并使用简单的条件聚合:

SELECT c.Customer, c."User", c."Revenue"
    ,1.0 * Revenue        / NULLIF(c2.sum_total,0) AS percentage
    ,1.0 * c2.sum_running / NULLIF(c2.sum_total,0) AS running_percentage
FROM t c
CROSS APPLY
        (SELECT SUM(Revenue) AS sum_total,
                SUM(CASE WHEN c2.Revenue >= c.Revenue THEN Revenue ELSE 0 END) 
                AS sum_running
        FROM t c2
        WHERE c."User" = c2."User") AS c2
ORDER BY "User", Revenue DESC;

很遗憾,这是不可能的。

在包含外部引用的聚合表达式中指定了多个列。如果要聚合的表达式包含外部引用,则该外部引用必须是表达式中引用的唯一列。

当然我可以用另一个子查询来绕过它,但是它变得有点“难看”:

SELECT c.Customer, c."User", c."Revenue"
    ,1.0 * Revenue        / NULLIF(c2.sum_total,0) AS percentage
    ,1.0 * c2.sum_running / NULLIF(c2.sum_total,0) AS running_percentage
FROM t c
CROSS APPLY
(   SELECT SUM(Revenue) AS sum_total,
           SUM(running_revenue) AS sum_running
     FROM (SELECT Revenue,
                  CASE WHEN c2.Revenue >= c.Revenue THEN Revenue ELSE 0 END 
                  AS running_revenue
           FROM t c2
           WHERE c."User" = c2."User") AS sub
) AS c2
ORDER BY "User", Revenue DESC

LiveDemo


Postgresql 版本。唯一的区别是LATERAL 而不是CROSS APPLY

SELECT c.Customer, c."User", c.Revenue
    ,1.0 * Revenue        / NULLIF(c2.sum_total,0) AS percentage 
    ,1.0 * c2.running_sum / NULLIF(c2.sum_total,0) AS running_percentage 
FROM t c
,LATERAL (SELECT SUM(Revenue) AS sum_total,
                 SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END) 
                 AS running_sum
        FROM t c2
        WHERE c."User" = c2."User") c2
ORDER BY "User", Revenue DESC;

SqlFiddleDemo

效果很好。


SQLite/MySQL 版本(这就是为什么我更喜欢LATERAL/CROSS APPLY):

SELECT c.Customer, c."User", c.Revenue,
    1.0 * Revenue / (SELECT SUM(Revenue)
                     FROM t c2
                     WHERE c."User" = c2."User") AS percentage,
    1.0 * (SELECT SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END)
           FROM t c2
          WHERE c."User" = c2."User")  / 
          (SELECT SUM(c2.Revenue)
           FROM t c2
           WHERE c."User" = c2."User") AS running_percentage
FROM t c
ORDER BY "User", Revenue DESC;

SQLFiddleDemo-SQLite SQLFiddleDemo-MySQL


我读过Aggregates with an Outer Reference:

限制的来源在SQL-92 标准中,SQL Server 继承自Sybase 代码库。问题是 SQL Server 需要确定哪个查询将计算聚合。

我不寻找显示如何规避它的答案。

问题是:

  1. 标准的哪一部分不允许或干扰它?
  2. 为什么其他 RDBMS 没有这种外部依赖的问题?
  3. 它们是否扩展了SQL StandardSQL Server 的行为应该如此,或者SQL Server 没有完全实现它(正确吗?)?。

我将非常感谢参考:

  • ISO standard(92 或更新)
  • SQL Server Standards Support
  • 来自任何 RDBMS 的官方文档来解释它 (SQL Server/Postgresql/Oracle/...)。

编辑:

我知道SQL-92 没有LATERAL 的概念。但是带有子查询的版本(如SQLite/MySQL)也不起作用。

LiveDemo

编辑 2:

为了简化一点,我们只检查相关子查询:

SELECT c.Customer, c."User", c.Revenue,
       1.0*(SELECT SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END)
              FROM t c2
              WHERE c."User" = c2."User") 
       / (SELECT SUM(c2.Revenue)
          FROM t c2
          WHERE c."User" = c2."User") AS running_percentage
FROM t c
ORDER BY "User", Revenue DESC;

以上版本在MySQL/SQLite/Postgresql 中运行良好。

SQL Server 我们得到错误。在用子查询包装它以将其“展平”到一个级别后,它可以工作:

SELECT c.Customer, c."User", c.Revenue,
      1.0 * (
              SELECT SUM(CASE WHEN r1 >= r2 THEN r1 ELSE 0 END)
              FROM (SELECT c2.Revenue AS r1, c.Revenue r2
                    FROM t c2
                    WHERE c."User" = c2."User") AS S)  / 
             (SELECT SUM(c2.Revenue)
              FROM t c2
              WHERE c."User" = c2."User") AS running_percentage
FROM t c
ORDER BY "User", Revenue DESC;

这个问题的重点是SQL standard是如何监管的。

LiveDemo

【问题讨论】:

    标签: sql sql-server postgresql tsql sql-standards


    【解决方案1】:

    LATERAL 的 SQL 标准中没有这样的限制。 CROSS APPLY 是 Microsoft 提供的特定于供应商的扩展(Oracle 后来为了兼容性而采用了它),它的限制显然不是由于 ISO/IEC SQL 标准,因为 MS 功能早于标准.

    LATERAL 在标准 SQL 中基本上只是一个连接修饰符,以允许连接树中的横向引用。可以引用的列数没有限制。

    一开始我看不出有什么奇怪的限制。可能是因为CROSS APPLY原本打算允许表值函数,后来扩展为允许sub-SELECTs。

    Postgres manual 是这样解释LATERAL 的:

    LATERAL 关键字可以位于子SELECT FROM 项之前。这允许 子SELECT 引用出现在它之前的FROM 项目的列 在FROM 列表中。 (没有LATERAL,每个子SELECT都会被评估 独立,因此不能交叉引用任何其他 FROM 项目。)

    您的查询的 Postgres 版本(没有更优雅的窗口函数)可以更简单:

    SELECT c.*
         , round(revenue        / c2.sum_total, 2) END AS percentage 
         , round(c2.running_sum / c2.sum_total, 2) END AS running_percentage 
    FROM   t c, LATERAL (
       SELECT NULLIF(SUM(revenue), 0)::numeric AS sum_total  -- NULLIF, cast once
            , SUM(revenue) FILTER (WHERE revenue >= c.revenue) AS running_sum
       FROM   t
       WHERE  "User" = c."User"
       ) c2
    ORDER  BY c."User", c.revenue DESC;
    
    • Postgres 9.4+ 具有更优雅的聚合 FILTER 用于条件聚合。

    • NULLIF 有道理,我只建议稍微简化一下。

    • sum_total 转换为numeric 一次。

    • 四舍五入的结果与您想要的结果相匹配。

    【讨论】:

    • 我知道FILTER 功能。 Postgresql 版本用于演示,这就是我希望它尽可能 1:1 的原因。 SQL Sever CROSS APPLY 版本也允许交叉引用。这个问题的重点是 SQL Standard 如何使用聚合函数来处理它。让我们暂时忘掉LATERAL,得到简单的相关子查询版本demo,它在MySQL/SQLite/Postgresql 中完美运行。 SQL 标准是否定义了我们可以从 agg 函数中引用哪一列?
    • Does SQL standard define to which column could we refer from agg function? 我会说,所有可见的列。 (没有实际调查 SQL 标准。)
    • 可能是SQL Server 限制强制所有列引用必须来自“同一级别”。直截了当:它是在标准中定义还是在特定供应商中未定义/松散定义。
    • 您引用的错误消息似乎根本不涉及可见性。具有外部引用的聚合表达式仅限于这个单一的外部引用。对我来说似乎是特定于供应商的。
    • @lad2025:你是对的。我不知何故飘到COALESCENULLIF 有道理。
    【解决方案2】:

    有一个更简单的解决方案:

    SELECT c.Customer, c."User", c."Revenue",
           1.0 * Revenue/ NULLIF(c2.sum_total, 0) AS percentage,
           1.0 * c2.sum_running / NULLIF(c2.sum_total, 0) AS running_percentage
    FROM t c CROSS APPLY
         (SELECT SUM(c2.Revenue) AS sum_total,
                 SUM(CASE WHEN c2.Revenue >= x.Revenue THEN c2.Revenue ELSE 0 END) 
                     as sum_running
          FROM t c2 CROSS JOIN
               (SELECT c.REVENUE) x
          WHERE c."User" = c2."User"
         ) c2
    ORDER BY "User", Revenue DESC;
    

    我不确定为什么或是否在 SQL '92 标准中有此限制。大约 20 年前,我确实把它记得很好,但我不记得那个特别的限制了。

    我应该注意:

    • 在 SQL 92 标准制定时,横向连接并没有真正引起人们的注意。 Sybase 肯定没有这样的概念。
    • 其他数据库确实存在外部引用问题。特别是,它们通常将范围限定在一个深度。
    • SQL 标准本身倾向于高度政治化(即供应商驱动),而不是由实际的数据库用户需求驱动。嗯,随着时间的推移,它确实朝着正确的方向发展。

    【讨论】:

    • 是的,或者使用CROSS APPLY Demo。它“隐藏”了子查询。
    • (VALUES (c.REVENUE)) x(REVENUE) 在我看来做得更好。
    • 语法糖取决于口味 :) 第一个注释是正确的,LATERAL(也是一种语法糖)是较新的概念。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-10
    • 2020-10-02
    • 1970-01-01
    • 1970-01-01
    • 2014-12-12
    相关资源
    最近更新 更多