← Назад к тренажёру

📘 SQL — теория и рецепты запросов

Часть 1 — каждый оператор SQL по отдельности с объяснением. Часть 2 — 5 типовых рецептов, у каждого 5 вариаций с растущей сложностью. Часть 3 — один и тот же рецепт, решённый разными способами, с разбором плюсов/минусов. Все примеры завязаны на схему тренажёра: EMPLOYEES, DEPARTMENTS, SALARY, PAYMENTS, PROJECTS, EMP_PROJECTS.
Часть 1. Операторы по отдельности — SELECT / DISTINCT — WHERE и операторы сравнения — ORDER BY / LIMIT / OFFSET — Виды JOIN (INNER / LEFT / CROSS) — GROUP BY / HAVING / агрегаты — CASE WHEN — Подзапросы: IN, EXISTS, скалярные Часть 2. Рецепты 1. INNER JOIN — объединение связанных таблиц 2. LEFT JOIN + COALESCE — когда данных может не быть 3. GROUP BY + агрегаты — суммы, счётчики, средние 4. Подзапросы (subquery) — сравнение со сводным значением 5. Многотабличные JOIN (3–4+ таблицы) Часть 3. Один рецепт — разными способами — «Сотрудники без выплат»: 3 способа — «Сумма по каждому сотруднику»: 2 способа — «Оклад выше среднего»: 2 способа — «Сотрудник с максимальной суммой»: 3 способа

Часть 1. Операторы по отдельности

SELECT / DISTINCT

SELECT определяет, какие колонки (или выражения) попадут в результат. SELECT * берёт все колонки — удобно для разведки, но в реальных запросах лучше перечислять колонки явно: так запрос не сломается, если в таблицу добавят новую колонку, и читателю сразу видно, что именно вы забираете. DISTINCT убирает повторяющиеся строки результата.

SELECT DISTINCT city FROM EMPLOYEES;
-- уникальный список городов, без повторов

Можно применить DISTINCT только к одной колонке внутри агрегата — посчитать не количество строк, а количество уникальных значений:

SELECT COUNT(DISTINCT city) AS city_count FROM EMPLOYEES;
-- сколько РАЗНЫХ городов встречается

WHERE и операторы сравнения

WHERE фильтрует строки до любой группировки. Операторы: =, <>(!=), <, >, <=, >=, а также:

BETWEEN a AND b — включительно, между a и b
IN (список) — совпадает с одним из значений
LIKE 'шаблон' — текстовый поиск: % = любое количество символов, _ = ровно один символ
IS NULL / IS NOT NULL — проверка на отсутствие значения (сравнивать NULL через = нельзя — так и будет ошибочно нигде не сработает)
AND / OR / NOT — логическое объединение условий, скобки нужны при смешивании AND и OR

SELECT name FROM EMPLOYEES
WHERE city = 'Москва' AND (sex = 'W' OR birth_date < '1980-01-01');
-- москвички, ИЛИ москвичи любого пола родившиеся раньше 1980
SELECT name FROM EMPLOYEES WHERE name LIKE '_а%';
-- вторая буква имени — "а" (Маша: М-а-ша)
SELECT name FROM EMPLOYEES WHERE dept_id IS NULL;
-- сотрудники без отдела (Олег)

ORDER BY / LIMIT / OFFSET

ORDER BY сортирует итоговый результат — можно по нескольким колонкам сразу, у каждой свой порядок (ASC по умолчанию, DESC — по убыванию). LIMIT n оставляет только первые n строк, OFFSET m пропускает первые m — вместе это классическая пагинация.

SELECT name, city, birth_date FROM EMPLOYEES
ORDER BY city ASC, birth_date DESC;
-- сначала по городу А→Я, внутри города — от молодых к старшим
SELECT name FROM EMPLOYEES ORDER BY id LIMIT 2 OFFSET 2;
-- "вторая страница" по 2 строки (пропустили первые 2)

Виды JOIN

INNER JOIN (или просто JOIN) — только строки, у которых есть пара по обе стороны.
LEFT JOIN — все строки левой таблицы, для отсутствующих справа — NULL.
RIGHT JOIN — зеркально LEFT (в SQLite не поддерживается — переставьте таблицы местами и используйте LEFT).
CROSS JOIN — декартово произведение, каждая строка с каждой (без условия ON) — используется редко, в основном для генерации комбинаций.

