Как повредить файл базы данных SQLite

База данных SQLite имеет высокую устойчивость к повреждениям. При сбое приложения, отключении питания или других форс-мажорных обстоятельствах часть данных будет автоматически откатана при следующем доступе к файлу базы данных, что защищает от потери информации

Вся рубрика SQLite: уроки, инструменты и примеры

Хотя SQLite демонстрирует значительную устойчивость к повреждениям базы данных, она не является полностью защищённой и может столкнуться с проблемами. В дальнейшем описываются типичные способы, которые могут привести к повреждению базы данных SQLite

По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы

Содержание
  1. Перезапись файла посторонним потоком или процессом
  2. Продолжение использования файлового дескриптора после его закрытия
  3. Резервное копирование или восстановление во время активной транзакции
  4. Удаление горячего журнала
  5. Несоответствие файлов базы данных и горячих журналов
  6. Проблемы с блокировкой файлов
  7. Файловые системы с неработающей или отсутствующей реализацией блокировок
  8. Снятие POSIX-блокировок отдельным потоком через close()
  9. Несколько копий SQLite, слинкованных в одном приложении
  10. Два процесса, использующие разные протоколы блокировки
  11. Удаление или переименование файла базы данных во время использования
  12. Несколько ссылок на один и тот же файл
  13. Перенос открытого соединения с базой данных через fork()
  14. Сбой синхронизации
  15. Дисковые накопители, не выполняющие запросы синхронизации
  16. Отключение синхронизации с помощью PRAGMA
  17. Сбои дисковых накопителей и флеш-памяти
  18. Контроллеры флеш-памяти, не обеспечивающие защиту при отключении питания
  19. Поддельные USB-накопители с завышенной ёмкостью
  20. Повреждение памяти
  21. Другие проблемы операционной системы
  22. Linux Threads
  23. Сбои mmap() в QNX
  24. Повреждение файловой системы
  25. Ошибки конфигурации SQLite
  26. Ошибки в SQLite
  27. Состояние гонки при записи в базу данных в режиме WAL
  28. Устаревшие индексы выражений
  29. Ложные сообщения о повреждении из-за уменьшения размера базы данных
  30. Повреждение после переключений между режимами отката и WAL
  31. Ошибка ввода-вывода при получении блокировки приводит к повреждению
  32. Утечка страниц базы данных из списка свободных страниц
  33. Повреждение после чередующихся операций записи из версий 3.6 и 3.7
  34. Состояние гонки при восстановлении в системах Windows
  35. Ошибка граничного значения во вторичных журналах, используемых вложенными транзакциями
  36. Типичные ошибки при работе с SQLite и как их избежать
  37. Что ещё изучить по теме

Перезапись файла посторонним потоком или процессом

Файлы базы данных SQLite функционируют как обычные дисковые файлы, что позволяет любому процессу открывать их и записывать случайные данные. Это создаёт уязвимость, так как библиотека SQLite не имеет средств для предотвращения таких действий

Продолжение использования файлового дескриптора после его закрытия

Мы исследовали случаи, когда файловый дескриптор, открытый для конкретного файла, затем закрывался и снова использовался для базы данных SQLite. В результате другой поток продолжал запись в старый файловый дескриптор, не осознавая, что исходный файл уже закрыт, что приводило к перезаписи данных и повреждению базы данных

Один из случаев произошёл 30 августа 2013 года в репозитории Fossil DVCS. В данной ситуации файловый дескриптор 2 (стандартный поток ошибок) был закрыт программой stunnel до вызова функции sqlite3_open_v2(), и дескриптор, используемый для базы данных репозитория, получил номер 2. Ошибка в приложении привела к выводу сообщения через оператор assert(), который вызвал write(2,…)

В результате сообщения об ошибке перезаписали данные в базе данных, поскольку файловый дескриптор 2 указывал на файл базы данных. С версии 3.8.1 (17 октября 2013 года) SQLite больше не использует низкие номера файловых дескрипторов для файлов баз данных

Ещё один пример повреждения, вызванного использованием закрытого файлового дескриптора, был описан инженерами Facebook в публикации в блоге от 2014-08-12

В Fossil 2019-07-11 был зафиксирован аналогичный случай. Файловый дескриптор открывался для вывода отладочной информации, затем закрывался и снова открывался для SQLite. В это время логика отладки продолжала записывать данные в старый дескриптор. Обсуждение на форуме содержит отчёт об ошибке и ссылку на исправление

Резервное копирование или восстановление во время активной транзакции

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

