Spec-Zone.ru › SQLite

Что, если бы OpenDocument использовал SQLite?

Введение

Предположим, что формат файлов OpenDocument, а именно формат презентаций OpenDocument «ODP», был бы построен на основе SQLite. Это дало бы следующие преимущества:

  • Файлы меньшего размера
  • Более быстрое сохранение файлов
  • Более быстрый запуск
  • Меньше используемой памяти
  • Управление версиями документов
  • Более удобная работа для пользователя

Обратите внимание: это всего лишь мысленный эксперимент. Мы не предлагаем менять OpenDocument. Эта статья также не является критикой нынешнего устройства OpenDocument. Цель этого эссе — предложить способы улучшить форматы файлов, которые будут создаваться в будущем.

Об OpenDocument и формате презентаций OpenDocument

Формат файлов OpenDocument используется в офисных приложениях: текстовых редакторах, электронных таблицах и приложениях для создания презентаций. Изначально он был разработан для пакета OpenOffice, но с тех пор был включён и в другие пакеты настольных приложений. Приложение OpenOffice несколько раз становилось основой для ответвлений и меняло название. Автор в основном использует OpenDocument для создания презентаций с помощью NeoOffice на Mac или LibreOffice на Linux и Windows.

Файл презентации OpenDocument, или «ODP», — это ZIP-архив, содержащий XML-файлы с описанием слайдов презентации и отдельные файлы изображений, включённых в презентацию. (Файлы OpenDocument для текстовых редакторов и электронных таблиц устроены похожим образом, но в этой статье не рассматриваются.) Содержимое файла ODP легко увидеть с помощью команды «zip -l». Например, ниже приведён результат выполнения команды «zip -l» для презентации из 49 слайдов о SQLite, представленной на конференции SouthEast LinuxFest в 2014 году:

Archive:  self2014.odp
  Length      Date    Time    Name
---------  ---------- -----   ----
       47  2014-06-21 12:34   mimetype
        0  2014-06-21 12:34   Configurations2/statusbar/
        0  2014-06-21 12:34   Configurations2/accelerator/current.xml
        0  2014-06-21 12:34   Configurations2/floater/
        0  2014-06-21 12:34   Configurations2/popupmenu/
        0  2014-06-21 12:34   Configurations2/progressbar/
        0  2014-06-21 12:34   Configurations2/menubar/
        0  2014-06-21 12:34   Configurations2/toolbar/
        0  2014-06-21 12:34   Configurations2/images/Bitmaps/
    54702  2014-06-21 12:34   Pictures/10000000000001F40000018C595A5A3D.png
    46269  2014-06-21 12:34   Pictures/100000000000012C000000A8ED96BFD9.png
... 58 other pictures omitted...
    13013  2014-06-21 12:34   Pictures/10000000000000EE0000004765E03BA8.png
  1005059  2014-06-21 12:34   Pictures/10000000000004760000034223EACEFD.png
   211831  2014-06-21 12:34   content.xml
    46169  2014-06-21 12:34   styles.xml
     1001  2014-06-21 12:34   meta.xml
     9291  2014-06-21 12:34   Thumbnails/thumbnail.png
    38705  2014-06-21 12:34   Thumbnails/thumbnail.pdf
     9664  2014-06-21 12:34   settings.xml
     9704  2014-06-21 12:34   META-INF/manifest.xml
---------                     -------
 10961006                     78 files

ZIP-архив ODP содержит четыре разных XML-файла: content.xml, styles.xml, meta.xml и settings.xml. Эти четыре файла определяют макет слайдов, текстовое содержимое и оформление. В этой презентации содержится 62 изображения — от полноэкранных фотографий до крошечных значков; каждое из них хранится в отдельном файле в папке Pictures. Файл «mimetype» содержит одну строку текста:

application/vnd.oasis.opendocument.presentation

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

Ограничения формата презентаций OpenDocument