Пример эмуляции RIGHT JOIN через LEFT (все отделы, даже без сотрудников):

SELECT d.name, e.name
FROM DEPARTMENTS d
LEFT JOIN EMPLOYEES e ON e.dept_id = d.id;
-- "правая" таблица (DEPARTMENTS) стала левой — и вот уже все отделы гарантированно есть
SELECT e.name, d.name FROM EMPLOYEES e, DEPARTMENTS d LIMIT 5;
-- CROSS JOIN (через запятую): 6 сотрудников × 3 отдела = 18 комбинаций, тут показаны первые 5

GROUP BY / HAVING / агрегатные функции

Агрегатные функции: COUNT(*) — число строк, SUM(x) — сумма, AVG(x) — среднее, MIN/MAX(x) — минимум/максимум. Без GROUP BY они считают одно число по всей таблице; с GROUP BY колонка — одно число на каждое уникальное значение колонки. Важное правило: в SELECT рядом с агрегатом можно указывать только те колонки, что перечислены в GROUP BY — иначе непонятно, какое из многих значений показывать. WHERE отсекает строки до группировки, HAVING — группы после (поэтому в HAVING разрешён SUM(), а в WHERE — нет: агрегата на момент WHERE ещё не существует).

SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name        -- группируем по сотруднику
HAVING SUM(p.amount) > 150;  -- оставляем только "крупных" получателей

CASE WHEN — условная колонка

Позволяет вычислить значение колонки по условию, прямо как if/else внутри SELECT. Полезно для перевода кодов в читаемый текст, категоризации, "бакетов".

SELECT name,
       CASE WHEN sex = 'M' THEN 'мужчина' ELSE 'женщина' END AS gender
FROM EMPLOYEES;

Подзапросы: IN, EXISTS, скалярные

Скалярный подзапрос(SELECT ...), возвращающий одно число/значение, можно сравнивать напрямую (>, = и т.д.). IN / NOT IN — сравнение со списком значений из подзапроса. EXISTS / NOT EXISTS — проверяет, есть ли вообще хоть одна подходящая строка, не важно какая — часто быстрее NOT IN и, в отличие от него, безопасен при NULL внутри подзапроса.

SELECT name FROM EMPLOYEES e
WHERE EXISTS (SELECT 1 FROM PAYMENTS p WHERE p.emp_id = e.id);
-- сотрудники, у которых есть хотя бы одна выплата
SELECT name FROM EMPLOYEES e
WHERE NOT EXISTS (SELECT 1 FROM PAYMENTS p WHERE p.emp_id = e.id);
-- сотрудники без единой выплаты — безопасный аналог NOT IN
⚠️ Ловушка: если в подзапросе для NOT IN хотя бы одно значение окажется NULL, весь NOT IN молча вернёт пустой результат (0 строк) для ВСЕХ строк — это известная особенность SQL. NOT EXISTS этой проблемы не имеет. Разбор — в Части 3 ниже.

Часть 2. Рецепты (типовые связки конструкций)

1.INNER JOIN — объединение связанных таблиц

INNER JOIN соединяет строки двух таблиц там, где условие ON выполняется. Если для строки слева нет пары справа — эта строка пропадает из результата целиком. Используйте, когда вам нужны только сущности, у которых точно есть связанная запись (например, только те сотрудники, у которых есть оклад).
2 таблицы · 0 доп. критериев

Простейшее соединение — сотрудник + его оклад.

SELECT e.name, s.amount
FROM EMPLOYEES e
JOIN SALARY s ON e.id = s.emp_id;
2 таблицы · 1 критерий (WHERE)

То же самое, но только для оклада больше 150.

SELECT e.name, s.amount
FROM EMPLOYEES e
JOIN SALARY s ON e.id = s.emp_id
WHERE s.amount > 150;
3 таблицы · 1 критерий

Сотрудник, его отдел и оклад — только по Москве.

SELECT e.name, d.name AS department, s.amount
FROM EMPLOYEES e
JOIN DEPARTMENTS d ON e.dept_id = d.id
JOIN SALARY s ON e.id = s.emp_id
WHERE e.city = 'Москва';
2 таблицы · 2 критерия + сортировка

