如果 OpenDocument 使用 SQLite 会怎样?

介绍

假设 OpenDocument文件格式,特别是“ODP”OpenDocument Presentation 格式,是围绕 SQLite 构建的。好处包括:

请注意,这只是一个思想实验。我们不建议更改 OpenDocument。这篇文章也不是对当前 OpenDocument 设计的批评。这篇文章的重点是提出改进未来文件格式设计的方法。

关于 OpenDocument 和 OpenDocument Presentation

OpenDocument 文件格式用于办公应用程序:文字处理器、电子表格和演示文稿。它最初是为 OpenOffice 套件设计的,但后来被合并到其他桌面应用程序套件中。OpenOffice 应用程序已经分叉并重命名了几次。作者对 OpenDocument 的主要用途是使用 Mac 上的NeoOffice或Linux 和 Windows 上的 LibreOffice构建幻灯片演示文稿。

OpenDocument 演示文稿或“ODP”文件是一个 ZIP 存档,其中包含描述演示文稿幻灯片的 XML 文件和作为演示文稿一部分包含的各种图像的单独图像文件。(OpenDocument 字处理器和电子表格文件的结构类似,但本文不予考虑。)读者可以使用“zip -l”命令轻松查看 ODP 文件的内容。例如,以下是 2014 SouthEast LinuxFest 会议上有关 SQLite 的 49 张幻灯片演示的“zip -l”输出:

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

ODP ZIP 存档包含四个不同的 XML 文件:content.xml、styles.xml、meta.xml 和 settings.xml。这四个文件定义了幻灯片布局、文本内容和样式。这个特定的演示文稿包含 62 张图片,从全屏图片到小图标,每张图片都作为单独的文件存储在图片文件夹中。“mimetype”文件包含一行文本,上面写着:

application/vnd.oasis.opendocument.presentation

作者目前不知道其他文件和文件夹的用途,但可能不难弄清楚。

OpenDocument 演示格式的局限性

使用 ZIP 存档来封装 XML 文件和资源是应用程序文件格式的一种优雅方法。它显然优于自定义二进制文件格式。但是使用 SQLite 数据库作为容器而不是 ZIP 会更加优雅。

ZIP 存档基本上是一个键/值数据库,针对一次写入/多次读取的情况和相对较少数量的不同键(几百到几千个)进行了优化,每个键都有一个大的 BLOB 作为其值。ZIP 存档可以看作是“文件堆”数据库。这行得通,但它相对于 SQLite 数据库有一些缺点,如下所示:

  1. 增量更新很难。

    很难更新 ZIP 存档中的单个条目。如果计算机在更新过程中断电和/或崩溃,以不破坏整个文档的方式更新 ZIP 存档中的单个条目尤其困难。做到这一点并非不可能,但它足够困难,以至于没有人真正做到这一点。相反,只要用户选择“文件/保存”,整个 ZIP 存档就会被重写。因此,“文件/保存”花费的时间比应有的时间长,尤其是在较旧的硬件上。较新的机器速度更快,但在 50 兆字节的演示文稿中更改单个字符会导致在 SSD 上消耗 50 兆字节的有限写入寿命,这仍然很麻烦。

  2. 启动很慢。

    为了与文件堆主题保持一致,OpenDocument 将所有幻灯片内容存储在一个名为“content.xml”的大 XML 文件中。LibreOffice 读取并解析整个文件只是为了显示第一张幻灯片。LibreOffice 似乎也将所有图像读入内存,这是有道理的,因为当用户执行“文件/保存”时,它将不得不再次将它们全部写回,即使它们都没有改变。最终效果是启动速度很慢。双击 OpenDocument 文件会弹出一个进度条,而不是第一张幻灯片。这会导致糟糕的用户体验。随着文档大小的增加,情况变得越来越烦人。

  3. 需要更多内存。

    由于 ZIP 存档针对存储大块内容进行了优化,因此它们鼓励一种编程风格,即在启动时将整个文档读入内存,所有编辑都在内存中进行,然后在“文件/保存”期间将整个文档写入磁盘。OpenOffice 及其后代采用了这种模式。

    有人可能会争辩说,在这个数 GB 的桌面时代,将整个文档读入内存是可以的。但这不行。一方面,使用的内存量远远超过磁盘上的(压缩)文件大小。因此,50MB 的演示文稿可能需要 200MB 或更多的 RAM。如果一次只编辑一个文档,那仍然不是问题。但是在进行演讲时,这位作者通常会同时准备 10 或 15 个不同的演示文稿(以便于从过去的演示文稿中复制/粘贴幻灯片),因此需要千兆字节的内存。添加一个或两个打开的网络浏览器和一些其他桌面应用程序,突然间磁盘在旋转,机器在交换。在使用 Ubuntu 改装的廉价 Chromebook 上工作时,即使只有一个文档也是一个问题。使用更少的内存总是更好。

  4. 崩溃恢复很困难。

    OpenOffice 的后代比商业竞争对手更容易出现段错误。也许出于这个原因,OpenOffice 分支会定期备份内存中的文档,以便在不可避免的应用程序崩溃确实发生时,用户不会丢失所有待处理的编辑。这会导致应用程序在每次备份时停顿几秒钟,令人沮丧。从崩溃中重新启动后,用户会看到一个对话框,引导他们完成恢复过程。以这种方式管理崩溃恢复涉及许多额外的应用程序逻辑,通常会让用户感到烦恼。

  5. 内容不可访问。

    使用通用工具无法轻松查看、更改或提取 OpenDocument 演示文稿的内容。查看或编辑 OpenDocument 文档的唯一合理方法是使用专门设计用于读取或写入 OpenDocument 的应用程序打开它(阅读:LibreOffice 或其同类产品之一)。情况可能会更糟。只需使用“zip”存档工具,就可以从演示文稿中提取和查看单个图像(比方说)。但是尝试从幻灯片中提取文本是不合理的。请记住,所有内容都存储在一个“context.xml”文件中。该文件是 XML,因此它是一个文本文件。但它不是可以用普通文本编辑器管理的文本文件。对于上面的示例演示,content.xml 文件仅包含两行。该文件的第一行只是:

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

    文件的第二行包含 211792 个字符的难以理解的 XML。是的,一行中有 211792 个字符。这个文件是一个很好的文本编辑器压力测试。值得庆幸的是,该文件不是某种晦涩的二进制格式,但就可访问性而言,它还不如用克林贡语编写。

