小巧。快速。可靠。
三选二。

如果OpenDocument使用SQLite会怎样?

简介

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

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

关于OpenDocument和OpenDocument演示文稿

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兆字节演示文稿中的单个字符会导致消耗50兆字节SSD的有限写入寿命仍然令人烦恼。

  2. 启动缓慢。

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

  3. 需要更多内存。

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

    有人可能会争辩说,在这个多千兆字节桌面时代,将整个文档读入内存是可以的。但事实并非如此。一方面,使用的内存量远远超过磁盘上的(压缩)文件大小。因此,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生成的原始ZIP档案格式(self2014.odp)以及使用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文件小约0.5%!怎么会这样?显然,NeoOffice中的ZIP档案生成器逻辑没有达到最佳效率,因为当使用命令行“zip”实用程序重新压缩相同的文件堆时,得到的文件(“zip.odp”)更小,又减少了0.5%,如上文第三行所示。因此,编写良好的ZIP档案可以比等效的SQLite数据库略小,正如预期的那样。但差异很小。关键的结论是SQLite数据库的大小与ZIP档案相当。

使用SQLite代替ZIP的另一个优势是,现在可以增量更新文档,如果在更新过程中发生电源故障或其他崩溃,不会有损坏文档的风险。(请记住,对SQLite数据库的写入是原子的。)确实,所有内容仍然保存在一个名为“content.xml”的大型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
);

在这个模式中,每个幻灯片不再具有决定其在演示文稿中顺序的页码,而是具有一个唯一的整数标识符,该标识符与其在序列中的位置无关。演示文稿中幻灯片的顺序由幻灯片 ID 列表决定,该列表作为文本字符串存储在 VERSION 表的 MANIFEST 列中。由于 VERSION 表中允许有多个条目,这意味着多个演示文稿可以存储在同一个文档中。

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

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

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

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

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

(旁注:是的,上面第二个使用“max(checkinTime)”的查询确实有效,并且确实在 SQLite 中返回了一个明确的答案。在许多其他 SQL 数据库引擎中,此类查询要么返回未定义的答案,要么生成错误,但在 SQLite 中,它会按预期工作:它返回具有最大 checkinTime 的条目的清单和版本 ID。)

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

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

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

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

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

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

等等…

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

SQLite 数据库具有很多功能,本文只是触及了其中的一部分。但希望这篇简短的介绍能够说服一些读者,将 SQL 数据库用作应用程序文件格式值得再次考虑。

一些读者可能会抵制使用 SQLite 作为应用程序文件格式,因为他们之前接触过企业级 SQL 数据库以及这些其他系统的注意事项和限制。例如,许多企业数据库引擎建议不要在数据库中存储大型字符串或 BLOB,而是建议将大型字符串和 BLOB 存储为单独的文件,并将文件名存储在数据库中。但 SQLite 并非如此。SQLite 数据库的任何列都可以保存大小约为 1 GB 的字符串或 BLOB。对于 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 为未来的项目构建更好的应用程序文件格式。

此页面上次修改于2023-10-10 17:29:48 UTC