DISTINCT ON.Да, он Postgres-specific. Нет, он не из SQL Standard. Зато для этой задачи он читается почти идеально.
Есть таблица заказов:
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id int,
status text,
created_at timestamptz
);
Старый классический вариант:
SELECT o.*
FROM orders o
INNER JOIN (
SELECT customer_id, MAX(created_at) AS latest
FROM orders
GROUP BY customer_id
) latest ON o.customer_id = latest.customer_id
AND o.created_at = latest.latest;
Работает, но есть нюансы.
Таблица читается два раза: сначала ищем
MAX(created_at) по каждому customer_id, потом джойнимся обратно за полной строкой.И ещё хуже: если у одного клиента два заказа с одинаковым
created_at, запрос может вернуть две строки. Нужен tie-breaker.В Postgres проще так:
SELECT DISTINCT ON (customer_id)
customer_id, status, created_at
FROM orders
ORDER BY customer_id, created_at DESC;
DISTINCT ON (customer_id) оставляет первую строку для каждого клиента после сортировки.То есть
ORDER BY customer_id, created_at DESC сначала кладёт свежий заказ клиента наверх, а DISTINCT ON забирает его.Чтобы это реально летало, добавляем индекс:
CREATE INDEX ON orders (customer_id, created_at DESC);
Теперь Postgres может идти по уже отсортированному индексу и не делать отдельную сортировку.
На небольшом тесте с 100k строк и 20k
customer_id получилось так:Без индекса:
•
MAX + self-join: 32.92ms•
DISTINCT ON: 34.75ms•
ROW_NUMBER(): 38.68msС индексом
(customer_id, created_at DESC):•
DISTINCT ON: 15.99ms•
ROW_NUMBER(): 25.36ms•
MAX + self-join: 38.01msГлавный вывод: для “одна последняя строка на группу” в Postgres я первым беру
DISTINCT ON.Если нужно не одну строку, а top N строк на группу, тогда уже
ROW_NUMBER():SELECT customer_id, status, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at DESC
) AS rn
FROM orders
) ranked
WHERE rn = 1;
DISTINCT ON — для простого “последний на группу”.Window functions — когда нужна гибкость.
👉 @SQLPortal
