小巧、快速、可靠。
任選三項。

如果 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 MB 簡報中的單一字元仍會導致 SSD 上有限的寫入壽命消耗 50 MB,這仍然很麻煩。

  2. 啟動速度慢。

    為了符合檔案堆疊主題,OpenDocument 將所有投影片內容儲存在名為「content.xml」的單一大型 XML 檔案中。LibreOffice 讀取並剖析整個檔案,只為顯示第一張投影片。LibreOffice 似乎也會將所有影像讀取到記憶體中,這合乎邏輯,因為當使用者執行「檔案/儲存」時,即使沒有任何影像變更,也必須將所有影像寫回。最終結果是啟動速度慢。雙擊 OpenDocument 檔案會顯示進度條,而不是第一張投影片。這會導致不良的使用者體驗。隨著文件大小增加,情況會變得更加惱人。

  3. 需要更多記憶體。

    由於 ZIP 檔案經過最佳化,可儲存大量內容區塊,因此鼓勵採用一種程式設計風格,在啟動時將整個文件讀取到記憶體中,所有編輯都在記憶體中進行,然後在「檔案/儲存」期間將整個文件寫入磁碟。OpenOffice 及其後繼版本採用這種模式。

    有人可能會爭論,在這個多 GB 桌機的時代,將整個文件讀取到記憶體中是沒問題的。但這是不行的。首先,使用的記憶體量遠遠超過磁碟上的(壓縮)檔案大小。因此,一個 50MB 的簡報可能會佔用 200MB 或更多 RAM。如果一次只編輯一個文件,這仍然不是問題。但當撰寫演講時,作者通常會同時開啟 10 或 15 個不同的簡報(以便於從過去的簡報複製/貼上投影片),因此需要數 GB 的記憶體。再加入一個或兩個開啟的網頁瀏覽器和一些其他桌面應用程式,磁碟突然開始旋轉,機器開始換頁。即使只有一個文件,在安裝了 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 檔案小約 0.5%!這怎麼可能?顯然 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
);

在此架構中,每個投影片不再有決定其在簡報中順序的頁碼,而是有一個與其在順序中出現位置無關的唯一整數識別碼。簡報中投影片的順序由投影片識別碼清單決定,儲存在 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。)

當使用者執行「檔案/儲存」時,應用程式現在可以為已新增或已變更的投影片在 SLIDE 表格中建立新條目,而不是覆寫已修改的投影片。然後,它會在 VERSION 表格中建立一個包含已修改清單的新條目。

上面顯示的 VERSION 表格有欄位來記錄簽入註解(可能是使用者提供的)以及執行「檔案/儲存」動作的時間和日期。它也會記錄父版本,以記錄變更歷程。清單可以儲存為父版本的增量,儘管通常清單會小到儲存增量比其價值更麻煩。SLIDE 表格也包含一個 derivedFrom 欄位,如果確定將投影片內容儲存為其先前版本的增量是有價值的最佳化,則可以使用這個欄位進行增量編碼。

因此,透過這個簡單的變更,ODP 檔案現在不只儲存對簡報的最新編輯,還儲存所有歷史編輯的歷程。使用者通常只想看到簡報的最新版本,但如果需要,使用者現在可以回溯時間,查看同一簡報的歷史版本。

或者,可以在同一個文件內儲存多個簡報。

有了此架構,應用程式不再需要定期將未儲存的變更備份至個別檔案,以避免在發生崩潰時遺失工作。相反地,可以配置一個特殊的「待處理」版本,並將未儲存的變更寫入待處理版本。由於只需要寫入變更,而非整個文件,因此儲存待處理的變更只會涉及寫入幾 KB 的內容,而非數 MB,而且會花費毫秒而非數秒,因此可以在背景中頻繁且無聲地執行。然後,當發生崩潰且使用者重新開機時,所有(或幾乎所有)的工作都會保留。如果使用者決定捨棄未儲存的變更,他們只需回到前一個版本即可。

這裡有一些細節需要填寫。或許可以提供一個畫面,顯示變更歷程(或許附有圖表),讓使用者選擇他們想要檢視或編輯的版本。或許可以提供一些功能,用於合併版本歷程中可能發生的分支。而且,或許應用程式應該提供一種方法來清除舊的和不需要的版本。重點是,使用 SQLite 資料庫來儲存內容,而非 ZIP 檔案,會讓所有這些功能更容易實作,這會增加它們最終會被實作的可能性。

等等...

在先前的章節中,我們已經看到如何從實作為 ZIP 檔案的鍵值儲存,轉移到只有三個表格的簡單 SQLite 資料庫,可以為應用程式檔案格式新增重要的功能。我們可以繼續使用新的表格來增強架構,新增索引以提升效能,使用觸發器和檢視以提升程式設計便利性,以及使用約束來強制內容的一致性,即使在發生程式設計錯誤時也是如此。進一步的增強構想包括

SQLite 資料庫有許多功能,而本文僅觸及皮毛。但希望這篇快速瀏覽已經讓一些讀者相信,使用 SQL 資料庫作為應用程式檔案格式值得再深入研究。

一些讀者可能會因為先前接觸過企業 SQL 資料庫及其注意事項和限制,而抗拒使用 SQLite 作為應用程式檔案格式。例如,許多企業資料庫引擎建議不要將大型字串或 BLOB 儲存在資料庫中,而建議將大型字串和 BLOB 儲存在獨立的檔案中,並將檔案名稱儲存在資料庫中。但 SQLite 並非如此。SQLite 資料庫的任何欄位都可以儲存大小約為 1 GB 的字串或 BLOB。而且,對於大小為 100 KB 或更小的字串和 BLOB,I/O 效能會比使用獨立的檔案更好。

一些讀者可能會因為被灌輸了所有 SQL 資料庫架構都必須分解成第三正規形式,而且只能儲存小型的基本資料類型(例如字串和整數)的想法,而猶豫是否要將 SQLite 視為應用程式檔案格式。當然,關聯理論很重要,而且設計師應該努力了解它。但是,如上所述,將複雜的資訊儲存在資料庫的文字欄位中,通常是可以接受的。做有用的事,而不是做你的資料庫教授告訴你應該做的事。

檢視使用 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