Выплаты сотрудников за конкретный месяц, сумма выше порога, отсортировано по убыванию.

SELECT e.name, p.pay_date, p.amount
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
WHERE p.pay_date BETWEEN '2007-04-01' AND '2007-04-30'
  AND p.amount >= 100
ORDER BY p.amount DESC;
3 таблицы · 3 критерия

Сотрудники-разработчики (роль), которые из Москвы и родились после 1980 — с названием проекта.

SELECT e.name, pr.name AS project
FROM EMPLOYEES e
JOIN EMP_PROJECTS ep ON e.id = ep.emp_id
JOIN PROJECTS pr ON ep.project_id = pr.id
WHERE ep.role = 'developer'
  AND e.city = 'Москва'
  AND e.birth_date > '1980-01-01';

2.LEFT JOIN + COALESCE — когда данных может не быть

LEFT JOIN сохраняет все строки левой таблицы, даже если для них нет пары справа — вместо недостающих значений подставляется NULL. COALESCE(x, значение_по_умолчанию) заменяет NULL на удобное число/текст. Это главный инструмент для отчётов "покажи всех, даже тех, у кого чего-то нет".
2 таблицы · 0 критериев

Все сотрудники и их оклад; если оклада нет — 0.

SELECT e.name, COALESCE(s.amount, 0) AS salary
FROM EMPLOYEES e
LEFT JOIN SALARY s ON e.id = s.emp_id;
2 таблицы · 1 критерий (поиск "пустот")

Сотрудники, у которых оклада нет вообще (типичный паттерн — LEFT JOIN + IS NULL).

SELECT e.name
FROM EMPLOYEES e
LEFT JOIN SALARY s ON e.id = s.emp_id
WHERE s.id IS NULL;
3 таблицы · 0 критериев

Сотрудник, отдел (или "без отдела") и оклад (или 0) — три LEFT JOIN-подобных случая сразу.

SELECT e.name,
       COALESCE(d.name, 'без отдела') AS department,
       COALESCE(s.amount, 0) AS salary
FROM EMPLOYEES e
LEFT JOIN DEPARTMENTS d ON e.dept_id = d.id
LEFT JOIN SALARY s ON e.id = s.emp_id;
2 таблицы · агрегат внутри COALESCE

Сумма всех выплат по каждому сотруднику, 0 если выплат не было (COALESCE вокруг SUM).

SELECT e.name, COALESCE(SUM(p.amount), 0) AS total_paid
FROM EMPLOYEES e
LEFT JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name;
3 таблицы · 2 критерия

Сотрудники без отдела ИЛИ без оклада, с их городом — найти "проблемные" записи.

SELECT e.name, e.city,
       CASE WHEN d.id IS NULL THEN 'нет отдела' ELSE d.name END AS department,
       COALESCE(s.amount, 0) AS salary
FROM EMPLOYEES e
LEFT JOIN DEPARTMENTS d ON e.dept_id = d.id
LEFT JOIN SALARY s ON e.id = s.emp_id
WHERE d.id IS NULL OR s.id IS NULL;

3.GROUP BY + агрегаты — суммы, счётчики, средние

GROUP BY схлопывает строки в группы по указанным колонкам; агрегатные функции (SUM, COUNT, AVG, MAX, MIN) считают одно число на группу. WHERE фильтрует строки до группировки, HAVING — группы после агрегации (поэтому в HAVING можно использовать SUM(), а в WHERE — нет).
1 таблица · 0 критериев

Сколько выплат было в каждом месяце.

SELECT pay_date, COUNT(*) AS payments_count
FROM PAYMENTS
GROUP BY pay_date;
2 таблицы · 1 критерий (WHERE до группировки)

Суммарные выплаты по сотрудникам мужского пола.

SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
WHERE e.sex = 'M'
GROUP BY e.id, e.name;
2 таблицы · 1 критерий (HAVING после группировки)

Сотрудники, чья суммарная выплата больше 150 — фильтр по результату агрегации.

SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name
HAVING SUM(p.amount) > 150;
3 таблицы · WHERE + HAVING вместе

По отделу "Разработка": сотрудники со средней выплатой выше 100.

