在线文档教程
Sqlite
其他 | Miscellaneous

What If OpenDocument Used SQLite?

在OpenDocument中使用SQLite

介绍

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

  • 较小的文件

  • 更快的文件/保存时间

  • 更快的启动时间

  • 使用的内存较少

  • 文档版本控制

  • 更好的用户体验

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

关于OpenDocument和OpenDocument演示文稿

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

OpenDocument演示文稿或“ODP”文件是一个ZIP存档文件,其中包含描述演示文稿幻灯片的XML文件以及作为演示文稿一部分包含的各种图像的独立图像文件。(OpenDocument文字处理器和spreedsheet文件结构相似,但本文不予考虑。)读者可以使用“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数据库的缺点,如下所示:

  • 增量更新很难。 更新ZIP档案中的单个条目很困难。如果计算机在更新过程中失去电源和/或崩溃,以不破坏整个文档的方式更新ZIP归档文件中的单个条目特别困难。做到这一点并非不可能,但实际上没有人真正做到这一点非常困难。相反,无论用户选择“文件/保存”,整个ZIP归档都会被重写。因此,“文件/保存”比它应该花费的时间更长,特别是在较旧的硬件上。较新的机器速度更快,但在50兆字节的演示文稿中更改单个字符会导致人们烧毁SSD上50兆字节的有限写入寿命仍然很麻烦。

  • 启动速度很慢。

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

  • 需要更多的内存。 由于ZIP存档已针对存储大块内容进行了优化,因此它们鼓励在启动时将整个文档读入内存的编程风格,所有编辑都发生在内存中,然后在“文件/保存”期间将整个文档写入磁盘。OpenOffice及其后代拥抱这种模式。有人可能会争辩说,在这个数十兆字节的桌面时代,把整个文档读入内存是可以的。但它不好。首先,使用的内存数量远远超过磁盘上的(压缩)文件大小。所以50MB的演示文稿可能需要200MB或更多的RAM。如果一次只编辑单个文档,这仍然不是问题。但是在谈话时,这位作者通常会同时提供10或15个不同的演示文稿(以便于从过去的演示文稿复制/粘贴幻灯片),因此需要千兆字节的内存。添加一个开放的网络浏览器或两个和其他几个桌面应用程序,并突然磁盘旋转和机器交换。即使只有一个文档,在使用改装Ubuntu的廉价Chromebook上工作时也是一个问题。使用更少的内存总是更好。

  • 崩溃恢复很困难。

