SELECT / DISTINCTSELECT определяет, какие колонки (или выражения) попадут в результат. 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 / OFFSETORDER 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)
JOININNER 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
ON выполняется. Если для строки слева нет пары справа — эта строка пропадает из результата целиком. Используйте, когда вам нужны только сущности, у которых точно есть связанная запись (например, только те сотрудники, у которых есть оклад).
Простейшее соединение — сотрудник + его оклад.
SELECT e.name, s.amount FROM EMPLOYEES e JOIN SALARY s ON e.id = s.emp_id;
То же самое, но только для оклада больше 150.
SELECT e.name, s.amount FROM EMPLOYEES e JOIN SALARY s ON e.id = s.emp_id WHERE s.amount > 150;
Сотрудник, его отдел и оклад — только по Москве.
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 = 'Москва';
Выплаты сотрудников за конкретный месяц, сумма выше порога, отсортировано по убыванию.
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;
Сотрудники-разработчики (роль), которые из Москвы и родились после 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';
NULL. COALESCE(x, значение_по_умолчанию) заменяет NULL на удобное число/текст. Это главный инструмент для отчётов "покажи всех, даже тех, у кого чего-то нет".
Все сотрудники и их оклад; если оклада нет — 0.
SELECT e.name, COALESCE(s.amount, 0) AS salary FROM EMPLOYEES e LEFT JOIN SALARY s ON e.id = s.emp_id;
Сотрудники, у которых оклада нет вообще (типичный паттерн — 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;
Сотрудник, отдел (или "без отдела") и оклад (или 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;
Сумма всех выплат по каждому сотруднику, 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;
Сотрудники без отдела ИЛИ без оклада, с их городом — найти "проблемные" записи.
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;
GROUP BY схлопывает строки в группы по указанным колонкам; агрегатные функции (SUM, COUNT, AVG, MAX, MIN) считают одно число на группу. WHERE фильтрует строки до группировки, HAVING — группы после агрегации (поэтому в HAVING можно использовать SUM(), а в WHERE — нет).
Сколько выплат было в каждом месяце.
SELECT pay_date, COUNT(*) AS payments_count FROM PAYMENTS GROUP BY pay_date;
Суммарные выплаты по сотрудникам мужского пола.
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;
Сотрудники, чья суммарная выплата больше 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;
По отделу "Разработка": сотрудники со средней выплатой выше 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;
Суммарный бюджет проектов по каждому отделу, только там, где бюджет больше 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;
(SELECT ...), возвращающий одно число) удобен для сравнения "больше среднего". IN / NOT IN с подзапросом — для проверки "входит / не входит в список". Подзапросы часто спасают от "задвоения" строк, которое возникает при JOIN с агрегатами из нескольких источников одновременно.
Сотрудники, чей оклад выше среднего оклада по компании.
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 name FROM EMPLOYEES WHERE id NOT IN (SELECT emp_id FROM PAYMENTS);
Для каждого сотрудника — сумма его выплат, посчитанная подзапросом (альтернатива 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;
По отделу: количество сотрудников и суммарный бюджет проектов — двумя независимыми подзапросами, а не двойным 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;
Сотрудник с максимальной суммой выплат — без оконных функций, просто сортировка и ограничение.
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;
Сотрудник → отдел → участие в проекте → название проекта и роль.
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;
Сотрудник, отдел, оклад и сумма выплат — выплаты считаются подзапросом, чтобы не размножить строки из-за нескольких платежей.
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;
Сотрудники, работающие над проектом отдела, который отличается от их собственного.
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;
По отделу: сумма (оклад минус выплаты) по всем его сотрудникам.
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;
Отделы, где суммарный оклад сотрудников превышает суммарный бюджет их проектов (сравнение двух независимых сумм через подзапросы).
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);
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. Фильтруем именно эти строки.
SELECT name FROM EMPLOYEES WHERE id NOT IN (SELECT emp_id FROM PAYMENTS);
Идея: получаем список всех emp_id, у кого есть выплаты, и берём сотрудников, которых в этом списке нет. Короче всего читается, но опасен, если emp_id в PAYMENTS может быть NULL (тогда результат неожиданно станет пустым — см. предупреждение в Части 1).
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 — в реальных проектах это самый безопасный вариант из трёх.
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, если требуется показать всех.
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). Минус: на больших таблицах может быть медленнее — подзапрос выполняется отдельно для каждой строки.
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) вычисляется один раз и превращается в обычное число, с которым сравнивается каждая строка.
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. Более многословно для одного числа, зато такой приём масштабируется — если понадобится сравнивать с несколькими сводными показателями сразу, их все можно вычислить в одном производном источнике вместо кучи скалярных подзапросов.
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 покажет только одного, "случайно" выбранного.
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).
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), корректно покажет всех при ничьей — но без оконных функций, годится даже в очень старых СУБД.