Отчёт показывал долю заказов с датой отправки как COUNT(shipped_at) * 1.0 / COUNT(*). Какое-то время это было верно. Потом в ту же таблицу начали попадать ожидающие заказы с shipped_at в NULL, и доля упала. В запросе не поменялось ничего.
COUNT(*) считает входные строки. COUNT(shipped_at) считает строки, где это значение не NULL. Доля всегда была «строки с датой среди всех строк», и под ней сдвинулось само множество строк.
Проверить нужно три вещи, и они не равноценны.
- Назвать знаменатель.
COUNT(*) FILTER (WHERE status <> 'pending')говорит, о каких строках эта доля. - Следить за делением. В PostgreSQL
countвозвращаетbigint, поэтомуCOUNT(a) / COUNT(*)обрезает до нуля любую долю меньше единицы. Отсюда* 1.0. - Спросить, что значит NULL в этом столбце. «Ещё не отправлен» и «дата отправки неизвестна» - разные факты, и подсчёт их не различит.
Хватит двух отправленных заказов и двух ожидающих, чтобы два варианта разошлись у всех на виду.
SELECT COUNT(shipped_at) * 1.0 / COUNT(*) AS naive,
COUNT(shipped_at) * 1.0
/ COUNT(*) FILTER (WHERE status <> 'pending')
AS shipped_share
FROM orders;
-- naive 0.5, shipped_share 1.0
В R есть такое же разделение. length(x) считает элементы NA, sum(!is.na(x)) их не считает. Когда отчёт собирается в R, я печатаю sum(is.na(x)) рядом с долей, чтобы читатель видел, сколько строк пропустил числитель.