Существует несколько безопасных подходов к созданию резервных копий баз данных SQLite — безопасных в том смысле, что они создают корректную, неповреждённую резервную копию. В произвольном порядке:

Утилита sqlite3_rsync (доступная с SQLite 3.47.0 (2024-10-21) и более поздних версий) может создать копию работающей базы данных SQLite через SSH, используя эффективный протокол

Команда VACUUM INTO filename копирует текущее состояние базы данных SQLite в отдельный файл

Backup API — это интерфейс на языке C, который может создать согласованную копию базы данных SQLite

Все перечисленные подходы работают даже с работающей базой данных. Также безопасно создать копию файла SQLite, если во время копирования не происходят транзакции. Если предыдущая транзакция записи завершилась неудачей, важно скопировать журналы отката или журнал с упреждающей записью вместе с файлом базы данных

Удаление горячего журнала

SQLite обычно хранит все содержимое в одном дисковом файле. Однако при выполнении транзакции информация, необходимая для восстановления базы данных после сбоя или отключения питания, хранится во вспомогательных файлах журналов, называемых «горячими». Эти журналы имеют то же имя, что и файл базы данных, с добавлением суффикса -journal или -wal

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

Ещё одним проявлением этой проблемы является повреждение базы данных, вызванное непоследовательным использованием имён файлов в формате 8+3

Несоответствие файлов базы данных и горячих журналов

Предыдущий пример является частным случаем более общей проблемы: состояние базы данных SQLite определяется как файлом базы данных, так и файлом журнала. В состоянии покоя файл журнала не существует и имеет значение только файл базы данных. Но если файл журнала существует, он должен храниться вместе с базой данных во избежание повреждения. Следующие действия с высокой вероятностью приведут к повреждению:

  • Обмен файлами журналов между двумя разными базами данных.
  • Перезапись файла журнала другим файлом журнала.
  • Перемещение файла журнала из одной базы данных в другую.
  • Копирование файла базы данных без одновременного копирования его журнала.
  • Перезапись файла базы данных другим файлом без одновременного удаления горячего журнала, связанного с исходной базой данных.

Проблемы с блокировкой файлов

SQLite использует блокировки файлов для файла базы данных, а также для файла журнала с упреждающей записью (WAL), чтобы координировать доступ между параллельными процессами. Без координации два потока или процесса могут попытаться одновременно внести несовместимые изменения в файл базы данных, что приведёт к его повреждению

Файловые системы с неработающей или отсутствующей реализацией блокировок

SQLite зависит от базовой файловой системы в части выполнения блокировок в соответствии с документацией. Однако некоторые файловые системы содержат ошибки в логике блокировок, из-за чего блокировки не всегда ведут себя так, как заявлено. Это особенно характерно для сетевых файловых систем и NFS в частности

Если SQLite используется в файловой системе, где примитивы блокировки содержат ошибки, и если два или более потока или процесса пытаются одновременно обратиться к одной и той же базе данных, это может привести к повреждению базы данных

Снятие POSIX-блокировок отдельным потоком через close()

Механизм блокировки, используемый SQLite на платформах Unix по умолчанию, — это консультативная блокировка POSIX (POSIX advisory locking). К сожалению, POSIX advisory locking имеет особенности проектирования, которые делают его склонным к неправильному использованию и сбоям

В частности, любой поток в том же процессе, имеющий файловый дескриптор с активной POSIX advisory lock, может переопределить эту блокировку с помощью другого файлового дескриптора. Особенно опасная проблема состоит в том, что системный вызов close() отменяет все POSIX advisory locks на одном и том же файле для всех потоков и всех файловых дескрипторов в процессе

Так, например, предположим, что многопоточный процесс имеет два или более потока с отдельными соединениями SQLite к одному и тому же файлу базы данных. Затем появляется третий поток, который хочет самостоятельно прочитать что-то из того же файла базы данных, не используя библиотеку SQLite. Возможно, третий поток хочет сделать резервную копию базы данных

Или, может быть, третий поток просто пытается определить тип файла и поэтому пробует прочитать первые 16 байт, чтобы убедиться, что это действительно база данных SQLite. Независимо от причины, третий поток выполняет open(), read(), а затем close(). Казалось бы, это безвредно. Но системный вызов close() привёл к тому, что блокировки, удерживаемые на базе данных всеми остальными потоками, были сняты

