SQL Server - условная агрегация с корреляцией

Фон:

Оригинальный случай был очень прост. Рассчитать общее количество пользователей с самого высокого дохода до самого низкого:

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);

Query:

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 colums, а c3 используется дважды.

Все работает так, как должно. Но когда мы смотрим ближе, c2 и c3 очень похожи. Итак, почему бы не объединить их и использовать простое условное агрегирование:

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


Я прочитал Агрегаты с внешней ссылкой:

Источник для ограничения находится в стандарте SQL-92, а SQL Server унаследовал его от кодовой базы Sybase. Проблема в том, что SQL Server должен выяснить, какой запрос будет вычислять агрегат.

Я не ищу ответы, которые только показывают, как обойти его.

Вопросы:

  • Какая часть стандартного запрета или вмешивается в это?
  • Почему у других RDBMS нет проблем с этой внешней зависимостью?
  • Расширяют ли они SQL Standard и SQL Server ведут себя так, как должно, или SQL Server не реализует его полностью (правильно?)?.

Я был бы очень благодарен за ссылки на:

EDIT:

Я знаю, что 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

Ответы

Ответ 1

Существует более простое решение:

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 имеет тенденцию к высокому политическому (то есть, основанному на поставщиках), а не обусловлен фактическими требованиями пользователей к базе данных. Ну, со временем он движется в правильном направлении.

Ответ 2

В стандарте SQL для LATERAL такого ограничения нет. CROSS APPLY является специфическим расширением от Microsoft (Oracle принял его позже для совместимости), и его ограничения, очевидно, не связаны со стандартом ISO/IEC SQL, поскольку функция MS предшествует стандарту.

LATERAL в стандартном SQL - это просто модификатор для объединений, позволяющий использовать боковые ссылки в дереве соединений. Количество столбцов, на которые можно сослаться, не ограничено.

Я не вижу причины для странного ограничения для начала. Может быть, это потому, что CROSS APPLY изначально предназначался для разрешения табличных функций, что позже было расширено, чтобы разрешить sub- SELECT s.

Руководство Postgres manual объясняет LATERAL так:

Ключевое слово LATERAL может предшествовать элементу sub- SELECT FROM. Это позволяет sub- SELECT для ссылки на столбцы элементов FROM, которые появляются перед ним в списке FROM. (Без LATERAL каждый sub- 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 один раз.

  • Округлить результат, чтобы соответствовать желаемому результату.