5 SQL‑ошибок

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

Сегодня разберем пять неочевидных нюансов 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-канале!

Read More

LEAVE A REPLY

Please enter your comment!
Please enter your name here