Эти другие потоки никак не могут узнать, что их блокировки только что были уничтожены — POSIX не предоставляет никакого механизма для определения этого, — и поэтому они продолжают работу в предположении, что их блокировки по-прежнему действительны. Это может привести к тому, что два или более потока или процесса попытаются одновременно выполнить запись в базу данных, что приведёт к её повреждению

Обратите внимание, что для двух или более потоков совершенно безопасно обращаться к одному и тому же файлу базы данных SQLite через библиотеку SQLite. Unix-драйверы SQLite знают об особенностях POSIX advisory locking и обходят их. Эта проблема возникает только тогда, когда поток пытается обойти библиотеку SQLite и напрямую читает файл базы данных

Начиная с версии SQLite 3.51.0 (2025-11-04), SQLite реализует дополнительные средства защиты, чтобы попытаться избежать проблем, вызванных блокировками, которые нарушаются вызовом close(). Эти новые средства защиты помогают, когда база данных находится в режиме WAL и к ней обращаются из нескольких процессов. Но они не являются универсальным решением

Чтобы избежать повреждений, разработчики должны следить за тем, чтобы никогда не использовать close() для файла базы данных SQLite, пока открыто одно или несколько соединений с базой данных, даже в других потоках

Несколько копий SQLite, слинкованных в одном приложении

Как указано в предыдущем разделе, SQLite предпринимает шаги для обхода особенностей POSIX advisory locking. Часть этого обходного решения включает ведение глобального списка (защищённого мьютексом) открытых файлов баз данных SQLite. Но если в одно приложение слинкованы несколько копий SQLite, то будет существовать несколько экземпляров этого глобального списка

Соединения с базой данных, открытые с использованием одной копии библиотеки SQLite, не будут знать о соединениях с базой данных, открытых с использованием другой копии, и не смогут обойти особенности POSIX advisory locking. Операция close() на одном соединении может незаметно снять блокировки с другого соединения с базой данных, что приведёт к её повреждению

Описанный выше сценарий звучит неправдоподобно. Но разработчикам SQLite известен как минимум один коммерческий продукт, выпущенный именно с этой ошибкой. Поставщик обратился к разработчикам SQLite за помощью в отслеживании нечастых проблем с повреждением базы данных, которые они наблюдали на Linux и Mac

В конечном счёте проблема была прослежена до того факта, что приложение компоновалось с двумя отдельными копиями SQLite. Решением стало изменение процедур сборки приложения таким образом, чтобы компоновка выполнялась только с одной копией SQLite вместо двух

Два процесса, использующие разные протоколы блокировки

Механизм блокировки, используемый SQLite на платформах Unix по умолчанию, — это POSIX advisory locking, но существуют и другие варианты. Выбрав альтернативный sqlite3_vfs с помощью интерфейса sqlite3_open_v2(), приложение может использовать другие протоколы блокировки, которые могут быть более подходящими для определённых файловых систем

Например, блокировка через dot-file может быть выбрана для использования в приложении, которое должно работать на файловой системе NFS, не поддерживающей POSIX advisory locking

Важно, чтобы все соединения с одним и тем же файлом базы данных использовали один и тот же протокол блокировки. Если одно приложение использует POSIX advisory locks, а другое — блокировку через dot-file, то два приложения не будут видеть блокировки друг друга и не смогут координировать доступ к базе данных, что может привести к её повреждению

Удаление или переименование файла базы данных во время использования

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

Обратите внимание, что это возможно только на системах POSIX и POSIX-подобных системах, которые разрешают удаление файла, пока он ещё открыт для чтения и записи. Windows не допускает этого. Поскольку файлы журнала отката и файлы WAL основаны на имени файла базы данных, два разных файла баз данных будут совместно использовать один и тот же файл журнала отката или WAL

Откат или восстановление для одной из баз данных может использовать содержимое другой базы данных, что приведёт к повреждению. Аналогичная проблема возникает, если файл базы данных переименовывается, пока он открыт, и с прежним именем создаётся новый файл

Иными словами, удаление или переименование открытого файла базы данных приводит к поведению, которое не определено и, вероятно, нежелательно

Начиная с версии SQLite 3.7.17 (2013-05-20), Unix-интерфейс ОС будет отправлять сообщения SQLITE_WARNING в журнал ошибок, если файл базы данных удаляется, пока он ещё используется

Несколько ссылок на один и тот же файл

Если один файл базы данных имеет несколько ссылок (жёстких или символических), то это просто другой способ сказать, что файл имеет несколько имён. Если два или более процессов открывают базу данных под разными именами, то они будут использовать разные файлы журнала отката и WAL