Использование ZIP-архива для объединения XML-файлов и ресурсов — элегантный подход к созданию формата файлов приложения. Он явно превосходит собственный двоичный формат файлов. Но использовать в качестве контейнера базу данных SQLite вместо ZIP было бы ещё элегантнее.

ZIP-архив — это, по сути, база данных «ключ/значение», оптимизированная для однократной записи и многократного чтения, а также для относительно небольшого числа различных ключей (от нескольких сотен до нескольких тысяч), каждому из которых соответствует большой BLOB. ZIP-архив можно рассматривать как базу данных «кучи файлов». Такой подход работает, но у него есть некоторые недостатки по сравнению с базой данных SQLite:

  1. Инкрементальное обновление затруднительно.

    Обновлять отдельные записи в ZIP-архиве сложно. Особенно сложно обновить отдельные записи так, чтобы весь документ не был повреждён, если во время обновления компьютер потеряет питание и/или выйдет из строя. Сделать это возможно, но настолько трудно, что на практике никто так не делает. Вместо этого при каждом выборе пользователем пункта «Файл/Сохранить» ZIP-архив перезаписывается целиком. Поэтому сохранение файла занимает больше времени, чем должно, особенно на старом оборудовании. Новые машины быстрее, но всё равно неприятно, что изменение одного символа в презентации размером 50 мегабайт приводит к расходу 50 мегабайт конечного ресурса записи SSD.

  2. Запуск происходит медленно.

    В соответствии с концепцией «кучи файлов» OpenDocument хранит содержимое всех слайдов в одном большом XML-файле с именем «content.xml». LibreOffice считывает и разбирает весь этот файл, чтобы показать первый слайд. Похоже, LibreOffice также загружает в память все изображения. Это понятно, поскольку при выборе пользователем пункта «Файл/Сохранить» их всё равно придётся записывать заново, даже если ни одно из них не изменилось. В результате запуск происходит медленно. При двойном щелчке по файлу OpenDocument вместо первого слайда появляется индикатор выполнения. Это ухудшает впечатление от работы с приложением. Чем больше документ, тем неприятнее становится ситуация.

  3. Требуется больше памяти.

    Поскольку ZIP-архивы оптимизированы для хранения больших блоков содержимого, они поощряют стиль программирования, при котором весь документ считывается в память при запуске, все изменения выполняются в памяти, а затем весь документ записывается на диск при выборе пункта «Файл/Сохранить». OpenOffice и его потомки используют именно такой подход.

    Можно возразить, что в эпоху настольных компьютеров с несколькими гигабайтами памяти загружать в память весь документ вполне допустимо. Но это не так. Во-первых, объём используемой памяти намного превышает размер файла на диске (после сжатия). Поэтому презентация размером 50 МБ может занимать 200 МБ или больше оперативной памяти. Если редактировать только один документ за раз, это не проблема. Но во время подготовки доклада автор обычно одновременно открывает 10 или 15 разных презентаций (чтобы копировать слайды из прошлых презентаций), а для этого требуются гигабайты памяти. Добавьте открытый веб-браузер или два и ещё несколько настольных приложений — и вскоре диск начинает активно работать, а машина использует файл подкачки. Даже работа всего с одним документом становится проблемой на недорогом Chromebook с установленной Ubuntu. Меньше используемой памяти — всегда лучше.

  4. Восстановление после сбоя затруднено.

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

  5. Содержимое недоступно.

    Просматривать, изменять или извлекать содержимое презентации OpenDocument с помощью универсальных инструментов непросто. Единственный разумный способ просмотреть или отредактировать документ OpenDocument — открыть его в приложении, специально предназначенном для чтения и записи файлов OpenDocument (то есть в LibreOffice или одном из его аналогов). Ситуация могла бы быть ещё хуже. Например, отдельные изображения из презентации можно извлечь и просмотреть с помощью обычного архиватора «zip». Но извлечь текст со слайда уже не так просто. Напомним, что всё содержимое хранится в одном файле «context.xml». Этот файл имеет формат XML, то есть является текстовым. Но это не тот текстовый файл, с которым можно работать в обычном текстовом редакторе. В приведённом выше примере файл content.xml состоит ровно из двух строк. Первая строка файла выглядит так:

    <?xml version="1.0" encoding="UTF-8"?>
    

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

