Когда регулярно ревьюируешь чужой код или менторишь младших коллег, начинаешь замечать паттерны. Есть ошибки, которые кочуют из скрипта в скрипт, и совершают их не только джуны, но и вполне уверенные специалисты.
Сегодня разберем пять неочевидных нюансов SQL, которые могут незаметно исказить бизнес-метрики, сломать воронку или просто заставить базу выполнять лишнюю работу.
1. COUNT(*) и COUNT(col)
Многие уверены, что COUNT(*), COUNT(1) и COUNT(имя_столбца) — это синонимы, и пишут их не глядя. На небольших или идеально чистых датасетах разница незаметна, но в реальности это мина замедленного действия.
Предположим, у нас есть таблица пользователей users, и нам нужно посчитать их количество.
Если вы напишете:
SELECT COUNT(email) FROM users;
вы получите количество заполненных имейлов. Все строки, где email IS NULL, будут проигнорированы агрегатной функцией.
А вот классические варианты:
SELECT COUNT(*) FROM users;
-- или
SELECT COUNT(1) FROM users;
честно посчитают физическое количество строк в таблице.
Используйте COUNT(имя_столбца) осознанно — только тогда, когда вам действительно нужно посчитать non-null значения. Во всех остальных случаях пишите COUNT(*) или COUNT(1).
2. Лишние поля в GROUP BY
Классическая задача: есть таблица transactions, нужно посчитать общую сумму покупок для каждого клиента.
Часто можно встретить такой код:
SELECT
client_id,
SUM(amount) AS sum_amount
FROM transactions
GROUP BY
client_id,
transact_id; -- Ошибка здесь
Скрипт отработает без синтаксических ошибок, но бизнес-логика сломается. Вместо одной строки на каждого client_id вы получите столько строк, сколько транзакций совершил клиент. Добавление уникального идентификатора транзакции в группировку не дает агрегатной функции «схлопнуть» данные.
Правильный вариант — оставлять в GROUP BY строго те неагрегированные измерения, в разрезе которых вы хотите получить метрику:
SELECT
client_id,
SUM(amount) AS sum_amount
FROM transactions
GROUP BY
client_id;
3. Рассинхрон селекта и группировки
Еще одна частая боль — использование исходного поля в GROUP BY, когда в SELECT на его основе вычисляется новый признак.
Например, мы хотим разметить старых и новых клиентов по дате старта и посчитать их оборот:
SELECT
client_id,
CASE WHEN start_date >= '2026-01-01' THEN 1 ELSE 0 END AS new_client_flag,
SUM(amount) AS sum_amount
FROM transactions
GROUP BY
client_id,
start_date; -- Группировка по сырой дате
Здесь данные сгруппируются по каждому уникальному дню (start_date), а не по нашему флагу new_client_flag. В результате один и тот же client_id может продублироваться, если у него были транзакции в разные дни.
Нужно группировать по самому условию.
SELECT
client_id,
CASE WHEN start_date >= '2026-01-01' THEN 1 ELSE 0 END AS new_client_flag,
SUM(amount) AS sum_amount
FROM transactions
GROUP BY
client_id,
CASE WHEN start_date >= '2026-01-01' THEN 1 ELSE 0 END;
Pro-tip: Во многих современных СУБД (например, PostgreSQL или ClickHouse) можно использовать алиасы или позиционные номера столбцов, чтобы не дублировать громоздкий CASE: GROUP BY client_id, new_client_flag или просто GROUP BY 1, 2.
4. LIKE без %
Аналитикам регулярно приходится чистить данные от тестового мусора. Заказчик просит собрать конверсию воронки рассылок, и нам нужно исключить из логов тестовые SMS.
Пишем:
WHERE name NOT LIKE 'test'
И получаем завышенный верх воронки. Почему? Потому что без знаков процента LIKE работает как обычное строгое равенство (=). Он отфильтрует только те строки, где поле name состоит ровно из четырех букв «test». Записи вида «test_push_1» или «autotest» спокойно пройдут дальше.
Для поиска подстроки всегда используйте джокеры:
WHERE name NOT LIKE '%test%'
Знак % до и после искомого слова гарантирует, что мы отсечем слово «test» в любой части строки.
5. DISTINCT не лечит дубли
Это моя любимая категория ошибок. «Заджойнил таблицы, полезли дубли — накину-ка я DISTINCT, и дело в шляпе».
DISTINCT — это не подорожник. Если дубли появились при джойне, значит, нарушена логика связи таблиц (связь «один-ко-многим» отработала как декартово произведение), и DISTINCT просто замаскирует симптомы, не вылечив болезнь.
Посмотрим на пример:
SELECT DISTINCT
u.client_id,
t.amount
FROM clients u
LEFT JOIN transactions t ON u.client_id = t.client_id;
Если у клиента было 10 транзакций с разными суммами, LEFT JOIN размножит запись клиента до 10 строк. DISTINCT посмотрит на эти 10 строк, увидит, что комбинации (client_id, amount) уникальны, и… оставит все 10 строк. Вы не избавились от дублей пользователя.
Правильный подход — понять бизнес-задачу и использовать агрегацию до или после джойна:
SELECT
u.client_id,
SUM(t.amount) AS total_amount
FROM clients u
LEFT JOIN transactions t ON u.client_id = t.client_id
GROUP BY
u.client_id;
SQL — язык декларативный: мы описываем, что хотим получить, а не как. Но понимание того, как база данных интерпретирует наши инструкции, спасает от часов дебага и неверных продуктовых решений.
А какие SQL-ошибки вы совершали в начале своего пути или чаще всего встречаете на код-ревью?
Если вам интересна аналитика как профессия или вы рассматриваете вход в неё осознанно, я делюсь практическими наблюдениями, кейсами и разбором ошибок в своём Telegram-канале!