Это означает, что если один процесс аварийно завершится, другой процесс не сможет восстановить выполнявшуюся транзакцию, поскольку будет искать соответствующий журнал не в том месте

Иными словами, открытие и использование файла базы данных, имеющего два или более имён, приводит к поведению, которое не определено и, вероятно, нежелательно

Начиная с версии SQLite 3.7.17 (2013-05-20), Unix-интерфейс ОС будет отправлять сообщения SQLITE_WARNING в журнал ошибок, если файл базы данных имеет несколько жёстких ссылок

Начиная с версии SQLite 3.10.0 (2016-01-06), Unix-интерфейс ОС будет пытаться разрешать символические ссылки и открывать файл базы данных по его каноническому имени. До версии 3.10.0 открытие файла базы данных через символическую ссылку было аналогично открытию файла базы данных с несколькими жёсткими ссылками и приводило к неопределённому поведению

Перенос открытого соединения с базой данных через fork()

Не открывайте соединение с базой данных SQLite, затем вызывайте fork(), а потом пытайтесь использовать это соединение в дочернем процессе. Это приведёт к всевозможным проблемам с блокировками, и вы легко можете получить повреждённую базу данных. SQLite не предназначен для поддержки такого поведения. Любое соединение с базой данных, используемое в дочернем процессе, должно быть открыто в дочернем процессе, а не унаследовано от родительского

Не вызывайте даже sqlite3_close() для соединения с базой данных из дочернего процесса, если соединение было открыто в родительском. Закрыть базовый файловый дескриптор безопасно, однако интерфейс sqlite3_close() может инициировать операции очистки, которые удалят содержимое из-под родительского процесса, что приведёт к ошибкам и, возможно, даже к повреждению базы данных

Сбой синхронизации

Чтобы гарантировать постоянную согласованность файлов базы данных, SQLite периодически запрашивает у операционной системы сброс всех ожидающих записей в постоянное хранилище и ожидает завершения этого сброса. Это достигается с помощью системного вызова fsync() в Unix и FlushFileBuffers() в Windows. Мы называем этот сброс ожидающих записей «синхронизацией» (sync)

На самом деле, если нас интересуют только атомарные и согласованные записи и мы готовы пожертвовать долговечностью записей, операция синхронизации не обязана ждать, пока содержимое полностью сохранится на постоянном носителе. Вместо этого операцию синхронизации можно рассматривать как барьер ввода-вывода (I/O barrier)

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

Дисковые накопители, не выполняющие запросы синхронизации

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

Это создаёт иллюзию более быстрой работы накопителей — что крайне важно для производителей, чтобы демонстрировать хорошие результаты в тестах в отраслевых изданиях. И, справедливости ради, эта ложь обычно не причиняет вреда, пока не происходит потери питания или жёсткого сброса до того, как содержимое буфера дорожки действительно будет записано на магнитный слой

Но если потеря питания или жёсткий сброс всё же произойдут, и если это приведёт к тому, что содержимое, записанное после синхронизации, окажется на магнитном слое, тогда как содержимое, записанное до синхронизации, всё ещё находится в буфере дорожки, — повреждение базы данных может произойти

USB-флеш-накопители, по всей видимости, особенно бессовестно лгут относительно запросов синхронизации. Это легко проверить, зафиксировав большую транзакцию в базе данных SQLite на USB-флеш-накопителе

Команда COMMIT вернётся относительно быстро, сигнализируя о том, что флеш-накопитель сообщил операционной системе, а та — SQLite, что всё содержимое надёжно находится в постоянном хранилище, — и тем не менее светодиод на конце флеш-накопителя продолжит мигать ещё несколько секунд. Извлечение флеш-накопителя, пока светодиод ещё мигает, нередко приводит к повреждению базы данных

Обратите внимание, что SQLite вынужден доверять тому, что сообщают ему операционная система и аппаратное обеспечение о состоянии запросов синхронизации. У SQLite нет способа обнаружить, что кто-то из них лжёт и что записи могут выполняться не в том порядке. Однако SQLite в режиме WAL значительно более устойчив к записям не в том порядке, чем в режимах журнала отката по умолчанию

В режиме WAL единственный момент, когда неудачная операция синхронизации может привести к повреждению базы данных, — это операция контрольной точки (checkpoint). Сбой синхронизации во время COMMIT может привести к потере долговечности, но не к повреждению файла базы данных

