Программа

Гайд по SQL

От SELECT до оконных функций

Интерактивный гайд для подготовки к собеседованиям. JOIN, GROUP BY, подзапросы, CTE, оконные функции — всё с примерами, иллюстрациями и задачами.

6 разделов
40+ примеров
🎯 7 задач с решениями

📋 SELECT, WHERE, ORDER BY

Основа любого SQL-запроса. SELECT выбирает данные, WHERE фильтрует, ORDER BY сортирует.

Таблица employees

idnamedepartmentsalaryhire_date
1АлисаEngineering1200002021-03-15
2БорисEngineering1000002020-07-01
3ВикаSales950002022-01-10
4ГлебSales1100002019-06-20
5ДанаMarketing850002023-02-28
6ЕгорMarketing900002021-11-05

Базовый SELECT

Выборка с фильтрацией и сортировкой
SELECT name, salary
FROM employees
WHERE department = 'Engineering'
ORDER BY salary DESC;

Результат

namesalary
Алиса120000
Борис100000

Операторы сравнения и логические операторы

Фильтрация: AND, OR, IN, BETWEEN, LIKE
-- Несколько условий с 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

SQL выполняется не в порядке записи! Реальный порядок:FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

🔗 JOIN — соединение таблиц

JOIN объединяет строки из двух (или более) таблиц на основе связанного столбца. Это один из самых важных инструментов SQL.

employees

idnamedept_id
1Алиса10
2Борис20
3Вика30
4ГлебNULL

departments

dept_iddept_name
10Engineering
20Sales
40HR

INNER JOIN

Возвращает только строки, где есть совпадение в обеих таблицах.

ABINNER JOIN
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 без сотрудников — не попали)

namedept_name
АлисаEngineering
БорисSales

LEFT JOIN

Все строки из левой таблицы + совпадения из правой. Нет совпадения → NULL.

ABLEFT JOIN
LEFT JOIN
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;

Результат: все сотрудники, даже без отдела

namedept_name
АлисаEngineering
БорисSales
ВикаNULL
ГлебNULL

RIGHT JOIN

Все строки из правой таблицы + совпадения из левой.

ABRIGHT JOIN
RIGHT JOIN
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;

Результат: все отделы, даже без сотрудников

namedept_name
АлисаEngineering
БорисSales
NULLHR

FULL OUTER JOIN

Все строки из обеих таблиц. Нет совпадения — NULL с соответствующей стороны.

ABFULL JOIN
FULL OUTER JOIN
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;

Результат: всё из обеих таблиц

namedept_name
АлисаEngineering
БорисSales
ВикаNULL
ГлебNULL
NULLHR

CROSS JOIN

Декартово произведение — каждая строка из A × каждая строка из B.

A3 rows×B4 rows= 12 rowsCROSS JOIN
CROSS JOIN
SELECT e.name, d.dept_name
FROM employees e
CROSS JOIN departments d;
-- Результат: 4 × 3 = 12 строк

SELF JOIN

Таблица соединяется сама с собой. Полезно для иерархий (сотрудник → менеджер).

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

idnamedepartmentsalary
1АлисаEngineering120000
2БорисEngineering100000
3ВикаSales95000
4ГлебSales110000
5ДанаMarketing85000
6ЕгорMarketing90000

Базовая агрегация

Статистика по отделам
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;

Результат

departmentemp_countavg_salarymin_salarymax_salarytotal_salary
Engineering2110000100000120000220000
Sales210250095000110000205000
Marketing2875008500090000175000

HAVING — фильтрация после группировки

WHERE фильтрует строки до группировки, HAVINGпосле.

HAVING vs WHERE
-- Отделы с средней зарплатой > 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! Используйте HAVING.❌ 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

Подзапросы в 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

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 создаёт именованный временный результат, доступный в основном запросе. Делает сложные запросы читаемыми.

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

Рекурсивный 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-секции собеседований.

