SQLite часто воспринимают как игрушку для прототипов, хотя это самая распространённая СУБД в мире: она работает в браузерах, телефонах, самолётах и на миллионах серверов. Вопрос не в том, «настоящая» ли это база, а в том, подходит ли модель «файл плюс библиотека» вашей нагрузке. Разберём, когда одного файла достаточно, а когда пора ставить PostgreSQL.
Что такое SQLite и чего в нём нет
SQLite — библиотека на C, которая хранит всю базу в одном файле. Нет серверного процесса, сетевого протокола, ролей и сетевой аутентификации. При этом транзакции соответствуют ACID, есть WAL, внешние ключи, JSON-функции, полнотекстовый поиск FTS5 и строгие таблицы STRICT. Формат файла стабилен и переносим между версиями и платформами.
Отсутствие сервера — не недостаток, а свойство. Оно убирает сетевой оверхед, администрирование и целый класс атак, но лишает вас удалённого доступа, репликации «из коробки» и тонких прав. Решение принимается по нагрузке, а не по привычке. Для небольшой команды, где все работают на одном сервере, файл часто проще и надёжнее.
Когда файл лучше сервера
Файл выигрывает, когда база живёт рядом с приложением: десктопная программа, мобильное приложение, CLI-утилита, кэш метаданных, тестовые фикстуры, небольшая внутренняя панель. Чтение при этом часто интенсивнее записи, а количество одновременных писателей невелико.
- однопользовательские и настольные приложения;
- аналитика по локальным данным и отчёты;
- встраиваемые системы и устройства без администратора;
- сайт с преимущественно чтением и редкой записью;
- промежуточное хранилище перед загрузкой в большую СУБД.
Когда нужен сервер
PostgreSQL или MariaDB стоит выбирать, если несколько процессов на разных машинах активно пишут одновременно, нужны роли и гранулярные права, потоковая репликация, автоматический failover, расширения вроде PostGIS или сложные аналитические запросы по большим объёмам. Сервер также упрощает централизованный бэкап и аудит.
При этом «клиент-сервер» не означает автоматически высокую скорость: на одном узле с интенсивным чтением SQLite нередко обгоняет сетевую СУБД за счёт отсутствия передачи данных по сети.
Настройки, которые решают
По умолчанию SQLite осторожна и не слишком быстра. Для серверного файла включите WAL: читатели перестанут блокировать писателя, а писатель — читателей. Режим сохраняется в файле, поэтому достаточно выполнить его один раз.
sqlite3 app.db "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"
sqlite3 app.db "PRAGMA foreign_keys=ON; PRAGMA foreign_key_check;"
sqlite3 app.db "PRAGMA cache_size=-65536; PRAGMA optimize;"
Внешние ключи включаются на каждое соединение — это частая причина вопроса, почему каскадное удаление не работает. busy_timeout задаёт ожидание блокировки вместо мгновенной ошибки database is locked. synchronous=NORMAL в режиме WAL безопасен для целостности, но при отключении питания может потерять последние транзакции.
Конкурентность и транзакции
В SQLite один писатель и много читателей. Если транзакция начинается как отложенная, а потом пытается писать, она может получить SQLITE_BUSY при апгрейде блокировки. Начинайте изменяющие транзакции явно через BEGIN IMMEDIATE.
sqlite3 app.db "BEGIN IMMEDIATE; UPDATE accounts SET balance=balance-100 WHERE id=1; COMMIT;"
sqlite3 app.db "SELECT COUNT(*) FROM accounts WHERE balance = 0;"
Не держите транзакцию открытой во время сетевых вызовов и долгих вычислений: писатель блокирует остальных. В приложении используйте один поток на запись и пул соединений на чтение, а ошибки busy обрабатывайте повтором с задержкой.
Резервное копирование
Копировать файл базы через cp во время записи нельзя: получите битую копию. Используйте встроенный механизм .backup, который работает через online backup API и не блокирует чтение.
sqlite3 app.db ".backup '/var/backups/app-$(date +%F).db'"
sqlite3 app.db "VACUUM INTO '/var/backups/app-compact.db';"
sqlite3 /var/backups/app-$(date +%F).db "PRAGMA quick_check;"
VACUUM INTO создаёт компактную копию без изменения исходника, но требует свободного места. После копирования обязательно проверьте её целостность и протестируйте восстановление — непроверенный бэкап не считается бэкапом.
Диагностика и запросы
Планировщик SQLite хорошо объясняет себя. Команда EXPLAIN QUERY PLAN показывает, используется ли индекс, происходит ли полный скан и почему сортировка выполняется отдельно.
sqlite3 app.db "PRAGMA integrity_check;"
sqlite3 app.db ".schema events"
sqlite3 app.db "EXPLAIN QUERY PLAN SELECT id, title FROM notes WHERE owner_id=42 ORDER BY created_at DESC LIMIT 20;"
sqlite3 app.db "CREATE INDEX IF NOT EXISTS idx_notes_owner_created ON notes(owner_id, created_at DESC);"
Составной индекс по фильтру и сортировке часто убирает и скан, и временную сортировку. Не индексируйте всё подряд: каждый индекс замедляет запись и занимает место. Периодически запускайте ANALYZE, чтобы статистика соответствовала данным.
Полнотекстовый поиск и строгие таблицы
Для поиска по тексту используйте FTS5: виртуальная таблица хранит собственный индекс и обновляется триггерами. Это дешевле, чем LIKE с процентами, который всегда читает всю таблицу. Строгие таблицы STRICT запрещают хранить число в текстовом столбце и ловят ошибки на этапе записи.
sqlite3 app.db "CREATE VIRTUAL TABLE notes_fts USING fts5(title, body, content='notes', content_rowid='id');"
sqlite3 app.db "INSERT INTO notes_fts(rowid, title, body) SELECT id, title, body FROM notes;"
sqlite3 app.db "CREATE TABLE tags (id INTEGER PRIMARY KEY, name TEXT NOT NULL) STRICT;"
FTS5 не заменяет обычные индексы: он ищет по словам, но не ускоряет точные сравнения и соединения. Для русскоязычных текстов задайте токенизатор unicode61 и проверьте, как обрабатываются дефисы и окончания.
Типичные ошибки
Хранить базу на NFS или SMB — прямой путь к повреждению блокировок. Держать один файл на сетевом диске и открывать его из нескольких контейнеров тоже нельзя без внешней координации. Крупные бинарные объекты лучше выносить в файловую систему, а в таблице хранить путь и хеш.
Ещё одна ошибка — миграции без транзакций и резервной копии. Изменение схемы в SQLite ограничено, поэтому сложные преобразования делают через новую таблицу, перенос данных и переименование. Всё это выполняется в одной транзакции.
Итог
SQLite — полноценная СУБД для одного узла и умеренного числа писателей. Файл удобнее сервера в настольных, встраиваемых и читающих сценариях, но проигрывает при конкурентной записи, удалённом доступе и требованиях отказоустойчивости. Включайте WAL, настраивайте busy_timeout, не копируйте файл на ходу и проверяйте бэкапы — тогда одного файла хватит надолго. Это особенно заметно на медленных дисках и в контейнерах.