SELECT e.name, AVG(p.amount) AS avg_payment
FROM EMPLOYEES e
JOIN DEPARTMENTS d ON e.dept_id = d.id
JOIN PAYMENTS p ON e.id = p.emp_id
WHERE d.name = 'Разработка'
GROUP BY e.id, e.name
HAVING AVG(p.amount) > 100;
3 таблицы · группировка по колонке из другой таблицы

Суммарный бюджет проектов по каждому отделу, только там, где бюджет больше 4000.

SELECT d.name, SUM(pr.budget) AS total_budget
FROM DEPARTMENTS d
JOIN PROJECTS pr ON pr.dept_id = d.id
GROUP BY d.id, d.name
HAVING SUM(pr.budget) > 4000;

4.Подзапросы (subquery) — сравнение со сводным значением

Подзапрос — это SELECT внутри другого запроса. Скалярный подзапрос ((SELECT ...), возвращающий одно число) удобен для сравнения "больше среднего". IN / NOT IN с подзапросом — для проверки "входит / не входит в список". Подзапросы часто спасают от "задвоения" строк, которое возникает при JOIN с агрегатами из нескольких источников одновременно.
2 таблицы · скалярный подзапрос

Сотрудники, чей оклад выше среднего оклада по компании.

SELECT e.name, s.amount
FROM EMPLOYEES e
JOIN SALARY s ON e.id = s.emp_id
WHERE s.amount > (SELECT AVG(amount) FROM SALARY);
2 таблицы · NOT IN

Сотрудники, которые ни разу не получали выплату.

SELECT name
FROM EMPLOYEES
WHERE id NOT IN (SELECT emp_id FROM PAYMENTS);
1 таблица · подзапрос в SELECT (коррелированный)

Для каждого сотрудника — сумма его выплат, посчитанная подзапросом (альтернатива JOIN+GROUP BY).

SELECT e.name,
       (SELECT COALESCE(SUM(p.amount), 0)
        FROM PAYMENTS p WHERE p.emp_id = e.id) AS total_paid
FROM EMPLOYEES e;
3 таблицы · подзапрос вместо второго JOIN (защита от задвоения)

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

SELECT d.name,
       (SELECT COUNT(*) FROM EMPLOYEES e WHERE e.dept_id = d.id) AS emp_count,
       (SELECT COALESCE(SUM(budget),0) FROM PROJECTS pr WHERE pr.dept_id = d.id) AS total_budget
FROM DEPARTMENTS d;
2 таблицы · подзапрос + ORDER BY + LIMIT

Сотрудник с максимальной суммой выплат — без оконных функций, просто сортировка и ограничение.

SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name
ORDER BY total DESC
LIMIT 1;

5.Многотабличные JOIN (3–4+ таблицы)

Соединяя 3+ таблицы, добавляйте JOIN по одному и мысленно проверяйте промежуточный результат после каждого. Частая ошибка — "фан-аут" (fan-out): если у сотрудника несколько выплат И несколько проектов одновременно, обычный JOIN всех таблиц размножит строки крест-накрест и агрегаты (SUM) станут неверными. Решение — считать агрегаты подзапросами до джойна остальных таблиц, как в примере ниже.
4 таблицы · цепочка JOIN

Сотрудник → отдел → участие в проекте → название проекта и роль.

SELECT e.name AS employee, d.name AS department, pr.name AS project, ep.role
FROM EMPLOYEES e
JOIN DEPARTMENTS d ON e.dept_id = d.id
JOIN EMP_PROJECTS ep ON e.id = ep.emp_id
JOIN PROJECTS pr ON ep.project_id = pr.id;
4 таблицы · LEFT JOIN + подзапрос-агрегат (без fan-out)

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

SELECT e.name,
       COALESCE(d.name, 'без отдела') AS department,
       COALESCE(s.amount, 0) AS salary,
       COALESCE((SELECT SUM(amount) FROM PAYMENTS p WHERE p.emp_id = e.id), 0) AS total_payments
FROM EMPLOYEES e
LEFT JOIN DEPARTMENTS d ON e.dept_id = d.id
LEFT JOIN SALARY s ON e.id = s.emp_id;
4 таблицы · сравнение своего и чужого отдела

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

