Эффективное вычисление скользящих сумм в PostgreSQL - PullRequest
0 голосов
/ 11 января 2019

Предположим, у меня есть набор транзакций (покупок) с датами для набора клиентов, я хочу рассчитать скользящую x дневную сумму покупки и количество покупок по клиенту в том же окне. Я заставил его работать с помощью оконной функции, но я должен указать даты, когда клиент не совершал никаких покупок. При этом я использую декартово произведение. Существует ли более эффективный подход, чтобы он был более масштабируемым при увеличении количества клиентов и временного окна?

Редактировать: Как отмечено в комментариях, я нахожусь на PostgreSQL v9.3.

Вот пример данных (обратите внимание, что у некоторых клиентов может быть 0, 1 или несколько покупок в определенный день):

| id | cust_id |   txn_date | amount |
|----|---------|------------|--------|
|  1 |     123 | 2017-08-17 |     10 |
|  2 |     123 | 2017-08-17 |      5 |
|  3 |     123 | 2017-08-18 |      5 |
|  4 |     123 | 2017-08-20 |     50 |
|  5 |     123 | 2017-08-21 |    100 |
|  6 |     456 | 2017-08-01 |      5 |
|  7 |     456 | 2017-08-01 |      5 |
|  8 |     456 | 2017-08-01 |      5 |
|  9 |     456 | 2017-08-30 |      5 |
| 10 |     456 | 2017-08-01 |   1000 |
| 11 |     789 | 2017-08-15 |   1000 |
| 12 |     789 | 2017-08-30 |   1000 |

А вот желаемый вывод:

| cust_id |   txn_date | sum_dly_txns | tot_txns_7d | cnt_txns_7d |
|---------|------------|--------------|-------------|-------------|
|     123 | 2017-08-17 |           15 |          15 |           2 |
|     123 | 2017-08-18 |            5 |          20 |           3 |
|     123 | 2017-08-20 |           50 |          70 |           4 |
|     123 | 2017-08-21 |          100 |         170 |           5 |
|     456 | 2017-08-01 |         1015 |        1015 |           4 |
|     456 | 2017-08-30 |            5 |           5 |           1 |
|     789 | 2017-08-15 |         1000 |        1000 |           1 |
|     789 | 2017-08-30 |         1000 |        1000 |           1 |

Вот SQL, который выдает итоги по желанию:

SELECT *
FROM (
    -- One row per day per user
    WITH daily_txns AS (
        SELECT
             t.cust_id
            ,t.txn_date AS txn_date
            ,SUM(t.amount) AS sum_dly_txns
            ,COUNT(t.id) AS cnt_dly_txns
        FROM transactions t
        GROUP BY t.cust_id, txn_date
    ),
    -- Every possible transaction date for every user
    dummydates AS (
        SELECT txn_date, uids.cust_id
        FROM (
            SELECT generate_series(
                 timestamp '2017-08-01'
                ,timestamp '2017-08-30'
                ,interval '1 day')::date
            ) d(txn_date)
        CROSS JOIN (SELECT DISTINCT cust_id FROM daily_txns) uids
    ),
    txns_dummied AS (
        SELECT 
             d.cust_id
            ,d.txn_date
            ,COALESCE(sum_dly_txns,0) AS sum_dly_txns
            ,COALESCE(cnt_dly_txns,0) AS cnt_dly_txns
        FROM dummydates d
        LEFT JOIN daily_txns dx
          ON d.txn_date = dx.txn_date
          AND d.cust_id = dx.cust_id
        ORDER BY d.txn_date, d.cust_id
    )

    SELECT
         cust_id
        ,txn_date
        ,sum_dly_txns
        ,SUM(COALESCE(sum_dly_txns,0)) OVER w AS tot_txns_7d
        ,SUM(cnt_dly_txns) OVER w AS cnt_txns_7d
    FROM txns_dummied
    WINDOW w AS (
        PARTITION BY cust_id
        ORDER BY txn_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW -- 7d moving window
        )
    ORDER BY cust_id, txn_date
    ) xfers
WHERE sum_dly_txns > 0 -- Omit dates with no transactions
;

SQL Fiddle

1 Ответ

0 голосов
/ 11 января 2019

Вместо ROWS BETWEEN 6 PRECEDING AND CURRENT ROW Вы хотели написать RANGE '6 days' PRECEEDING?

Это должно быть то, что вы ищете:

SELECT DISTINCT
       cust_id
      ,txn_date
      ,SUM(amount) OVER (PARTITION BY cust_id, txn_date) sum_dly_txns
      ,SUM(amount) OVER (PARTITION BY cust_id ORDER BY txn_date RANGE '6 days' PRECEDING)
      ,COUNT(*) OVER (PARTITION BY cust_id ORDER BY txn_date RANGE '6 days' PRECEDING)
from transactions
ORDER BY cust_id, txn_date

Редактировать : Поскольку вы используете старую версию (я тестировал ту, что выше, на моем postgresql 11), вышеприведенный пункт не будет иметь особого смысла, поэтому вам потребуется старомодный SQL (то есть без оконных функций).
Это немного менее эффективно, но делает честно.

WITH daily_txns AS (
        SELECT
        t.cust_id
        ,t.txn_date AS txn_date
        ,SUM(t.amount) AS sum_dly_txns
        ,COUNT(t.id) AS cnt_dly_txns
        FROM transactions t
        GROUP BY t.cust_id, txn_date
)
SELECT t1.cust_id, t1.txn_date, t1.sum_dly_txns, SUM(t2.sum_dly_txns), SUM(t2.cnt_dly_txns)
from daily_txns t1
join daily_txns t2 ON t1.cust_id = t2.cust_id and t2.txn_date BETWEEN t1.txn_date - 7 and t1.txn_date
group by t1.cust_id, t1.txn_date, t1.sum_dly_txns
order by t1.cust_id, t1.txn_date
...