Первое улучшение: заменить ZIP на SQLite

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

CREATE TABLE OpenDocTree(
  filename TEXT PRIMARY KEY,  -- Name of file
  filesize BIGINT,            -- Size of file after decompression
  content BLOB                -- Compressed file content
);

В этом первом эксперименте больше ничего в формате файла не меняется. OpenDocument по-прежнему представляет собой «кучу файлов», только теперь каждый файл — это строка в базе данных SQLite, а не запись в ZIP-архиве. Это простое изменение не использует возможности реляционной базы данных. И всё же оно даёт некоторые улучшения.

Удивительно, но при использовании SQLite вместо ZIP файл презентации становится меньше. Именно так. Можно было бы подумать, что файл реляционной базы данных будет больше ZIP-архива, но, по крайней мере, в случае NeoOffice это не так. Ниже приведён снимок экрана с размерами одной и той же презентации NeoOffice: в исходном формате ZIP-архива, созданном NeoOffice (self2014.odp), и после переупаковки в базу данных SQLite с помощью утилиты SQLAR:

-rw-r--r--  1 drh  staff  10514994 Jun  8 14:32 self2014.odp
-rw-r--r--  1 drh  staff  10464256 Jun  8 14:37 self2014.sqlar
-rw-r--r--  1 drh  staff  10416644 Jun  8 14:40 zip.odp

Файл базы данных SQLite («self2014.sqlar») примерно на полпроцента меньше соответствующего файла ODP! Как такое возможно? По-видимому, логика создания ZIP-архива в NeoOffice не настолько эффективна, насколько могла бы быть: если повторно сжать ту же «кучу файлов» с помощью консольной утилиты «zip», получится ещё меньший файл («zip.odp»), как видно из третьей строки выше. Таким образом, хорошо созданный ZIP-архив может быть немного меньше соответствующей базы данных SQLite, как и следовало ожидать. Но разница невелика. Главное, что следует отметить: по размеру база данных SQLite вполне конкурентоспособна с ZIP-архивом.

Ещё одно преимущество использования SQLite вместо ZIP заключается в том, что теперь документ можно обновлять инкрементально, не рискуя повредить его при отключении питания или другом сбое во время обновления. (Помните, что записи в базах данных SQLite выполняются атомарно.) Конечно, всё содержимое по-прежнему хранится в одном большом XML-файле («content.xml»), который придётся полностью перезаписать, если изменится хотя бы один символ. Но с SQLite нужно изменять только этот файл. Остальные 77 файлов в хранилище можно оставить без изменений. Их не нужно перезаписывать, благодаря чему сохранение файла выполняется намного быстрее, а износ SSD снижается.

Второе улучшение: разделить содержимое на более мелкие части

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

Итак, вместо хранения содержимого всех слайдов в одном огромном XML-файле («content.xml») предположим, что содержимое каждого слайда хранится отдельно в собственной таблице. Схема таблицы могла бы выглядеть примерно так:

CREATE TABLE slide(
  pageNumber INTEGER,   -- The slide page number
  slideContent TEXT     -- Slide content as XML or JSON
);
CREATE INDEX slide_pgnum ON slide(pageNumber); -- Optional

Содержимое каждого слайда по-прежнему можно хранить в виде сжатого XML. Но теперь каждая страница хранится отдельно. Поэтому при открытии нового документа приложение может просто выполнить:

SELECT slideContent FROM slide WHERE pageNumber=1;

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

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

