Недооценённая фича SQLite, о которой мало кто знает

11 июля 2026 года разработчик Эван Хан опубликовал разбор функции SQLite, которая существует с 2021 года, но по-прежнему используется реже, чем могла бы, — strict-таблицы. Тема прямо касается тех, кто администрирует self-hosted инфраструктуру: SQLite давно перестал быть "игрушечной" базой для мобильных приложений — на нём построены метаданные Garage (разбирали в статье про S3-хранилища), конфиги множества self-hosted сервисов, и просто удобная встраиваемая БД для небольших проектов на своём VPS.

В чём проблема обычных таблиц SQLite

По умолчанию SQLite использует "гибкую" типизацию — колонка с типом INTEGER спокойно примет туда текстовую строку:

CREATE TABLE people_nonstrict (age INTEGER);
INSERT INTO people_nonstrict (age) VALUES ('garbage');
-- сохранится без единой ошибки

Для тех, кто привык к строгой типизации PostgreSQL или MySQL, это неприятный сюрприз: ошибка, которая должна была бы упасть на этапе записи, всплывает позже — где-нибудь в коде, который ожидал число, а получил строку.

Похожая история с созданием самой таблицы — SQLite без возражений создаст колонку с несуществующим типом:

CREATE TABLE tbl (name GARBAGE);
CREATE TABLE tbl (name DATETIME);
CREATE TABLE tbl (name UUID);
-- всё это отработает без ошибок, хотя ни один из этих типов SQLite не поддерживает

Часть таких случаев — банальные опечатки, часть — путаница с типами, которых в SQLite попросту нет (своего DATETIME или JSON-типа в SQLite нет, даты и JSON хранятся как текст или blob).

Что даёт STRICT

Достаточно дописать одно слово при создании таблицы:

CREATE TABLE people (name TEXT) STRICT;

После этого:

  • Вставка неверного типа падает с ошибкойage INTEGER, попытка записать туда текст, вернёт error: cannot store TEXT value in INTEGER column, а не тихо сохранится
  • Бессмысленные типы колонок при создании таблицы тоже отклоняютсяGARBAGE, UUID, BLOBB (опечатка) и подобное вызовут ошибку уже на этапе CREATE TABLE
  • Разрешены только реальные типы SQLite: INT, INTEGER, REAL, TEXT, BLOB, ANY
  • Колонка обязана иметь тип — CREATE TABLE tbl (name) без указания типа в strict-таблице не сработает

Отдельно стоит ANY — если для конкретной колонки гибкость всё же нужна, ANY разрешает в неё что угодно, даже в строгой таблице. Полезно для action-логов или key-value-подобных колонок внутри в целом строгой схемы.

Важный нюанс: если значение можно без потерь привести к нужному типу, strict-таблица его пропустит — строка '123' спокойно ляжет в INTEGER-колонку, потому что конвертируется однозначно. Ошибка возникает только там, где преобразование действительно теряет данные или бессмысленно.

Минусы, которые стоит знать заранее

Нельзя "превратить" существующую таблицу в strict через ALTER. Придётся создавать новую таблицу с STRICT, копировать данные и переименовывать:

CREATE TABLE new_people (name TEXT) STRICT;
INSERT INTO new_people SELECT * FROM people;
DROP TABLE people;
ALTER TABLE new_people RENAME TO people;

Если в старых данных уже есть "мусорные" значения (текст там, где ожидалось число), миграция упадёт с ошибкой — придётся сначала почистить данные или явно привести типы через CAST.

Сами разработчики SQLite с этим подходом не вполне согласны. У проекта есть отдельная страница документации, где объясняются практические сценарии, где гибкая типизация — это фича, а не баг: чистое key-value хранилище, таблица с разнородными атрибутами, или прямой импорт "грязного" CSV, где терять данные из-за несовпадения типа не хочется.

Работает только с SQLite 3.37.0 (ноябрь 2021) и новее. Более старая версия SQLite не то что не поддерживает strict-таблицы — она вообще не сможет прочитать файл базы, где такая таблица есть, даже если пытается читать другие, non-strict таблицы из того же файла.

Производительность — теоретически проверка типов при каждой вставке должна чуть замедлять запись, но на практике разница не обнаруживается даже при вставке миллионов строк в широкую таблицу.

Практическая рекомендация для self-hosted проектов

Если администрируешь сервисы на SQLite (или сам пишешь что-то небольшое поверх него) — практичный подход такой:

  1. Проверить версию SQLite перед началом: sqlite3 --version — нужна 3.37.0+
  2. Для новых таблиц в новых проектах — сразу писать STRICT, это ничего не стоит и отсекает целый класс багов на старте
  3. Для существующих БД — не переписывать всё разом, а мигрировать таблицы на STRICT по мере рефакторинга, начиная с самых критичных (там, где ошибка типа дороже всего обходится)
  4. Не пытаться силой сделать строгими key-value-подобные таблицы — для них гибкая типизация или ANY-колонки действительно уместнее

Для Garage, который использует SQLite для метаданных, это, к слову, забота самого проекта, а не администратора — трогать внутреннюю схему Garage не нужно. Но если рядом крутится собственный сервис на SQLite (небольшая CRM, бот, внутренний трекер задач) — decision "делать таблицы strict с первого дня" стоит принять осознанно, а не по умолчанию.