Почему нельзя просто cp файла
База SQLite — это один файл, и от этого так и тянет сделать бэкап обычным копированием. Иногда срабатывает. Чаще — нет.
Сломаться может в двух местах:
- Другое соединение пишет посреди транзакции, когда вы копируете. На выходе — файл с наполовину применённой транзакцией, битый при открытии.
- База в режиме WAL (а это дефолт для большинства современных приложений). Свежие изменения лежат в отдельном файле
database.db-wal. Скопировали только основной файл — и тихо потеряли часть данных.
В SQLite для этого есть нормальные инструменты. Они правильно обрабатывают блокировки, WAL и параллельную запись. Берите их вместо cp.
Команда .backup
Самый быстрый способ снять бэкап из CLI — dot-команда .backup:
sqlite3 app.db
sqlite> .backup backup.db
sqlite> .quit
Эта команда пишет полноценную копию app.db в backup.db. Работает даже под активным чтением и записью со стороны других процессов: backup API берёт серию мелких блокировок вместо одной большой, копирует страницы инкрементально и перечитывает те, что изменились во время копирования.
Результат — полноценная база SQLite. Открывайте её как обычно:
sqlite3 backup.db
sqlite> .tables
Можно сделать всё одной shell-командой — именно так чаще всего и выглядят cron-джобы:
sqlite3 app.db ".backup '/var/backups/app-$(date +%Y%m%d).db'"
Один файл на входе, один файл на выходе. Никаких dump/restore-петель, никакого парсинга SQL — просто страницы копируются на уровне хранилища.
VACUUM INTO для компактной копии
VACUUM INTO — родственник, но другой инструмент. Он пишет свежесобранную копию базы в новый файл:
На выходе — та же логическая база, но переписанная с нуля: каждая страница плотно упакована, без фрагментации, без оставшихся свободных страниц от удалённых строк. То есть бэкап получается максимально компактным.
Когда что выбирать:
.backup— обычные регулярные бэкапы. Быстрее, прекрасно уживается с параллельной записью, побайтово точный.VACUUM INTO— периодические снимки, когда заодно нужен аккуратный, минимально возможный по размеру файл. Медленнее, потому что переписывает всё, и берёт write-лок на исходник на всё время операции.
В обоих случаях получается готовый .db-файл, который можно открывать сразу.
Online backup API из приложения
В коде приложения никто не запускает sqlite3 через шелл. Вы используете online backup API, который предоставляет ваш драйвер. В стандартном Python sqlite3 это Connection.backup:
import sqlite3
source = sqlite3.connect("app.db")
dest = sqlite3.connect("backup.db")
with dest:
source.backup(dest)
source.close()
dest.close()
Метод backup копирует страницы из source в dest, не мешая работе других соединений. Можно ещё передать pages=, чтобы копировать порциями, и progress=, чтобы получать колбэк — пригодится на больших базах, где хочется ограничивать скорость или показывать прогресс.
В большинстве драйверов в других языках доступен тот же C API (sqlite3_backup_init, _step, _finish) под похожим именем. Форма всегда одна: открыть исходник, открыть приёмник, проитерировать страницы, закрыть.
Бэкап под живой нагрузкой
Вот тут SQLite тихо хорош. И .backup, и online backup API заточены под hot-бэкапы — исходная база может быть открыта и работать всё время операции.
Что происходит на самом деле:
- Бэкап берёт shared-лок и начинает копировать страницы.
- Если писатель меняет страницу, которую ещё не успели скопировать, бэкап это замечает и перечитывает её.
- Копия завершается, когда каждая страница согласована.
Не нужно ни останавливать приложение, ни выгонять соединения, ни планировать downtime. На нагруженной базе бэкапу может потребоваться лишний цикл-другой, чтобы сойтись, но он сойдётся. На выходе — целостный снимок на конкретный момент времени.
Один момент: если вы в WAL-режиме, периодически выполняйте PRAGMA wal_checkpoint(TRUNCATE);, чтобы файл WAL не разрастался без меры. Сам бэкап с WAL разбирается корректно — это просто общая гигиена WAL.
Восстановление из бэкапа
Восстановление SQLite на удивление скучное — и это плюс. Файл бэкапа и есть база. Чтобы воспользоваться, просто откройте его:
sqlite3 backup.db
sqlite> SELECT COUNT(*) FROM notes;
Чтобы восстановить поверх живой базы — например, после потери данных, — безопасный порядок такой:
- Остановите все процессы, у которых открыта база.
- Удалите существующие файлы
app.db,app.db-walиapp.db-shm. Оставшиеся WAL/SHM от старой базы запутают SQLite в паре с восстановленным основным файлом. - Скопируйте бэкап на место:
cp backup.db app.db. - Запустите приложение заново.
Файлы -wal и -shm важны. Если пропустить шаг 2, SQLite может попытаться накатить устаревший WAL поверх восстановленного основного файла — и вы получите либо повреждение, либо странную мешанину данных.
Внутри CLI есть и зеркальная команда — .restore:
sqlite3 app.db
sqlite> .restore backup.db
sqlite> .quit
Она перезаписывает содержимое подключённой базы содержимым backup.db. Под капотом — тот же online backup API, только в обратную сторону.
.dump — это про другое
В старых руководствах часто встречается .dump. Это не бэкап в том же смысле — он выдаёт SQL-текст с CREATE и INSERT:
sqlite3 app.db .dump > app.sql
Чтобы восстановить — проигрываете этот SQL:
sqlite3 new.db < app.sql
Полезно, когда нужно перейти между версиями SQLite, отдиффить схему в git или перевезти данные в другой движок. Это медленнее, объёмнее и более «лоссовый» вариант, чем .backup (для пользовательских коллаций, генерируемых столбцов и некоторых прагм нужны дополнительные предосторожности). Для настоящего бэкапа рабочей базы используйте .backup или VACUUM INTO.
Адекватный регламент бэкапов
Для большинства приложений хорошо работает такая схема:
- Регулярный
.backupпо расписанию — раз в час, раз в сутки, как позволяет ваш RPO. Дёшево, быстро, под нагрузкой. - Раз в неделю —
VACUUM INTOв отдельный путь. Ловит дрейф, даёт компактный снимок, прогоняет другой код. - Политика хранения: последние N ежедневных, последние M еженедельных. Базы SQLite хорошо жмутся, так что
gzip backup.dbпосле копии — стоит делать. - Иногда восстанавливайте один из бэкапов и гоняйте по нему пару запросов. Непротестированный бэкап — это надежда, а не бэкап.
# Ежедневно, по cron:
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app-$(date +%F).db'"
gzip "/var/backups/app-$(date +%F).db"
# Еженедельно:
sqlite3 /var/lib/app/app.db "VACUUM INTO '/var/backups/app-weekly-$(date +%F).db'"
Обе команды безопасны под живой нагрузкой.
Дальше: настройки PRAGMA
Бэкапы — это одна сторона эксплуатации; настройка поведения в рантайме — другая. SQLite раскрывает свои ручки через PRAGMA-выражения: режим журнала, уровень синхронизации, размер кэша, контроль внешних ключей. На следующей странице пройдёмся по тем, что стоит знать.
Часто задаваемые вопросы
Как сделать резервную копию базы SQLite?
Из CLI — выполнить .backup path/to/backup.db в подключении к исходной базе. Из приложения — воспользоваться online backup API (sqlite3_backup_init в C или его эквивалентом в драйвере вашего языка). Оба способа дадут согласованную копию даже под активной записью.
Можно просто скопировать .db-файл как бэкап?
Только если вы абсолютно уверены, что ни один процесс не пишет в базу. Иначе можно скопировать файл посреди транзакции и получить битый бэкап — или потерять данные, оставшиеся в WAL. Используйте .backup или VACUUM INTO: они корректно обрабатывают и блокировки, и содержимое WAL.
В чём разница между .backup и VACUUM INTO?
.backup использует online backup API и делает побайтово точную копию, включая неиспользуемые страницы. VACUUM INTO 'file.db' пишет свежесобранную копию — компактнее и без фрагментации, но переписывает каждую страницу. Для регулярных бэкапов берите .backup, для ситуаций, когда заодно хочется ужать файл, — VACUUM INTO.
Как восстановить SQLite из бэкапа?
Если бэкап — это .db-файл, просто откройте его: база SQLite — это и есть один файл. Чтобы восстановить поверх существующей базы, остановите приложение, замените файл (и удалите оставшиеся -wal/-shm), затем откройте заново. Из CLI можно ещё выполнить .restore path/to/backup.db в подключении к свежей базе.