第一个改进:用 SQLite 替换 ZIP

让我们假设,OpenDocument 没有使用 ZIP 存档来存储其文件,而是使用了一个非常简单的 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 演示文稿大小的实际屏幕截图,包括由 NeoOffice (self2014.odp) 生成的原始 ZIP 存档格式,以及使用 SQLAR实用程序重新打包为 SQLite 数据库的格式:

-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 文件小大约半个百分点!怎么会这样?显然,NeoOffice 中的 ZIP 存档生成器逻辑并不像它应该的那样高效,因为当使用命令行“zip”实用程序重新压缩同一堆文件时,一个人会得到一个文件(“zip.odp”)更小,再小 0.5 个百分点,如上面第三行所示。因此,正如人们所期望的那样,一个编写良好的 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 表将内容分成更小的部分可以提高实施的灵活性。应用程序可以选择在启动时将所有内容读入内存。或者它可以只将几页读入内存并将其余部分保存在磁盘上。或者它一次只能将一页读入内存。并且不同版本的应用程序可以做出不同的选择,而无需对文件格式进行任何更改。当所有内容都在 ZIP 存档中的单个大 XML 文件中时,此类选项不可用。

将内容拆分成更小的部分也有助于文件/保存操作进行得更快。应用程序不必在执行文件/保存时写回所有页面的内容,而只需写回那些实际已更改的页面。

将内容拆分成更小的部分的一个小缺点是压缩在较短的文本上效果不佳,因此文档的大小可能会增加。但由于文档空间的大部分用于存储图像,因此文本内容的压缩效率的小幅降低几乎不会引起注意,并且对于改善用户体验来说是一个很小的代价。

第三个改进:版本控制

一旦对单独存储每张幻灯片的概念感到满意,就可以向支持演示文稿的版本控制迈出一小步。考虑以下架构:

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
);

在此架构中,不是每张幻灯片都有一个页码来确定其在演示文稿中的顺序,而是每张幻灯片都有一个唯一的整数标识符,该标识符与其顺序出现的位置无关。幻灯片在演示文稿中的顺序由 slideId 列表确定,该列表作为文本字符串存储在 VERSION 表的 MANIFEST 列中。由于 VERSION 表中允许有多个条目,这意味着多个演示文稿可以存储在同一个文档中。

启动时,应用程序首先决定要显示哪个版本。由于 versionId 会随着时间自然增加,并且通常希望看到最新版本,因此适当的查询可能是:

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

或者应用程序可能更愿意使用最近的 checkinTime:

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

使用如上所述的单个查询,应用程序获取演示文稿中所有幻灯片的 slideId 列表。然后应用程序查询第一张幻灯片的内容,并像以前一样解析和显示该内容。

(旁白:是的,上面使用“max(checkinTime)”的第二个查询确实有效,并且确实在 SQLite 中返回了明确定义的答案。这样的查询要么返回未定义的答案,要么在许多其他 SQL 数据库引擎中产生错误,但在 SQLite 中它会做你所期望的:它返回具有最大 checkinTime 的条目的清单和 versionId。)

当用户执行“文件/保存”操作时,应用程序现在可以在 SLIDE 表中为那些已添加或更改的幻灯片创建新条目,而不是覆盖修改后的幻灯片。然后它在包含修改后的清单的 VERSION 表中创建一个新条目。

上面显示的 VERSION 表包含记录签入评论(可能由用户提供)以及文件/保存操作发生的时间和日期的列。它还记录父版本以记录更改历史。也许清单可以存储为父版本的增量,但通常清单会足够小,以至于存储增量可能比它的价值更麻烦。SLIDE 表还包含一个 derivedFrom 列,如果确定将幻灯片内容保存为其先前版本的增量是值得优化的,则该列可用于增量编码。

