Режим WAL в SQLite полезен там, где приложению нужно одновременно держать быстрые чтения и регулярные записи без постоянных блокировок. На практике это мобильные приложения, локальные desktop-инструменты, embedded-сервисы и внутренние панели, где один процесс пишет данные, а несколько потоков или фоновых задач читают их почти непрерывно.
Если вы настраиваете SQLite для production-нагрузки, важно понимать не только команду PRAGMA journal_mode=WAL;, но и сопутствующие ограничения: checkpointing, рост -wal файла, требования к локальной файловой системе и редкие случаи SQLITE_BUSY. Ниже разберём, как WAL работает, когда его действительно стоит включать и по каким сигналам проверять, что режим приносит пользу, а не добавляет скрытую сложность
Содержание
- SQLite на SMB-диске: почему WAL не сливается в основную БД и что с этим делать
- Что такое SQLite WAL и зачем он нужен
- Как работает Write-Ahead Log в SQLite
- Контрольные точки (checkpointing) в WAL
- 2.2. Параллельная работа
- 2.3. Соображения о производительности
- Как включить и настроить режим WAL в SQLite
- 3.1. Автоматическое создание контрольных точек
- 3.2. Контрольные точки, инициируемые приложением
- 3.3. Сохранение режима WAL
- 4. WAL-файл
- 5. Базы данных только для чтения
- Контроль размера WAL-файла: предотвращение чрезмерного роста
- 7. Реализация разделяемой памяти для WAL-индекса
- 8. Использование WAL без разделяемой памяти
- 9. Иногда запросы возвращают SQLITE_BUSY в режиме WAL
- 10. Обратная совместимость
- 11. Ошибка WAL-Reset
- 11.1. Подробности об ошибке
- 11.2. Низкая вероятность возникновения
- Часто задаваемые вопросы по SQLite WAL
SQLite на SMB-диске: почему WAL не сливается в основную БД и что с этим делать
Если у вас на SMB-диске «не сливается» WAL в основную SQLite-базу, это часто означает не одну проблему, а смесь из двух разных явлений — подробный разбор проблемы читай в этой статье
Что такое SQLite WAL и зачем он нужен
SQLite использует для атомарной фиксации и отката транзакций механизм под названием журнал отката (rollback journal). Начиная с версии 3.7.0 (21 июля 2010 года) появился параметр «Write-Ahead Log» (журнал с упреждающей записью, WAL), который предлагает альтернативный способ управления транзакциями.
Использование WAL вместо журнала отката даёт два главных преимущества: запись часто становится быстрее, а чтение реже конфликтует с записью. SQLite сначала дописывает изменения в -wal, а уже потом переносит их в основной файл базы. Для мобильных приложений, локальных API и desktop-инструментов с короткими транзакциями это обычно полезнее классического DELETE-режима.
Но у WAL есть и ограничения, которые важно принять заранее:
Все процессы, использующие базу данных, должны находиться на одном хост-компьютере; WAL не работает через сетевую файловую систему. Это связано с тем, что WAL требует, чтобы все процессы совместно использовали небольшой объём памяти, а процессы на разных хост-машинах не могут совместно использовать память друг с другом.
Транзакции, затрагивающие изменения в нескольких подключённых (ATTACH) базах данных, являются атомарными для каждой отдельной базы данных, но не являются атомарными для всех баз данных в совокупности.
Изменить размер страницы после перехода в режим WAL невозможно — ни для пустой базы данных, ни с помощью VACUUM, ни при восстановлении из резервной копии через backup API; для этого необходимо вернуться в режим журнала отката.
Базы данных в режиме WAL не могут быть открыты в режиме только для чтения. Чтобы открыть такую базу, процесс должен иметь права на запись к файлу разделяемой памяти wal-index (-shm), связанному с базой данных, или к каталогу, содержащему файл базы данных. С версии 3.22.0 (22 января 2018 года) возможно открыть базу данных в режиме WAL только для чтения, если файлы -shm и -wal уже существуют или если база данных является неизменяемой.
WAL может быть незначительно медленнее в приложениях, которые почти не пишут и в основном читают.
Каждая база данных связана с дополнительным квазипостоянным файлом -wal и файлом разделяемой памяти -shm, что делает использование SQLite менее привлекательным в качестве формата файла приложения.
Существует дополнительная операция контрольной точки (checkpointing), которая требует хотя бы базового мониторинга. Если её игнорировать, можно получить растущий -wal файл и нестабильные latency. Практический критерий простой: для обычного CRUD, очередей задач и телеметрии WAL обычно оправдан. Если же приложение почти не пишет или делает очень крупные пакетные транзакции, режим нужно проверять отдельно по размеру -wal, p95 чтения и числу SQLITE_BUSY.
Как работает Write-Ahead Log в SQLite
Классический журнал отката работает так: исходное неизменённое содержимое базы данных записывается в отдельный файл журнала отката, после чего изменения осуществляются непосредственно в файл базы данных. В случае сбоя или выполнения ROLLBACK содержимое, хранящееся в журнале отката, воспроизводится в файл базы данных, восстанавливая исходное состояние. COMMIT происходит при удалении журнала отката.
Подход WAL инвертирует этот процесс. Исходное содержимое сохраняется в файле базы данных, а изменения добавляются в отдельный файл WAL. COMMIT происходит, когда в WAL добавляется специальная запись, указывающая на фиксацию транзакции.
Таким образом, COMMIT может произойти без какой-либо записи в исходную базу данных, что позволяет читателям продолжать работу с исходной неизменённой базой данных, пока изменения одновременно фиксируются в WAL. Несколько транзакций могут быть добавлены в конец одного файла WAL.
На практике это удобно понимать через простой сценарий. Допустим, у вас есть локальная база app.db, из которой экран читает список задач, а фоновая синхронизация обновляет статусы. В DELETE-режиме писатель чаще конкурирует с читателями за основной файл. В WAL-режиме чтение продолжает работать по стабильному снимку, а новые страницы копятся в app.db-wal.
Когда вы видите на диске файлы app.db, app.db-wal и app.db-shm, это нормальная картина для активной базы в WAL. Проверочный критерий здесь такой: после переключения режима чтения не должны чаще падать с SQLITE_BUSY, а файл -wal должен периодически уменьшаться после checkpoint, а не расти бесконечно.
Контрольные точки (checkpointing) в WAL
Разумеется, в какой-то момент необходимо перенести все транзакции, добавленные в файл WAL, обратно в исходную базу данных. Перемещение транзакций из файла WAL обратно в базу данных называется «контрольной точкой» (checkpoint).
Другой способ осмыслить разницу между журналом отката и журналом с упреждающей записью состоит в следующем: в подходе с журналом отката существуют две примитивные операции — чтение и запись, тогда как при использовании WAL их становится три: чтение, запись и контрольная точка.
По умолчанию SQLite автоматически выполняет контрольную точку, когда файл WAL достигает порогового размера в 1000 страниц. (Параметр компиляции SQLITE_DEFAULT_WAL_AUTOCHECKPOINT может использоваться для задания другого значения по умолчанию.) Приложениям, использующим WAL, не нужно предпринимать никаких действий для выполнения этих контрольных точек.
Однако при желании приложения могут настроить порог автоматической контрольной точки, отключить автоматические контрольные точки и выполнять их в периоды простоя либо в отдельном потоке или процессе.
2.2. Параллельная работа
Когда в базе данных в режиме WAL начинается чтение, SQLite фиксирует для читателя консистентную точку видимости. Дальше reader берёт страницы либо из основного файла базы, либо из -wal, если там лежит более новая версия до этой точки. Для ускорения используется wal-index в разделяемой памяти, поэтому читателям не нужно каждый раз просматривать весь файл WAL.
Именно это ограничение и привязывает WAL к одной машине: общий индекс нельзя безопасно разделить между разными хостами через NFS или SMB.
Писатель при этом просто дописывает новые страницы в конец WAL, поэтому чтение и запись чаще всего идут параллельно. Но писатель остаётся только один, а checkpoint не может пройти дальше страницы, которую ещё видит активный reader. Отсюда главный практический вывод: длинные транзакции чтения могут мешать не записи напрямую, а нормальной очистке WAL. Если -wal растёт и не уменьшается, сначала ищите долгоживущие readers, а уже потом тюньте SQLite.
2.3. Соображения о производительности
Транзакции записи в WAL обычно быстрые, потому что изменения пишутся последовательно в один файл. Но за это приходится платить checkpoint: рано или поздно накопленные страницы нужно перенести обратно в основную базу, а эта операция тяжелее и требует синхронизации. Поэтому в production почти всегда наблюдается одна и та же картина: большинство COMMIT быстрые, а отдельные COMMIT или фоновые checkpoint заметно медленнее.
Чтение тоже зависит от размера WAL. Чем дольше файл не сбрасывается, тем выше стоимость работы с ним, даже с учётом wal-index. Отсюда ключевой компромисс: маленький WAL помогает читателям, а редкие checkpoint помогают писателям. Значение по умолчанию в 1000 страниц часто достаточно, поэтому менять его стоит только после измерений.
Полезный operational-рецепт: следите за размером -wal, временем checkpoint и p95 чтения после всплесков записи. Если после batch-операций UI или API начинает «задумываться», есть смысл вынести checkpoint в контролируемое окно простоя. Если же запись критична к latency, а риск потери последних транзакций приемлем, отдельно тестируйте сочетание journal_mode=WAL и synchronous=NORMAL на staging, а не на боевой базе.
Как включить и настроить режим WAL в SQLite
Соединение с базой данных SQLite по умолчанию использует journal_mode=DELETE. Чтобы переключиться в режим WAL, используйте следующую прагму:
PRAGMA journal_mode=WAL;
Прагма journal_mode возвращает строку, содержащую новый режим журналирования. В случае успеха прагма вернёт строку wal. Если переключение в режим WAL не удалось выполнить (например, если VFS не поддерживает необходимые примитивы разделяемой памяти), режим журналирования останется без изменений, а строка, возвращённая примитивом, будет содержать предыдущий режим журналирования (например, delete).
В реальном проекте одного переключения режима обычно недостаточно. После PRAGMA journal_mode=WAL; проверьте три вещи: база лежит на локальной файловой системе, процесс может создать -wal и -shm, а PRAGMA journal_mode; действительно возвращает wal. Минимальный smoke test: открыть базу, сделать короткую запись, запросить PRAGMA wal_autocheckpoint; и убедиться, что рядом появились служебные файлы.
Для Python-проекта этого достаточно на старте: один раз включить WAL, залогировать фактический ответ SQLite и на staging посмотреть, уменьшается ли -wal после периодов простоя. Если файл уходит в сотни мегабайт, ищите долгие readers и проблемы с checkpoint.
3.1. Автоматическое создание контрольных точек
По умолчанию SQLite автоматически создаёт контрольную точку, когда WAL достигает 1000 страниц, а также при закрытии последнего соединения. При стандартном размере страницы это около 4 МБ, и для многих приложений такой порог работает без ручной настройки.
Если нужно больше контроля, можно использовать wal_checkpoint, sqlite3_wal_checkpoint() или менять wal_autocheckpoint. Но практический совет простой: не трогайте порог, пока не увидите проблему в метриках. Сначала посмотрите на размер -wal, длительность reader-транзакций и время записи под нагрузкой. Только после этого имеет смысл переносить checkpoint в отдельное окно простоя или менять его частоту.
3.2. Контрольные точки, инициируемые приложением
Приложение может инициировать checkpoint через sqlite3_wal_checkpoint() или sqlite3_wal_checkpoint_v2(). Важные режимы здесь такие: PASSIVE почти не мешает рабочим запросам, FULL агрессивнее пытается завершить перенос страниц, а RESTART и TRUNCATE полезны, когда вы можете контролировать отсутствие долгих readers и хотите предсказуемо уменьшить -wal.
Хороший production-подход простой: запускать checkpoint в фоне по фактическому порогу, например после batch-записи или когда -wal вырос сильнее обычного. Если после PASSIVE файл не уменьшается, почти всегда проблема не в лимитах SQLite, а в долгоживущих читателях.
3.3. Сохранение режима WAL
В отличие от других режимов журналирования, PRAGMA journal_mode=WAL является постоянной. Если процесс устанавливает режим WAL, а затем закрывает и повторно открывает базу данных, база данных снова откроется в режиме WAL. Напротив, если процесс устанавливает (например) PRAGMA journal_mode=TRUNCATE, а затем закрывает и повторно открывает базу данных, она откроется в режиме отката по умолчанию DELETE, а не в предыдущем режиме TRUNCATE.
Постоянство режима WAL означает, что приложения можно перевести на использование SQLite в режиме WAL без каких-либо изменений в самом приложении. Достаточно лишь выполнить команду PRAGMA journal_mode=WAL; для файла(ов) базы данных с помощью командной оболочки или другой утилиты, а затем перезапустить приложение — я сам пользовался этим подходом при миграции нескольких проектов, и он работает именно так, как описано.
Режим журналирования WAL будет установлен для всех соединений с одним и тем же файлом базы данных, если он установлен хотя бы для одного соединения.
4. WAL-файл
Пока соединение с базой данных в режиме WAL открыто, SQLite поддерживает дополнительный файл журнала, называемый «Write Ahead Log» или «WAL-файл». Имя этого файла на диске обычно совпадает с именем файла базы данных с добавленным суффиксом -wal, хотя могут применяться иные правила именования, если SQLite скомпилирован с параметром SQLITE_ENABLE_8_3_NAMES.
WAL-файл существует до тех пор, пока хотя бы одно соединение с базой данных остаётся открытым. Как правило, WAL-файл автоматически удаляется при закрытии последнего соединения с базой данных.
Однако если последний процесс, работавший с базой данных, завершился без корректного закрытия соединения, или если используется файловый контроль SQLITE_FCNTL_PERSIST_WAL, WAL-файл может остаться на диске после закрытия всех соединений с базой данных. WAL-файл является частью постоянного состояния базы данных и должен храниться вместе с базой данных при её копировании или перемещении.
Если файл базы данных будет отделён от своего WAL-файла, ранее зафиксированные транзакции могут быть потеряны или файл базы данных может оказаться повреждённым. Единственный безопасный способ удалить WAL-файл — открыть файл базы данных с помощью одного из интерфейсов sqlite3_open(), а затем немедленно закрыть базу данных с помощью sqlite3_close().
Формат WAL-файла точно определён и является кроссплатформенным.
5. Базы данных только для чтения
Более старые версии SQLite не могли читать базу данных в режиме WAL, открытую только для чтения. Иными словами, для чтения базы данных в режиме WAL требовался доступ на запись. Это ограничение было снято начиная с версии SQLite 3.22.0 (2018-01-22).
В более новых версиях SQLite база данных в режиме WAL на носителе только для чтения или база данных в режиме WAL, не имеющая разрешения на запись, всё равно может быть прочитана при выполнении одного или нескольких из следующих условий:
Файлы -shm и -wal уже существуют и доступны для чтения.
Имеется разрешение на запись в каталог, содержащий базу данных, чтобы файлы -shm и -wal могли быть созданы.
Соединение с базой данных открыто с использованием параметра запроса immutable.
Несмотря на то что открытие базы данных в режиме WAL только для чтения возможно, рекомендуется перевести базу данных в режим PRAGMA journal_mode=DELETE перед записью образа базы данных SQLite на носитель только для чтения.
На практике это особенно важно для приложений, где база поставляется как часть инсталлятора, Docker image или мобильного bundle. Если вы распространяете заранее подготовленный .db файл вместе с приложением, безопаснее завершить все checkpoint, вернуть базу в DELETE и только потом делать финальный артефакт.
Иначе на чужой машине вы можете получить неприятную смесь из readonly-носителя, отсутствующих -wal и -shm файлов и непредсказуемых ошибок открытия.
Рабочий чек-лист для readonly-сценария такой: убедиться, что база закрыта корректно, выполнить явный checkpoint, проверить отсутствие writer-процессов и только затем архивировать или копировать базу. Если readonly-доступ нужен уже к действующей WAL-базе, проверьте версию SQLite не ниже 3.22.0, наличие рядом файлов -wal и -shm и права на каталог.
Контроль размера WAL-файла: предотвращение чрезмерного роста
В штатном режиме WAL редко требует ручного вмешательства: файл вырастает примерно до 1000 страниц, после чего SQLite запускает checkpoint и начинает использовать WAL повторно. Проблемы начинаются, когда автоматический checkpoint отключён, когда в системе висят долгие reader-транзакции или когда одно приложение пишет слишком большими пакетами. Во всех трёх случаях -wal может вырасти сильнее ожидаемого и начать бить по latency чтения и по диску.
Самый частый сценарий в реальных проектах — checkpoint starvation. Один или несколько readers держат снимок слишком долго, поэтому checkpoint не может завершиться и лишь частично продвигается вперёд. Если это похоже на ваш случай, ищите не «магическую» прагму, а место, где приложение слишком долго держит открытый SELECT. Полезно логировать длительность reader-транзакций и периодически смотреть фактический размер *.db-wal на staging и production.
Вторая типичная причина — крупные batch-записи. Когда одна транзакция обновляет много страниц, WAL закономерно растёт до её завершения. Поэтому лучший практический рецепт такой: держать транзакции разумно короткими, не отключать checkpoint без ясной причины и для аварийной очистки использовать управляемый wal_checkpoint, а не ручное удаление файлов.
7. Реализация разделяемой памяти для WAL-индекса
WAL-индекс хранится в разделяемой памяти, чтобы читатели быстро находили нужные страницы в -wal без полного прохода по файлу. SQLite использует для этого файл в том же каталоге, что и база, потому что так все процессы гарантированно смотрят в одну и ту же область данных на любой поддерживаемой платформе.
Практическое следствие важнее внутренней реализации: если у процесса нет нормального доступа к каталогу базы или если база лежит на сетевой файловой системе, WAL перестаёт быть хорошим выбором. Для большинства приложений этого правила достаточно, чтобы принять архитектурное решение и не разбирать детали wal-index глубже.
8. Использование WAL без разделяемой памяти
Начиная с версии SQLite 3.7.4, WAL можно использовать и без разделяемой памяти, если база гарантированно открыта только одним процессом и locking_mode=EXCLUSIVE включён до первого доступа. Это полезно для узких сценариев: одиночный embedded-процесс, нестандартный VFS или окружение без полноценной shared memory.
В обычном прикладном коде такой режим лучше считать исключением. Как только вам нужен второй процесс, нормальный multi-reader доступ или предсказуемая эксплуатация, возвращайтесь к стандартной схеме с -shm. Проверочный критерий простой: если вы не можете доказать, что база всегда открыта ровно одним процессом, не стройте конфигурацию на EXCLUSIVE.
9. Иногда запросы возвращают SQLITE_BUSY в режиме WAL
Одно из ключевых преимуществ режима WAL состоит в том, что писатели не блокируют читателей, а читатели не блокируют писателей. В большинстве случаев это действительно так. Однако существуют некоторые редкие ситуации, когда запрос к базе данных в режиме WAL может вернуть SQLITE_BUSY, поэтому приложения должны быть готовы к такому развитию событий — я рекомендую всегда предусматривать обработку этой ошибки в коде.
Случаи, когда запрос к базе данных в режиме WAL может вернуть SQLITE_BUSY, включают следующее:
Если другое соединение с базой данных открыло базу данных в режиме эксклюзивной блокировки, то все запросы к этой базе данных будут возвращать SQLITE_BUSY. Например, Chrome и Firefox открывают свои файлы баз данных в режиме эксклюзивной блокировки, поэтому попытки читать базы данных Chrome или Firefox во время работы этих приложений столкнутся с данной проблемой.
Когда последнее соединение с конкретной базой данных закрывается, это соединение на короткое время захватывает эксклюзивную блокировку, пока выполняет очистку файлов WAL и разделяемой памяти. Если в то время, пока первое соединение ещё находится в процессе очистки, будет предпринята отдельная попытка открыть и запросить базу данных, второе соединение может получить ошибку SQLITE_BUSY.
Если последнее соединение с базой данных аварийно завершилось, то первое новое соединение, открывающее базу данных, запустит процесс восстановления. Во время восстановления удерживается эксклюзивная блокировка. Таким образом, если третье соединение с базой данных попытается вмешаться и выполнить запрос, пока второе соединение выполняет восстановление, третье соединение получит ошибку SQLITE_BUSY.
10. Обратная совместимость
Формат файла базы данных не изменился для режима WAL. Однако файл WAL и wal-index являются новыми концепциями, поэтому более старые версии SQLite не будут знать, как восстановить аварийно завершившуюся базу данных SQLite, которая работала в режиме WAL в момент сбоя.
Чтобы предотвратить попытки более старых версий SQLite (до версии 3.7.0, выпущенной 2010-07-22) восстановить базу данных в режиме WAL (и не усугубить ситуацию), номера версий формата файла базы данных (байты 18 и 19 в заголовке базы данных) увеличены с 1 до 2 в режиме WAL.
Таким образом, если более старая версия SQLite попытается подключиться к базе данных SQLite, работающей в режиме WAL, она сообщит об ошибке вида «file is encrypted or is not a database».
Можно явно выйти из режима WAL с помощью такой прагмы:
PRAGMA journal_mode=DELETE;
Намеренный выход из режима WAL возвращает номера версий формата файла базы данных обратно к 1, чтобы более старые версии SQLite снова могли получить доступ к файлу базы данных.
11. Ошибка WAL-Reset
3 марта 2026 года один из разработчиков SQLite (Дэн) обнаружил и исправил ошибку, которая в редких случаях могла приводить к повреждению базы данных. Мы называем её «ошибкой WAL-reset». Ключевые моменты:
Ошибка, вероятно, присутствует во всех версиях SQLite начиная с 3.7.0 (2010-07-21) по 3.51.2 (2026-01-09). Она исправлена в версии 3.51.3 (2026-03-13) и более поздних. Бэкпорты исправления доступны для некоторых более ранних выпусков: 3.44.6 и 3.50.7.
Ошибка затрагивает только базы данных в режиме WAL, когда к одному и тому же файлу открыты два или более соединений с базой данных в отдельных потоках или процессах, и когда эти два соединения пытаются выполнить запись или контрольную точку в один и тот же момент.
Ошибка представляет собой гонку данных (data race) с жёсткими временными ограничениями. В обычном использовании она вряд ли возникнет. Разработчикам никогда не удавалось воспроизвести эту ошибку органически, и им пришлось добавить специальную логику тестирования в SQLite, которая намеренно создаёт обстоятельства возникновения ошибки, чтобы убедиться в том, что проблема исправлена.
11.1. Подробности об ошибке
Механика ошибки сводится к неудачному совпадению двух checkpoint и параллельной записи. Первая контрольная точка успешно заканчивается, затем почти сразу стартует вторая, а параллельная транзакция сбрасывает WAL и начинает заново писать в его начало. Из-за гонки данных второй checkpoint может посчитать часть страниц уже перенесёнными в основную базу, хотя это не так.
Проблема проявляется не мгновенно: после ещё нескольких записей следующий checkpoint способен пропустить часть данных и база получает повреждённое состояние. Поэтому практический вывод простой: если у вас несколько процессов работают с WAL, версию SQLite нужно держать актуальной и не откладывать обновление бесконечно.
11.2. Низкая вероятность возникновения
Вероятность возникновения ошибки крайне низкая: разработчики SQLite не смогли воспроизвести её обычным путём и использовали специальную тестовую логику, чтобы поймать нужный момент гонки. Это не emergency-фикс, ради которого нужно мгновенно останавливать сервисы.
Но последствия у бага тяжёлые, поэтому правильная реакция остаётся практической: проверить версию SQLite в проекте, обновить её в ближайшее плановое окно и отдельно посмотреть на многопроцессные сценарии с одновременными checkpoint и записями. Такая профилактика стоит дешевле, чем восстановление повреждённой базы.
Часто задаваемые вопросы по SQLite WAL
Можно ли использовать режим WAL через сетевую файловую систему (NFS, SMB)?
Нет. WAL требует общего wal-index, а его нормальная работа предполагает один хост и локальный доступ к файлам базы.
Как включить режим WAL и можно ли его отключить обратно?
Для включения выполните PRAGMA journal_mode=WAL;, а для возврата к обычному режиму — PRAGMA journal_mode=DELETE;. Настройка сохраняется между перезапусками.
Почему файл WAL иногда не уменьшается в размере?
Чаще всего из-за долгих reader-транзакций: checkpoint не может завершиться и сбросить накопленные страницы. Сначала ищите зависшие чтения, затем уже пробуйте ручной checkpoint.
Что произойдёт, если удалить файл -wal вручную?
Это опасно: можно потерять уже зафиксированные изменения или повредить базу. Безопасный путь — дать SQLite самой завершить checkpoint и корректно закрыть соединения.
Нужно ли обновляться из-за ошибки WAL-Reset?
Да, обновление рекомендуется, особенно если у вас несколько процессов пишут в одну базу. Это не emergency-фикc, но откладывать его надолго не стоит.



