Обычно SQL используют ради отчётов, аналитики и унылого «выгрузить за вчера». Но у языка запросов есть и другая, неожиданная сторона: если относиться к нему как к инструменту для сочинительства, можно попробовать написать рассказ. Сюжет, герои, диалоги — всё это вполне собирается на голом SQL. В статье я делюсь экспериментом, который начался ради шутки, а закончился странным ощущением, что база данных умеет рассказывать истории.
SQL я впервые выучил не ради красоты — нужен был для работы. Тогда казалось: язык скучный, служебный, без «души». SELECT, WHERE, JOIN… будто молоток или отвёртка. Но однажды, копаясь в старой демо-базе, я обратил внимание на то, что данные сами по себе напоминали короткие предложения. И пришла мысль: а что, если воспринимать таблицу не как набор строк, а как страницу романа?
Сначала это выглядело как дурацкая затея, но чем дальше я шёл, тем больше SQL переставал быть «сухим инструментом» и начинал вести себя как настоящий рассказчик.

Таблицы вместо сеттинга
Любой рассказ начинается с декораций. У писателя — это сеттинг, у нас — таблица. Простейший каркас истории выглядит так:
CREATE TABLE Characters (
id INT PRIMARY KEY,
name VARCHAR(50),
role VARCHAR(50),
mood VARCHAR(50)
);
CREATE TABLE Places (
id INT PRIMARY KEY,
name VARCHAR(50),
description TEXT
);
CREATE TABLE Events (
id INT PRIMARY KEY,
place_id INT,
character_id INT,
action TEXT,
FOREIGN KEY (place_id) REFERENCES Places(id),
FOREIGN KEY (character_id) REFERENCES Characters(id)
);
На этом этапе это всё ещё больше похоже на заготовку для какой-нибудь текстовой RPG, но я-то собираюсь писать «литературу».
Герои истории
Герои без базы данных не заведутся. Пусть будет программист, аналитик и ироничный ИИ.
INSERT INTO Characters (id, name, role, mood) VALUES
(1, 'Алекс', 'программист', 'усталый'),
(2, 'Ира', 'аналитик', 'вдохновлённая'),
(3, 'Бот', 'ИИ-ассистент', 'саркастичный');
Я заметил, что когда вручную вставляешь такие строки, они начинают звучать не как данные, а как заметки автора в блокноте. Почти слышишь голос персонажа.
Пространство как сцена
Чтобы что-то происходило, нужно место. Добавим его:
INSERT INTO Places (id, name, description) VALUES
(1, 'Офис', 'Открытое пространство с лампами дневного света'),
(2, 'Кафе', 'Небольшое место с запахом корицы и кофе');
Тут и началась магия: SQL внезапно стал похож на описание сцены в пьесе. Если убрать ключевые слова и скобки, получится почти готовый кусочек прозы.
Сюжетные события
Обычная литература движется событиями, у нас — INSERT INTO Events
.
INSERT INTO Events (id, place_id, character_id, action) VALUES
(1, 1, 1, 'пишет SQL-запрос'),
(2, 1, 2, 'смотрит на код и улыбается'),
(3, 2, 3, 'шепчет комментарии с сарказмом');
В этот момент я впервые ощутил, что делаю нечто среднее между «базой данных для CRM» и «литературным редактором».
SELECT как рассказчик
Теперь соберём первые фразы.
SELECT c.name || ' (' || c.role || ', ' || c.mood || ')' ||
' находится в ' || p.name || ' и ' || e.action AS narrative
FROM Events e
JOIN Characters c ON e.character_id = c.id
JOIN Places p ON e.place_id = p.id;
Результат оказался неожиданно похож на черновик:
Алекс (программист, усталый) находится в Офис и пишет SQL-запрос
Ира (аналитик, вдохновлённая) находится в Офис и смотрит на код и улыбается
Бот (ИИ-ассистент, саркастичный) находится в Кафе и шепчет комментарии с сарказмом
SQL впервые заговорил «человеческим» языком.
Диалоги через UNION
Чтобы оживить историю, нужны диалоги. В SQL это делается через UNION
.
SELECT c.name || ': "Хватит писать баги, пора писать истории!"' AS dialog
FROM Characters c WHERE c.id = 1
UNION
SELECT c.name || ': "Сюжет всегда живее цифр."'
FROM Characters c WHERE c.id = 2
UNION
SELECT c.name || ': "Запишу всё это в лог, на всякий случай."'
FROM Characters c WHERE c.id = 3;
Получилось:
Алекс: «Хватит писать баги, пора писать истории!»
Ира: «Сюжет всегда живее цифр.»
Бот: «Запишу всё это в лог, на всякий случай.»
Никогда раньше не думал, что UNION
может работать как драматург.
Конфликт как двигатель
Литература без конфликта мертва. Сделаем генератор конфликтов:
SELECT c1.name || ' спорит с ' || c2.name || ' о ' ||
(CASE WHEN RANDOM() % 2 = 0 THEN 'смысле данных' ELSE 'будущем технологий' END) AS conflict
FROM Characters c1
JOIN Characters c2 ON c1.id < c2.id;
Каждый запуск давал разные сцены:
Алекс спорит с Ирой о смысле данных
Алекс спорит с Бот о будущем технологий
Ира спорит с Бот о смысле данных
Вот она, драматургия через псевдослучайность.
Финал рассказа
Историю нужно завершить. Пусть SQL решает за нас:
SELECT name || ' понимает, что ' ||
CASE mood
WHEN 'усталый' THEN 'работа бесконечна'
WHEN 'вдохновлённая' THEN 'даже данные можно превратить в искусство'
ELSE 'машины тоже умеют иронизировать'
END AS ending
FROM Characters;
Финал получился неожиданно философским:
Алекс понимает, что работа бесконечна
Ира понимает, что даже данные можно превратить в искусство
Бот понимает, что машины тоже умеют иронизировать
Бонус: интеграция с Python
Чтобы история выглядела не только как таблица, я попробовал вытянуть её в текстовую новеллу через Python:
import sqlite3
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
# Тут код создания таблиц и вставки данных такой же, как выше
for row in cursor.execute("""
SELECT c.name || ' (' || c.role || ', ' || c.mood || ')' ||
' в ' || p.name || ' ' || e.action AS narrative
FROM Events e
JOIN Characters c ON e.character_id = c.id
JOIN Places p ON e.place_id = p.id;
"""):
print(row[0])
Когда выводишь это в консоль построчно, ощущение, что читаешь минималистичную новеллу. Ничего лишнего, только данные и их соединение.
Заключение
Можно ли написать рассказ на чистом SQL? Можно. И пусть это не станет бестселлером, но эксперимент показал, что язык запросов способен быть больше, чем инструментом для отчётов. SQL умеет быть и автором.
И, честно говоря, после этого опыта я стал относиться к базе данных чуть теплее. Вдруг где-то там, в её индексах, давно живёт маленький писатель?
levge
Можно пойти дальше и попросить LLM сгенерировать много таких данных, и заполнить таблицы, получиться текстовая игра.