Поэтому одна из линий защиты от повреждения базы данных из-за неудачных операций синхронизации — использовать SQLite в режиме WAL и выполнять контрольные точки как можно реже

Отключение синхронизации с помощью PRAGMA

Операции синхронизации, которые SQLite выполняет для обеспечения целостности, можно отключить во время выполнения с помощью pragma synchronous. Установив PRAGMA synchronous=OFF, можно полностью исключить все операции синхронизации

Это создаёт иллюзию более быстрой работы SQLite, однако также позволяет операционной системе свободно переупорядочивать записи, что может привести к повреждению базы данных в случае сбоя питания или жёсткого сброса до того, как всё содержимое попадёт в постоянное хранилище

Для максимальной надёжности и устойчивости к повреждению базы данных SQLite всегда следует запускать с настройкой синхронизации по умолчанию — FULL

Сбои дисковых накопителей и флеш-памяти

База данных SQLite может быть повреждена, если содержимое файла изменится вследствие сбоя дискового накопителя или флеш-памяти. Это происходит крайне редко, однако диски иногда переворачивают бит в середине сектора

Контроллеры флеш-памяти, не обеспечивающие защиту при отключении питания

Нам сообщали, что в некоторых контроллерах флеш-памяти логика выравнивания износа (wear leveling) может вызывать случайные повреждения файловой системы при прерывании питания во время записи. Это может проявляться, например, в виде случайных изменений в середине файла, который даже не был открыт в момент потери питания

Так, например, устройство записывало содержимое в MP3-файл во флеш-памяти, когда произошла потеря питания, — и это могло привести к повреждению базы данных SQLite, даже если база данных в тот момент вообще не использовалась

Поддельные USB-накопители с завышенной ёмкостью

В обращении находится множество мошеннических USB-накопителей, которые сообщают о высокой ёмкости (например, 8 ГБ), тогда как реально способны хранить значительно меньший объём данных (например, 1 ГБ). Попытки записи на такие устройства нередко приводят к перезаписи несвязанных файлов. Любое использование мошеннического устройства флеш-памяти может легко привести к повреждению базы данных

Поисковые запросы вроде «fake capacity usb» выдают множество тревожной информации об этой проблеме

Повреждение памяти

SQLite — это C-библиотека, работающая в том же адресном пространстве, что и обслуживаемое ею приложение. Это означает, что блуждающие указатели, переполнения буфера, повреждение кучи или другие неисправности в приложении могут повредить внутренние структуры данных SQLite и в конечном счёте привести к повреждению файла базы данных

Обычно подобные проблемы проявляются в виде ошибок сегментации (segfault) ещё до того, как происходит какое-либо повреждение базы данных, однако бывали случаи, когда ошибки в коде приложения приводили к тому, что SQLite давал сбой незаметно — так, что база данных повреждалась, а не происходило аварийное завершение

Проблема повреждения памяти становится острее при использовании ввода-вывода с отображением в память (memory-mapped I/O). Когда весь файл базы данных или его часть отображается в адресное пространство приложения, блуждающий указатель, перезаписывающий любую часть этого отображённого пространства, немедленно повреждает файл базы данных — без необходимости последующего системного вызова write() со стороны приложения

Другие проблемы операционной системы

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

Linux Threads

Некоторые старые версии Linux использовали библиотеку LinuxThreads для поддержки потоков. LinuxThreads похожа на Pthreads, но тонко отличается от неё в части обработки консультативных блокировок POSIX. SQLite версий с 2.2.3 по 3.6.23 определял во время выполнения, что используется LinuxThreads, и предпринимал соответствующие действия для обхода нестандартного поведения LinuxThreads

Однако большинство современных реализаций Linux используют более новую и корректную реализацию Pthreads — NPTL. Начиная с версии SQLite 3.7.0 (2010-07-21), использование NPTL предполагается по умолчанию. Никаких проверок не выполняется

Следовательно, последние версии SQLite будут работать с незначительными сбоями и могут повреждать файлы баз данных при использовании в многопоточных приложениях, работающих на старых системах Linux, применяющих LinuxThreads

Сбои mmap() в QNX

В QNX существует некая тонкая проблема с mmap(), при которой повторный вызов mmap() для одного дескриптора файла может привести к обнулению памяти, полученной при первом вызове mmap(). SQLite на Unix использует mmap() для создания области разделяемой памяти при координации транзакций в режиме WAL, и при крупных транзакциях он вызывает mmap() несколько раз