PARTITION BY department ORDER BY salary DESCПартиция: EngineeringAlice120krn=1Bob100krn=2Carol90krn=3WINDOWПартиция: SalesDave110krn=1Eve95krn=2Frank85krn=3Sliding Window — SUM(salary) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)id=1sal=100Σ=100id=2sal=120Σ=220id=3sal=90Σ=210id=4sal=110Σ=200← окно (2 строки) →сдвигается вправо

Синтаксис

Общий синтаксис оконной функции
функция() OVER (
    PARTITION BY ...   -- разбиение на группы (необязательно)
    ORDER BY ...       -- сортировка внутри окна
    ROWS/RANGE BETWEEN ... AND ...  -- рамка окна (необязательно)
)

employees

idnamedepartmentsalary
1АлисаEngineering120000
2БорисEngineering100000
3ВикаEngineering100000
4ГлебSales110000
5ДанаSales95000
6ЕгорSales85000

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 (Борис и Вика)

namedepartmentsalaryrow_numrnkdense_rnk
АлисаEngineering120000111
БорисEngineering100000222
ВикаEngineering100000322
ГлебSales110000111
ДанаSales95000222
ЕгорSales85000333

В чём разница?

  • ROW_NUMBER — всегда уникальный номер (1, 2, 3)
  • RANK — одинаковые значения получают одинаковый ранг, затем пропуск (1, 2, 2, 4)
  • DENSE_RANK — как RANK, но без пропуска (1, 2, 2, 3)

LAG и LEAD — доступ к соседним строкам

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 — следующая

namedepartmentsalaryprev_salarynext_salarydiff
АлисаEngineering120000NULL100000NULL
БорисEngineering100000120000100000-20000
ВикаEngineering100000100000NULL0
ГлебSales110000NULL95000NULL
ДанаSales9500011000085000-15000
ЕгорSales8500095000NULL-10000

Агрегатные оконные функции

AVG, SUM, доля — через OVER
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;

Результат

namedepartmentsalarydept_avgrunning_totalpct_of_dept
АлисаEngineering12000010666712000037.5%
БорисEngineering10000010666722000031.3%
ВикаEngineering10000010666732000031.3%
ГлебSales1100009666711000037.9%
ДанаSales950009666720500032.8%
ЕгорSales850009666729000029.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 для всех задач

idnamedepartmentsalaryhire_datemanager_id
1АлисаEngineering1200002021-03-15NULL
2БорисEngineering1000002020-07-011
3ВикаSales950002022-01-101
4ГлебSales1100002019-06-201
5ДанаMarketing850002023-02-284
6ЕгорMarketing900002021-11-054
7ЖаннаEngineering1300002018-04-10NULL
8ЗахарSales950002022-08-154
1

Вторая по величине зарплата

Найдите вторую по величине зарплату в компании. Если есть дубликаты — учесть (нужна именно 2-я уникальная).

2

Самая высокая зарплата в каждом отделе

Для каждого отдела найдите сотрудника с наибольшей зарплатой.

3

Нарастающий итог зарплат

Для каждого сотрудника (в порядке найма) покажите нарастающий итог зарплат.

4

Разница с предыдущей записью

Для каждого сотрудника в отделе (отсортированного по зарплате) покажите разницу с предыдущей зарплатой в этом отделе.

5

Сотрудники выше средней зарплаты отдела

Найдите сотрудников, чья зарплата выше средней по их отделу. Покажите имя, отдел, зарплату, среднюю по отделу и разницу.

6

Топ-N по зарплате в каждом отделе

Выведите топ-2 сотрудника по зарплате в каждом отделе. Если на 2-м месте несколько с одинаковой зарплатой — вывести всех.

7

Количество сотрудников нарастающим итогом по годам

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

Готов к SQL-собеседованию? 🚀

Реши SQL-задачи в интерактивном тренажёре. Реальные вопросы, автоматическая проверка, детальный фидбэк.

Перейти к тренажёру