OpenOffice的后代往往比商业竞争对手更频繁地发生段错误。也许因为这个原因,OpenOffice forks会定期备份他们的内存文档,这样当不可避免的应用程序崩溃发生时,用户不会失去所有待处理的编辑。这导致应用程序中的每个备份进行几秒钟之后出现令人沮丧的暂停。从崩溃重新启动后,用户将看到一个对话框,指导他们完成恢复过程。以这种方式管理崩溃恢复需要大量额外的应用程序逻辑,并且通常会给用户带来麻烦。

  • 内容无法访问。 使用通用工具不能轻松查看,更改或提取OpenDocument演示文稿的内容。查看或编辑OpenDocument文档的唯一合理方法是使用专门设计用于读取或写入OpenDocument的应用程序(阅读:LibreOffice或其一个表兄弟)来打开它。情况可能会更糟。人们可以使用“zip”归档工具从演示文稿中提取和查看单个图像(比如说)。但尝试从幻灯片中提取文本是不合理的。请记住,所有内容都存储在一个“context.xml”文件中。该文件是XML文件,因此它是一个文本文件。但它不是一个可以用普通的文本编辑器管理的文本文件。对于上面的示例演示,content.xml文件恰好由两行组成。该文件的第一行是:< ?xml version =“1.0”encoding =“UTF-8”?>该文件的第二行包含211792个不可穿透的XML字符。是的,211792个字符全部在一行上。这个文件对于文本编辑器来说是一个很好的压力测试。值得庆幸的是,这个文件并不是一些晦涩难懂的二进制格式,但在可访问性方面,它可能会写在克林贡。第一次改进:用SQLite替换ZIP让我们假设,而不是使用ZIP存档来存储它的文件,OpenDocument使用了一个非常简单的SQLite数据库,其中包含以下单表模式:CREATE TABLE OpenDocTree(filename TEXT PRIMARY KEY, - Name of文件文件大小BIGINT, - 解压缩内容后的文件大小BLOB - 压缩文件内容); 对于第一个实验,文件格式没有其他变化。OpenDocument仍然是一堆文件,现在只有每个文件都是SQLite数据库中的一行,而不是ZIP文件中的一个条目。这种简单的更改不会使用关系数据库的强大功能。即便如此,这个简单的改变显示出一些改进。令人惊讶的是,使用SQLite代替ZIP使演示文件文件更小。真。人们会认为关系数据库文件会比ZIP压缩文件大,但至少在NeoOffice的情况下并非如此。以下是一个实际的屏幕截图,显示了NeoOffice演示文稿的大小,包括NeoOffice生成的原始ZIP存档格式(self2014.odp),以及作为SQLite数据库使用 这种简单的更改不会使用关系数据库的强大功能。即便如此,这个简单的改变显示出一些改进。令人惊讶的是,使用SQLite代替ZIP使演示文件文件更小。真。人们会认为关系数据库文件会比ZIP压缩文件大,但至少在NeoOffice的情况下并非如此。以下是一个实际的屏幕截图,显示了NeoOffice演示文稿的大小,包括NeoOffice生成的原始ZIP存档格式(self2014.odp),以及作为SQLite数据库使用 这种简单的更改不会使用关系数据库的强大功能。即便如此,这个简单的改变显示出一些改进。令人惊讶的是,使用SQLite代替ZIP使演示文件文件更小。真。人们会认为关系数据库文件会比ZIP压缩文件大,但至少在NeoOffice的情况下并非如此。以下是一个实际的屏幕截图,显示了NeoOffice演示文稿的大小,包括NeoOffice生成的原始ZIP存档格式(self2014.odp),以及作为SQLite数据库使用 但至少在NeoOffice的情况下,情况并非如此。以下是一个实际的屏幕截图,显示了NeoOffice演示文稿的大小,包括NeoOffice生成的原始ZIP存档格式(self2014.odp),以及作为SQLite数据库使用 但至少在NeoOffice的情况下,情况并非如此。以下是一个实际的屏幕截图,显示了NeoOffice演示文稿的大小,包括NeoOffice生成的原始ZIP存档格式(self2014.odp),以及作为SQLite数据库使用SAVED实用程序:-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”),如上面第三行所示,仍然较小,再降半个百分点。所以,正如人们所期望的那样,一个写得很好的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, - 幻灯片页码slideContent TEXT - 以XML或JSON形式播放内容); CREATE INDEX slide_pgnum ON幻灯片(pageNumber); - 可选每张幻灯片的内容仍可以存储为压缩的XML。但现在每个页面都单独存储。因此,当打开一个新文档时,应用程序可以简单地运行:SELECT slideContent FROM slide WHERE pageNumber = 1;该查询将快速高效地返回第一张幻灯片的内容,然后可以快速解析并向用户显示。为了渲染第一个屏幕,只需要读取和解析一个页面,这意味着第一个屏幕显得快得多,并且不再需要恼人的进度条。如果应用程序想要将所有内容保留在内存中,则可以在绘制第一页后使用后台线程继续读取和解析其他页面。或者,由于从SQLite阅读非常高效,应用程序可能会选择减少其内存占用量,并且一次只保留一个内存中的一张幻灯片。或者,它可能会保留当前幻灯片和下一张幻灯片,以便快速转换到下一张幻灯片。请注意,使用SQLite表将内容划分为更小的部分会给实现带来灵活性。应用程序可以选择在启动时将所有内容读入内存。或者它可以只读几页到内存中,并将其余的保留在磁盘上。或者它一次只能读取单个页面到内存中。而不同版本的应用程序可以做出不同的选择,而无需对文件格式进行任何更改。当所有内容位于ZIP归档中的单个大XML文件中时,此类选项不可用。将内容分割成更小的部分也有助于文件/保存操作变得更快。在执行文件/保存时,不必回写所有页面的内容,应用程序只需回写实际更改的页面。将内容分割成小块的一个小缺点是,对于较短的文本,压缩不起作用,因此文档的大小可能会增加。但是由于文档空间的大部分用于存储图像,文本内容的压缩效率的小幅降低几乎不会引人注意,并且为改善用户体验而付出的代价很小。第三种改进:版本控制一旦人们对分别存储每张幻灯片的概念感到满意,这是支持演示文稿版本化的一小步。考虑以下模式:CREATE TABLE slide(slideId INTEGER PRIMARY KEY,derivedFrom INTEGER REFERENCES slide,内容TEXT - XML或JSON或其他); CREATE TABLE版本(versionId INTEGER PRIMARY KEY,priorVersion INTEGER REFERENCES版本,checkinTime DATETIME, - 保存此版本时注释TEXT, - 此版本清单的说明TEXT - 整数slideIds列表); 在这个模式中,每个幻灯片都有一个唯一的整数标识符,而不是每个幻灯片的页码,这个页面号决定了它在演示文稿中的顺序。演示文稿中幻灯片的顺序由幻灯片列表确定,幻灯片列表存储为VERSION表的MANIFEST列中的文本字符串。由于VERSION表中允许有多个条目,这意味着可以将多个演示文稿存储在同一个文档中。在启动时,应用程序首先决定要显示哪个版本。由于versionId会自然增加并且通常会希望查看最新版本,因此适当的查询可能是:SELECT manifest,versionId FROM version ORDER BY versionId DESC LIMIT 1; 或者,应用程序宁愿使用最近的checkinTime:SELECT manifest,versionId,max(checkinTime)FROM version; 使用上述单个查询,应用程序将获取演示文稿中所有幻灯片的幻灯片列表。应用程序随后会查询第一张幻灯片的内容,并像以前一样解析并显示该内容。(另外:是的,上面第二个使用“max(checkinTime)”的查询确实有效,并且确实在SQLite中返回了定义明确的答案。这样的查询要么返回一个未定义的答案,要么在其他许多SQL数据库引擎中产生一个错误,但在SQLite中它会做你所期望的:它返回具有最大checkinTime的条目的清单和versionId。)当用户执行“文件/保存”,而不是覆盖修改过的幻灯片,应用程序现在可以在幻灯片中为已添加或更改的幻灯片创建新条目。然后它在包含修订后清单的VERSION表中创建一个新条目。上面显示的VERSION表具有用于记录签入注释(可能由用户提供)以及文件/保存操作发生的时间和日期的列。它还会记录父版本以记录更改的历史记录。也许清单可以作为父版本的增量存储,尽管通常情况下舱单会很小,以至于存储一个三角洲可能比它的价值更麻烦。如果确定将幻灯片内容保存为与之前版本的差值是值得优化的,则SLIDE表格还包含可用于增量编码的derivedFrom列。因此,通过这种简单的更改,ODP文件现在不仅可以存储对演示文稿的最近编辑,还可以存储所有历史编辑的历史记录。用户通常只想看到演示文稿的最新版本,但如果需要,用户现在可以及时向后浏览以查看相同演示文稿的历史版本。或者,多个演示文稿可以存储在同一个文档中。有了这样的架构,应用程序不再需要将未保存的更改定期备份到单独的文件,以避免在发生崩溃时丢失工作。相反,可以分配特殊的“未决”版本,并可以将未保存的更改写入暂挂版本。因为只需要写入更改,而不是整个文档,保存待处理的更改只需要写入几千字节的内容,而不是几兆字节,并且需要几毫秒而不是几秒钟,因此可以经常以静默方式进行的背景。然后,当发生崩溃并且用户重新启动时,他们所有(或几乎全部)的工作都会被保留。如果用户决定放弃未保存的更改,则只需返回到以前的版本。这里有详细的内容。也许可以提供一个屏幕来显示历史变化(可能用图表),允许用户选择他们想要查看或编辑的版本。也许可以提供一些工具来合并版本历史中可能出现的叉子。也许应用程序应该提供一种方法来清除旧的和不需要的版本。关键在于使用SQLite数据库来存储内容而不是ZIP存档,使得所有这些功能更加容易实现,这增加了它们最终实现的可能性。在前面的章节中,我们已经看到了如何从一个ZIP存档实现的键/值存储转换为只有三个表的简单SQLite数据库,可以为应用程序文件格式添加显着的功能。我们可以继续使用新表格增强架构,添加了用于性能的索引,带有编程方便的触发器和视图,以及即使面对编程错误时也能实现内容一致性的约束。其他增强思想包括:

  • 将自动撤销/重做堆栈存储在数据库表中,以便“撤消”可以返回到之前的编辑会话。

  • 将全文搜索功能添加到幻灯片平台或跨多个幻灯片平台。

  • 将“settings.xml”文件分解为一种SQL表格类型,可以通过单独的应用程序更容易地查看和编辑。

  • 将每张幻灯片中的“演示者备注”分成单独的表格,以便于从第三方应用程序和/或脚本进行访问。

  • 除了简单的线性幻灯片序列之外,还可以增强演示概念,以便根据观众的响应方式进行旁路和游览。

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

