Почему LLM пишут неправильный SQL — и что это значит для вашей БД

Большая часть SQL, который генерируют LLM, не падает. Он выполняется. Возвращает строки. И в этом — главная проблема.

Когда AI пишет код, который не компилируется, вы это сразу видите. Когда AI пишет SQL, который возвращает 1000 строк вместо правильных 1043 — никто этого не замечает до тех пор, пока кто-то не сравнит цифры с другим источником.

Пять типов правдоподобно-неправильного SQL

1. Тихие JOIN-ы

LLM знает, что таблицы orders и customers связаны. Спросишь его про «orders by customer name» — он напишет INNER JOIN, и всё выглядит хорошо. Но в данных есть orders с customer_id = NULL (гость-заказы, легаси-импорт). INNER JOIN молча их выбросит. Вы получаете цифру «всех заказов», которая отличается от настоящей на 4%.

Правильный SQL должен быть LEFT JOIN либо явно отфильтрованный inner. LLM по умолчанию выбирает INNER, потому что в учебных примерах он чаще встречается.

2. Неправильные time-buckets

«Покажи продажи по неделям за последний год.» LLM генерирует GROUP BY DATE_TRUNC('week', sale_date). Это работает, но:

  • DATE_TRUNC('week', …​) в Postgres начинает неделю с понедельника. В вашей компании — с воскресенья.

  • sale_date хранится в UTC, а отчёт нужен в локальной timezone — заказы после 22:00 каждый день уезжают в следующий день.

  • Граница года: первая «неделя» 2026-го может включать дни декабря 2025-го.

Каждый из этих багов даёт график, который выглядит правдоподобно, но смещён.

3. Aggregations поверх дубликатов

SELECT customer_id, SUM(amount) FROM orders JOIN order_items …​ без понимания структуры связи. Если у одного order несколько items, SUM(amount) посчитает order-level amount несколько раз. Цифры умножаются.

LLM не знает кардинальность ваших таблиц. Он угадывает из имён. И часто угадывает неверно.

4. Игнорирование soft-delete

В вашей схеме у users есть колонка deleted_at. Всем известно, что для активных юзеров нужен WHERE deleted_at IS NULL. LLM этого не знает, если ему не сказать. Запросы возвращают активных + удалённых, ваши метрики растут «на ровном месте».

5. Subtle window-function ошибки

RANK() vs DENSE_RANK() vs ROW_NUMBER() — три разных функции, которые в коротких примерах выглядят как одно и то же. LLM иногда выбирает RANK() там, где смысл — ROW_NUMBER(), и при ничьих ваши топ-10 превращаются в топ-15.

Почему это происходит

LLM учились на publicly visible SQL — то есть туториалы, Stack Overflow, документация. В этом корпусе:

  • Схемы простые, без soft-delete и legacy-полей.

  • JOIN-ы всегда INNER, потому что туториалы не показывают edge cases.

  • Timezones явно не упоминаются.

  • Дубликаты в данных не считаются — потому что в туториалах их нет.

LLM не знает ваши данные. Он знает «типичный SQL», и его типичный SQL — это SQL для несуществующей идеальной схемы.

Что с этим делать архитектурно

1. Source of truth — не AI

Важные метрики (revenue, MAU, churn) должны вычисляться через curated queries, ревьюенные дата-инженерами. LLM-сгенерированный SQL можно использовать для эксплоратики и one-off вопросов, но не для production-дашбордов.

2. Тесты с известным ответом

Для каждого «продакшен-запроса» должна быть пара (запрос, известный правильный результат на тестовых данных). Если AI потом перепишет запрос, тест поймает регрессию.

3. Контекст в промпте

Если вы всё-таки даёте AI генерировать SQL, передавайте ему:

  • схему таблиц с комментариями (soft-delete, nullability, кардинальность);

  • примеры известно-правильных запросов схожего типа;

  • список «грязных» полей (legacy, deprecated).

Это отнимает работу до запроса, но снижает галлюцинации в разы.

4. Read replica + slow query log

LLM-сгенерированные запросы часто неэффективны (full scans, бессмысленные подзапросы). Прогоняйте их через read-replica, а не главную БД, и логируйте всё дольше 1 секунды для ревью.

5. UI, который показывает «AI guessed»

Каждый SQL, сгенерированный AI, должен быть помечен в UI и недоступен для запуска одной кнопкой «execute». Минимальный friction: explain plan + diff с похожим curated-запросом перед запуском.

Главное

LLM не делают явно плохой SQL — они делают правдоподобно плохой SQL. Это хуже: ошибка диффундирует в данные, в отчёты, в принимаемые решения. Чтобы безопасно использовать AI с БД, нужно встроить процесс верификации между генерацией и production-использованием. Иначе ваш data-driven становится LLM-driven, что не одно и то же.