Было продемонстрировано, что mmap() в QNX повреждает файл базы данных в подобном сценарии. Инженеры QNX осведомлены об этой проблеме и работают над её решением; к моменту, когда вы читаете это, проблема, возможно, уже устранена

При работе на QNX рекомендуется никогда не использовать ввод-вывод с отображением в память. Кроме того, для использования режима WAL рекомендуется, чтобы приложения применяли режим эксклюзивной блокировки с целью использования WAL без разделяемой памяти

Повреждение файловой системы

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

Например, 2013-10-01 база данных SQLite, хранящая Wiki для Tcl/Tk, оказалась повреждена спустя несколько дней после того, как хост-компьютер был перенесён на сомнительную сборку ядра Linux, имевшую проблемы на уровне файловой системы

В том случае файловая система в итоге оказалась настолько сильно повреждена, что машина стала непригодна к использованию, однако самым ранним симптомом неполадок стала именно повреждённая база данных SQLite

Ошибки конфигурации SQLite

SQLite имеет множество встроенных средств защиты от повреждения базы данных. Однако многие из этих средств защиты могут быть отключены параметрами конфигурации. Если средства защиты отключены, повреждение базы данных может произойти

Ниже приведены примеры отключения встроенных механизмов защиты SQLite:

  • Установка PRAGMA synchronous=OFF может привести к повреждению базы данных при сбое операционной системы или отключении питания, хотя этот параметр безопасен в случае аварийного завершения приложения.
  • Изменение PRAGMA schema_version при наличии других открытых подключений к базе данных.
  • Использование PRAGMA journal_mode=OFF или PRAGMA journal_mode=MEMORY и аварийное завершение приложения в середине транзакции записи.
  • Установка PRAGMA writable_schema=ON с последующим изменением схемы базы данных с помощью операторов DML может сделать базу данных полностью нечитаемой, если не выполнять это с осторожностью.

Ошибки в SQLite

SQLite очень тщательно тестируется, чтобы обеспечить максимальную свободу от ошибок. Среди многочисленных тестов, проводимых для каждой версии SQLite, есть тесты, имитирующие сбои питания, ошибки ввода-вывода и ошибки нехватки памяти (OOM — out of memory), чтобы убедиться в отсутствии повреждений базы данных при любом из этих событий. SQLite также проверен в реальных условиях — приблизительно два миллиарда активных развёртываний без серьёзных проблем

Тем не менее ни одна программа не является стопроцентно совершенной. В истории SQLite было несколько ошибок (ныне исправленных), которые могли приводить к повреждению базы данных. И, возможно, ещё несколько остаются необнаруженными. Благодаря обширному тестированию и широкому использованию SQLite ошибки, приводящие к повреждению базы данных, как правило, очень редки

Вероятность того, что приложение столкнётся с ошибкой SQLite, невелика. Для иллюстрации этого ниже приводится описание всех ошибок, приводящих к повреждению базы данных, обнаруженных в SQLite за четырёхлетний период с 2009-04-01 по 2013-04-15. Это описание должно дать читателю интуитивное представление о том, какого рода ошибки в SQLite умудряются проскользнуть сквозь процедуры тестирования и попасть в релиз

Состояние гонки при записи в базу данных в режиме WAL

Когда два или более подключений к базе данных существуют в отдельных потоках или процессах, оба открытых на одной базе данных в режиме WAL, и если оба подключения пытаются выполнить запись или контрольную точку одновременно, возникает состояние гонки (race condition), которое может повредить файл базы данных. Это ошибка сброса WAL (WAL-reset bug). Она существовала во всех версиях SQLite с 3.7.0 по 3.51.2. Подробности см. в документации по ошибке сброса WAL

Устаревшие индексы выражений

«Индекс выражения» (expression index) — это индекс, ссылающийся на выражение или VIRTUAL-генерируемый столбец. Предполагается, что индексы выражений используют только детерминированные функции. Однако при переносе базы данных между различными платформами или при смене версии SQLite иногда одна из этих предположительно детерминированных функций незначительно изменяет свой вывод

Это может привести к тому, что индекс будет выглядеть повреждённым. Дополнительные сведения см. в документации по устаревшим индексам выражений

Ложные сообщения о повреждении из-за уменьшения размера базы данных

Если база данных записана SQLite версии 3.7.0 или более поздней, а затем снова записана SQLite версии 3.6.23 или более ранней таким образом, что размер файла базы данных уменьшается, то при следующем обращении SQLite версии 3.7.0 к файлу базы данных он может сообщить о повреждении файла базы данных. Однако файл базы данных на самом деле не повреждён. Версия 3.7.0 просто проявляла излишнюю строгость в обнаружении повреждений

