Как получить первую запись по каждому 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 без агрегации - он не даёт контроля над порядком строк.