По умолчанию SQLite использует журнал отката: писатель блокирует базу на время транзакции, читатели ждут. Режим WAL меняет схему и позволяет читать во время записи. Это не «включаем и забываем»: у него свои правила контрольных точек, свой профиль надёжности и ограничения по файловым системам. Разберём, как WAL работает, какие pragma задать и как не получить распухший файл журнала.

Что меняется в файлах

В режиме WAL рядом с основной базой появляются два файла: app.db-wal с последними страницами и app.db-shm с разделяемой памятью. Читатель видит снимок на момент начала транзакции, писатель дописывает изменения в WAL. При контрольной точке страницы переносятся в основной файл, и журнал может быть переиспользован.

Отсюда главное свойство: читатели и писатель не блокируют друг друга. Но писатель по-прежнему один. Две одновременные записи невозможны, вторая получит SQLITE_BUSY или подождёт, если задан busy_timeout.

Включение

Режим задаётся pragma и сохраняется в файле базы. Проверить текущее значение можно той же командой без аргумента. Для нового приложения разумно включать WAL сразу после создания файла.

sqlite3 app.db "PRAGMA journal_mode=WAL;"
sqlite3 app.db "PRAGMA journal_mode;"
sqlite3 app.db "PRAGMA synchronous=NORMAL;"
sqlite3 app.db "PRAGMA wal_autocheckpoint=1000;"

Значение synchronous=NORMAL в WAL считается безопасным компромиссом: база не повреждается при сбое питания, но последние зафиксированные транзакции могут пропасть. Для финансовых операций ставят FULL, принимая падение скорости. autocheckpoint задаёт порог в страницах, после которого запускается автоматическая контрольная точка.

Один писатель и очередь

Приложение должно открывать соединение с таймаутом и начинать изменяющие транзакции явно. Режим BEGIN IMMEDIATE берёт блокировку записи сразу, поэтому конфликт возникает в начале, а не в середине работы. Это проще обрабатывать повтором.

import sqlite3

con = sqlite3.connect("app.db", timeout=5.0, isolation_level=None)
con.execute("PRAGMA journal_mode=WAL")
con.execute("PRAGMA synchronous=NORMAL")
con.execute("PRAGMA busy_timeout=5000")
con.execute("PRAGMA foreign_keys=ON")

Параметр isolation_level=None переводит драйвер в режим автокоммита, и транзакции открываются вручную. Для пакетной вставки это удобно: одна транзакция на тысячу строк работает в разы быстрее, чем тысяча отдельных. При ошибке SQLITE_BUSY стоит повторить операцию с небольшой задержкой, а не увеличивать таймаут бесконечно.

Контрольные точки

Контрольная точка переносит страницы из WAL в основной файл. PASSIVE не мешает читателям и может завершиться частично. FULL ждёт читателей, TRUNCATE дополнительно обрезает журнал до нуля, RESTART ждёт завершения всех читателей. Возвращаемые значения показывают, сколько страниц перенесено и осталось.

sqlite3 app.db "PRAGMA wal_checkpoint(PASSIVE);"
sqlite3 app.db "PRAGMA wal_checkpoint(FULL);"
sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"
sqlite3 app.db "PRAGMA wal_checkpoint(RESTART);"

Частая ошибка — длинные читающие транзакции. Они держат снимок и не дают контрольной точке продвинуться, поэтому WAL растёт. Если файл журнала измеряется гигабайтами, ищите забытый курсор или открытую транзакцию в другом процессе. Помогает и journal_size_limit, но он ограничивает размер после точки, а не во время работы.

Надёжность и сбои

WAL не отменяет правила про fsync. При synchronous=FULL каждая фиксация доходит до диска, при NORMAL часть записей может остаться в кеше ОС. База при этом остаётся целой: незавершённые страницы не попадут в основной файл. После аварийного завершения SQLite восстановит состояние по журналу при следующем открытии.

Проверка целостности выполняется отдельно и на большой базе занимает время. Её стоит запускать после сбоя питания или подозрительных ошибок ввода-вывода.

sqlite3 app.db "PRAGMA integrity_check;"
sqlite3 app.db "PRAGMA quick_check;"
sqlite3 app.db "PRAGMA page_count;"
sqlite3 app.db "PRAGMA page_size;"

quick_check быстрее, но проверяет меньше. Для регулярного мониторинга достаточно quick_check раз в сутки, а полный integrity_check — перед ответственным обновлением.

Резервное копирование

Копировать файл базы при работающем приложении нельзя: получится несогласованный снимок. Правильный путь — команда .backup или VACUUM INTO. Обе создают целостную копию с учётом WAL. Для регулярных бэкапов удобно делать снимок, проверять его и только потом отправлять в хранилище.

sqlite3 app.db ".backup 'backup.db'"
sqlite3 app.db "VACUUM INTO 'backup-compact.db'"
sqlite3 backup.db "PRAGMA integrity_check;"
sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"

VACUUM INTO заодно пересобирает файл и убирает фрагментацию, но требует свободного места. Для баз в сотни гигабайт такой подход может быть тяжёлым, тогда используют инкрементальные методы и внешние инструменты.

Ограничения

WAL плохо работает на сетевых файловых системах: блокировки и разделяемая память требуют корректной поддержки, которой нет у NFS и многих SMB-монтирований. Держите базу на локальном диске. Режим только для чтения тоже имеет нюанс: для открытия нужен доступ к -shm, иначе SQLite не сможет создать разделяемую память.

Ещё одно ограничение — версия формата. Старые сборки SQLite могут не понимать базу, созданную новой версией. Перед обновлением приложения проверяйте PRAGMA user_version и держите совместимую версию библиотеки.

Мониторинг в продакшене

Достаточно нескольких метрик: размер -wal, число ошибок SQLITE_BUSY, длительность контрольных точек и свободное место. Размер журнала удобно снимать вместе с выводом wal_checkpoint(PASSIVE): если поле checkpointed перестало расти, значит читатель держит старый снимок. Ещё одна полезная привычка — логировать время каждой транзакции: медленная запись часто означает не диск, а ожидание блокировки.

sqlite3 app.db "PRAGMA wal_checkpoint(PASSIVE);"
ls -lh app.db app.db-wal
sqlite3 app.db "PRAGMA journal_size_limit;"
sqlite3 app.db "PRAGMA busy_timeout;"

Если журнал растёт даже при коротких транзакциях, проверьте, не открывает ли приложение соединение на каждый запрос и не держит ли его в пуле без закрытия. Иногда виноват ORM, который оставляет незавершённую транзакцию после исключения. Явный rollback в обработчике ошибок решает проблему быстрее, чем увеличение таймаутов.

Чек-лист

  • База на локальном диске, не на NFS.
  • journal_mode=WAL зафиксирован в миграции.
  • busy_timeout задан на уровне соединения.
  • Изменения начинаются с BEGIN IMMEDIATE.
  • Длинные чтения закрывают транзакции.
  • Размер -wal под мониторингом.
  • Бэкап через .backup или VACUUM INTO.

Итог

WAL превращает SQLite в предсказуемую базу для многопоточного приложения: читатели не ждут писателя, а сбой не разрушает файл. Но выигрыш появляется только при дисциплине — таймаутах, явных транзакциях, контроле журнала и правильном бэкапе. Настройка занимает несколько pragma, а понимание компромиссов экономит часы отладки. Для локальных приложений и небольших сервисов это почти всегда правильный выбор.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте