Гайд по SQL
От SELECT до оконных функций
Интерактивный гайд для подготовки к собеседованиям. JOIN, GROUP BY, подзапросы, CTE, оконные функции — всё с примерами, иллюстрациями и задачами.
📋 SELECT, WHERE, ORDER BY
Основа любого SQL-запроса. SELECT выбирает данные, WHERE фильтрует, ORDER BY сортирует.
Таблица employees
| id | name | department | salary | hire_date |
|---|---|---|---|---|
| 1 | Алиса | Engineering | 120000 | 2021-03-15 |
| 2 | Борис | Engineering | 100000 | 2020-07-01 |
| 3 | Вика | Sales | 95000 | 2022-01-10 |
| 4 | Глеб | Sales | 110000 | 2019-06-20 |
| 5 | Дана | Marketing | 85000 | 2023-02-28 |
| 6 | Егор | Marketing | 90000 | 2021-11-05 |
Базовый SELECT
SELECT name, salary
FROM employees
WHERE department = 'Engineering'
ORDER BY salary DESC;Результат
| name | salary |
|---|---|
| Алиса | 120000 |
| Борис | 100000 |
Операторы сравнения и логические операторы
-- Несколько условий с AND/OR
SELECT name, department, salary
FROM employees
WHERE salary >= 90000
AND department IN ('Engineering', 'Sales')
ORDER BY department, salary DESC;
-- BETWEEN для диапазонов
SELECT name, hire_date
FROM employees
WHERE hire_date BETWEEN '2020-01-01' AND '2022-12-31'
ORDER BY hire_date;
-- LIKE для поиска по паттерну
SELECT name
FROM employees
WHERE name LIKE 'А%'; -- начинается с 'А'DISTINCT и LIMIT
-- Уникальные отделы
SELECT DISTINCT department
FROM employees;
-- Топ-3 по зарплате
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;Порядок выполнения SQL
FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT🔗 JOIN — соединение таблиц
JOIN объединяет строки из двух (или более) таблиц на основе связанного столбца. Это один из самых важных инструментов SQL.
employees
| id | name | dept_id |
|---|---|---|
| 1 | Алиса | 10 |
| 2 | Борис | 20 |
| 3 | Вика | 30 |
| 4 | Глеб | NULL |
departments
| dept_id | dept_name |
|---|---|
| 10 | Engineering |
| 20 | Sales |
| 40 | HR |
INNER JOIN
Возвращает только строки, где есть совпадение в обеих таблицах.
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;Результат: только совпавшие (Вика без dept, Глеб NULL, HR без сотрудников — не попали)
| name | dept_name |
|---|---|
| Алиса | Engineering |
| Борис | Sales |
LEFT JOIN
Все строки из левой таблицы + совпадения из правой. Нет совпадения → NULL.
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;Результат: все сотрудники, даже без отдела
| name | dept_name |
|---|---|
| Алиса | Engineering |
| Борис | Sales |
| Вика | NULL |
| Глеб | NULL |
RIGHT JOIN
Все строки из правой таблицы + совпадения из левой.
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;Результат: все отделы, даже без сотрудников
| name | dept_name |
|---|---|
| Алиса | Engineering |
| Борис | Sales |
| NULL | HR |
FULL OUTER JOIN
Все строки из обеих таблиц. Нет совпадения — NULL с соответствующей стороны.
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;Результат: всё из обеих таблиц
| name | dept_name |
|---|---|
| Алиса | Engineering |
| Борис | Sales |
| Вика | NULL |
| Глеб | NULL |
| NULL | HR |
CROSS JOIN
Декартово произведение — каждая строка из A × каждая строка из B.
SELECT e.name, d.dept_name
FROM employees e
CROSS JOIN departments d;
-- Результат: 4 × 3 = 12 строкSELF JOIN
Таблица соединяется сама с собой. Полезно для иерархий (сотрудник → менеджер).
-- Найти сотрудников и их менеджеров
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;Когда какой JOIN?
- INNER — нужны только совпадения
- LEFT — нужны все записи из основной таблицы
- FULL — нужен полный обзор данных, включая "сироты"
- CROSS — генерация всех комбинаций (редко, но бывает)
- SELF — иерархии, сравнение записей внутри таблицы
📊 GROUP BY + HAVING
GROUP BY группирует строки с одинаковыми значениями. Агрегатные функции (COUNT, SUM, AVG, MIN, MAX) вычисляют значение по группе.
employees
| id | name | department | salary |
|---|---|---|---|
| 1 | Алиса | Engineering | 120000 |
| 2 | Борис | Engineering | 100000 |
| 3 | Вика | Sales | 95000 |
| 4 | Глеб | Sales | 110000 |
| 5 | Дана | Marketing | 85000 |
| 6 | Егор | Marketing | 90000 |
Базовая агрегация
SELECT
department,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary,
MIN(salary) AS min_salary,
MAX(salary) AS max_salary,
SUM(salary) AS total_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;Результат
| department | emp_count | avg_salary | min_salary | max_salary | total_salary |
|---|---|---|---|---|---|
| Engineering | 2 | 110000 | 100000 | 120000 | 220000 |
| Sales | 2 | 102500 | 95000 | 110000 | 205000 |
| Marketing | 2 | 87500 | 85000 | 90000 | 175000 |
HAVING — фильтрация после группировки
WHERE фильтрует строки до группировки, HAVING — после.
-- Отделы с средней зарплатой > 90000
SELECT
department,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 90000
ORDER BY avg_salary DESC;
-- WHERE + HAVING вместе
SELECT
department,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary
FROM employees
WHERE salary > 80000 -- фильтр строк ДО группировки
GROUP BY department
HAVING COUNT(*) >= 2 -- фильтр групп ПОСЛЕ группировки
ORDER BY avg_salary DESC;Частая ошибка
❌ WHERE COUNT(*) > 2 — ошибка
✅ HAVING COUNT(*) > 2 — правильноGROUP BY с несколькими колонками
-- Количество сотрудников по отделу и году найма
SELECT
department,
EXTRACT(YEAR FROM hire_date) AS hire_year,
COUNT(*) AS hired
FROM employees
GROUP BY department, EXTRACT(YEAR FROM hire_date)
ORDER BY department, hire_year;🔄 Подзапросы и CTE (WITH)
Подзапросы — это запросы внутри запросов. CTE (Common Table Expressions) делают сложные запросы читаемыми.
Подзапрос в WHERE
-- Сотрудники с зарплатой выше средней
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- Сотрудники из отделов с > 5 людьми
SELECT name, department
FROM employees
WHERE department IN (
SELECT department
FROM employees
GROUP BY department
HAVING COUNT(*) > 5
);Подзапрос в SELECT
-- Зарплата и средняя по отделу (коррелированный подзапрос)
SELECT
name,
salary,
department,
(SELECT AVG(salary)
FROM employees
WHERE .department = .department) AS dept_avg
FROM employees ;Коррелированный vs некоррелированный
Коррелированный — выполняется для каждой строки, ссылается на внешний запрос. Медленнее, но мощнее.
EXISTS / NOT EXISTS
-- Отделы, в которых есть хотя бы один сотрудник с зарплатой > 100k
SELECT d.dept_name
FROM departments d
WHERE EXISTS (
SELECT 1
FROM employees e
WHERE e.dept_id = d.dept_id
AND e.salary > 100000
);
-- Отделы БЕЗ сотрудников
SELECT d.dept_name
FROM departments d
WHERE NOT EXISTS (
SELECT 1
FROM employees e
WHERE e.dept_id = d.dept_id
);CTE (WITH) — Common Table Expressions
CTE создаёт именованный временный результат, доступный в основном запросе. Делает сложные запросы читаемыми.
-- Средняя зарплата по отделам + сравнение с общей средней
WITH dept_stats AS (
SELECT
department,
AVG(salary) AS avg_salary,
COUNT(*) AS emp_count
FROM employees
GROUP BY department
),
overall AS (
SELECT AVG(salary) AS company_avg
FROM employees
)
SELECT
ds.department,
ds.avg_salary,
ds.emp_count,
o.company_avg,
ROUND(ds.avg_salary - o.company_avg) AS diff_from_avg
FROM dept_stats ds
CROSS JOIN overall o
ORDER BY ds.avg_salary DESC;Рекурсивный CTE
-- Иерархия сотрудников (менеджер → подчинённые)
WITH RECURSIVE org_tree AS (
-- Базовый случай: CEO (нет менеджера)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Рекурсивный шаг
SELECT e.id, e.name, e.manager_id, t.level + 1
FROM employees e
INNER JOIN org_tree t ON e.manager_id = t.id
)
SELECT
REPEAT(' ', level - 1) || name AS org_chart,
level
FROM org_tree
ORDER BY level, name;Когда CTE лучше подзапроса?
- Читаемость — CTE именованные, легко понять логику
- Переиспользование — CTE можно ссылать несколько раз
- Рекурсия — только через CTE
- Дебаг — каждый CTE можно запустить отдельно
🪟 Оконные функции
Оконные функции выполняют вычисления по набору строк, связанных с текущей строкой, не схлопывая их (в отличие от GROUP BY). Это самый частый вопрос на SQL-секции собеседований.
Синтаксис
функция() OVER (
PARTITION BY ... -- разбиение на группы (необязательно)
ORDER BY ... -- сортировка внутри окна
ROWS/RANGE BETWEEN ... AND ... -- рамка окна (необязательно)
)employees
| id | name | department | salary |
|---|---|---|---|
| 1 | Алиса | Engineering | 120000 |
| 2 | Борис | Engineering | 100000 |
| 3 | Вика | Engineering | 100000 |
| 4 | Глеб | Sales | 110000 |
| 5 | Дана | Sales | 95000 |
| 6 | Егор | Sales | 85000 |
ROW_NUMBER, RANK, DENSE_RANK
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rnk
FROM employees;Результат: обратите внимание на строки с одинаковой salary (Борис и Вика)
| name | department | salary | row_num | rnk | dense_rnk |
|---|---|---|---|---|---|
| Алиса | Engineering | 120000 | 1 | 1 | 1 |
| Борис | Engineering | 100000 | 2 | 2 | 2 |
| Вика | Engineering | 100000 | 3 | 2 | 2 |
| Глеб | Sales | 110000 | 1 | 1 | 1 |
| Дана | Sales | 95000 | 2 | 2 | 2 |
| Егор | Sales | 85000 | 3 | 3 | 3 |
В чём разница?
- ROW_NUMBER — всегда уникальный номер (1, 2, 3)
- RANK — одинаковые значения получают одинаковый ранг, затем пропуск (1, 2, 2, 4)
- DENSE_RANK — как RANK, но без пропуска (1, 2, 2, 3)
LAG и LEAD — доступ к соседним строкам
SELECT
name,
department,
salary,
LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS prev_salary,
LEAD(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS next_salary,
salary - LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary DESC) AS diff
FROM employees;Результат: LAG — предыдущая строка, LEAD — следующая
| name | department | salary | prev_salary | next_salary | diff |
|---|---|---|---|---|---|
| Алиса | Engineering | 120000 | NULL | 100000 | NULL |
| Борис | Engineering | 100000 | 120000 | 100000 | -20000 |
| Вика | Engineering | 100000 | 100000 | NULL | 0 |
| Глеб | Sales | 110000 | NULL | 95000 | NULL |
| Дана | Sales | 95000 | 110000 | 85000 | -15000 |
| Егор | Sales | 85000 | 95000 | NULL | -10000 |
Агрегатные оконные функции
SELECT
name,
department,
salary,
-- Средняя зарплата по отделу
ROUND(AVG(salary) OVER (PARTITION BY department)) AS dept_avg,
-- Нарастающий итог по отделу
SUM(salary) OVER (
PARTITION BY department
ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total,
-- Доля от общей зарплаты отдела
ROUND(salary * 100.0 / SUM(salary) OVER (PARTITION BY department), 1) AS pct_of_dept
FROM employees;Результат
| name | department | salary | dept_avg | running_total | pct_of_dept |
|---|---|---|---|---|---|
| Алиса | Engineering | 120000 | 106667 | 120000 | 37.5% |
| Борис | Engineering | 100000 | 106667 | 220000 | 31.3% |
| Вика | Engineering | 100000 | 106667 | 320000 | 31.3% |
| Глеб | Sales | 110000 | 96667 | 110000 | 37.9% |
| Дана | Sales | 95000 | 96667 | 205000 | 32.8% |
| Егор | Sales | 85000 | 96667 | 290000 | 29.3% |
Рамка окна (Window Frame)
-- Скользящее среднее за 3 строки
SELECT
date,
revenue,
AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS
FROM daily_revenue;
-- Нарастающий итог с начала до текущей строки
SUM(amount) OVER (
ORDER BY date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_sum
-- Всё в партиции (общая сумма)
SUM(amount) OVER (
PARTITION BY category
-- без ORDER BY = вся партиция
) AS category_totalОпции рамки
ROWS BETWEEN ... AND ...
├── UNBOUNDED PRECEDING (от начала партиции)
├── N PRECEDING (N строк назад)
├── CURRENT ROW (текущая строка)
├── N FOLLOWING (N строк вперёд)
└── UNBOUNDED FOLLOWING (до конца партиции)🎯 Практические задачи
Классические задачи с собеседований. Попробуйте решить самостоятельно перед просмотром решения!
Таблица employees для всех задач
| id | name | department | salary | hire_date | manager_id |
|---|---|---|---|---|---|
| 1 | Алиса | Engineering | 120000 | 2021-03-15 | NULL |
| 2 | Борис | Engineering | 100000 | 2020-07-01 | 1 |
| 3 | Вика | Sales | 95000 | 2022-01-10 | 1 |
| 4 | Глеб | Sales | 110000 | 2019-06-20 | 1 |
| 5 | Дана | Marketing | 85000 | 2023-02-28 | 4 |
| 6 | Егор | Marketing | 90000 | 2021-11-05 | 4 |
| 7 | Жанна | Engineering | 130000 | 2018-04-10 | NULL |
| 8 | Захар | Sales | 95000 | 2022-08-15 | 4 |
Вторая по величине зарплата
Найдите вторую по величине зарплату в компании. Если есть дубликаты — учесть (нужна именно 2-я уникальная).
Самая высокая зарплата в каждом отделе
Для каждого отдела найдите сотрудника с наибольшей зарплатой.
Нарастающий итог зарплат
Для каждого сотрудника (в порядке найма) покажите нарастающий итог зарплат.
Разница с предыдущей записью
Для каждого сотрудника в отделе (отсортированного по зарплате) покажите разницу с предыдущей зарплатой в этом отделе.
Сотрудники выше средней зарплаты отдела
Найдите сотрудников, чья зарплата выше средней по их отделу. Покажите имя, отдел, зарплату, среднюю по отделу и разницу.
Топ-N по зарплате в каждом отделе
Выведите топ-2 сотрудника по зарплате в каждом отделе. Если на 2-м месте несколько с одинаковой зарплатой — вывести всех.
Количество сотрудников нарастающим итогом по годам
Для каждого года найма покажите количество нанятых сотрудников и накопленное общее количество.
Готов к SQL-собеседованию? 🚀
Реши SQL-задачи в интерактивном тренажёре. Реальные вопросы, автоматическая проверка, детальный фидбэк.
Перейти к тренажёру