因此,通过这个简单的更改,ODP 文件现在不仅存储了对演示文稿的最新编辑,还存储了所有历史编辑的历史记录。用户通常只想查看演示文稿的最新版本,但如果需要,用户现在可以及时倒退以查看同一演示文稿的历史版本。

或者,可以将多个演示文稿存储在同一个文档中。

有了这样的架构,应用程序将不再需要定期将未保存的更改备份到单独的文件中,以避免在发生崩溃时丢失工作。相反,可以分配一个特殊的“待定”版本,并且可以将未保存的更改写入待定版本。因为只需要写入更改,而不是整个文档,保存待处理的更改只需要写入几千字节的内容,而不是几兆字节,并且需要几毫秒而不是几秒钟,因此可以在的背景。然后,当发生崩溃并且用户重新启动时,他们所有(或几乎所有)的工作都会保留。如果用户决定放弃未保存的更改,他们只需返回到以前的版本。

这里有详细信息需要填写。或许可以提供一个屏幕来显示历史变化(可能带有图表),允许用户选择他们想要查看或编辑的版本。也许可以提供一些工具来合并版本历史中可能出现的分支。也许应用程序应该提供一种方法来清除旧的和不需要的版本。关键是使用 SQLite 数据库而不是 ZIP 存档来存储内容,可以使所有这些功能更容易实现,从而增加它们最终实现的可能性。

等等...

在前面的部分中,我们已经看到从作为 ZIP 存档实现的键/值存储迁移到只有三个表的简单 SQLite 数据库如何为应用程序文件格式添加重要功能。我们可以继续使用新表来增强模式,添加索引以提高性能,使用触发器和视图以方便编程,以及约束以强制内容的一致性,即使在面对编程错误时也是如此。进一步的增强想法包括:

SQLite 数据库有很多功能,本文才刚刚开始涉及。但希望这种快速一瞥已经让一些读者相信,使用 SQL 数据库作为应用程序文件格式值得再看一遍。

由于之前接触过企业 SQL 数据库以及其他系统的注意事项和限制,一些读者可能会拒绝使用 SQLite 作为应用程序文件格式。例如,许多企业数据库引擎建议不要在数据库中存储大字符串或 BLOB,而是建议将大字符串和 BLOB 作为单独的文件存储,并将文件名存储在数据库中。但是 SQLite 不是这样的。SQLite 数据库的任何列都可以容纳一个字符串或 BLOB,大小可达 1 GB。对于 100 KB 或更小的字符串和 BLOB, I/O 性能优于使用单独的文件。

一些读者可能不愿意将 SQLite 视为一种应用程序文件格式,因为他们一直被灌输这样一种观念,即所有 SQL 数据库模式都必须分解为第三范式,并且只存储小的原始数据类型,例如字符串和整数。当然关系理论很重要,设计师应该努力去理解它。但是,如上所示,在数据库的文本字段中将复杂信息存储为 XML 或 JSON 通常是完全可以接受的。做有用的事,而不是你的数据库教授说你应该做的事。

回顾使用 SQLite 的好处

总而言之,这篇文章的主张是,使用 SQLite 作为 OpenDocument 等应用程序文件格式的容器并在该容器中存储许多较小的对象比使用包含一些较大对象的 ZIP 存档要好得多。以机智:

  1. SQLite 数据库文件的大小与包含相同信息的 ZIP 存档文件的大小大致相同,在某些情况下甚至更小。

  2. SQLite的原子更新功能 允许将小的增量更改安全地写入文档。这减少了总磁盘 I/O 并提高了文件/保存性能,从而增强了用户体验。

  3. 通过允许应用程序只读入初始屏幕显示的内容来减少启动时间。这在很大程度上消除了打开新文档时显示进度条的需要。该文档立即弹出,进一步增强了用户体验。

  4. 通过仅加载与当前显示相关的内容并将大部分内容保留在磁盘上,可以显着减少应用程序的内存占用。SQLite 的快速查询功能使其成为始终将所有内容保存在内存中的可行替代方案。当应用程序使用更少的内存时,它会提高整个计算机的响应速度,从而进一步增强用户体验。

  5. SQL 数据库的模式能够比 ZIP 存档等键/值数据库更直接、更简洁地表示信息。这使得第三方应用程序和脚本更容易访问文档内容,并促进高级功能,例如内置文档版本控制,以及在崩溃后增量保存进行中的工作以进行恢复。

这些只是使用 SQLite 作为应用程序文件格式的一些好处 — 这些好处似乎最有可能改善 OpenOffice 等应用程序的用户体验。其他应用程序可能以不同的方式受益于 SQLite。有关其他想法,请参阅应用程序文件格式 文档。

最后,让我们重申一下,这篇论文是一个思想实验。OpenDocument 格式已经很成熟并且设计良好。没有人真正相信应该将 OpenDocument 更改为使用 SQLite 作为其容器而不是 ZIP。这篇文章也不是批评 OpenDocument 没有选择 SQLite 作为它的容器,因为 OpenDocument 早于 SQLite。相反,本文的重点是使用 OpenDocument 作为具体示例,说明如何使用 SQLite 为未来的项目构建更好的应用程序文件格式。