Вычисляемая колонка — это колонка, значение которой считается формулой
Вычисляемая колонка (generated column) в SQLite — это колонка, значение которой берётся из выражения, а не из INSERT. Формула задаётся один раз в CREATE TABLE, а дальше SQLite всё делает сам. Писать в такую колонку нельзя — попытка вставки или обновления приведёт к ошибке.
Самый короткий пример:
total мы никуда не вставляли, но он есть в каждой строке. SQLite пересчитывает его из price + tax при каждом чтении. Поменяете любую из этих колонок — total подтянется автоматически.
Ключевое слово GENERATED ALWAYS AS обязательно. ALWAYS — это формальность из стандарта SQL, в SQLite другого варианта всё равно нет.
VIRTUAL и STORED: в чём разница
У вычисляемой колонки в SQLite есть два режима. По умолчанию используется VIRTUAL:
Как это устроено в голове:
VIRTUAL— ноль байт на диске, но CPU тратится при каждом чтении. Дёшево добавить, дёшево потом поменять.STORED— занимает место на диске, зато читается без дополнительных расходов. Окупается, когда выражение тяжёлое или колонку читают намного чаще, чем пишут.
Если ключевое слово не указали — получите VIRTUAL. И почти всегда это именно то, что нужно по умолчанию.
Зачем это всё? Индекс по вычисляемой колонке
Главная фишка — на generated column можно навесить индекс. Это даёт быстрый поиск по производным значениям, причём без переписывания всех запросов.
Допустим, нужен поиск по email без учёта регистра:
Индекс покрывает значение в нижнем регистре. Запрос с фильтром по email_lower пользуется этим индексом напрямую. В SQLite, конечно, есть и индексы по выражению (CREATE INDEX ... ON users(lower(email))), но вычисляемая колонка делает производное значение полноценным столбцом — его можно SELECT-нуть, использовать во вьюхах и переиспользовать из кода приложения.
Извлечение значений из JSON
Особенно ярко вычисляемые колонки sqlite раскрываются поверх JSON. Поддержка JSON в SQLite даёт оператор ->> для извлечения скалярного значения; заверните его в generated column — и получите типизированное индексируемое поле поверх гибкого блоба.
user_id и kind для ваших запросов выглядят как обычные колонки, но сами данные хранятся в payload. Поменяете JSON — значения в колонках обновятся. А индекс по user_id делает выборку быстрой.
Правила и ограничения
SQLite навязывает несколько правил — лучше знать о них заранее, чем нарваться на ошибку:
- Выражение должно быть детерминированным.
random(),datetime('now')и прочие недетерминированные функции под запретом. Значение обязано однозначно вычисляться из самой строки. - В выражении можно ссылаться только на колонки той же строки. Никаких подзапросов, агрегатов или обращений к другим таблицам.
- Записать значение в вычисляемую колонку напрямую через
INSERTилиUPDATEнельзя.INSERT INTO products (total) VALUES (5)выдаст ошибку. - Колонки с типом
STOREDнельзя добавить черезALTER TABLE ... ADD COLUMN. Постфактум добавляются толькоVIRTUAL-колонки. - На вычисляемые колонки можно навешивать
NOT NULL,CHECK,UNIQUEи дажеFOREIGN KEY. С точки зрения этих ограничений они ведут себя как обычные колонки.
Небольшая демонстрация правила про запись:
sqlite> INSERT INTO products (price, tax, total) VALUES (10, 1, 999);
Runtime error: cannot INSERT into generated column "total"
Решение простое: уберите вычисляемую колонку из списка INSERT и дайте SQLite самому её посчитать.
Что выбрать — VIRTUAL или STORED
Обычно всё упирается в соотношение чтения и записи и в то, насколько тяжёлое у вас выражение:
Практические ориентиры:
- По умолчанию берите
VIRTUAL. На запись это бесплатно, и для большинства задач этого достаточно. - Переключайтесь на
STORED, когда строите индекс по колонке в таблице с активной записью (индексу всё равно нужно физически хранить значение) или когда выражение действительно дорогое. - Не зацикливайтесь на выборе. Тип хранения — часть схемы, но колонку всегда можно удалить и пересоздать, если передумаете — по крайней мере для
VIRTUAL.
Вычисляемые колонки против представлений (VIEW)
С представлениями есть пересечение: и те и другие отдают вычисленные значения, не храня их (ну, иногда). Разделение обычно такое:
- Вычисляемая колонка принадлежит одной строке одной таблицы. Используйте её для построчных производных значений — форматирование email, извлечение поля из JSON, подсчёт суммы.
- Представление (view) — это сохранённый запрос. Берите его, когда вычисление включает джойны, агрегацию или фильтрацию по нескольким строкам.
Их можно комбинировать. View может делать SELECT из таблицы с вычисляемыми колонками и подтягивать дополнительный контекст джойнами. Вычисляемые колонки живут на уровне хранения, представления — на уровне запросов.
Дальше: ATTACH DATABASE
Вычисляемые колонки позволяют таблице считать свои собственные значения. На следующей странице двинемся в обратную сторону: подключим несколько баз SQLite одновременно через ATTACH DATABASE, чтобы один запрос мог работать сразу с несколькими файлами.
Часто задаваемые вопросы
Что такое generated-колонка в SQLite?
Это колонка, значение которой вычисляется по выражению на основе других колонок той же строки. Объявляется в CREATE TABLE через GENERATED ALWAYS AS (выражение). Напрямую в неё писать нельзя — SQLite сам пересчитает значение при чтении или записи строки.
В чём разница между VIRTUAL и STORED?
VIRTUAL пересчитывается при каждом чтении и не занимает места на диске — это режим по умолчанию. STORED вычисляется один раз при записи и физически хранится в файле БД: чтение дешевле, запись — чуть дороже. Индексировать можно оба варианта, но STORED обычно выгоднее, когда выражение тяжёлое или колонку читают сильно чаще, чем пишут.
Можно ли построить индекс по generated-колонке?
Да. CREATE INDEX спокойно работает и с VIRTUAL, и с STORED. Собственно, ради этого их чаще всего и заводят: индекс по производному значению (например, lower(email) или поле из JSON, вытащенное через ->>) даст планировщику возможность использовать его автоматически — переписывать запросы не придётся.
Можно ли добавить generated-колонку через ALTER TABLE?
Можно, но только VIRTUAL. Команда ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) VIRTUAL отрабатывает штатно. А вот добавить STORED-колонку через ALTER TABLE нельзя — придётся пересоздавать таблицу. Если планируется stored, продумывайте схему заранее.