Условие
По таблице коммуникаций comms(ow_id, sent_date, channel_nm, partner_id, sent, delivered, opened, clicked, cell_pk) собрать в одном запросе по месяцам:
- кол-во отправленных / доставленных / открытых / с переходом коммуникаций;
- среднее число отправленных коммуникаций на клиента;
- среднее число партнёров, по которым клиент получил коммуникацию;
- среднее число каналов, по которым клиент получил коммуникацию.
Решение
Подход
Разбиваем на два уровня агрегации: сначала на уровне (месяц, клиент) считаем количества и distinct’ы, потом на уровне месяца берём суммы и средние.
Реализация
WITH per_client_month AS (
SELECT
DATE_TRUNC('month', sent_date) AS m,
ow_id,
SUM(sent) AS sent_cnt,
SUM(delivered) AS delivered_cnt,
SUM(opened) AS opened_cnt,
SUM(clicked) AS clicked_cnt,
COUNT(DISTINCT partner_id) AS partners_cnt,
COUNT(DISTINCT channel_nm) AS channels_cnt
FROM comms
GROUP BY DATE_TRUNC('month', sent_date), ow_id
)
SELECT
m AS month,
SUM(sent_cnt) AS sent_total,
SUM(delivered_cnt) AS delivered_total,
SUM(opened_cnt) AS opened_total,
SUM(clicked_cnt) AS clicked_total,
AVG(sent_cnt)::numeric(10,2) AS avg_sent_per_client,
AVG(partners_cnt)::numeric(10,2) AS avg_partners_per_client,
AVG(channels_cnt)::numeric(10,2) AS avg_channels_per_client
FROM per_client_month
GROUP BY m
ORDER BY m;Альтернатива одним уровнем (без CTE)
Можно избежать вложенного агрегирования через COUNT(DISTINCT) FILTER и условные суммы, но среднее «на клиента» всё равно требует первого уровня группировки по ow_id. CTE прозрачнее.
Подводные камни
- Среднее на отправленную коммуникацию vs на клиента. «Среднее количество отправленных на клиента» = total_sent / unique_clients_who_got_anything. Если посчитать
AVG(sent)напрямую по таблице — получим среднее на строку (на коммуникацию), а это бессмысленная метрика. COUNT(DISTINCT)дороже, чемCOUNT. На большом объёме лучше предагрегировать в CTE.- Клиенты, которым ничего не отправили в месяце, в среднее не попадают. Если задача предполагает «среднее по всей аудитории», нужен LEFT JOIN со справочником клиентов и
COALESCE(... , 0). - Дубли коммуникаций. Если
cell_pkуникален — ОК. Если в таблице бывают дубли —SUM(sent)посчитает их повторно. Возможна предагрегация поcell_pk. - Часовой пояс / сдвиг месяца.
DATE_TRUNC('month', sent_date)зависит от локали БД.
Эталонный ответ
Двухуровневая агрегация: сначала per (месяц, клиент) с SUM и COUNT(DISTINCT), потом per месяц с SUM и AVG. Все требуемые метрики в одном итоговом SELECT.