Как получить первую запись по каждому customer_id с сортировкой в SQL

    Задача выборки первой записи для каждого клиента (customer_id) с учётом сортировки по нескольким полям - одна из частых в аналитике и разработке. Простой GROUP BY не подходит, так как он теряет контроль над порядком строк внутри группы. Рассмотрим рабочие решения на основе оконных функций и подзапросов.

    Почему GROUP BY не работает

    Запрос вида SELECT * FROM stream GROUP BY customer_id ORDER BY unit_importance, updated_at не даёт нужного результата. GROUP BY агрегирует строки, и ORDER BY применяется уже после группировки, а не внутри неё. В итоге вы получаете одну произвольную запись на клиента, а не первую по заданным критериям.

    Решение с оконной функцией ROW_NUMBER()

    Самый эффективный и читаемый способ - использовать оконную функцию ROW_NUMBER(). Она нумерует строки внутри каждой группы (партиции) в указанном порядке. Затем вы выбираете только те строки, где номер равен 1.

    WITH ranked AS (  SELECT *,    ROW_NUMBER() OVER (      PARTITION BY customer_id      ORDER BY unit_importance, updated_at    ) AS rn  FROM stream)SELECT * FROM ranked WHERE rn = 1;

    В этом запросе:

    • PARTITION BY customer_id - делит данные на группы по клиентам.
    • ORDER BY unit_importance, updated_at - задаёт порядок сортировки внутри каждой группы.
    • ROW_NUMBER() - присваивает уникальный номер каждой строке в группе, начиная с 1.
    • Внешний запрос отфильтровывает только первые записи (rn = 1).

    Альтернатива: подзапрос с корреляцией

    Если оконные функции недоступны (например, в старых версиях MySQL), можно использовать коррелированный подзапрос. Однако этот метод менее производителен на больших таблицах.

    SELECT s.*FROM stream sWHERE (s.unit_importance, s.updated_at) = (  SELECT MIN(t.unit_importance), MIN(t.updated_at)  FROM stream t  WHERE t.customer_id = s.customer_id  GROUP BY t.customer_id);

    Здесь для каждой строки внешнего запроса подзапрос находит минимальные значения unit_importance и updated_at для того же клиента. Совпадение всех полей гарантирует, что мы берём именно первую запись. Недостаток - сложность с дубликатами и низкая скорость.

    Что делать при одинаковых значениях сортировки

    Если у нескольких записей одного клиента совпадают unit_importance и updated_at, ROW_NUMBER() всё равно присвоит им разные номера (порядок может быть непредсказуем). Чтобы гарантировать детерминированный результат, добавьте в ORDER BY дополнительное уникальное поле, например, id.

    ROW_NUMBER() OVER (  PARTITION BY customer_id  ORDER BY unit_importance, updated_at, id)

    Особенности для разных СУБД

    Оконные функции поддерживаются в PostgreSQL, MySQL 8+, SQL Server, Oracle, SQLite 3.25+. В более старых версиях MySQL (5.x) используйте подзапрос с JOIN или переменные. Для больших объёмов данных предпочтительнее ROW_NUMBER() - он выполняется за один проход по таблице.

    Заключение

    Чтобы корректно получить первую запись по каждому customer_id с сортировкой по unit_importance и updated_at, используйте оконную функцию ROW_NUMBER() с PARTITION BY. Это надёжный, быстрый и стандартный способ. Избегайте GROUP BY без агрегации - он не даёт контроля над порядком строк.

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