SELECT e.name AS employee, pr.name AS project, d.name AS project_department
FROM EMPLOYEES e
JOIN EMP_PROJECTS ep ON e.id = ep.emp_id
JOIN PROJECTS pr ON ep.project_id = pr.id
JOIN DEPARTMENTS d ON pr.dept_id = d.id
WHERE e.dept_id <> pr.dept_id;
4 таблицы · GROUP BY по отделу + вложенный подзапрос

По отделу: сумма (оклад минус выплаты) по всем его сотрудникам.

SELECT d.name,
       SUM(COALESCE(s.amount,0) -
           COALESCE((SELECT SUM(amount) FROM PAYMENTS p WHERE p.emp_id = e.id), 0)) AS diff
FROM EMPLOYEES e
JOIN DEPARTMENTS d ON e.dept_id = d.id
LEFT JOIN SALARY s ON e.id = s.emp_id
GROUP BY d.id, d.name;
3 таблицы · HAVING на многотабличной агрегации

Отделы, где суммарный оклад сотрудников превышает суммарный бюджет их проектов (сравнение двух независимых сумм через подзапросы).

SELECT d.name,
       (SELECT COALESCE(SUM(s.amount),0) FROM EMPLOYEES e
        JOIN SALARY s ON e.id = s.emp_id WHERE e.dept_id = d.id) AS total_salary,
       (SELECT COALESCE(SUM(budget),0) FROM PROJECTS pr WHERE pr.dept_id = d.id) AS total_budget
FROM DEPARTMENTS d
WHERE (SELECT COALESCE(SUM(s.amount),0) FROM EMPLOYEES e
       JOIN SALARY s ON e.id = s.emp_id WHERE e.dept_id = d.id)
    > (SELECT COALESCE(SUM(budget),0) FROM PROJECTS pr WHERE pr.dept_id = d.id);

Часть 3. Один рецепт — разными способами

Одну и ту же задачу в SQL почти всегда можно решить несколькими путями. Разные подходы часто дают одинаковый результат, но отличаются по читаемости, устойчивости к "ловушкам" (NULL, дублирование строк) и иногда по производительности. Ниже — 4 задачи, каждая решена 2–3 способами, с разбором отличий.

A.«Сотрудники без единой выплаты» — 3 способа

Способ 1 — LEFT JOIN + IS NULL
SELECT e.name
FROM EMPLOYEES e
LEFT JOIN PAYMENTS p ON e.id = p.emp_id
WHERE p.id IS NULL;

Идея: присоединяем выплаты, но оставляем всех сотрудников (LEFT JOIN). Там, где выплаты не нашлось, все колонки из PAYMENTS — NULL. Фильтруем именно эти строки.

Способ 2 — NOT IN с подзапросом
SELECT name
FROM EMPLOYEES
WHERE id NOT IN (SELECT emp_id FROM PAYMENTS);

Идея: получаем список всех emp_id, у кого есть выплаты, и берём сотрудников, которых в этом списке нет. Короче всего читается, но опасен, если emp_id в PAYMENTS может быть NULL (тогда результат неожиданно станет пустым — см. предупреждение в Части 1).

Способ 3 — NOT EXISTS
SELECT e.name
FROM EMPLOYEES e
WHERE NOT EXISTS (
    SELECT 1 FROM PAYMENTS p WHERE p.emp_id = e.id
);

Идея: для каждого сотрудника проверяем "существует ли хотя бы одна его выплата". Логически то же самое, что NOT IN, но не ломается от NULL в PAYMENTS.emp_id — в реальных проектах это самый безопасный вариант из трёх.

Все три варианта дают на нашей схеме одинаковый результат: Иван, Анна, Олег. Вывод: для учебных задач и коротких запросов — любой; в проде — предпочитайте NOT EXISTS или LEFT JOIN + IS NULL, а к NOT IN относитесь настороженно.

B.«Сумма выплат по каждому сотруднику» — 2 способа

Способ 1 — JOIN + GROUP BY (классика)
SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name;

Стандартный подход. Минус: если по сотруднику нет ни одной выплаты, он вообще пропадёт из результата (INNER JOIN) — нужно менять на LEFT JOIN, если требуется показать всех.