Проблема была исправлена 2011-02-20. Исправление впервые появилось в SQLite версии 3.7.6 (2011-04-12)

Повреждение после переключений между режимами отката и WAL

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

Эта проблема была обнаружена в ходе внутреннего тестирования и никогда не наблюдалась в реальных условиях. Проблема была исправлена 2011-01-27 и в версии 3.7.5

Ошибка ввода-вывода при получении блокировки приводит к повреждению

Если операционная система возвращает ошибку ввода-вывода при попытке получить определённую блокировку общей памяти в режиме WAL, SQLite может не выполнить сброс кэша, что при последующих операциях записи может привести к повреждению базы данных

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

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

Эта проблема была исправлена 2010-09-20 в SQLite версии 3.7.3

Утечка страниц базы данных из списка свободных страниц

Когда содержимое удаляется из базы данных SQLite, страницы, которые больше не используются, добавляются в список свободных страниц (freelist) и повторно используются для хранения содержимого, добавляемого последующими операциями вставки. Ошибка в SQLite, присутствовавшая в версиях с 3.6.16 по 3.7.2, могла приводить к исчезновению страниц из списка свободных страниц при использовании incremental_vacuum

Это не приводило к потере данных. Однако это приводило к тому, что файл базы данных становился больше, чем необходимо. И это приводило к тому, что pragma integrity_check сообщала об отсутствующих страницах в списке свободных страниц

Эта проблема была исправлена 2010-08-23 в SQLite версии 3.7.2

Повреждение после чередующихся операций записи из версий 3.6 и 3.7

SQLite версии 3.7.0 представила ряд новых улучшений формата файла базы данных SQLite (таких как, помимо прочего, WAL). Выпуск 3.7.0 был ознакомительным релизом для этих новых функций. Мы ожидали обнаружить проблемы и не были разочарованы

Если база данных была изначально создана с использованием SQLite версии 3.7.0, затем записана SQLite версии 3.6.23.1 таким образом, что размер файла базы данных увеличился, а затем снова записана SQLite версии 3.7.0, файл базы данных мог оказаться повреждённым

Эта проблема была исправлена 2010-08-04 в SQLite версии 3.7.1

Состояние гонки при восстановлении в системах Windows

SQLite версии 3.7.16.2 исправляет тонкое состояние гонки в логике блокировок в системах Windows. Когда файл базы данных нуждается в восстановлении, потому что предыдущий процесс, выполнявший запись в него, аварийно завершился в середине транзакции, и два или более процессов пытаются открыть эту базу данных одновременно, состояние гонки может привести к тому, что один из этих процессов получит ложное указание на то, что восстановление уже завершено, позволяя этому процессу продолжить использование файла базы данных без предварительного выполнения восстановления

Если этот процесс выполняет запись в файл, файл может оказаться повреждённым. Это состояние гонки, по всей видимости, существовало во всех предыдущих версиях SQLite для Windows, начиная с 2004 года. Однако гонка была очень редкой. Практически говоря, вам нужна быстрая многоядерная машина, на которой вы запускаете два процесса для выполнения восстановления в один и тот же момент на двух отдельных ядрах

Этот дефект существовал только в системах Windows и не затрагивал интерфейс POSIX ОС

Ошибка граничного значения во вторичных журналах, используемых вложенными транзакциями

Когда вложенная транзакция запускается с использованием SAVEPOINT, SQLite использует вторичный журнал отката для отслеживания изменений вложенной транзакции на случай, если внутреннюю транзакцию потребуется откатить. Вторичные журналы не участвуют в защите базы данных от повреждений из-за аварийного завершения программы или отключения питания. Вторичные журналы задействуются только при откате внутренней транзакции вложенной транзакции

Эти вторичные журналы могут храниться либо в памяти, либо в виде временных файлов на диске. Поведение по умолчанию — хранить их на диске. Однако это можно изменить с помощью параметра компиляции -DSQLITE_TEMP_STORE или во время выполнения с помощью оператора PRAGMA temp_store. Ошибка возникает только тогда, когда вторичные журналы хранятся в памяти

В SQLite версии 3.35.0 (2021-03-12) была добавлена новая оптимизация, позволяющая использовать меньше памяти при хранении вторичных журналов в памяти. К сожалению, проверка граничного значения в новой логике была закодирована неправильно. Оператор, который должен был быть <, был закодирован как <=. Эта ошибка могла привести к тому, что вторичный журнал переходил в несогласованное состояние при его откате