一些读者可能因为之前暴露于企业SQL数据库以及其他系统的警告和限制而拒绝将SQLite用作应用程序文件格式。例如,许多企业数据库引擎建议不要在数据库中存储大型字符串或BLOB,而是建议将大型字符串和BLOB存储为单独的文件,并将文件名存储在数据库中。但SQLite不是那样的。SQLite数据库的任何列都可以容纳一个字符串或BLOB,大小最多可达1 GB。对于100千字节或更少的字符串和BLOB,I / O性能要比使用单独的文件要好。

有些读者可能不愿意将SQLite视为应用程序文件格式,因为他们已经灌输了所有SQL数据库模式必须分解成第三种常规形式并仅存储小型基本数据类型(如字符串和整数)的想法。当然,关系理论很重要,设计师应该努力去理解它。但是,如上所示,在数据库的文本字段中存储XML或JSON等复杂信息通常是可以接受的。做什么都行,而不是你的数据库教授说你应该做的。

回顾使用SQLite的好处

总之,本文的主张是使用SQLite作为OpenDocument等应用程序文件格式的容器,并且在该容器中存储大量较小的对象比使用包含几个较大对象的ZIP存档更有效。以机智:

  • 与保存相同信息的ZIP存档相比,SQLite数据库文件大小几乎相同,有时甚至更小。

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

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

  • 只需加载与当前显示相关的内容并将大部分内容保存在磁盘上,即可显着降低应用程序的内存占用量。SQLite的快速查询功能使得它成为一种可以随时将所有内容保存在内存中的可行选择。而当应用程序使用较少的内存时,它会使整个计算机响应更快,从而进一步增强用户体验。

  • SQL数据库的模式能够比密钥/数值数据库(如ZIP存档)更直接简洁地表示信息。这使得文档内容可以更容易地被第三方应用程序和脚本访问,并且有助于高级功能(如内置文档版本控制)以及在崩溃后恢复正在进行的工作的增量保存。

这些仅仅是将SQLite用作应用程序文件格式的几个好处 - 这些好处似乎最有可能改善OpenOffice等应用程序的用户体验。其他应用程序可能以不同的方式受益于SQLite。请参阅应用程序文件格式文档了解其他想法。

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

SQLite is in the Public Domain.