SQLLab
Все статьи

10 частых ошибок в SQL и как их избежать

Разбираем самые распространённые ошибки в SQL: NULL-ловушки, неправильные JOIN, SELECT *, потеря строк в GROUP BY. Примеры и как правильно.

22 марта 2026 г.·4 мин чтения·

Большинство ошибок в 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

Это одна из самых коварных ошибок — запрос выполняется, но возвращает неправильные данные.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →