Как отсортировать дни рождения по ближайшей дате в PostgreSQL
Задача вывести список имён и дат рождения, отсортировав их так, чтобы сверху оказался ближайший день рождения, часто возникает при разработке CRM, напоминаний или HR-систем. В MySQL для этого можно использовать кортежное сравнение, но в PostgreSQL подобный синтаксис работает иначе. Разберём, как правильно реализовать сортировку дней рождения в PostgreSQL, чтобы результат был точным и предсказуемым.
Проблема с кортежным сравнением в PostgreSQL
Исходный запрос из MySQL использует конструкцию ORDER BY (m,d) < (MONTH(CURDATE()), DAY(CURDATE())). В PostgreSQL прямой аналог ORDER BY (EXTRACT(DAY FROM birthday), EXTRACT(MONTH FROM birthday)) < (EXTRACT(DAY FROM CURRENT_DATE), EXTRACT(MONTH FROM CURRENT_DATE)) не даёт нужной сортировки, потому что PostgreSQL сравнивает кортежи по правилам лексикографического порядка, а не как булево выражение. В результате записи с днём и месяцем, меньшими текущей даты, не выводятся первыми, как ожидается.
Правильный запрос для PostgreSQL
Чтобы получить корректную сортировку ближайших дней рождения, нужно использовать CASE или OVER с оконной функцией. Вот рабочий вариант с CASE:
SELECT name, birthday
FROM users
ORDER BY
CASE
WHEN (EXTRACT(MONTH FROM birthday), EXTRACT(DAY FROM birthday)) >= (EXTRACT(MONTH FROM CURRENT_DATE), EXTRACT(DAY FROM CURRENT_DATE))
THEN 0 ELSE 1
END,
EXTRACT(MONTH FROM birthday),
EXTRACT(DAY FROM birthday);Этот запрос сначала выводит дни рождения, которые ещё не наступили в текущем году (начиная с сегодняшней даты), а затем - уже прошедшие, сохраняя внутри каждого блока сортировку по месяцу и дню.
Альтернативный подход с оконной функцией
Можно использовать оконную функцию для вычисления следующей даты дня рождения и сортировки по ней:
SELECT name, birthday
FROM (
SELECT *,
CASE
WHEN (EXTRACT(MONTH FROM birthday), EXTRACT(DAY FROM birthday)) >= (EXTRACT(MONTH FROM CURRENT_DATE), EXTRACT(DAY FROM CURRENT_DATE))
THEN make_date(EXTRACT(YEAR FROM CURRENT_DATE)::int, EXTRACT(MONTH FROM birthday)::int, EXTRACT(DAY FROM birthday)::int)
ELSE make_date((EXTRACT(YEAR FROM CURRENT_DATE) + 1)::int, EXTRACT(MONTH FROM birthday)::int, EXTRACT(DAY FROM birthday)::int)
END AS next_birthday
FROM users
) sub
ORDER BY next_birthday;Этот метод вычисляет фактическую дату следующего дня рождения (в текущем или следующем году) и сортирует по ней. Он более нагляден и позволяет легко добавить фильтрацию.
Что важно учитывать
- Високосные годы: для 29 февраля используйте
make_dateс проверкой, иначе в невисокосные годы будет ошибка. Можно заменить на 28 февраля. - Производительность: индексы на вычисляемые поля не работают, поэтому для больших таблиц рассмотрите материализованные представления или триггеры.
- Сравнение по времени: если в таблице есть время рождения, используйте
birthday::dateдля игнорирования времени.
Заключение
Кортежное сравнение из MySQL не работает в PostgreSQL как ожидается. Используйте CASE или оконные функции для правильной сортировки дней рождения по ближайшей дате. Выбирайте подход в зависимости от сложности данных и требований к производительности.