Способ 2 — коррелированный подзапрос в SELECT
SELECT e.name,
       (SELECT SUM(p.amount) FROM PAYMENTS p WHERE p.emp_id = e.id) AS total
FROM EMPLOYEES e;

Для каждой строки EMPLOYEES отдельно считается сумма. Плюс: не нужен GROUP BY, легко добавлять ещё несколько независимых сумм из разных таблиц без риска "перемножить" строки (fan-out, см. Часть 2, рецепт 5). Минус: на больших таблицах может быть медленнее — подзапрос выполняется отдельно для каждой строки.

Одинаковый результат при отсутствии дополнительных JOIN. Правило выбора: если джойните только одну "дочернюю" таблицу — берите GROUP BY; если нужно вытащить агрегаты из нескольких независимых таблиц одновременно (как оклад + выплаты + проекты) — подзапросы в SELECT надёжнее.

C.«Сотрудники с окладом выше среднего» — 2 способа

Способ 1 — скалярный подзапрос в WHERE
SELECT e.name, s.amount
FROM EMPLOYEES e
JOIN SALARY s ON e.id = s.emp_id
WHERE s.amount > (SELECT AVG(amount) FROM SALARY);

Подзапрос (SELECT AVG(amount) FROM SALARY) вычисляется один раз и превращается в обычное число, с которым сравнивается каждая строка.

Способ 2 — производная таблица (derived table) в JOIN
SELECT e.name, s.amount
FROM EMPLOYEES e
JOIN SALARY s ON e.id = s.emp_id
JOIN (SELECT AVG(amount) AS avg_amount FROM SALARY) a
  ON s.amount > a.avg_amount;

Подзапрос оформлен как временная "таблица из одной строки" a и присоединяется через JOIN. Более многословно для одного числа, зато такой приём масштабируется — если понадобится сравнивать с несколькими сводными показателями сразу, их все можно вычислить в одном производном источнике вместо кучи скалярных подзапросов.

Результат идентичен (Маша, оклад 300). Для одного простого сравнения — способ 1 короче и привычнее. Способ 2 стоит использовать, когда сводных показателей несколько и хочется вычислить их за один проход.

D.«Сотрудник с максимальной суммой выплат» — 3 способа

Способ 1 — GROUP BY + ORDER BY + LIMIT
SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name
ORDER BY total DESC
LIMIT 1;

Самый читаемый вариант: считаем сумму по каждому, сортируем по убыванию, берём первую строку. Минус — если максимум делят несколько сотрудников поровну, LIMIT 1 покажет только одного, "случайно" выбранного.

Способ 2 — оконная функция RANK()
SELECT name, total FROM (
  SELECT e.name, SUM(p.amount) AS total,
         RANK() OVER (ORDER BY SUM(p.amount) DESC) AS rnk
  FROM EMPLOYEES e
  JOIN PAYMENTS p ON e.id = p.emp_id
  GROUP BY e.id, e.name
) WHERE rnk = 1;

RANK() OVER (...) — оконная функция, присваивает каждой строке ранг по сумме, не схлопывая строки. В отличие от Способа 1, если несколько сотрудников делят первое место — покажет их всех (у них у всех rnk=1). Более mощный инструмент, доступен в PostgreSQL, современном MySQL (8+), SQLite, MS SQL — но не во всех древних версиях MySQL (<8.0).

Способ 3 — HAVING с подзапросом на MAX
SELECT e.name, SUM(p.amount) AS total
FROM EMPLOYEES e
JOIN PAYMENTS p ON e.id = p.emp_id
GROUP BY e.id, e.name
HAVING SUM(p.amount) = (
  SELECT MAX(t) FROM (
    SELECT SUM(amount) AS t FROM PAYMENTS GROUP BY emp_id
  )
);

Сначала подзапросом находим "рекордную" сумму среди всех сотрудников, затем HAVING оставляет тех, кто её достиг. Как и Способ 2 (RANK), корректно покажет всех при ничьей — но без оконных функций, годится даже в очень старых СУБД.

На наших данных все три способа сходятся на одном ответе: Федор, 600. Разница проявляется только при "ничьей" за первое место — тогда LIMIT 1 обрежет лишних, а RANK()/HAVING честно покажут всех. Для отчётов, где важна полнота, выбирайте Способ 2 или 3.
← Назад к тренажёру