Обратите внимание: разделение содержимого на более мелкие части с помощью таблицы SQLite даёт гибкость при реализации. Приложение может выбрать загрузку всего содержимого в память при запуске. Или оно может загрузить в память лишь несколько страниц, оставив остальные на диске. Или оно может загружать в память только одну страницу за раз. Разные версии приложения могут выбирать разные подходы, не изменяя формат файла. Таких возможностей нет, когда всё содержимое находится в одном большом XML-файле в ZIP-архиве.

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

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

Третье улучшение: управление версиями

Если привыкнуть к идее хранить каждый слайд отдельно, то до поддержки версий презентации останется всего один шаг. Рассмотрим следующую схему:

CREATE TABLE slide(
  slideId INTEGER PRIMARY KEY,
  derivedFrom INTEGER REFERENCES slide,
  content TEXT     -- XML or JSON or whatever
);
CREATE TABLE version(
  versionId INTEGER PRIMARY KEY,
  priorVersion INTEGER REFERENCES version,
  checkinTime DATETIME,   -- When this version was saved
  comment TEXT,           -- Description of this version
  manifest TEXT           -- List of integer slideIds
);

В этой схеме у каждого слайда вместо номера страницы, определяющего его порядок в презентации, есть уникальный целочисленный идентификатор, не связанный с его местом в последовательности. Порядок слайдов в презентации определяется списком slideIds, сохранённым в виде текстовой строки в столбце MANIFEST таблицы VERSION. Поскольку в таблице VERSION может быть несколько записей, в одном документе можно хранить несколько презентаций.

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

SELECT manifest, versionId FROM version ORDER BY versionId DESC LIMIT 1;

Или приложение может предпочесть самую позднюю дату checkinTime:

SELECT manifest, versionId, max(checkinTime) FROM version;

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

(Отступление: да, второй запрос выше, в котором используется «max(checkinTime)», действительно работает и действительно возвращает определённый результат в SQLite. Во многих других системах баз данных SQL такой запрос либо возвращает неопределённый результат, либо приводит к ошибке. Но в SQLite он делает именно то, чего можно ожидать: возвращает манифест и versionId записи с максимальным значением checkinTime.)

При выборе пользователем пункта «Файл/Сохранить» приложение может не перезаписывать изменённые слайды, а создавать новые записи в таблице SLIDE только для добавленных или изменённых слайдов. Затем оно создаёт новую запись в таблице VERSION с обновлённым манифестом.

В показанной выше таблице VERSION есть столбцы для комментария при сохранении (предположительно, вводимого пользователем), а также времени и даты сохранения файла. Кроме того, в ней записывается родительская версия, чтобы сохранять историю изменений. Возможно, манифест можно было бы хранить в виде отличий от родительской версии, хотя обычно он будет достаточно небольшим, и хранение различий может доставить больше хлопот, чем пользы. В таблице SLIDE также есть столбец derivedFrom, который можно использовать для дельта-кодирования, если будет решено, что сохранение содержимого слайда в виде отличий от предыдущей версии — полезная оптимизация.

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

Кроме того, в одном документе можно хранить несколько презентаций.

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

Здесь ещё предстоит проработать детали. Возможно, можно добавить экран со списком всех прошлых изменений (например, в виде графика), чтобы пользователь мог выбрать версию для просмотра или редактирования. Возможно, следует предусмотреть возможность объединения веток, появившихся в истории версий. Кроме того, приложение может предоставить способ удалять старые и ненужные версии. Главное — использование базы данных SQLite для хранения содержимого вместо ZIP-архива значительно упрощает реализацию всех этих возможностей и повышает вероятность того, что со временем они действительно появятся.

И так далее...

