SQL SELECT: WHERE, GROUP BY, HAVING, ORDER BY
Структура запроса SELECT, порядок выполнения предложений, фильтрация WHERE, агрегатные функции и группировка, отличие HAVING от WHERE, подзапросы.
SELECT — основной запрос, вокруг которого строится практическая часть курсовой по базам данных. Разобравшись с порядком выполнения его предложений, вы перестанете гадать, почему СУБД ругается на псевдоним столбца.
SELECT d.name AS отдел, COUNT(*) AS сотрудников, AVG(e.salary) AS средняя
FROM employees e
JOIN departments d ON d.id = e.dept_id
WHERE e.hired_at >= '2024-01-01'
GROUP BY d.name
HAVING COUNT(*) > 3
ORDER BY средняя DESC
LIMIT 10;
Порядок выполнения
| № | Предложение | Что делает |
|---|---|---|
| 1 | FROM, JOIN | Собирает исходный набор строк |
| 2 | WHERE | Отсеивает строки до группировки |
| 3 | GROUP BY | Складывает строки в группы |
| 4 | HAVING | Отсеивает уже готовые группы |
| 5 | SELECT | Вычисляет выражения и псевдонимы |
| 6 | ORDER BY | Сортирует результат |
| 7 | LIMIT / OFFSET | Отрезает нужную порцию |
Фильтрация в WHERE
WHERE salary BETWEEN 50000 AND 90000 -- включая границы
WHERE dept_id IN (1, 2, 5)
WHERE name LIKE 'Ив%' -- % любое число символов, _ ровно один
WHERE manager_id IS NULL -- только IS NULL, не = NULL
WHERE EXTRACT(YEAR FROM hired_at) = 2025
WHERE NOT (status = 'closed' OR archived)
NULL не равен ничему, включая другой NULL. Сравнение = NULL всегда даёт неопределённость, поэтому существует отдельный оператор IS NULL. Из-за этого же строки с NULL молча выпадают из результата условия вроде salary <> 100000.
Агрегатные функции
| Функция | Смысл | Учитывает NULL |
|---|---|---|
| COUNT(*) | Число строк | Да, считает все строки |
| COUNT(col) | Число непустых значений | Нет, пропускает NULL |
| SUM(col) | Сумма | Нет |
| AVG(col) | Среднее | Нет — делит на количество непустых |
| MIN / MAX | Минимум и максимум | Нет |
GROUP BY и правило совместимости
Каждый столбец в SELECT должен быть либо перечислен в GROUP BY, либо обёрнут в агрегатную функцию. Иначе СУБД не знает, какое из значений группы показать. PostgreSQL и стандартный SQL выдадут ошибку, MySQL в нестрогом режиме молча вернёт произвольное значение — и это хуже ошибки.
-- Неверно: city не в GROUP BY и не агрегирован
SELECT dept_id, city, COUNT(*) FROM employees GROUP BY dept_id;
-- Верно
SELECT dept_id, city, COUNT(*) FROM employees GROUP BY dept_id, city;
-- Или агрегируем
SELECT dept_id, MAX(city), COUNT(*) FROM employees GROUP BY dept_id;
WHERE или HAVING
| WHERE | HAVING | |
|---|---|---|
| Фильтрует | Отдельные строки | Готовые группы |
| Когда | До группировки | После группировки |
| Агрегаты | Нельзя | Можно и нужно |
| Пример | WHERE salary > 50000 | HAVING AVG(salary) > 50000 |
Правило выбора: условие относится к отдельной записи — WHERE, к характеристике группы — HAVING. Если можно отфильтровать в WHERE, делайте это там: строк до группировки останется меньше и запрос отработает быстрее.
Подзапросы
-- Скалярный: возвращает одно значение
SELECT name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- В списке значений
SELECT * FROM departments
WHERE id IN (SELECT dept_id FROM employees WHERE salary > 100000);
-- Коррелированный: выполняется для каждой строки внешнего запроса
SELECT d.name FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e
WHERE e.dept_id = d.id AND e.salary > 150000);
-- CTE — читается сверху вниз, удобно для курсовой
WITH avg_by_dept AS (
SELECT dept_id, AVG(salary) AS avg_sal
FROM employees GROUP BY dept_id
)
SELECT d.name, a.avg_sal
FROM avg_by_dept a JOIN departments d ON d.id = a.dept_id
WHERE a.avg_sal > 80000;
Типичные ошибки
- Сравнение с NULL через = вместо IS NULL — условие никогда не выполняется.
- Агрегат в WHERE: WHERE COUNT(*) > 3 вместо HAVING COUNT(*) > 3.
- Псевдоним из SELECT в предложении WHERE.
- JOIN без условия ON — получается декартово произведение на миллионы строк.
- ORDER BY по номеру столбца: работает, но ломается при любой правке списка полей.
Частые вопросы
Чем отличается COUNT(*) от COUNT(поле)?
COUNT(*) считает все строки группы, COUNT(поле) — только те, где поле не NULL. Разница этих двух чисел показывает количество пропусков.
Можно ли использовать GROUP BY без агрегатных функций?
Можно: получится список уникальных комбинаций, как при DISTINCT. Но если задача только в уникальности, DISTINCT читается понятнее.
Что быстрее: подзапрос или JOIN?
В современных СУБД оптимизатор часто приводит их к одному плану. Коррелированные подзапросы опаснее — они могут выполняться для каждой строки. Смотрите план через EXPLAIN, а в курсовой приложите его как доказательство обоснованности решения.