Скользящая сумма / количество / среднее за интервал дат

20

В базе данных транзакций, охватывающей тысячи субъектов в течение 18 месяцев, я хотел бы выполнить запрос для группировки каждого возможного 30-дневного периода по entity_idсумме их сумм транзакций и количеству их транзакций за этот 30-дневный период, и вернуть данные таким образом, что я могу затем запросить. После большого тестирования этот код выполняет большую часть того, что я хочу:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

И я буду использовать в более крупном запросе, структурированном что-то вроде:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

Случай, который не покрывается этим запросом, - это когда количество транзакций будет составлять несколько месяцев, но все равно будет в течение 30 дней друг от друга. Возможен ли этот тип запроса с Postgres? Если это так, я приветствую любой вклад. Многие другие темы обсуждают " бегущие " агрегаты, а не прокатку .

Обновить

CREATE TABLEСкрипт:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Пример данных можно найти здесь . Я использую PostgreSQL 9.1.16.

Идеальный результат будет включать SUM(amount)и COUNT()всех транзакций в течение 30-дневного периода. Посмотрите это изображение, например:

Пример строк, которые в идеале должны быть включены в «набор», но не потому, что мой набор статичен по месяцам.

Зеленая подсветка даты указывает на то, что включено в мой запрос. Желтая подсветка строки указывает на записи, что я хотел бы стать частью набора.

Предыдущее чтение:

tufelkinder
источник
1
К every possible 30-day period by entity_idвам означает , что период может начать любой день, так что 365 возможных периодов в (не високосный) год? Или вы хотите рассматривать дни с фактической транзакцией как начало периода отдельно для любого entity_id ? В любом случае, пожалуйста, укажите определение таблицы, версию Postgres, некоторые примеры данных и ожидаемый результат для образца.
Эрвин Брандштеттер,
Теоретически я имел в виду любой день, но на практике нет необходимости рассматривать дни, когда нет транзакций. Я разместил пример данных и определение таблицы.
Туфелькиндер
Таким образом, вы хотите накапливать одинаковые строки entity_idв 30-дневном окне, начиная с каждой фактической транзакции. Может ли быть несколько транзакций для одного (trans_date, entity_id)и того же или эта комбинация определена уникально? В определении вашей таблицы нет UNIQUEограничения или ограничения PK, но ограничения, по-видимому, отсутствуют ...
Эрвин Брандштеттер,
Единственное ограничение на idпервичный ключ. Может быть несколько транзакций на объект в день.
tufelkinder
О распределении данных: есть ли записи (на entity_id) в течение большинства дней?
Эрвин Брандштеттер

Ответы:

26

Запрос у вас есть

Вы можете упростить запрос, используя WINDOWпредложение, но это только сокращает синтаксис, а не изменяет план запроса.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • Кроме того, используя немного быстрее count(*), так как idэто определено NOT NULL?
  • И вам не нужно, ORDER BY entity_idтак как вы ужеPARTITION BY entity_id

Вы можете упростить и дальше:
не добавляйте ORDER BYв определение окна вообще, это не относится к вашему запросу. Тогда вам не нужно определять пользовательскую рамку окна, либо:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Проще, быстрее, но все же просто лучшая версия того, что у вас есть , со статичными месяцами.

Запрос, который вы можете захотеть

... не четко определено, поэтому я буду опираться на эти предположения:

Подсчитывать транзакции и суммы за каждый 30-дневный период в пределах первой и последней транзакции любого entity_id. Исключите начальные и конечные периоды без активности, но включите все возможные 30-дневные периоды в пределах этих внешних границ.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

Здесь перечислены все 30-дневные периоды для каждого entity_idс вашими агрегатами и с trans_dateпервым днем ​​(вкл.) Периода. Чтобы получить значения для каждой отдельной строки, присоединитесь к базовой таблице еще раз ...

Основная сложность та же, что и здесь:

Определение фрейма окна не может зависеть от значений текущей строки.

А точнее позвоните generate_series()с timestampвводом:

Запрос, который вы на самом деле хотите

После обновления вопроса и обсуждения:
накапливайте одинаковые строки entity_idв 30-дневном окне, начиная с каждой фактической транзакции.

Поскольку ваши данные распределены редко, более эффективным будет запуск самосоединения с условием диапазона , тем более что Postgres 9.1 пока не имеет LATERALобъединений:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL Fiddle.

Скользящее окно может иметь смысл (в отношении производительности) только для данных в течение большинства дней.

Это не объединяет дубликаты (trans_date, entity_id)за день, но все строки одного и того же дня всегда включаются в 30-дневное окно.

Для большой таблицы такой индекс покрытия может очень помочь:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

Последний столбец amountполезен только в том случае, если вы получаете только сканирование по индексу. Остальное брось.

Но он все равно не будет использоваться, пока вы выбираете всю таблицу. Он будет поддерживать запросы для небольшого подмножества.

Эрвин Брандштеттер
источник
Это выглядит действительно хорошо, тестируя его на данных сейчас, и пытаясь понять все, что на самом деле делает ваш запрос ...
tufelkinder
@tufelkinder: добавлено решение для обновленного вопроса.
Эрвин Брандштеттер
Рассматривая это сейчас. Я заинтригован тем, что он работает в SQL Fiddle ... Когда я пытаюсь запустить его непосредственно на моей транзакции column "t0.amount" must appear in the GROUP BY clause...
db
@tufelkinder: я сократил контрольный пример до 100 строк. sqlfiddle ограничивает размер тестовых данных. Джейк (автор) сократил лимит лимита пару месяцев назад, чтобы сайт не так просто застопорился.
Эрвин Брандштеттер
1
Извините за задержку, необходимо проверить ее на полной базе данных. Ваш ответ был великолепно всесторонним и образовательным, как всегда. Спасибо!
tufelkinder