Если вносились дополнительные изменения и внешняя транзакция в конечном итоге фиксировалась, база данных могла оказаться в несогласованном состоянии

Эта проблема была обнаружена независимым исследователем, который пытался найти ошибки в SQLite с помощью фаззера (fuzzer). Фаззер обнаружил сбой в операторе assert(), который используется для проверки внутреннего состояния вторичного журнала

Ошибка была достаточно неочевидным граничным случаем, который мог оставаться незамеченным в течение многих лет, если бы не интенсивное использование операторов assert() в SQLite, настойчивость и упорство исследователей безопасности, а также их специализированный фаззер последнего поколения

Эта проблема была исправлена в версии 3.37.2 (2022-01-06)

Типичные ошибки при работе с SQLite и как их избежать

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

Не отключайте синхронизацию без понимания последствий. PRAGMA synchronous=OFF даёт заметный прирост скорости, но при сбое питания или жёстком сбросе системы база данных может оказаться в несогласованном состоянии. Если скорость критична, рассмотрите режим WAL вместо отключения синхронизации — он даёт хорошую производительность при сохранении защиты от повреждений при COMMIT

Никогда не копируйте файл базы данных «вживую» без специальных инструментов. Простое копирование файла во время активной транзакции гарантированно создаёт повреждённую резервную копию. Используйте VACUUM INTO, Backup API или sqlite3_rsync — все три инструмента корректно работают с работающей базой данных

Следите за файлами журналов рядом с базой данных. Файлы -journal и -wal — это не мусор, который можно удалить. Если они существуют рядом с файлом базы данных, они содержат данные, необходимые для восстановления. Удаление или перемещение этих файлов без самого файла базы данных приведёт к потере данных

Не используйте fork() с уже открытыми соединениями SQLite. Любое соединение, которое должно использоваться в дочернем процессе, обязано быть открыто уже в нём. Унаследованные соединения создают неопределённое поведение с блокировками и могут повредить базу данных

Убедитесь, что в приложение слинкована только одна копия SQLite. Наличие двух копий библиотеки в одном процессе — редкая, но реальная проблема, которая ломает внутренний механизм обхода особенностей POSIX advisory locking и приводит к незаметным повреждениям

Что ещё изучить по теме

Можно ли восстановить повреждённую базу данных SQLite?

В ряде случаев — да. Команда .recover в утилите командной строки sqlite3 пытается извлечь максимум данных из повреждённого файла. Также можно попробовать PRAGMA integrity_check для диагностики и PRAGMA writable_schema для ручного исправления схемы. Однако гарантий полного восстановления нет — лучшая стратегия это регулярные резервные копии через Backup API или VACUUM INTO

Безопасно ли использовать SQLite в многопоточном приложении?

Да, при соблюдении правил. Несколько потоков могут безопасно обращаться к одной базе данных через библиотеку SQLite — она сама обходит особенности POSIX advisory locking. Проблемы возникают только когда один из потоков обходит библиотеку и напрямую открывает файл базы данных, а затем закрывает его через close(), тем самым снимая блокировки всех остальных потоков

Чем режим WAL лучше режима журнала отката с точки зрения защиты от повреждений?

В режиме WAL сбой синхронизации во время COMMIT может привести к потере долговечности транзакции, но не к повреждению файла базы данных. В режиме журнала отката неудачная синхронизация может повредить базу данных напрямую. Единственный уязвимый момент в режиме WAL — операция контрольной точки (checkpoint), поэтому рекомендуется выполнять её как можно реже

Что произойдёт, если извлечь USB-флеш-накопитель, пока мигает светодиод после COMMIT?

Это один из наиболее частых сценариев повреждения. Команда COMMIT возвращается быстро, потому что накопитель солгал операционной системе о том, что данные записаны. Реальная запись ещё продолжается. Извлечение накопителя в этот момент нередко приводит к повреждению базы данных, поскольку часть данных, записанных до синхронизации, может не попасть на носитель

Как проверить целостность базы данных SQLite?

Используйте PRAGMA integrity_check — она проверяет структуру всех таблиц и индексов и сообщает об обнаруженных проблемах. Для быстрой проверки без полного сканирования существует PRAGMA quick_check. Если integrity_check сообщает об ошибках, это сигнал к немедленному созданию резервной копии через VACUUM INTO и анализу причины повреждения по списку сценариев из этого документа

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x