Накопительная сумма по набору строк в MySQL

У меня сложный запрос (содержащий несколько объединений, объединений), который возвращает набор строк, содержащих id, day, hr, amount. Результат запроса выглядит так:

id day    hr  amount 
1   1      1   10       
1   1      2   25       
1   1      3   30        
1   2      1   10       
1   2      2   40       
1   2      2   30        
2   1      1   10       
2   1      2   15        
2   1      3   30       
2   2      1   10       
2   2      2   20      
2   2      2   30  

Мне нужно найти совокупную сумму для каждого идентификатора за каждый час дня. Результат должен быть таким:

id day    hr  amount cumulative total
1   1      1   10       10
1   1      2   25       35
1   1      3   30       65 
1   2      1   10       10
1   2      2   40       50
1   2      2   30       80 
2   1      1   10       10
2   1      2   15       25 
2   1      3   30       55
2   2      1   10       10
2   2      2   20       30
2   2      2   30       60

Мой первоначальный запрос, который дает первый результат, выглядит так:

select id, day, hr, amount from
( //multiple joins on multiple tables)a
left join
(//unions on multiple tables)b
on a.id=b.id;

Какой sql-запрос для получения кумулятивной суммы, как описано во втором выводе? SET не следует использовать в растворе.

Спасибо.


person user1051577    schedule 15.07.2013    source источник
comment
Перед более запутанными (но возможными) решениями: рассматривали ли вы (а) достаточно ли WITH ROLLUP для ваших нужд и (б) насколько легче это может быть достигнуто в коде приложения, а не в SQL?   -  person Wrikken    schedule 16.07.2013
comment
@Wrikken: a) WITH ROLLUP добавляет дополнительные строки к выводу, мне это нужно в виде столбца, можно ли этого добиться с помощью WITH ROLLUP? б) Я согласен, что в коде приложения это будет проще, но я столкнулся с ситуацией, когда это нужно делать в самом SQL.   -  person user1051577    schedule 16.07.2013
comment
Я бы использовал хранимую процедуру, которая делает что-то вроде этого: 1. Создайте временную таблицу с результатом вашего запроса, 2. Добавьте столбец в временную таблицу, 3. Обновите каждую строку временной таблицы с кумулятивной суммой.   -  person Barranka    schedule 16.07.2013
comment
То, что вы спрашиваете таким образом, мне нужно найти кумулятивную сумму для каждого идентификатора, поскольку каждый час дня НЕ отображается в таблице. У вас есть ID 1, DAY 2, HR 2 дважды в вашем образце вывода. Кажется, невозможно решить эту проблему без использования переменных, поскольку у вас фактически нет первичного ключа для использования в этой таблице.   -  person Mosty Mostacho    schedule 16.07.2013
comment
@MostyMostacho Исправил вывод.   -  person user1051577    schedule 16.07.2013
comment
Ответов на повторяющийся вопрос недостаточно; запросы в этих ответах не приведут к набору результатов, указанному в этом вопросе. (Одна большая проблема заключается в том, что эти запросы не производят промежуточную сумму для каждой группы и не сбрасывают ее для следующей группы, как указано в этом вопросе. Эти запросы производят только промежуточную сумму для всего набора. Это существенно другой вопрос.   -  person spencer7593    schedule 16.07.2013


Ответы (3)


ОБНОВЛЕНИЕ

MySQL 8.0 представляет «оконные функции», функциональные возможности, эквивалентные «оконным функциям» SQL Server (с разделением и упорядочиванием, обеспечиваемым синтаксисом Transact-SQL OVER), и «аналитическими функциями» Oracle.

Справочное руководство MySQL 12.21 Функции окна https://dev.mysql.com/doc/refman/8.0/en/window-functions.html.

Приведенный здесь ответ - подход для версий MySQL до 8.0.


ОРИГИНАЛЬНЫЙ ОТВЕТ

MySQL не предоставляет аналитическую функцию типа, которую вы использовали бы для получения текущей «совокупной суммы», как аналитические функции, доступные в других СУБД (например, Oracle или SQL Server).

Но можно эмулировать некоторые аналитические функции, используя MySQL.

Есть (как минимум) два работоспособных подхода:

Один из них - использовать коррелированный подзапрос для получения промежуточной суммы. Этот подход может быть дорогостоящим для больших наборов и сложным, если предикаты внешнего запроса сложны. Это действительно зависит от того, насколько сложным является «несколько объединений в нескольких таблицах». (К сожалению, MySQL также не поддерживает CTE.)

Другой подход - использовать пользовательские переменные MySQL для обработки прерывания управления. «Уловка» здесь заключается в том, чтобы результаты вашего запроса были отсортированы (с использованием ORDER BY), а затем ваш запрос был заключен в другой запрос.

Приведу пример последнего подхода.

Из-за порядка, в котором MySQL выполняет операции, столбец cumulative_total должен быть вычислен до того, как значение из id и day из текущей строки будет сохранено в пользовательских переменных. Проще всего поставить эту колонку на первое место.

Встроенное представление с псевдонимом i (в запросе ниже) предназначено только для инициализации пользовательских переменных, на всякий случай, если они уже установлены в сеансе. Если им уже присвоены значения, мы хотим игнорировать их текущие значения, и самый простой способ сделать это - инициализировать их.

Исходный запрос заключен в круглые скобки, а в приведенном ниже примере ему будет присвоен псевдоним c. Единственное изменение в исходном запросе - это добавление предложения ORDER BY, поэтому мы можем быть уверены, что обрабатываем строки из запроса последовательно.

Внешний выбор проверяет, совпадают ли значения id и day из текущей строки с предыдущей строкой. Если это так, мы добавляем amount из текущей строки к совокупному промежуточному итогу. Если они не совпадают, мы сбрасываем совокупный промежуточный итог до нуля и добавляем сумму из текущей строки (или, проще говоря, просто назначаем сумму из текущей строки).

После того, как мы вычислили совокупную сумму, мы сохраняем значения id и day из текущей строки в пользовательские переменные, чтобы они были доступны при обработке следующей строки.

Например:

SELECT IF(@prev_id = c.id AND @prev_day = c.day
         ,@cumtotal := @cumtotal + c.amount
         ,@cumtotal := c.amount) AS cumulative_total
     , @prev_id  := c.id  AS `id`
     , @prev_day := c.day AS `day`
     , c.hr
     , c.amount AS `amount'
  FROM ( SELECT @prev_id  := NULL
              , @prev_day := NULL
              , @subtotal := 0
       ) i
  JOIN (

         select id, day, hr, amount from
         ( //multiple joins on multiple tables)a
         left join
         (//unions on multiple tables)b
         on a.id=b.id

         ORDER BY 1,2,3
       ) c

Если необходимо вернуть столбцы в другом порядке с кумулятивным итогом в качестве последнего столбца, тогда один из вариантов - обернуть весь этот оператор набором скобок и использовать этот запрос как встроенное представление:

SELECT d.id
     , d.day
     , d.hr
     , d.amount
     , d.cumulative_total
FROM (
       // query from above
     ) d
person spencer7593    schedule 15.07.2013
comment
вы можете написать простой запрос ... посмотрите на ответ ниже, это простой запрос, который помогает. - person SQL.injection; 16.07.2013
comment
В простом запросе исходный запрос OP нужно было бы указать дважды вместо foo. (Если в исходный запрос внесены какие-либо изменения, его необходимо будет изменить в двух местах.) Выходные данные простого запроса не соответствуют спецификации, по крайней мере, в случае пятой строки в примере OP, поскольку там две строки id=1 day=2 hr=2. Согласно спецификации, промежуточный итог пятой строки не должен включать сумму из шестой строки. - person spencer7593; 16.07.2013
comment
N.B. Ответы на вопрос, который был помечен как повторяющийся, не приведут к указанному вами набору результатов. Это будет промежуточная сумма для всего набора, а не для каждой группы. Эти запросы также не обрабатывают повторяющиеся экземпляры ключа группировки. - person spencer7593; 16.07.2013
comment
Этот подход тоже очень быстрый. У меня были проблемы с получением совокупной суммы примерно по 15 столбцам в нескольких миллионах строк. Я пробовал много разных подходов. Решение от @ lukas-eder хорошее, но оно займет все больше и больше времени, если количество строк (для суммирования) увеличивается. Это решение от @ spencer7593 на самом деле не заботится о размерах вашего набора, и оно останется быстрым. 25000 rows in 0.19 sec против 4.43 sec - person Thomas Lobker; 11.06.2019

Если вы используете MySQL 8 или новее, вы должны использовать оконные функции для этого. Ваш запрос будет выглядеть так:

SELECT
  id, day, hr, amount,
  SUM (amount) OVER (PARTITION BY id, day ORDER BY hr) AS `cumulative total`
FROM t

Где t - ваш стол, b слева присоединенный к a. Некоторые примечания:

  • Предложение PARTITION BY гарантирует, что вы получите кумулятивную сумму по id и day, поэтому каждый день мы начинаем суммировать заново.
  • Предложение ORDER BY определяет, в каком порядке должна происходить кумуляция.
person Lukas Eder    schedule 17.09.2018
comment
Приятно видеть, что все больше и больше СУБД поддерживают такие конструкции. Следующая большая вещь, которую я хотел бы реализовать, - это MATCH_RECOGNIZE - кстати. ваш блог действительно отличный (10 трюков SQL, которые вы не считали возможными вдохновили меня :) - person Lukasz Szozda; 01.01.2019
comment
@LukaszSzozda: Спасибо за хорошие слова. Я не уверен, станет ли MATCH_RECOGNIZE приоритетом MySQL в ближайшее время, но мало ли! - person Lukas Eder; 02.01.2019
comment
почему мы не получаем кумулятивного, если не добавляем заказ по пункту? даже если я не отдаю приказ, не следует ли добавлять каждую строку к новой сумме? - person haneulkim; 25.05.2020
comment
@Ambleu: Если вы используете ORDER BY, то RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW является неявным (семантика совокупной суммы). Если вы не используете ORDER BY, то RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING является неявным (агрегирование всей семантики раздела). На самом деле это довольно полезное поведение по умолчанию. - person Lukas Eder; 26.05.2020

вот и ваша кульминация ...

select f1.id, f1.day, f1.hr, f1.amount, sum(f2.amount) as culminative_total from foo f1
 inner join foo f2 on (f1.day = f2.day and f1.id=f2.id)
 where f2.hr <= f1.hr
 group by f1.id, f1.day, f1.hour;
person SQL.injection    schedule 15.07.2013
comment
Это не возвращает указанный набор результатов, если есть дубликаты (id, day, hr), как в пятой и шестой строках в примере OP. В случае OP foo - это не простая таблица, а скорее запрос, который включает несколько таблиц. Этот запрос нужно будет указать дважды (вместо foo), что означает, что MySQL материализует этот запрос дважды. К сожалению, MySQL еще не поддерживает общие табличные выражения (CTE), что было бы одним из способов избежать дублирования подзапроса. - person spencer7593; 16.07.2013
comment
действительно очень резкое :) не заметил этой детали в исходных данных - person SQL.injection; 16.07.2013
comment
Если бы у нас была гарантия уникальности (для столбцов, которые нам нужны для соединения), то подход полусоединения, используемый запросом в этом ответе, вернул бы указанный результат. - person spencer7593; 16.07.2013
comment
Да, я знаю. Я не заметил дубликатов, когда проводил первоначальную проверку проблемы ... - person SQL.injection; 16.07.2013