Ошибки в SQL-запросах: тексты сообщений, причины и исправления
Частые ошибки SQL с дословными текстами PostgreSQL и DuckDB: GROUP BY, column does not exist, ambiguous, division by zero, типы и даты, и пять запросов, которые молча врут.
Содержание статьи
Ошибку в SQL обычно ищут, скопировав сообщение в поиск: column "users.country" must appear in the GROUP BY clause, relation "payment" does not exist, division by zero. Эта страница устроена под такой поиск: заголовок — дословный текст сообщения, под ним причина и исправление. Тексты получены на PostgreSQL 16 и на DuckDB — той базе, на которой работает песочница SQL-курса. Во второй половине — ошибки опаснее: запрос выполняется, а число неверное. На учебной базе курса такой запрос показывает, что платят 100% пользователей каждого канала, хотя на самом деле платят от 8,5% до 26,8%.
Коротко
Сообщение об ошибке почти всегда называет и место, и объект. Прочитайте его целиком, включая строку LINE, стрелку под ней и HINT: в половине случаев там уже написано исправление.
- Колонка без агрегата должна стоять в GROUP BY. Условие на агрегат — в HAVING, а не в WHERE.
- Алиас из SELECT PostgreSQL не видит в WHERE и HAVING, DuckDB видит. Запрос из песочницы может упасть на рабочей базе.
- Строки в SQL пишут в одинарных кавычках. Двойные означают имя колонки:
"organic"— это колонка organic, которой нет. - PostgreSQL останавливает запрос на делении на ноль, DuckDB возвращает Infinity.
NULLIF(знаменатель, 0)даёт NULL в обеих. - Самые дорогие ошибки выполняются без сообщения: условие на правую таблицу LEFT JOIN в WHERE, JOIN двух таблиц фактов, OR без скобок,
<>рядом с NULL и неполный последний день.
Сообщение, причина и исправление: сводная таблица
Тексты PostgreSQL 16. Соответствующие сообщения DuckDB приведены в разделах ниже.
| Сообщение PostgreSQL | Причина | Исправление |
|---|---|---|
| syntax error at or near "FROM" | лишняя запятая перед FROM или слово без AS | убрать запятую, алиас писать через AS |
| column "users.country" must appear in the GROUP BY clause or be used in an aggregate function | колонка без агрегата не в GROUP BY | добавить в GROUP BY или обернуть в агрегат |
| aggregate functions are not allowed in WHERE | count, sum и другие в WHERE | перенести условие в HAVING |
| window functions are not allowed in WHERE | row_number и другие окна в WHERE | посчитать окно в CTE, фильтровать снаружи |
| column "amout" does not exist | опечатка, алиас в WHERE, двойные кавычки | проверить имя, повторить выражение |
| relation "payment" does not exist | нет такой таблицы в search_path | проверить имя и схему |
| missing FROM-clause entry for table "p" | алиас таблицы не объявлен в FROM | добавить JOIN или исправить алиас |
| column reference "user_id" is ambiguous | колонка есть в двух таблицах | указать алиас таблицы |
| division by zero | знаменатель равен нулю | NULLIF(знаменатель, 0) |
| operator does not exist: date = integer | сравнение разных типов | литерал даты в кавычках или CAST |
| invalid input syntax for type integer: "abc" | строка не приводится к типу колонки | сравнивать с значением нужного типа |
| more than one row returned by a subquery used as an expression | скалярный подзапрос вернул несколько строк | агрегат, LIMIT 1 или JOIN |
| each UNION query must have the same number of columns | разное число колонок в частях UNION | выровнять списки колонок |
Как читать сообщение об ошибке SQL
PostgreSQL отвечает тремя-четырьмя строками. ERROR: — сам текст. LINE 1: — фрагмент запроса, и стрелка ^ под ним показывает символ, на котором разбор остановился. HINT: — подсказка, если база знает, что предложить. Стрелка указывает, где база сломалась, а не где ошиблись вы: при пропущенной запятой она стоит на следующем слове.
DuckDB начинает сообщение с класса ошибки, и он сразу говорит, на каком шаге всё сломалось. Parser Error — запрос не разобран, это синтаксис. Catalog Error — нет таблицы или функции. Binder Error — база не смогла связать имена: колонку, алиас, группировку. Conversion Error — значение не приводится к типу. Invalid Input Error — проблема в данных при выполнении.
Песочница SQL-курса работает на DuckDB и показывает те же ошибки по-русски, с подсказкой о вероятной причине. В DBeaver, psql или ноутбуке вы увидите оригинальный английский текст — именно его и имеет смысл копировать в поиск.
SELECT sum(amout) FROM payments;
-- ERROR: column "amout" does not exist
-- LINE 1: SELECT sum(amout) FROM payments;
-- ^
-- HINT: Perhaps you meant to reference the column "payments.amount".syntax error at or near: запятые, кавычки и алиасы без AS
Лишняя запятая перед FROM — самая частая синтаксическая ошибка: SELECT channel, count(*) AS users, FROM users. PostgreSQL отвечает syntax error at or near "FROM". DuckDB такую запятую прощает и выполняет запрос. Поэтому запрос, отлаженный в песочнице, может упасть при переносе в PostgreSQL.
Пропущенная запятая даёт ошибку в неожиданном месте. В SELECT channel count(*) … обе базы пишут syntax error at or near "(": они прочитали count как алиас колонки channel и споткнулись о скобку.
Алиасы без AS — вторая ловушка. SELECT channel order FROM users падает в обеих базах, потому что ORDER — зарезервированное слово. DuckDB строже: count(*) share и channel ref там дают Parser Error: syntax error at or near "share", хотя PostgreSQL такие алиасы принимает. С AS все эти имена, включая AS order и AS user, работают в обеих базах. Правило простое: пишите AS всегда.
Одинарные и двойные кавычки не взаимозаменяемы. WHERE channel = "organic" ищет колонку с именем organic. PostgreSQL: column "organic" does not exist. DuckDB: Referenced column "organic" not found in FROM clause! со списком похожих колонок. Строковые значения — только в одинарных кавычках: 'organic'.
column must appear in the GROUP BY clause or be used in an aggregate function
Запрос SELECT channel, country, count(*) FROM users GROUP BY channel просит одну строку на канал, но в каждом канале несколько стран, и база не знает, какую показать. PostgreSQL отвечает column "users.country" must appear in the GROUP BY clause or be used in an aggregate function. DuckDB — column "country" must appear in the GROUP BY clause or must be part of an aggregate function. и предлагает ANY_VALUE(country), если конкретное значение неважно.
Исправления два, и выбор зависит от вопроса. Если нужна разбивка по каналу и стране, добавьте country в GROUP BY: строк станет больше. Если нужна одна строка на канал, оберните country в агрегат: count(DISTINCT country) или string_agg. ANY_VALUE подходит только для колонок, которые внутри группы одинаковы по смыслу, иначе результат случаен.
Та же ошибка появляется и без GROUP BY. SELECT channel FROM users HAVING count(*) > 10 превращает всю таблицу в одну группу, и channel снова оказывается без агрегата.
SELECT channel, country, count(*) AS users
FROM users
GROUP BY channel, country
ORDER BY users DESC
LIMIT 3;
-- organic RU 1015, referral RU 614, paid_search RU 601aggregate functions are not allowed in WHERE
WHERE фильтрует строки до группировки, поэтому агрегата в нём ещё нет. WHERE count(*) > 1000 в PostgreSQL даёт aggregate functions are not allowed in WHERE, в DuckDB — WHERE clause cannot contain aggregates!. Условие на результат группировки пишут в HAVING: HAVING count(*) > 1000 оставляет organic, referral и paid_search.
С оконными функциями то же самое, только HAVING не поможет. WHERE row_number() OVER (…) = 1 падает с window functions are not allowed in WHERE в PostgreSQL и WHERE clause cannot contain window functions! в DuckDB. Окно считают в CTE, фильтруют снаружи. В DuckDB есть сокращение QUALIFY, но PostgreSQL его не знает и отвечает syntax error at or near "row_number".
Вложить агрегат в агрегат тоже нельзя: avg(count(*)) в обеих базах даёт aggregate function calls cannot be nested. Среднее по группам считают в два шага: сначала count по группе в подзапросе или CTE, потом avg снаружи.
WITH ranked AS (
SELECT
user_id,
paid_at,
row_number() OVER (PARTITION BY user_id ORDER BY paid_at, payment_id) AS rn
FROM payments
)
SELECT count(*) AS first_payments
FROM ranked
WHERE rn = 1;
-- 912column does not exist: опечатка, алиас в WHERE и HAVING
Опечатку в имени PostgreSQL ловит с подсказкой: column "amout" does not exist и HINT: Perhaps you meant to reference the column "payments.amount". DuckDB: Referenced column "amout" not found in FROM clause! и список кандидатов, первым из которых стоит amount.
Сложнее, когда имя верное, но база его ещё не знает. SELECT amount * 0.8 AS net FROM payments WHERE net > 30 падает в PostgreSQL с column "net" does not exist: WHERE выполняется раньше SELECT, и алиаса net на этом шаге нет. DuckDB этот запрос выполняет. То же с HAVING: HAVING user_count > 1000 в PostgreSQL даёт column "user_count" does not exist, в DuckDB работает.
Есть и вовсе странный вариант. Если алиас совпал с именем таблицы, как count(*) AS users в запросе к таблице users, то HAVING users > 1000 в PostgreSQL падает с operator does not exist: users > integer: имя таблицы там обозначает целую строку. Переносимое исправление одно — повторить выражение: WHERE amount * 0.8 > 30, HAVING count(*) > 1000. Если выражение длинное, вынесите его в CTE.
relation does not exist и missing FROM-clause entry
relation "payment" does not exist означает, что PostgreSQL не нашёл таблицу с таким именем в схемах из search_path. Причин три: опечатка (здесь — единственное число вместо payments), таблица в другой схеме и нужно писать analytics.payments, или имя создано в кавычках с заглавными буквами и теперь требует тех же кавычек. DuckDB пишет Table with name payment does not exist! и сразу предлагает Did you mean "payments"?
missing FROM-clause entry for table "p" — вы обращаетесь к алиасу, которого нет в FROM. Обычно JOIN удалили или закомментировали, а колонку p.amount в SELECT оставили. DuckDB: Referenced table "p" not found! со списком доступных алиасов.
column reference is ambiguous
После JOIN колонка user_id есть и в users, и в payments. SELECT user_id … GROUP BY user_id не говорит, какую взять. PostgreSQL: column reference "user_id" is ambiguous. DuckDB подсказывает оба варианта: Ambiguous reference to column name "user_id" (use: "u.user_id" or "p.user_id").
Исправление — указать алиас таблицы: u.user_id. Для LEFT JOIN это не формальность: у пользователей без платежей p.user_id будет NULL, а u.user_id — нет. Если колонки соединения называются одинаково, JOIN payments p USING (user_id) склеивает их в одну, и неоднозначности не возникает.
division by zero: деление на ноль в SQL
Конверсия, средний чек и доля — всё это деление, и знаменатель может оказаться нулём: в сегменте нет пользователей, в дне нет заказов. PostgreSQL останавливает весь запрос с division by zero, даже если ноль был в одной группе из ста.
DuckDB не падает. Деление через / там возвращает Infinity, целочисленное // — NULL. Infinity опаснее ошибки: он проходит в отчёт, ломает средние и сортировку, а в BI может выглядеть как очень большое число.
Переносимое решение — NULLIF(знаменатель, 0): при нуле он превращается в NULL, и результат деления тоже NULL в обеих базах. Отдельно следите за целочисленным делением: в PostgreSQL 912 / 4613 равно 0, а не 0,198. Подробно об этом — в статье про типы данных и CAST.
SELECT
channel,
count(*) / NULLIF(sum(CASE WHEN country = 'XX' THEN 1 ELSE 0 END), 0) AS ratio
FROM users
GROUP BY channel
ORDER BY channel;
-- без NULLIF: PostgreSQL — ERROR: division by zero, DuckDB — Infinity
-- с NULLIF: NULL во всех четырёх строкахoperator does not exist и invalid input syntax: ошибки типов
Дату сравнили с числом: WHERE signup_date = 20260715. PostgreSQL: operator does not exist: date = integer и совет добавить явное приведение. DuckDB: Conversion Error: Unimplemented type for cast (INTEGER -> DATE). Дату пишут строкой в формате ISO: '2026-07-15' или DATE '2026-07-15'.
Дату написали в русском формате: WHERE signup_date >= '15.07.2026'. PostgreSQL: date/time field value out of range: "15.07.2026" и HINT: Perhaps you need a different "datestyle" setting. DuckDB: invalid date field format: "15.07.2026", expected format is (YYYY-MM-DD). Хуже, когда число дня не больше двенадцати: '03.08.2026' PostgreSQL при настройке MDY молча прочитает как 8 марта.
Число сравнили со строкой: WHERE user_id = 'abc'. PostgreSQL: invalid input syntax for type integer: "abc", DuckDB: Could not convert string 'abc' to INT32. Такое сообщение часто приходит из BI-фильтра, который подставил текст в числовое поле.
Функции другой базы дают свою ошибку: datediff в PostgreSQL — function datediff(unknown, date, date) does not exist. Какие функции даты есть в какой СУБД, собрано в справочнике функций даты.
more than one row returned by a subquery, UNION и ORDER BY
Скалярный подзапрос в SELECT обязан вернуть одну строку. (SELECT p.amount FROM payments p WHERE p.user_id = u.user_id) возвращает несколько строк у тех, кто продлевал подписку. PostgreSQL: more than one row returned by a subquery used as an expression. DuckDB: More than one row returned by a subquery used as an expression - scalar subqueries can only return a single row. Исправление — агрегат в подзапросе (sum, max) или JOIN с группировкой.
Части UNION должны возвращать одинаковое число колонок. PostgreSQL: each UNION query must have the same number of columns, DuckDB: Set operations can only apply to expressions with the same number of result columns.
ORDER BY по номеру колонки, которой нет: ORDER BY 3 при двух колонках. PostgreSQL: ORDER BY position 3 is not in select list, DuckDB: ORDER term out of range - should be between 1 and 2. Обычно колонку удалили из SELECT, а номер в сортировке остался. Поэтому сортируйте по имени, а не по номеру.
Запрос выполнился, но число неверное: LEFT JOIN и условие в WHERE
Ошибки с сообщением безопасны: запрос не выполнился, и неверное число никуда не ушло. Дальше — пять ошибок, которые выполняются без сообщения. Все они найдены на учебной базе, и во всех PostgreSQL и DuckDB дают одинаковый неверный ответ.
Задача: доля плательщиков по каналам. Пользователей соединяют с платежами через LEFT JOIN, чтобы не потерять неплативших, а условие «только первый платёж» пишут в WHERE. У неплативших все колонки payments равны NULL, и WHERE p.payment_type = 'first' отбрасывает их. LEFT JOIN превращается в INNER: в выборке 912 пользователей, все они плательщики, доля в каждом канале — 100%.
Условие на правую таблицу LEFT JOIN пишут в ON: LEFT JOIN payments p ON p.user_id = u.user_id AND p.payment_type = 'first'. Тогда в выборке 4 613 пользователей, и доля получается честной. Подробнее о потерях и дублях в соединениях — в статье про JOIN.
| Канал | WHERE: пользователей | WHERE: доля | ON: пользователей | ON: доля |
|---|---|---|---|---|
| organic | 410 | 100% | 1 747 | 23,5% |
| referral | 278 | 100% | 1 037 | 26,8% |
| partner | 138 | 100% | 814 | 17,0% |
| paid_search | 86 | 100% | 1 015 | 8,5% |
Две таблицы фактов в одном JOIN раздувают сумму
Задача: выручка и число открытий приложения по каналам в одной таблице. Естественный запрос соединяет users с payments и с events. Но у пользователя несколько платежей и много событий, и каждая пара «платёж × событие» становится строкой. Выручка умножается на число открытий, открытия — на число платежей.
Итог по всей базе: вместо 30 639 выручки запрос показывает 235 833, в 7,7 раза больше. Ошибка неравномерная: у organic выручка завышена в 8 раз, у paid_search — в 4,2. Сравнение каналов искажено, а числа выглядят правдоподобно.
Лечится сменой порядка: сначала каждую таблицу фактов свернуть до зерна «канал», потом соединить свёрнутые результаты. Две CTE и один JOIN по каналу — и выручка сходится с суммой по payments.
Всего 235 833 против 30 639. Учебная база SQL-курса, одинаково в PostgreSQL 16 и DuckDB.
WITH revenue AS (
SELECT u.channel, sum(p.amount) AS revenue
FROM users u
JOIN payments p ON p.user_id = u.user_id
GROUP BY u.channel
),
opens AS (
SELECT u.channel, count(*) AS app_opens
FROM users u
JOIN events e ON e.user_id = u.user_id
WHERE e.event_name = 'app_open'
GROUP BY u.channel
)
SELECT r.channel, r.revenue, o.app_opens
FROM revenue r
JOIN opens o ON o.channel = r.channel
ORDER BY r.channel;
-- organic 14114 / 12265, paid_search 2467 / 3870,
-- partner 4732 / 4576, referral 9326 / 7982AND без скобок и <>, который теряет NULL
AND выполняется раньше OR. Условие «платные каналы на мобильных» часто пишут так: WHERE channel = 'paid_search' OR channel = 'partner' AND device = 'mobile'. База читает его как «весь paid_search плюс мобильный partner» и возвращает 1 343 пользователя. Правильный ответ — 998: WHERE channel IN ('paid_search', 'partner') AND device = 'mobile'. Когда в условии есть и AND, и OR, ставьте скобки или заменяйте OR на IN.
Сравнение с NULL не бывает истинным. WHERE country <> 'RU' возвращает 1 843 пользователя, хотя не из России 1 898: 55 человек без страны пропали, потому что NULL <> 'RU' — это NULL, а не true. Если пропуски должны попасть в выборку, пишите country IS DISTINCT FROM 'RU' или добавляйте OR country IS NULL.
Неполный последний день выглядит как падение
Выгрузка учебной базы закрыта 30 августа в 12:59. За этот день 226 событий против 588 накануне и 570–690 в остальные дни недели. На графике по дням последняя точка обрывается вниз, и первый вопрос в чате — «что сломалось 30 августа». Не сломалось ничего: день ещё не закончился.
Правило: перед построением ряда проверьте max(event_time) и отрежьте незакрытый период или подпишите его как неполный. То же с последней неделей и последним месяцем. Инциденты в событиях — повторную доставку, поздние данные и неполные дни — разбирает пятнадцатая глава курса.
30 августа — неполный день: выгрузка закрыта в 12:59. Учебная база SQL-курса.
Чеклист перед отправкой результата
Пять минут проверки дешевле, чем исправление в чужой презентации.
- Сумма по группам сходится с общим итогом, посчитанным отдельным простым запросом.
- Число строк результата соответствует зерну: одна строка на пользователя, канал или день.
- После JOIN count(*) и count(DISTINCT ключ) совпадают, если строка должна быть одна на ключ.
- Условия на правую таблицу LEFT JOIN стоят в ON, а не в WHERE.
- В условиях с AND и OR расставлены скобки, сравнения с
<>учитывают NULL. - Каждое деление защищено
NULLIF(…, 0), хотя бы один операнд не целый. - Последний период полный или явно подписан как неполный.
- Запрос проверен в той базе, где он будет работать, а не только в песочнице.
Частые вопросы
Почему запрос работает в DuckDB и падает в PostgreSQL? DuckDB мягче: принимает висячую запятую, алиасы в WHERE и HAVING, QUALIFY, не падает на делении на ноль. Перед переносом проверьте эти места.
Что значит «ошибка выполнения SQL-запроса» в BI? Это обёртка BI-инструмента. Оригинальное сообщение базы обычно лежит в подробностях ошибки или в логе запроса, и искать нужно по нему.
Почему HAVING не видит алиас? В PostgreSQL HAVING выполняется до SELECT, и алиасов там ещё нет. Повторите агрегат: HAVING count(*) > 1000.
Как найти, в какой части длинного запроса ошибка? Запускайте CTE по одной: SELECT * FROM первая_cte LIMIT 10, затем следующую. Ошибка окажется в первой, которая не выполнится или вернёт неожиданное число строк.
Итог
Сообщение об ошибке — подсказка, а не приговор: оно называет объект, место и часто исправление. Опаснее запросы, которые выполнились. Их ловят не чтением кода, а сверкой: итог с суммой по группам, count(*) с count(DISTINCT), последний день с предыдущими.
Аудиту чужого SQL посвящена четырнадцатая глава курса: там на заданиях с проверкой ищут раздутую выручку, сломанный LEFT JOIN и дубли событий. Первые главы курса открыты бесплатно.
Материалы по теме
CASE WHEN в SQL: сегменты, условные метрики и порядок условий
CASE WHEN в SQL на учебной базе продукта: синтаксис, сегменты пользователей, почему порядок WHEN меняет ответ, ELSE и NULL, CASE внутри COUNT, в GROUP BY и ORDER BY.
Что такое SQL простыми словами: реляционная база, таблицы и запросы
Что такое SQL и реляционная база данных: таблица, строка и колонка, первый запрос с результатом, JOIN, команды языка, СУБД и диалекты, что SQL умеет и чего не умеет.
Функции даты в SQL: текущая дата, разница дат, EXTRACT и формат
Справочник функций даты в SQL на учебной базе: текущая дата, часть даты, date_trunc, разница дат в днях, часах и месяцах, интервалы, формат вывода и отличия СУБД.