Большинство ошибок в SQL повторяются снова и снова — у новичков и у опытных разработчиков. Вот 10 самых частых с объяснением почему это проблема.
1. Сравнение с NULL через =
-- Ошибка: всегда вернёт 0 строк
SELECT * FROM users WHERE email = NULL;
-- Правильно
SELECT * FROM users WHERE email IS NULL;
NULL — не значение, а отсутствие значения. Любое сравнение с NULL возвращает NULL, а не TRUE.
2. Забыть про NULL в NOT IN
-- Если в таблице orders есть хоть один NULL в user_id — вернёт 0 строк
SELECT name FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- Правильно: отфильтровать NULL
SELECT name FROM users WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);
-- Или использовать NOT EXISTS
SELECT name FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
3. SELECT * в продакшн-запросах
-- Плохо: тянет все данные, включая большие поля
SELECT * FROM orders;
-- Хорошо: только нужные колонки
SELECT id, user_id, amount, created_at FROM orders;
SELECT * замедляет запросы, тянет лишние данные по сети и ломается при изменении схемы таблицы.
4. Фильтрация в HAVING вместо WHERE
-- Медленно: сначала группирует всё, потом фильтрует
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id
HAVING user_id > 100;
-- Быстро: сначала фильтрует, потом группирует
SELECT user_id, SUM(amount)
FROM orders
WHERE user_id > 100
GROUP BY user_id;
WHERE фильтрует строки до GROUP BY. HAVING — после. Если условие не требует агрегата — используйте WHERE.
5. Функции в WHERE по индексированным колонкам
-- Плохо: индекс по created_at не используется
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
SELECT * FROM orders WHERE DATE(created_at) = '2024-03-15';
-- Хорошо: индекс используется
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
SELECT * FROM orders WHERE created_at >= '2024-03-15' AND created_at < '2024-03-16';
Любая функция над колонкой делает индекс бесполезным.
6. Дубли из-за JOIN
-- Если у пользователя несколько заказов — users появится несколько раз
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id;
-- Если нужно количество — агрегировать
SELECT u.name, COUNT(o.id) AS orders_count, SUM(o.amount) AS total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
7. Потеря строк из-за INNER JOIN
-- Пользователи без заказов не войдут в результат
SELECT u.name, SUM(o.amount) AS total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- LEFT JOIN включает всех пользователей, даже без заказов
SELECT u.name, COALESCE(SUM(o.amount), 0) AS total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
Всегда думайте: «Нужны ли мне строки без совпадения?» Если да — LEFT JOIN.
8. GROUP BY без всех неагрегированных колонок
-- Ошибка: name не в GROUP BY и не в агрегате
SELECT department, name, COUNT(*)
FROM employees
GROUP BY department;
-- Правильно
SELECT department, name, COUNT(*)
FROM employees
GROUP BY department, name;
PostgreSQL строго это проверяет. MySQL с некоторыми настройками пропустит, но вернёт непредсказуемые данные.
9. DISTINCT вместо правильного GROUP BY
-- Медленно и неточно для больших таблиц
SELECT DISTINCT user_id FROM orders WHERE status = 'completed';
-- То же самое, но явнее
SELECT user_id FROM orders WHERE status = 'completed' GROUP BY user_id;
-- DISTINCT нужен там где нет смысла GROUP BY
SELECT DISTINCT status FROM orders; -- уникальные статусы — нормально
10. Неправильный порядок WHERE и ON в JOIN
-- Одинаковый результат для INNER JOIN:
SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'completed';
SELECT * FROM users u JOIN orders o ON u.id = o.user_id AND o.status = 'completed';
-- Разный для LEFT JOIN!
-- WHERE фильтрует после JOIN (убирает строки с NULL)
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'completed';
-- ↑ Эффективно превращается в INNER JOIN, пользователи без заказов пропадут
-- ON фильтрует при JOIN (сохраняет пользователей без completed-заказов)
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'completed';
-- ↑ Пользователи без completed-заказов останутся с NULL в колонках orders
Это одна из самых коварных ошибок — запрос выполняется, но возвращает неправильные данные.