TGViewer
DataДжунгли🌳 DataДжунгли🌳 @data_jungle · 286 subscribers
Post #117 210
🗄️ Последняя транзакция каждого клиента: одна задача с собеса - три решения #SQLWednesday - и не важно что сегодня не среда.

Продолжаю разбор задач с собеседований, под новым углом. Сегодня классика, на которой сыпется половина кандидатов.

❓ Задача
Есть таблица транзакций. Нужно вернуть последнюю по времени транзакцию каждого клиента(успешную или нет не важно) - целиком, со всеми полями, а не только max(время). top-1 на группу (greatest-N-per-group).

табличка транзакций

CREATE TABLE Transactions (
id BIGINT PRIMARY KEY, -- id транзакции
customer_id BIGINT, -- клиент
amount DECIMAL(12,2), -- сумма
status VARCHAR, -- статус
created_at TIMESTAMP -- время транзакции
);

Пример данных

id | customer | amount | status | created_at
101 | 1 | 50.00 | done | 2026-07-01 10:00
102 | 1 | 75.00 | done | 2026-07-10 14:30
201 | 2 | 20.00 | done | 2026-07-05 09:00
202 | 2 | 30.00 | failed | 2026-07-15 09:00
203 | 2 | 30.00 | done | 2026-07-15 09:00
301 | 3 | 100.00 | done | 2026-07-12 12:00

Итак какие у нас варианты решений - да их больше чем 1:

1) Коррелированный подзапрос. Просто в лоб и напрашивается само.

SELECT t.*
FROM Transactions t
WHERE t.created_at = (
SELECT MAX(t2.created_at)
FROM Transactions t2
WHERE t2.customer_id = t.customer_id
);


Да работает, но:
• На каждую строку лезет в подзапрос за MAX (оптимизатор иногда перепишет, иногда нет);
• БАГ: у customer_id=2 две транзакции с равным created_at → вернутся ОБЕ. Должно быть 3 строки а будет 4. Придется думать над доп фильтром;

2) Оконная функция ROW_NUMBER() - конечно же.

WITH ranked AS (
SELECT t.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM Transactions t
)
SELECT * FROM ranked WHERE rn = 1;


Почему это стандарт для подобных задач?:
• один проход по таблице;
• тай-брейк вторым ключом (id DESC) - детерминированно одна строка, бага с дублями нет;

⚡️ ROW_NUMBER, а не RANK — RANK при равных значениях вернёт дубли.

Но есть ли еще что то ? Еще более быстро и красиво?
Знакомьтес с мистером LATERALL и CROSS APPLY

3) LATERAL (Postgres) / CROSS APPLY (Sql Server)


— Postgres
SELECT t.*
FROM Customers c
CROSS JOIN LATERAL (
SELECT *
FROM Transactions t
WHERE t.customer_id = c.customer_id
ORDER BY t.created_at DESC, t.id DESC
LIMIT 1
) t;

— Sql Server
SELECT t.*
FROM Customers c
CROSS APPLY (
SELECT TOP 1 *
FROM Transactions t
WHERE t.customer_id = c.customer_id
ORDER BY t.created_at DESC, t.id DESC
) t;

🟢Фишка этого решения - при индексе (customer_id, created_at DESC) движок делает index seek на каждую группу - это почти O(числа клиентов), а не сортировка всей таблицы.

CREATE INDEX ix_txn ON Transactions (customer_id, created_at DESC, id DESC);

маловероятно что в продакшн таблица транзакций не будет иметь индексов.

⚖️ Что выбрать - через призму архитектуры
• Классическая OLTP-СУБД с индексом (Postgres / SQL Server) → LATERAL / APPLY обычно быстрее: seek по индексу вместо полной сортировки.
• Колоночный движок / lakehouse (BigQuery, Snowflake, Spark, Iceberg + Trino) → индексов нет, зато дёшево сканировать и сортировать колонки → выигрывает ROW_NUMBER, и он отлично параллелится.
• Коррелированный подзапрос → ответ уровня а как ещё можно - не более :).

🔴 Ловушки, на которых валят
• RANK вместо ROW_NUMBER = дубли при равных ключах.
• Нет тай-брейка (id) в ORDER BY → результат недетерминирован, на равном времени каждый раз может быть разная строка.
• NULL в created_at: при ORDER BY ... DESC в Postgres NULL уезжает наверх (NULLS FIRST) и притворяется «последним». Пиши NULLS LAST.
• Нужны и клиенты без транзакций? Подзапрос и оконка их не вернут — бери LEFT JOIN LATERAL.

#SQL #window_functions #ETL #DataJungle
  • 🔥 3
More from @data_jungle
  1. Sep 29, 2026Мы уже проводим не первое интервью кандидатов на работе и вот что я могу посоветовать вам…
  2. Sep 11, 2026Неужели литкод это начало конца ? Google сменил стратегию интервью:) Что думаете обсудим ?
  3. Sep 1, 2026Всем привет 👋 Очень советую посмотреть это видео на ютубчике. Всегда с большим интересом…
  4. Aug 7, 2026🔍 Как читать план запроса: EXPLAIN, оценки и почему оптимизатор врёт #SQLWednesday В пост…
  5. Aug 3, 2026🧱 Parquet под капотом: почему «размер файла имеет значение» Мы шли сверху вниз: разделили…
  6. Jul 31, 2026🏝️ Gaps & Islands: как из потока событий собрать сессии одним оконным трюком #SQLWednesda…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →