Article

Data-to-Text без LLM: как генерировать тысячи описаний прямо в SQL

Я генерирую 50 000 markdown-отчётов за 30 секунд в ClickHouse — детерминированно, бесплатно, со 100% точностью чисел. Пока все играются с RAG, я пишу CASE WHEN.

Data EngineeringSQLAI без AI14 мин
Мадияр Хамзанов
Мадияр Хамзанов
1 мая 2026

Я столкнулся с реальной задачей: для проекта по аналитике госзакупок Казахстана нужно было сгенерировать длинный markdown-отчёт по каждой из ~50 000 организаций-заказчиков — с метриками, трендами, рейтингом и рекомендациями. Если каждую гонять через GPT-4o или Claude API — улетит несколько тысяч долларов и часы ожидания. И главное — нестабильно: одна и та же организация в разные дни даст разный текст.

Решение, которое я нашёл, до боли скучное и до боли эффективное — Data-to-Text прямо в SQL. Шаблоны с CASE WHEN, конкатенация строк, форматирование цифр в «млрд / млн / тыс ₸», интерпретация корреляций как «сильный рост». Результат — полноценный markdown-отчёт, посчитанный за миллисекунды, бесплатно, детерминированно, на 100% точно.

Пока все играются с RAG, агентами и MCP-серверами — я пишу CASE WHEN и не плачу за токены. Это не вместо LLM — это до LLM. И в 90% задач описаний по структурированным данным этого хватает с головой.

$0.01
стоимость 100k описаний
только compute, без токенов
30 сек
время генерации
на ClickHouse-кластере
100%
детерминизм
один и тот же ввод = один и тот же текст
$1 250
та же задача через GPT-4o
100k × 5k токенов output

Проблема: 100k описаний и LLM-счёт на $1 250

Прикинем на пальцах. У меня было ~50 000 организаций. Для каждой нужен отчёт на ~5 000 токенов вывода (markdown-таблицы, интерпретация трендов, топ-10 закупок, скоринг). Берём актуальные цены Anthropic и OpenAI.

базовая цена
GPT-4o (output)
$625
50k × 5k = 250M output tokens × $2.50/M
ещё и rate limits
Claude Sonnet 4
$562
250M tokens × $2.25/M output
125 000× дешевле
SQL D2T
$0.005
30 секунд compute на ClickHouse

И это только output-токены. Если ещё забивать в input сами данные по организации — добавляем ещё 1-2 тыс. input-токенов на запрос. По реальной нагрузке — даже rate limits Tier 4 у OpenAI (1M TPM) не позволят пройти 50k запросов быстрее, чем за 5-10 часов. Ну и батч-обработка через Batch API режет цену вдвое, но всё равно — это не «бесплатно за 30 секунд».

Ещё одна фишка LLM, о которой все молчат: числовые галлюцинации. Я тестировал — модель легко превращает «total_volume = 1 234 567 890» в «1.23 миллиона тенге» вместо «1.23 млрд». На сотне отчётов этого не заметишь. На 50 000 — это репутационная катастрофа.

Что такое Data-to-Text (и почему это не AI-новинка)

Data-to-Text generation (D2T) — подзадача Natural Language Generation (NLG): берём структурированные данные (таблицу, JSON, граф) и на выходе получаем человеческий текст. Задаче больше 30 лет — академический интерес начался ещё в 1990-х (Reiter & Dale, «Building Natural Language Generation Systems», 2000).

Подходов исторически три:

MY CHOICE
Template-based
Шаблоны с подстановкой переменных и условной логикой. Быстро, дёшево, детерминированно. Это именно то, что я делаю в SQL. Используется в meteo-сводках, спортивных отчётах, e-commerce.
Rule-based
Грамматические правила, словари, морфология. Гибче шаблонов, но требует лингвиста на проекте. Используется в академических системах и mature enterprise решениях вроде Arria NLG.
Neural / LLM
GPT, Claude, T5. Самый гибкий, самый креативный — и самый дорогой / непредсказуемый. Хорош для свободного текста, плох для длинного фактологического вывода по структурированным данным.

Огромный пласт ресёрча — например RotoWire dataset (NBA-отчёты) и Wiseman et al. (2017) — посвящены попыткам обучить нейронки писать про спорт по таблице со статистикой. Спойлер: они до сих пор делают фактологические ошибки. Шаблоны — нет.

Индустриальные D2T-движки — Arria NLG, Yseop, Automated Insights — десятилетиями зарабатывают деньги на финансовых отчётах и автомате новостей. Bloomberg, Forbes, Yahoo Finance долгое время использовали Wordsmith от Automated Insights для генерации квартальных отчётов. Никаких LLM — чистые правила и шаблоны.

Самый дешёвый и надёжный способ сгенерировать текст — это не сгенерировать его в рантайме. Сгенерируйте его один раз заранее в SQL и кэшируйте.
Автор

LLM против SQL-шаблонов: честное сравнение

Я не говорю «D2T лучше LLM везде». Я говорю — у каждого свой кейс. Вот разрез, после которого выбор очевиден.

ПараметрLLM (Claude / GPT)SQL Templates
Скорость на 1 описание5–30 секунд<1 миллисекунда
Стоимость на 100k записей$200 – $2 000$0 (только compute)
ДетерминизмНет (даже при temperature=0)100%
Точность чиселВозможны галлюцинацииТочно по БД
Креативность / вариативностьВысокаяОграничена шаблоном
Изменение текстаПоправить промптПереписать SQL + пересчёт
Требует интернетаДаНет
Privacy / on-premСложноЛегко (всё в БД)
Работает на 1B+ записейНевозможноСтандартный workload

Профиль: LLM vs SQL D2T (0–100, выше = лучше)

Субъективная оценка автора по 5 параметрам на основе годового опыта обоих подходов в проде.

Реальный кейс: 50 000 отчётов по госзакупкам Казахстана

Контекст: я работаю с открытыми данными Госзакупок РК — там есть API и около миллиона лотов в год. Дата-стек: ClickHouse — потому что это самая быстрая колоночная БД для агрегаций и текстовых функций. Цель: для каждой организации-заказчика сделать markdown-отчёт с разделами:

  • Главные показатели (объём, лотов, контрагентов)
  • Характер закупок (категории, типичные коды)
  • Динамика за 2 года + сезонность
  • Топ-10 закупок по сумме
  • Финансовые метрики (средний чек, экономия от стартовой цены)
  • Скоринг 0–100 с интерпретацией

На выходе — ~5 000 символов markdown на организацию × 50 000 организаций = ~250 МБ текста, который генерится за 30 секунд одним SQL-запросом из таблицы метрик org_scoring_base в таблицу org_scoring_raw. Дальше — индексация в Postgres + поиск, и текст просто читается из таблицы. Никаких LLM-вызовов в рантайме.

Пример 1: Форматирование чисел человекочитаемо

Самая частая микрозадача в D2T — 1234567890 превратить в 1.23 млрд ₸. В ClickHouse есть multiIf — компактнее каскада CASE WHEN.

-- ClickHouse: форматирование объёма закупок
multiIf(
    org_total_volume >= 1000000000,
        concat(toString(ROUND(org_total_volume / 1000000000.0, 2)), ' млрд ₸'),
    org_total_volume >= 1000000,
        concat(toString(ROUND(org_total_volume / 1000000.0, 1)), ' млн ₸'),
    org_total_volume >= 1000,
        concat(toString(ROUND(org_total_volume / 1000.0, 0)), 'к ₸'),
    concat(toString(toUInt64(org_total_volume)), ' ₸')
) AS volume_human

Один проход по миллиарду строк — секунды. Без AI. Без галлюцинаций.

Пример 2: Интерпретация трендов через CASE WHEN

У меня посчитана Pearson-корреляция квартальных объёмов закупок с временем — число от -1 до 1. Хочу превратить его в человеческий текст.

-- Перевод корреляции в текстовое описание тренда
CASE
    WHEN org_volume_trend > 0.7  THEN
        concat('Сильный рост (корреляция: +',
               toString(ROUND(org_volume_trend, 2)), ')')
    WHEN org_volume_trend > 0.3  THEN 'Умеренный рост'
    WHEN org_volume_trend > -0.3 THEN 'Стабильный объём'
    WHEN org_volume_trend > -0.7 THEN 'Умеренное снижение'
    ELSE
        concat('Сильное снижение (корреляция: ',
               toString(ROUND(org_volume_trend, 2)), ')')
END AS trend_label

Статистика → текст за один JOIN. Никакого «model.generate()».

Пример 3: Markdown-таблица генерится прямо в SQL

Самое красивое — markdown собирается concat из колонок. Пользователь увидит готовую таблицу без preprocessing на бэкенде.

-- Генерация markdown-таблицы прямо в строку
concat(
    '## Главные показатели\n\n',
    '| Метрика       | Значение            |\n',
    '|---------------|---------------------|\n',
    '| Объём закупок | ', volume_human,           ' |\n',
    '| Лотов         | ', toString(lots_count),    ' |\n',
    '| Контрагентов  | ', toString(suppliers),     ' |\n',
    '| Средний чек   | ', avg_check_human,         ' |\n',
    '| Скоринг       | ', toString(total_score),  '/100 |\n\n'
) AS markdown_table

В колонке БД у меня лежит готовый markdown-кусок. Никаких Jinja-шаблонов на бэкенде, никакой логики в API — отрендерил и отдал.

Пример 4: Числа → слова (месяц, день недели, регион)

Сезонность — топ-1 или топ-2 по числу закупок месяц. Для читаемости показываем словами.

-- Месяц-пик в виде слова
CASE peak_month
    WHEN 1  THEN 'Январь'   WHEN 2  THEN 'Февраль'
    WHEN 3  THEN 'Март'     WHEN 4  THEN 'Апрель'
    WHEN 5  THEN 'Май'      WHEN 6  THEN 'Июнь'
    WHEN 7  THEN 'Июль'     WHEN 8  THEN 'Август'
    WHEN 9  THEN 'Сентябрь' WHEN 10 THEN 'Октябрь'
    WHEN 11 THEN 'Ноябрь'   WHEN 12 THEN 'Декабрь'
END AS peak_month_label

Пример 5: Полный отчёт — сборка всего вместе

Финал — гигантский concat всех частей в одну колонку. Это и есть итоговое поле org_report_md в таблице org_scoring_raw.

INSERT INTO org_scoring_raw
SELECT
    org_id,
    org_name,
    concat(
        '# ', org_name, '\n\n',
        '> Скоринг: **', toString(total_score), '/100**',
        ' • ', score_grade, '\n\n',
        markdown_table,
        '## Динамика закупок\n\n',
        'Тренд за 2 года: **', trend_label, '**.\n',
        'Пик активности — **', peak_month_label, '**.\n\n',
        '## Рекомендации\n\n',
        recommendations_md
    ) AS org_report_md
FROM org_scoring_base
WHERE org_total_volume > 0;

Замер на моём кейсе: 50 000 организаций × ~5 000 символов = 250 МБ markdown. ClickHouse-кластер из 3 нод проходит за 32 секунды. На макбуке локально — около 2 минут. То же самое через Claude Sonnet API заняло бы по моим прикидкам ~10 часов и стоило бы $562 за один прогон.

Архитектура решения

Главная идея — текст хранится в БД как готовое поле. Никаких генераций на лету, никаких prompt-кэшей, никаких inference-серверов. Изменился шаблон — пересчитал таблицу за 30 секунд. Это в чистом виде ELT-подход (extract → load → transform), как его проповедуют dbt и современные дата-стеки.

1
Сырые данные
Госзакупки → ClickHouse stg-слой. Партиционирование по месяцам, ZSTD-сжатие, dictionary encoding для категориальных полей.
2
SQL-агрегация
stg → org_scoring_base. Считаем все метрики: объёмы, тренды, корреляции, скоринг 0–100. Чистые числа.
3
SQL-template
org_scoring_base → org_scoring_raw. CASE WHEN, multiIf, concat — собираем готовый markdown в колонке org_report_md.
4
API + кэш
Postgres / Redis / S3 — где удобно. Но главное: текст уже сделан. Сервер просто отдаёт строку из БД за единицы миллисекунд.

Ключевой инсайт: при таком подходе у тебя на проде нет зависимости от LLM-провайдера. OpenAI упал, Anthropic поднял цены, Gemini поменял API — тебе всё равно. Текст уже в БД.

Стоимость: реальные цифры

Считаем стоимость генерации 100 000 описаний по 5 000 output-токенов = 500M output-токенов суммарно. Цены input я опускаю для простоты — они меньше output, но добавили бы ещё 10–30% к итоговому счёту.

Стоимость генерации 100k × 5k токенов = 500M output, $

Цены на май 2026 (Anthropic, OpenAI, Google AI). Лог-шкала.

Даже самая дешёвая модель — GPT-4o mini — обойдётся в ~$45 за прогон. SQL D2T — 0.01 цента (это даже не доллар). Разница в ~5 000–125 000 раз. И это без учёта rate limits, retry-логики, fallback на другую модель и стоимости разработки prompt-инфры.

Время на 100k описаний (секунды, лог-шкала)

Учитывая rate limits публичных API и concurrency. Источник — собственные замеры + публичные лимиты Anthropic.

Когда D2T работает идеально

Любая задача с фиксированной структурой и преобладанием цифр над эмоциями. Вот реальные индустриальные кейсы — большинство этих систем работают на template-based подходе годами.

E-commerce
Описания товаров из характеристик. Wildberries, Amazon, Ozon генерят миллионы карточек по шаблонам — материал → цвет → размер → текст.
Спорт-аналитика
Otчёты о матчах: «Команда X выиграла со счётом Y благодаря Z». Это та самая RotoWire-задача NBA-репортов из 2017.
Финансы
Квартальные отчёты компаний. Bloomberg, Forbes, AP Wire долго генерили их Wordsmith'ом — без LLM.
Метеорология
BBC и Met Office используют D2T для прогнозов: числа → «Sunny intervals with light winds, 12°C».
Госзакупки / тендеры
Мой кейс. Метрики организации → структурированный отчёт на 5 000 символов. Идеальное попадание.
Логистика / недвижимость
Описания объектов из карточки: 80м², 3 комнаты, 5/9 этаж, метро 7 минут → готовое объявление.

Ещё один индустриальный пример — Yseop делает финотчёты для Société Générale и Crédit Agricole с 2007 года. Все эти годы — никаких LLM. Просто очень умные rule-based движки.

Когда D2T не подойдёт (и я честно говорю)

D2T — не серебряная пуля. Я бы не стал применять этот подход в задачах, где:

  • Нужна креативность и вариативность — маркетинговые слоганы, тизеры, лонгриды
  • Структура текста часто меняется — каждую неделю редактор переписывает форму
  • Контекст из 100+ переменных с нелинейными связями — комбинаторный взрыв CASE WHEN
  • Нужны диалоги, чат-боты с открытыми темами
  • Перевод между языками с морфологическими нюансами (хотя для шаблонной части D2T переводится в i18n тривиально)

Простое правило: если ты можешь нарисовать дерево решений для текста на одной А4 — D2T справится. Если нет — нужен LLM.

Гибридный подход: лучшее из двух миров

В реальной жизни я не выбираю «или–или». Я делаю D2T для фактологии + LLM для оживления раз в N запросов. Схема:

1
SQL: факты + структура
Числа, таблицы, тренды, скоринг — всё через template. 99% контента отчёта.
2
LLM: вступление + выводы
Раз в месяц или по запросу пользователя — Claude Haiku пишет 3-абзацное executive summary. ~$0.001 за штуку.
3
Кэш
Результат LLM-кусочка кэшируем в Redis / Postgres с TTL. Токены тратим только на новые / изменившиеся записи.

Альтернативно — если нужен интерактивный чат «расскажи мне про эту организацию подробнее», я не гоню весь отчёт в контекст. Делаю RAG по pgvector / Qdrant — с эмбеддингами по тем же D2T-сгенерированным кускам. Получается быстро и дёшево: D2T-куски как factual ground truth, LLM как переводчик в диалоговый формат.

Чеклист: когда я выбираю D2T

Если у тебя >=5 пунктов из этого списка — даже не думай, бери template-based. LLM добавишь потом точечно.

  1. Источник — структурированные данные: БД, JSON, CSV. Не аудио, не видео, не свободный текст.
  2. Структура отчёта — фиксированная: «секция А, потом таблица B, потом график C» — одинаково для всех записей.
  3. Объём ≥ 1 000 записей: на маленьких объёмах LLM выгоднее по цене разработки. На больших — D2T побеждает экономически.
  4. Точность чисел критична: финансы, медицина, право, госзакупки — везде, где галлюцинация = жалоба или иск.
  5. Нужна детерминированность: юридические тексты, отчёты для регуляторов, пользователь должен видеть одно и то же.
  6. Низкая латентность важна: нужно отдавать текст за миллисекунды, без ожидания API.
  7. Privacy / on-prem: данные не должны уходить во внешний LLM-API. SQL крутится у тебя на сервере.

Я давно перестал подходить к таким задачам через «давайте натренируем модель» или «давайте промпт сделаем». Сначала всегда вопрос: «это можно решить SQL-ом?» — и в 9 случаях из 10 ответ да. LLM остаётся для оставшегося 1 случая, где нужна творческая работа с текстом. И это не отступление от AI-эпохи — это инженерная зрелость.

LLM — это не молоток, которым нужно бить по каждому гвоздю. Это очень дорогой и очень умный инструмент. Дёшевые гвозди забивайте дёшевым молотком — SQL'ом.
Автор, urok из проды