Почему при использовании фильтра в PostgreSQL возвращается больше записей, чем ожидалось?

Добрый день! У меня есть таблица appointments в базе данных PostgreSQL, содержащая информацию о посещениях клиентов. Я пытаюсь составить запрос для нахождения всех новых клиентов, которые пришли в этом году. Вот запрос, который я использую:

<pre><code class="sql">SELECT apps.customer_id  FROM appointments apps 
	WHERE apps.in_trash is not true and apps.status = 4 and apps.filial_id = 1
GROUP BY apps.customer_id
HAVING MIN(apps.time_start) BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'</code></pre>

Однако, когда я пытаюсь ограничить выборку клиентов только теми, у которых были сформированы счета, мой запрос выглядит так:

<pre><code class="sql">SELECT apps.customer_id  FROM appointments apps 
	WHERE apps.in_trash is not true and apps.status = 4 and apps.filial_id = 1
		and apps.invoice_id is not null
GROUP BY apps.customer_id
HAVING MIN(apps.time_start) BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'</code></pre>

При этом я получаю выборку, которая является <b>большей</b> по количеству записей, чем в предыдущем запросе. Можете подсказать, в чем может заключаться моя ошибка?
  • 15 декабря 2024 г. 12:43
Ответы на вопрос 4
Ваша проблема с тем, что второй запрос возвращает больше записей, чем первый, связана с тем, как выполняется фильтрация данных в сочетании с агрегатными функциями и условиями.

В первом запросе вы используете `HAVING MIN(apps.time_start) BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'`, что подразумевает, что вы ищете клиентов, у которых время их первого посещения (минимальное значение `time_start`) находится в указанном диапазоне. При этом вы отфильтровали по статусу, филиалу и тому, что `in_trash` не является истинным.

Во втором запросе вы добавляете дополнительное условие `AND apps.invoice_id IS NOT NULL`. Однако это условие применяется до агрегации, т.е. фильтрует строки на уровне отдельных записей перед группировкой. В результате, если клиент имел хотя бы один визит с `invoice_id` не равным `NULL`, он будет включен в результирующий набор, даже если его самый ранний визит (который показан через `MIN(apps.time_start)`) не соответствует времени в заданном диапазоне.

Таким образом, объем возвращаемых записей может резко измениться, если определенные клиенты имели визиты как с действительными, так и с недействительными счетами. Чтобы достичь желаемого результата, вы можете использовать `HAVING` для фильтрации после группировки, или встроить условие для `invoice_id` прямо в агрегацию.

Вот пример того, как можно изменить ваш запрос:

```sql
SELECT apps.customer_id  
FROM appointments apps 
WHERE apps.in_trash IS NOT TRUE AND apps.status = 4 AND apps.filial_id = 1
GROUP BY apps.customer_id
HAVING MIN(CASE WHEN apps.invoice_id IS NOT NULL THEN apps.time_start END) BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'
```

Используя `CASE` в `HAVING`, вы отфильтровываете минимальное значение `time_start` только для записей с действительными счетами. Теперь этот запрос должен вернуть только тех клиентов, которые имели счета и первые визиты в этом году.
Смотрите свои данные. Совершенно логично, что если у вас есть клиент №5 с такими данными: 
insert into appointments (customer_id, in_trash, status, filial_id, invoice_id, time_start) values 
(5, false, 4, 1, null, '2023-12-23'),
(5, false, 4, 1, 123, '2024-01-13')

то он не попадёт в результат первого запроса, но попадёт в результат второго. И оба ответа верны, а вот какой из них хочет знать бизнес - уточнять надо у бизнеса.
Потому что time_start у некоторых, пришедших не в этом году, имеет invoice_id is null по старым записям. Раньше эти записи попадали в предыдущие года и давали min(time_start) в прошлом, а после дополнительного условия time_start у всех отобранных попадает уже в текущий год и min(time_start) попадает в условие отбора.
До группировки в первом случае в исходную выборку попадают строки, в которых invoice_id == NULL, среди которых есть такие, что time_start < '2024-01-01 00:00:00'. Соответственно, customer_id из них отсеивается в HAVING. 
Пример:
|-------------+------------+---------------------|
| customer_id | invoice_id |      time_start     |
|-------------+------------+---------------------|
|         1   |     null   | 2023-12-31 00:00:00 |
|         1   |        1   | 2024-02-01 00:00:00 |
|-------------+------------+---------------------|

В первом случае в предварительную выборку попадают обе строки и, поскольку MIN(time_start) == '2023-12-31 00:00:00' < '2024-01-01 00:00:00', то customer_id == 1 в финальную выборку не попадает.
Во втором случае в предварительную выборку попадает только вторая строка и, поскольку MIN(time_start) == '2024-02-01 00:00:00' > '2024-01-01 00:00:00', то customer_id == 1 попадает в финальную выборку.
Похожие вопросы