Как отсортировать дни рождения по ближайшей дате в 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 или оконные функции для правильной сортировки дней рождения по ближайшей дате. Выбирайте подход в зависимости от сложности данных и требований к производительности.

    Часто задаваемые вопросы