В предыдущих разделах мы увидели, как переход от хранилища «ключ/значение», реализованного в виде ZIP-архива, к простой базе данных SQLite всего с тремя таблицами может значительно расширить возможности формата файлов приложения. Схему можно и дальше улучшать, добавляя новые таблицы и индексы для повышения производительности, триггеры и представления для удобства программирования, а также ограничения для обеспечения согласованности содержимого даже при наличии ошибок в программе. Вот ещё несколько идей по улучшению:

  • Хранить в таблице базы данных автоматический стек отмены и повтора действий, чтобы отмена могла возвращать пользователя к предыдущим сеансам редактирования.
  • Добавить к презентации или нескольким презентациям возможности полнотекстового поиска.
  • Разделить файл «settings.xml» на таблицы SQL, которые другим приложениям будет проще просматривать и редактировать.
  • Вынести «заметки докладчика» для каждого слайда в отдельную таблицу, чтобы упростить доступ к ним из сторонних приложений и/или скриптов.
  • Расширить концепцию презентации за пределы простой линейной последовательности слайдов, чтобы в зависимости от реакции аудитории можно было переходить к ответвлениям и дополнительным материалам.

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

Некоторые читатели могут не захотеть использовать SQLite в качестве формата файлов приложения из-за прежнего опыта работы с корпоративными базами данных SQL и их особенностей и ограничений. Например, многие корпоративные системы баз данных не рекомендуют хранить в базе данных большие строки и BLOB-объекты, а вместо этого советуют сохранять их в отдельных файлах, занося в базу данных имя файла. Но SQLite устроена иначе. В любом столбце базы данных SQLite можно хранить строку или BLOB размером примерно до гигабайта. А для строк и BLOB размером до 100 килобайт операции ввода-вывода выполняются быстрее, чем при использовании отдельных файлов.

Некоторые читатели могут не захотеть рассматривать SQLite в качестве формата файлов приложения, поскольку им внушили, что все схемы баз данных SQL должны быть приведены к третьей нормальной форме (3NF) и хранить только небольшие примитивные типы данных, например строки и целые числа. Конечно, реляционная теория важна, и разработчикам следует стремиться её понимать. Но, как показано выше, сложную информацию вполне допустимо хранить в текстовых полях базы данных в формате XML или JSON. Делайте то, что работает, а не то, что, по словам преподавателя баз данных, вы должны делать.

Обзор преимуществ SQLite

Подводя итог, это эссе утверждает, что использование SQLite в качестве контейнера для формата файлов приложения, такого как OpenDocument, и хранение множества небольших объектов в этом контейнере намного эффективнее, чем использование ZIP-архива с несколькими крупными объектами. А именно:

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

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

  3. Время запуска сокращается, поскольку приложение может считывать только содержимое, которое будет отображаться на первом экране. Благодаря этому почти отпадает необходимость показывать индикатор выполнения при открытии нового документа. Документ просто появляется сразу же, что ещё больше улучшает впечатление от работы приложения.

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

  5. Схема базы данных SQL позволяет представлять информацию более непосредственно и лаконично, чем база данных «ключ/значение», например ZIP-архив. Благодаря этому содержимое документа становится доступнее для сторонних приложений и скриптов, а также упрощается реализация расширенных возможностей, таких как встроенное управление версиями документа и инкрементальное сохранение незавершённой работы для восстановления после сбоя.

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

И наконец, повторим: это эссе — мысленный эксперимент. Формат OpenDocument уже хорошо зарекомендовал себя и изначально был хорошо спроектирован. Никто всерьёз не считает, что OpenDocument следует изменить, заменив ZIP-контейнер на SQLite. Эта статья также не критикует OpenDocument за отказ от SQLite в качестве контейнера, поскольку OpenDocument появился раньше SQLite. Скорее, цель статьи — использовать OpenDocument как конкретный пример того, как SQLite можно применять для создания более совершенных форматов файлов приложений в будущих проектах.

Последнее обновление этой страницы: 2025-05-12 11:56:41Z

SQLite is in the Public Domain.
https://sqlite.org/affcase1.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API