2 Punkte von GN⁺ 2023-09-19 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn OpenDocument Presentation (ODP) statt eines ZIP-Archivs in einem SQLite-Container gespeichert würde, könnten Dokumentenspeicherung, Start und Wiederherstellung sicherer und schneller entworfen werden
  • Derzeit ist ODP so aufgebaut, dass XML- und Bilddateien in einem ZIP-Archiv gebündelt werden; eine Präsentationsdatei mit 49 Folien im Beispiel besteht aus insgesamt 78 Einträgen wie content.xml, styles.xml, meta.xml, settings.xml und Bildern
  • Bei einer ZIP-basierten Struktur wird schon bei kleinen Änderungen leicht das gesamte Archiv neu geschrieben, wodurch inkrementelle Updates schwierig sind und Verzögerungen bei File/Save sowie mehr Schreibvorgänge auf SSDs entstehen
  • Mit SQLite lassen sich Dateien als Tabellenzeilen speichern; darüber hinaus kann man Inhalte und Versionen pro Folie aufteilen, sodass nur die erste Folie gelesen oder nur geänderte Folien gespeichert werden
  • Dies ist weder eine Kritik an OpenDocument noch ein Vorschlag, es zu ändern, sondern ein Beispiel dafür, wie SQLite in Anwendungsdateiformaten atomisches Speichern, Zugänglichkeit, Versionsverwaltung und Wiederherstellungsfunktionen einfacher machen kann

Umfang und Gegenstand des Gedankenexperiments

  • Gegenstand ist innerhalb von OpenDocument insbesondere das Präsentationsformat ODP (OpenDocument Presentation)
  • Ziel ist nicht, OpenDocument tatsächlich zu ändern, sondern den Einsatz von SQLite als Container bei der Gestaltung künftiger Dateiformate zu prüfen
  • Erwartete Vorteile sind kleinere Dokumente, schnelleres File/Save, schnellerer Start, geringerer Speicherverbrauch, Dokumentversionsverwaltung und eine bessere User Experience

Aktuelle ODP-Dateistruktur

  • Eine ODP-Datei ist ein ZIP-Archiv, das XML-Dateien und Bildressourcen enthält
  • Als Beispiel hat eine 49-Folien-Präsentation zu SQLite von der SouthEast LinuxFest 2014 laut Ausgabe von zip -l insgesamt 78 Einträge
    • Die vier XML-Dateien content.xml, styles.xml, meta.xml und settings.xml definieren Folienlayout, Textinhalte und Stile
    • Die Präsentationsdatei speichert 62 Bilder separat, von Vollbildfotos bis zu kleinen Icons
    • Die Datei mimetype enthält eine einzige Zeile: application/vnd.oasis.opendocument.presentation
  • OpenDocument-Dateien für Textverarbeitung und Tabellenkalkulation sind ähnlich aufgebaut, aber analysiert wird hier ODP

Grenzen von ZIP-basiertem ODP

  • Ein ZIP-Archiv ähnelt eher einer Key/Value-Datenbank, die für einmaliges Schreiben und vielfaches Lesen optimiert ist, und passt zu einer Struktur mit wenigen Schlüsseln und großen BLOB-Werten
  • Da einzelne Einträge schwer zu aktualisieren sind, wird beim Befehl File/Save üblicherweise das gesamte ZIP-Archiv neu geschrieben
    • Man könnte einzelne Einträge zwar so aktualisieren, dass das gesamte Dokument auch bei Stromausfall oder Absturz nicht beschädigt wird, aber das ist schwierig genug, dass es praktisch kaum genutzt wird
    • In einer 50-MB-Präsentation kann schon die Änderung eines einzelnen Zeichens dazu führen, dass die gesamten 50 MB neu geschrieben werden
  • Die Startzeit kann langsamer werden
    • ODP speichert alle Folieninhalte in einer einzigen großen XML-Datei namens content.xml
    • LibreOffice liest und parst diese komplette Datei, um die erste Folie anzuzeigen
    • Offenbar werden auch alle Bilder in den Speicher geladen; deshalb erscheint beim Doppelklick auf die Datei zunächst ein Fortschrittsbalken statt der ersten Folie
  • Der Speicherverbrauch steigt
    • Die ZIP-Struktur fördert Implementierungen, die beim Start das gesamte Dokument in den Speicher laden, dort bearbeiten und beim Speichern das komplette Dokument auf die Festplatte schreiben
    • Eine 50-MB-Präsentationsdatei kann mehr als 200 MB RAM belegen
    • Wenn mehrere Präsentationen gleichzeitig geöffnet sind und dazu Browser und Desktop-Apps laufen, kann Swapping auftreten
  • Die Wiederherstellung nach Abstürzen wird umständlicher
    • Anwendungen aus der OpenOffice-Familie sichern das im Speicher befindliche Dokument periodisch, um Abstürze abzufangen
    • Während der Sicherung kann die App einige Sekunden einfrieren, und nach dem Neustart ist ein separater Wiederherstellungsdialog nötig
  • Die Zugänglichkeit der Inhalte ist gering
    • Mit ZIP-Werkzeugen lassen sich Bilder extrahieren, aber Folientexte mit allgemeinen Tools auszulesen oder zu bearbeiten ist schwierig
    • In der Beispieldatei enthält content.xml in der ersten Zeile die XML-Deklaration und in der zweiten Zeile 211.792 Zeichen XML in einer einzigen Zeile

Erste Verbesserung: ZIP durch SQLite ersetzen

  • Der erste Schritt ist eine einfache Struktur, bei der ZIP-Einträge durch Zeilen einer SQLite-Tabelle ersetzt werden
CREATE TABLE OpenDocTree(
  filename TEXT PRIMARY KEY,
  filesize BIGINT,
  content BLOB
);
  • In diesem Schritt wird die übrige Struktur des Dateiformats nicht verändert
    • Es bleibt weiterhin eine „Dateisammlung“, nur dass jede Datei statt eines ZIP-Eintrags nun eine Zeile in einer SQLite-Datenbank ist
  • Der Größenvergleich einer SQLite-Datei, die denselben Inhalt wie self2014.odp von NeoOffice mit dem Dienstprogramm SQLAR neu verpackt, sieht so aus
    • self2014.odp: 10.514.994 Byte
    • self2014.sqlar: 10.464.256 Byte
    • zip.odp, mit dem Kommandozeilen-zip neu komprimiert: 10.416.644 Byte
  • Die SQLite-Datei war etwa 0,5 % kleiner als die von NeoOffice erzeugte ODP-Datei
    • Eine mit dem Kommandozeilen-zip gut komprimierte ZIP-Datei war wiederum etwa 0,5 % kleiner als SQLite
    • SQLite-Datenbanken können bei der Dateigröße mit ZIP-Archiven konkurrieren
  • SQLite bietet atomische Schreibvorgänge, sodass inkrementelle Änderungen gespeichert werden können, ohne das Risiko einer Dokumentbeschädigung bei Absturz oder Stromausfall
    • Die Einschränkung, dass content.xml weiterhin vollständig neu geschrieben werden muss, bleibt bestehen
    • Trotzdem können die übrigen 77 Dateien unverändert bleiben, was File/Save beschleunigt und SSD-Schreibvorgänge reduziert

Zweite Verbesserung: Inhalte in kleinere Teile aufspalten

  • Da SQLite nicht nur große Blöcke, sondern auch viele kleine Teile effizient speichern kann, lässt sich eine Inhaltstabelle pro Folie anlegen
CREATE TABLE slide(
  pageNumber INTEGER,
  slideContent TEXT
);
CREATE INDEX slide_pgnum ON slide(pageNumber);
  • Um die erste Ansicht anzuzeigen, muss die Anwendung nur die erste Folie lesen
SELECT slideContent FROM slide WHERE pageNumber=1;
  • In dieser Struktur lässt sich nur der Inhalt der ersten Folie schnell laden, parsen und anzeigen, ohne beim Start die komplette content.xml lesen zu müssen
  • Dadurch ergeben sich auch mehr Implementierungsoptionen
    • Nach dem Anzeigen der ersten Folie können die übrigen Seiten in einem Background-Thread geladen werden
    • Es kann nur die aktuelle Folie im Speicher gehalten werden
    • Für schnelle Übergänge können auch nur die aktuelle und die nächste Folie im Speicher bleiben
  • Auch beim Speichern müssen nur geänderte Seiten neu geschrieben werden, wodurch File/Save schneller wird
  • Bei kurzen Textfragmenten sinkt die Kompressionseffizienz, was die Dokumentgröße erhöhen kann
    • Da der Großteil des Speicherplatzes eines Dokuments jedoch von Bildern belegt wird, kann die geringere Kompressionseffizienz bei Text als kleiner Preis für die verbesserte User Experience gelten

Dritte Verbesserung: Versionsverwaltung

  • Wenn Folien als einzelne Objekte gespeichert werden, lässt sich eine Versionshistorie innerhalb desselben Dokuments ablegen
CREATE TABLE slide(
  slideId INTEGER PRIMARY KEY,
  derivedFrom INTEGER REFERENCES slide,
  content TEXT
);
CREATE TABLE version(
  versionId INTEGER PRIMARY KEY,
  priorVersion INTEGER REFERENCES version,
  checkinTime DATETIME,
  comment TEXT,
  manifest TEXT
);
  • Jede Folie hat nicht mehr eine Seitenzahl, sondern eine eindeutige slideId; die Reihenfolge wird über eine Liste von slideId-Werten im manifest der Tabelle version festgelegt
  • Beim Start wählt die Anwendung zunächst die anzuzeigende Version aus, normalerweise die neueste
SELECT manifest, versionId FROM version ORDER BY versionId DESC LIMIT 1;
  • Es ist auch eine Abfrage möglich, die die neueste Version anhand von checkinTime holt
SELECT manifest, versionId, max(checkinTime) FROM version;
  • In SQLite liefert die obige max(checkinTime)-Abfrage ein definiertes Ergebnis, während viele andere SQL-Datenbanken ein undefiniertes Ergebnis liefern oder einen Fehler ausgeben würden
  • Wenn der Benutzer File/Save ausführt, können nur die geänderten Folien als neue Zeilen in die Tabelle slide eingefügt und eine neue Zeile in version mit einem aktualisierten manifest erzeugt werden
  • Die Tabelle version speichert Check-in-Zeit, Benutzerkommentar und Elternversion und bewahrt so die Änderungshistorie
  • Es wäre auch möglich, mehrere Präsentationen innerhalb desselben Dokuments zu speichern
  • Statt einer separaten Sicherungsdatei könnte eine spezielle pending-Version verwendet werden, um ungespeicherte Änderungen häufig und unauffällig zu protokollieren
    • Da nicht das gesamte Dokument, sondern nur die Änderungen geschrieben werden, handelt es sich nicht um einige MB, sondern um einige KB
    • Die Speicherzeit könnte im Millisekundenbereich statt bei mehreren Sekunden liegen
    • Selbst nach einem Neustart infolge eines Absturzes könnte der Großteil oder fast die gesamte Arbeit des Benutzers erhalten bleiben
    • Wenn der Benutzer ungespeicherte Änderungen verwerfen möchte, kann er einfach zu einer früheren Version zurückkehren

Weitere mögliche Funktionen in einem SQLite-Dateiformat

  • Schon mit nur drei Tabellen kann ein SQLite-Container wichtige Funktionen zu einem Anwendungsdateiformat hinzufügen
  • Zusätzlich lassen sich Schema, Indizes, Trigger, Views und Constraints nutzen, um Performance, Komfort und Konsistenz zu erhöhen
  • Mögliche Erweiterungen sind unter anderem
    • Speicherung eines automatischen Undo/Redo-Stacks in Datenbanktabellen, sodass Rückgängig/Wiederholen sogar über frühere Editiersitzungen hinweg möglich wird
    • Hinzufügen von Volltextsuche für ein Foliendeck oder mehrere Foliendecks
    • Zerlegung von settings.xml in SQL-Tabellen, damit andere Anwendungen es einfacher anzeigen und bearbeiten können
    • Auslagerung der Sprechernotizen jeder Folie in eine separate Tabelle, sodass Third-Party-Apps oder Skripte leicht darauf zugreifen können
    • Unterstützung von Präsentationsstrukturen, die über eine einfache lineare Folienreihenfolge hinausgehen und je nach Publikumsreaktion unterschiedliche Pfade und Umwege erlauben

Häufige Vorbehalte gegenüber SQLite und Gegenargumente

  • Aufgrund von Erfahrungen mit Enterprise-SQL-Datenbanken kann es Vorbehalte geben, SQLite als Anwendungsdateiformat zu verwenden
  • Viele Enterprise-Datenbanken empfehlen, große Strings oder BLOBs nicht in der Datenbank, sondern in separaten Dateien zu speichern, aber SQLite ist anders
    • SQLite kann in jeder Spalte Strings oder BLOBs von bis zu etwa 1 GB speichern
    • Bei Strings und BLOBs unter 100 KB ist die I/O-Performance besser als bei separaten Dateien
  • Auch die Vorstellung, jedes SQL-Schema müsse in Dritter Normalform (3NF) vorliegen und nur kleine primitive Typen speichern, kann einschränkend wirken
    • Relationale Theorie ist wichtig, aber in realen Dateiformaten kann es eine zulässige Wahl sein, komplexe Informationen wie XML oder JSON in Textfeldern zu speichern

SQLite als Anwendungsdateiformat

  • SQLite-Datenbankdateien haben nahezu dieselbe Größe wie ZIP-Archive mit denselben Informationen und können in manchen Fällen sogar kleiner sein
  • Dank atomischer Updates lassen sich kleine Änderungen sicher in Dokumente schreiben, was Festplatten-I/O reduziert und die Performance von File/Save verbessert
  • Anwendungen können nur die für die erste Ansicht benötigten Inhalte lesen und so die Startzeit verkürzen
  • Es muss nur der Inhalt im Speicher gehalten werden, der für die aktuelle Anzeige relevant ist; der Rest kann auf dem Datenträger bleiben, was den Speicherverbrauch deutlich senkt
  • SQL-Schemata können Informationen direkter und kompakter ausdrücken als eine Key/Value-Struktur wie ZIP
    • Das verbessert die Zugänglichkeit für Third-Party-Apps und Skripte
    • Fortgeschrittene Funktionen wie eingebaute Dokumentversionsverwaltung und Wiederherstellung von Arbeit nach Abstürzen lassen sich leichter umsetzen
  • OpenDocument ist bereits ein etabliertes und gut entworfenes Format, und da SQLite später als OpenDocument entstanden ist, ist dies keine Kritik an der damaligen Entscheidung
  • Das Dokument Application File Format bietet weitere Ideen zur Verwendung von SQLite als Anwendungsdateiformat

1 Kommentare

 
GN⁺ 2023-09-19
Meinungen auf Hacker News
  • Ich entwickle eine App, die SQLite als Dateiformat verwendet.
    Weil ich den üblichen Ablauf beibehalten möchte, bei dem sich die Datei nur ändert, wenn der Nutzer ein Dokument bearbeitet und dann speichert, kopiere ich sie beim Öffnen in eine :memory:-Datenbank: https://www.sqlite.org/inmemorydb.html
    Der Nutzer kann nach Belieben Änderungen vornehmen, und die App spiegelt sie ohne separates Dokumentmodell direkt im Datenbankformat wider. Beim Speichern schreibe ich sie mit VACUUM wieder in die Datenbankdatei zurück: https://www.sqlite.org/lang_vacuum.html
    Bei Dateien angemessener Größe funktioniert das gut, und in meiner App bleiben sie immer innerhalb dieses Bereichs.

    • Ich verstehe nicht, warum man eine zusätzliche flüchtige Datenbank verwenden sollte. Wenn ein Nutzer eine Datei bearbeitet, wird vermutlich nicht einmal einmal pro Sekunde geschrieben, also ist der Performance-Gewinn auch nicht groß.
      Besser wäre es, direkt automatisch in der Datenbank zu speichern und den Speichern-Button abzuschaffen. Das ist robuster gegenüber Abstürzen, mit nur einer Datenbank gibt es weniger Code und weniger Bugs, und SQLite-Schreibvorgänge gelingen oder schlagen fehl, ohne Zwischenzustand. Dagegen kann, wie im Dokument zitiert, bei VACUUM INTO die Ausgabe-Datenbank bei unerwartetem Beenden oder Stromausfall unvollständig oder beschädigt sein.
      Wenn man SQLite so verwendet, wie es ursprünglich gedacht war, muss man sich für die Lebensdauer von SQLite darüber keine Gedanken machen.
    • Dass es sich wie eine normale App verhält, bedeutet, dass man bei einem App-Absturz oder Stromausfall nicht gespeicherte Daten verliert.
      Viel besser wäre es, nach jeder Aktion an einem temporären Ort zu speichern, etwa nach XDG-Verzeichnissen unter ~/.local/share/application/yourapp, und die Datei an den gewünschten Ort zu kopieren, wenn der Nutzer auf Speichern klickt. Wenn man die App nach einem Stromausfall wieder öffnet, kann man fast an derselben Stelle wiederherstellen und verliert höchstens die letzten paar Sekunden.
    • Noch einfacher könnte es sein, die Datenbank beim Öffnen in den WAL-Modus zu versetzen und automatische Checkpoints abzuschalten: https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint
      Wenn der Nutzer speichert, führt man einen Checkpoint aus und führt den WAL-Inhalt in die Hauptdatenbank zusammen.
    • Laut Dokumentation kopiert VACUUM den Inhalt in eine temporäre Datenbankdatei und überschreibt dann das Original; beim Überschreiben verwendet es wie eine normale Transaktion ein Rollback-Journal oder WAL. Daher wird ungefähr bis zum Doppelten der Größe des Originals an freiem Speicher benötigt.
      VACUUM INTO verwendet statt einer temporären Datenbank die bei INTO angegebene Datei und lässt den Schritt des erneuten Kopierens über das Original weg. Entscheidend ist, ob tatsächlich das auch gegen Stromausfälle robuste VACUUM verwendet wird oder VACUUM INTO, das während des Schreibens anfällig für Stromausfälle ist und bei einem bestehenden Dateinamen offenbar Schäden verursachen kann.
    • Ich habe einmal einen ähnlichen Ansatz verwendet: Die Datenbank lief im Speicher als Cache, wurde regelmäßig auf die Festplatte gespeichert, und ich habe die backup API genutzt: https://www.sqlite.org/backup.html
  • Das Problem mit SQLite ist, dass es kein standardisiertes Dateiformat ist.
    Es ist gut dokumentiert und weithin verstanden, aber kein ISO-Standard definiert im Detail, wie eine SQLite-Datei zu interpretieren ist. Für alternative Implementierungen gilt dasselbe.
    Zip und XML haben eine viel kleinere API-Oberfläche als SQLite. Die API von SQLite geht über ein paar C-Funktionen hinaus und umfasst die SQL-Sprache selbst; einen SQL-Parser, Query-Optimizer, Compiler, eine Bytecode-VM, eine Volltextsuchmaschine usw. ohne Datenbeschädigung zu implementieren, ist ein deutlich größeres Unterfangen als ein XML-Parser.
    Für domänenspezifische, geschlossene Apps, bei denen Interoperabilität oder ISO-Standardisierung nicht wichtig sind, ist SQLite ein gutes Dateiformat, aber ich verstehe, dass es bei OpenOffice tatsächlich solche Bedenken gab.

    • Es ist unklar, worauf sich dieses Problem bezieht. Das SQLite-Dateiformat ist Public Domain, gut dokumentiert, und es gibt Parser in mehreren Sprachen.
      Auch die SQLite-C-Bibliothek ist Public Domain, der Quellcode ist vollständig offen, sie verarbeitet das Dateiformat, und die Dokumentation ist besser als bei den meisten ISO-Standards. Bindings für die wichtigsten Sprachen gibt es ebenfalls fast alle.
      Wenn das Problem darin besteht, dass ein OpenDocument-Format, das innerhalb einer SQLite-Datei gespeichert wird, erst noch erstellt und dokumentiert werden muss, ist das etwas anderes. ISO-Standards sind gut, aber wenn man warten müsste, bis ISO ein Dateiformat definiert, gäbe es sehr wenig, was man verwenden könnte.
    • Um ein Standard-Dateiformat zu sein, muss man nicht den gesamten SQL-Parser, Query-Optimizer, Compiler, die Bytecode-VM und die Volltextsuchmaschine implementieren.
      Genauso wenig muss man alle Tabellenkalkulationsfunktionen implementieren, um eine LibreOffice-Tabelle zu lesen. Nötig ist die Fähigkeit, die Tabellen zu rekonstruieren; die gewünschten Informationen kann man anschließend mit imperativem Code in der Sprache der Wahl durchlaufen und gewinnen.
    • Das ist auch für die Library of Congress kein Problem. Sie hat SQLite zusammen mit CSV, XML und JSON als empfohlenes Speicherformat für Datensätze definiert.
    • Es scheint, als würden hier das Dateiformat und seine Nutzung vermischt. Eine App, die das SQLite-Dateiformat nutzt, kann die SQLite-Bibliothek als Teil der App verwenden.
      Diese Bibliothek neu zu implementieren wäre eine große Aufgabe, aber von derselben Art wie die Neuimplementierung von Code, der das OpenDocument-Dateiformat verwendet. Das Dateiformat selbst ist ziemlich einfach.
    • Ein Standard ist nicht unbedingt erforderlich. Jede Interaktion zwischen Anwendung und Dokument erfolgt über SQL, und SQL ist zumindest in den wichtigen Teilen standardisiert.
      Wenn Kompatibilität Sorgen bereitet, kann man das Dokument so gestalten, dass es auch über eine andere Datenbank wie MySQL zugänglich ist.
  • Ich hatte erwartet, dass Audacity mit der Einführung von SQLite seine Dateispeicherfunktion deutlich verbessern würde, aber in der Praxis gab es viele Fallstricke.
    Unter Linux schlug das Speichern als neue Datei auf einem per /etc/fstab eingebundenen NTFS-Mount fehl, der zwar root gehörte, aber für alle beschreibbar war, offenbar wegen eines Berechtigungsfehlers; das Speichern in eine bestehende Datei funktionierte dagegen problemlos.
    Sobald man ein Projekt bearbeitet, wird die Datei auf der Festplatte verändert. Wenn man ein Audacity-Projekt als Binärklumpen in Git ablegt, entstehen dadurch unnötige Git-Diffs. Selbst nach dem Speichern bleiben veraltete oder gelöschte Daten bis zum Schließen des Projektfensters in der SQLite-Datei; wenn man das Fenster vor dem Commit nicht schließt, können sie ins Repository gelangen. Ich erinnere mich, dass man früher die .aup3-Datei direkt VACUUMen musste, heute reicht es, das Fenster zu schließen. Es erinnert an Fast Save in Word 2003.

    • Wenn Audacity abstürzt oder abnormal beendet wird, findet überhaupt keine Bereinigung statt, was lästig ist. Früher wurde man im Wiederherstellungsprozess darauf hingewiesen, dass es verwaiste Blöcke gibt, und konnte wählen, ob man sie behalten oder löschen möchte.
      Wenn ein Projekt, das nur ein paar Hundert MB groß sein sollte, mehrere GB groß wurde und man Speicherplatz sparen musste, war Mix and Render bei einfachen Einspur-Arbeiten die Lösung. Es änderte den Ton nicht, konnte aber beim Speichern und Beenden den Müll bereinigen.
      Das ist kein Problem von SQLite selbst, sondern eindeutig ein Problem der Anwendungsschicht. Audacity 2 hatte, glaube ich, ein Konzept für temporären Arbeitsbereich; Audacity 3 scheint die .aup3-Datei selbst als Arbeitsbereich zu verwenden.
      Ich habe mir das Audacity-3-Format angesehen: Die Projektdaten, die früher der .aup-Datei entsprachen, werden als XML in einer Tabelle mit einer einzigen Zeile gespeichert, aber nicht als Klartext, sondern mit einem einfachen Wörterbuch-Coder kodiert. Das fand ich sehr merkwürdig. Es macht Interoperabilität und Prüfung deutlich schwieriger, schadet der Performance zumindest ein klein wenig und die Platzersparnis dürfte bei Audiodateien von Hunderten MB nur ein Rundungsfehler von ein paar KB sein.
    • Man muss das Verhalten exakt nachbilden, das Nutzer erwarten. Alles sollte in temporären Dateien gespeichert werden, und nur bei einer expliziten Speicheraktion sollte die Originaldatei überschrieben werden.
      Aus Git-Sicht ist ein Textformat vorteilhaft, bei dem Diffs und Merges einfach sind. Ich weiß nicht, wie gut ein SQLite-Dump in dieser Hinsicht funktioniert.
    • Meine Frau nutzt Audacity den ganzen Tag, und alle paar Tage entsteht eine beschädigte SQLite-Datei. Es gibt Duplicate-Key-Fehler, und ich weiß nicht, wie man das in Audacity reparieren oder erneut importieren könnte.
      Wenn es wichtig ist, kann man es manuell reparieren, aber meistens funktioniert es wieder, wenn man die Datei wegwirft.
  • Guter Artikel. Allerdings gefällt mir, dass OpenDocument aus einem Bündel von XML-Dateien in einem Zip-Archiv besteht.
    Auch ohne schwere Bibliothek, die das Dokumentformat kennt, kann man ziemlich einfach Dokumente wie Spreadsheets erzeugen.
    Manchmal möchten Nutzer eines Webdienstes die als Tabellenzeilen exportierten Daten in verschiedenen Tools verwenden. UTF-8-CSV ist offen, konventionell und brauchbar, aber wer Endnutzern schon einmal CSV angeboten hat, kennt den Schmerz, wenn Spreadsheet-Apps daran scheitern.
    Ich habe ein Beispiel-Spreadsheet als OpenDocuments ODS und als Microsofts XML-Monster OOXML im XLSX-Format gespeichert und mir dann nur die Grundlagen des XML-Formats erschlossen. Ich habe das Zip-Archiv auf die notwendigen Bestandteile reduziert, Platzhalter für die Inhalte markiert und erzeuge auf Anfrage eine neue Spreadsheet-Datei. Jetzt kann ich dieselben Daten als CSV, ODS, XLSX und JSON ausgeben.
    Mit SQLite wäre das auch möglich, aber etwas komplizierter und die Entwicklung langsamer. Dass man in einer Office-Suite ein Vorlagendokument erstellen und dann im XML der gespeicherten Datei herumgraben kann, ist eine nischige, aber gute Funktion.
    Besonders problematisch ist Excel in Locales wie nl_NL, wo es sich so verhält, als sei das Spaltentrennzeichen in CSV-Dateien hart auf Semikolon codiert. Microsoft hat nämlich berüchtigterweise entschieden, dass Niederländer in Dateien mit comma separated values keine Kommas verwenden.

    • Dieses Verhalten ist nicht vollständig hart codiert, sondern hängt vom Wert von localeconv()->decimal_point ab. Ist der Wert ,, verwendet Excel sowohl in CSV-Dateien als auch in der Formelsprache Semikolons.
      Früher konnte man das beim Öffnen von CSV/TXT in Excel konfigurieren, und in LibreOffice geht das immer noch; im Zuge der allgemeinen UI-Vereinfachung wurde es aber irgendwo in das Menü bzw. Ribbon-Tab Data verschoben. Man muss eine neue Arbeitsmappe öffnen und die richtigen Optionen finden; wenn man Zeit sparen will, ist LibreOffice die bessere Wahl.
  • Dieser Teil hat mich wirklich überrascht. Kaum zu glauben, dass keine verschachtelte Abfrage nötig ist.
    SELECT manifest, versionId, max(checkinTime) FROM version;
    In SQLite funktioniert diese zweite Abfrage mit max(checkinTime) tatsächlich gut und liefert eine definierte Antwort. Andere SQL-Datenbank-Engines liefern entweder eine undefinierte Antwort oder einen Fehler, aber SQLite gibt manifest und versionId des Eintrags mit dem größten checkinTime zurück.

    • Das kann eine nützliche Funktion sein, aber ehrlich gesagt würde ich nicht erwarten, dass eine solche Abfrage genau so zurückkommt.
      In diesem Fall braucht man keine verschachtelte Abfrage; man kann nach checkinTime sortieren und auf einen Eintrag begrenzen: select manifest, versionId, checkinTime from version order by checkinTime desc limit 1
      Zumindest in SQLite und PostgreSQL sollte das funktionieren. Bei Oracle musste man, soweit ich mich erinnere, where rownum=1 verwenden, weshalb eine verschachtelte Abfrage nötig war.
    • Man kann das grob als Kurzform für GROUP BY manifest, versionId ORDER BY 3 DESC LIMIT 1 betrachten oder für eine CTE, die zuerst den maximalen checkinTime ermittelt und dann joint.
      Wenn es allerdings mehrere Zeilen mit demselben maximalen checkinTime gibt, kommt Zufälligkeit ins Spiel; das kann zur Fußangel werden, daher nutze ich solche SQLite3-spezifischen Features ungern. Um deterministisch die beste Zeile auszuwählen, braucht man eine explizite Variante ähnlich der oben genannten.
    • Das ist nicht das Verhalten, das man in SQL normalerweise erwartet, aber SQLite weicht oft von Erwartungen ab. In diesem Fall ist es praktisch, aber nicht standardkonform.
    • Es wirkt, als sei ein praktischer Nebeneffekt der Implementierung später zum offiziellen Verhalten geworden. Ähnlich wie die Schlüsselreihenfolge in Python-Dictionaries.
      In Postgres kann man mit einer DISTINCT ON-Abfrage etwas Ähnliches erreichen. Das war eine der Aufgaben, die in SQL einfach aussehen, sich für mich aber am schwierigsten angefühlt haben.
    • Dass man behaupten kann, dies sei definiertes Verhalten, ist ziemlich erstaunlich. manifest und versionId sind nämlich nicht funktional abhängig von max(checkinTime).
      Zum Beispiel könnten zwei Zeilen denselben checkinTime-Wert haben und dieser Wert könnte der maximale sein.
  • Ich habe einmal ein Produkt veröffentlicht, das sowohl SQLite als auch XML-Dateien verwendete.
    Eine der Verbesserungen bestand darin, einige Tabellen mit wenig Daten in XML-Dateien auszulagern. Die Dateien waren klein und wurden kaum genutzt, wodurch Datenzugriffsschicht und Diagnose einfacher wurden; sie waren als XML mit mehrzeiliger Tab-Einrückung angelegt.
    Technische Ansprechpartner, die ein Produkt diagnostizieren sollten, zu bitten, eine SQLite-Datenbank zu öffnen, war eine ziemliche Hürde. In den zentralen Teilen des Produkts war SQLite den XML-Dateien aber klar überlegen. Frühere Versionen verwendeten XML-Dateien, doch XML-Dateien bieten keine gute Möglichkeit für inkrementelle Updates, was zu Skalierungsproblemen führte.
    Der Vorteil von XML, ein für Menschen gut lesbares Format zu sein, funktioniert nur dann wirklich, wenn die Datei klein ist und das Schema auf gut lesbares XML ausgelegt wurde. Dass man jedes Mal die gesamte XML-Datei neu schreiben muss, und die mit wachsendem Funktionsumfang entstehende Komplexität schmälern XMLs größten Vorteil sehr schnell.
    Es kommt selten genug vor, dass normale Nutzer das Innere von Office-Dokumenten direkt bearbeiten müssen; daher ist es eine akzeptable Einstiegshürde, den Umgang mit einem SQLite-Reader zu lernen. Die Grenzen von XML+Zip bei beliebigen Schreibvorgängen mitten in der Datei lassen sich auch mit Moores Gesetz nicht überwinden.

    • Mir ist nicht ganz klar, wie das native Format von SQLite ohne Zip eine ähnliche Größe wie XML+Zip erreichen soll. Ich frage mich, ob TEXT- oder BLOB-Felder in SQLite komprimiert werden oder ob angenommen wird, dass der Aufrufer das BLOB vor dem Schreiben komprimiert.
  • ODT wurde mit Blick auf Standardisierung entworfen. Das frühere Format war zwar sehr ähnlich, stützte sich aber stark auf bestehende Standards wie XHTML, SVG und CSS.
    Wenn man nicht auf bestehende Standards verweisen kann, würde die ODT-Spezifikation selbst schlagartig riesig werden. Auch der Aufwand, den Standard zu aktualisieren, scheint erheblich zu sein, und in den letzten Jahren gab es kaum Fortschritte.
    Realistisch gesehen könnte ein SQLite-Format zwar als Option angeboten werden, aber der Zug für Office-Dokumentformate scheint bereits abgefahren zu sein. Allerdings liefert das ein gutes Argument dafür, die SQLite-Spezifikation als offiziellen Standard auszuarbeiten.

    • Die Spezifikation ist sehr knapp formuliert und legt hauptsächlich die Syntax statt Effekte und Verhalten fest, ist aber dennoch mit 840 Seiten sehr umfangreich.
      Abgesehen von einigen Schwächen, etwa dem Problem, dass lokale Styles und Textbereiche wegen des Attributs ooo:rsid explodieren, nicht dünn besetzten Spreadsheets und dem seltsamen Mechanismus für Tabellen-Styling, ist es ein sehr gut entworfenes Markup für solche Dokumentdaten. Es findet eine gute Balance zwischen semantischem Markup und dem, was Nutzer tatsächlich ausdrücken wollen.
      Office OpenXML hat dagegen zustandsbehaftete leere Tags für Formatierung, die in DOCX umschalten, ob der nachfolgende Text fett dargestellt wird.
  • Es riecht irgendwie falsch, ein Dateiformat an SQLite zu koppeln.
    SQLite ist gut, aber in diesem Bereich ziemlich einzigartig, weil es so viel leistet und sich deshalb nicht einfach 1:1 nachbauen lässt.
    In diesem Fall braucht man aber nicht so viele Funktionen. Grundlegende, sichere Transaktionssemantik und die Fähigkeit, einfache Tabellenstrukturen zu speichern, reichen aus; den kompletten SQL-Standard oder gar einen Query Optimizer braucht man nicht.
    Es mag bessere Dateiformate geben, aber besser wäre eines, das von SQLite getrennt ist.

    • Ich sehe nicht, warum das nicht gehen sollte: https://www.sqlite.org/appfileformat.html
      Die Größe liegt unter 1 MB, siehe https://sqlite.org/footprint.html, und selbst mit allen aktivierten Features sind es 750 KB: https://www.sqlite.org/about.html
      Zur Compile-Zeit lassen sich etliche Features weglassen, und es scheint auch Optionen zu geben, den Query Planner anzupassen oder zu verkleinern: https://www.sqlite.org/compile.html
      Außerdem heißt es: „SQLite konkurriert nicht mit Client/Server-Datenbanken. SQLite konkurriert mit fopen()“: https://www.sqlite.org/whentouse.html
      Letztlich braucht man nicht die Datenbank selbst, sondern eine Bibliothek, die eine Datenbank-API und entsprechendes Verhalten bereitstellt.
    • Der Transaktionsaspekt war, besonders bei gleichzeitigem Dateizugriff, schwieriger als gedacht. Damals war der Umgang mit SQLITE_BUSY ziemlich mühsam.
      Mir ist klar, dass bei Transaktionen Serialisierungsfehler zu erwarten sind, aber bei SQLite war es schwer, anhaltende Fehler wie eine Art Selbst-Deadlock von vorübergehenden Problemen durch gleichzeitige Updates zu unterscheiden. Bei einem temporären Fehler kann man die Closure, die die Transaktionsarbeit definiert, erneut ausführen; bei einem dauerhaften Fehler ist das sinnlos.
      Ein Teil des Problems ist, dass sqlite3_stmt sowohl die Eigenschaften eines vorbereiteten Statements als auch eines Result Sets vereint. Man hält es lange, um kompilierten Bytecode zu cachen; wenn man mitten in der Iteration stoppt, kann es zu diesem Zeitpunkt Locks halten. Dadurch können unerwartete Fehlschläge beim Lock-Upgrade entstehen.
      Am Ende habe ich mit sqlite3_next_stmt, sqlite3_stmt_busy und sqlite3_sql ein detailliertes Fehler-Reporting gebaut und das Problem beseitigt. Obwohl es nur für den persönlichen Gebrauch war, war der Code für Transaktions-Retries voller optionalem Logging und Kommentaren. Die Retry-Logik für Transaktionen mit PostgreSQL war deutlich einfacher.
      Eine weitere Überraschung war die Dokumentation, dass im WAL-Modus mit synchronous=NORMAL committete Transaktionen nach Stromausfall oder Systemabsturz zurückgerollt werden können: https://sqlite.org/pragma.html#pragma_synchronous
      Für meine Anwendung war das nicht relevant.
    • SQLite wird bereits genau für solche Zwecke eingesetzt. Es wird als OGC GeoPackage verwendet, und auch Mapbox-/Maptiler-Datensätze nutzen es.
    • Manche Formate sind vor allem für den Austausch entworfen. Das Argument der SQLite-Seite läuft darauf hinaus, dass App-Anbieter den Nutzern das SQLite-Format aufzwingen und es so faktisch zum Standard machen sollen; die Arbeit, daraus einen rechtlichen Standard zu machen, fehlt dabei.
      Wenn Richard Hipp und sein Unternehmen einen ISO/IEC/ANSI/ETSI-Standard für SQLite vorweisen, von dem sie garantiert nie abweichen, dazu eine rechtliche Prüfung, dass keine relevanten Patente im Weg stehen, und mehrere kompatible SQLite-Implementierungen, bei denen alle Vorteile erhalten bleiben, dann kann man darüber sprechen, es als Dateiformat zu empfehlen. Andernfalls bedeutet es, eine starke Abhängigkeit von einer Single-Source-Implementierung einzugehen und sie auch den Nutzern aufzubürden.
      XML, ASN.1 und JFIF sind offizielle Standards, und auch ZIP ist ein offizieller Standard, der im Zuge der OpenDocument-Standardisierung als ISO/IEC 21320-1:2015 übernommen wurde.
      Das Wichtigste bei Dokumenten ist, dass alle anderen sie lesen können. Die Zeit für Disk-Updates zu reduzieren, ist zweitrangig. Man sollte aus Microsofts Versuch, Standardisierungsgremien zu verbiegen, um Lock-in zu erhalten, etwas gelernt haben: https://arstechnica.com/uncategorized/2008/10/norwegian-standards-body-implodes-over-ooxml-controversy/
    • Wenn man sich Apple-Apps ansieht, verwenden die meisten SQLite als Speicherformat. iMovie, iPhoto, Sprachaufnahmen und andere tun das, und Docker ebenso.
      So falsch kann diese Wahl also nicht sein.
  • Ein weiteres Beispiel sind Raster-Kartenkacheln. Im Grunde sind das kleine quadratische Bilder, deren Anzahl in die Millionen gehen kann.
    Zip, tar, das Dateisystem und SQLite wurden alle ausprobiert, und SQLite war am schnellsten und am kleinsten, sogar besser als allgemeine Archive ohne Overhead.

    • Viele Dateisysteme bekommen Probleme, wenn in einem einzelnen Verzeichnis mehr als Zehntausende Dateien liegen, und bei Kartenkacheln entsteht genau diese Situation. Dass SQLite schneller ist, überrascht nicht.
    • Wenn SQLite schneller ist, liegt das Problem an der verwendeten Zip-Bibliothek.
      SQLite hat einen großen Nachteil: Ein BLOB aus der Datenbank kann man nicht mmapen, sondern muss es an eine andere Stelle kopieren. Eine Zip-Datei kann man, wenn sie unkomprimiert ist oder mit einer speziellen Kodierung wie PVRTC komprimiert wurde, direkt per mmap einbinden.
  • OpenDocument besteht aus komprimierten Bildern und XML. Das bedeutet letztlich, dass das gesamte Format geparst und in den Arbeitsspeicher geladen wird.
    Mir ist nicht ganz klar, wie SQLite das verbessern soll. XML ist zwar nicht ideal, aber da es als Zip komprimiert ist, ist auch der Größen-Overhead nicht besonders groß.
    Alle Vorteile, die im SQLite-Artikel aufgezählt werden, ließen sich umsetzen, wenn man SQLite als Runtime-Modell des Dokuments verwendet. Das geht sowohl auf der Festplatte als auch im Arbeitsspeicher, aber SQLite muss dafür nicht das Übertragungsformat sein.
    Im Gegenteil: SQLite könnte größer werden als das aktuelle Format. Nach Änderungen kann ungenutzter Speicher entstehen, es kann fragmentiert und löchrig werden. Wenn man jedes Mal optimieren muss, verschwinden auch Vorteile wie schnelles Speichern.
    Formate, die Delta-Updates und schnelle Index-Lookups benötigen und nicht die gesamte Datei in den Speicher laden sollen, verwenden SQLite tatsächlich häufig als Dateiformat. Allerdings habe ich das Gefühl, dass OpenDocument in diesem hypothetischen Szenario ein schlechtes Beispiel für ein SQLite-Ziel war.

    • XML und Zip beherrschen inkrementelle Updates nicht richtig. Beim Speichern muss die gesamte Anwendungsdatei geschrieben werden, und wenn beim Schreiben ein Problem auftritt, kann es zu Beschädigungen kommen.
      Wenn man SQLite als On-Disk-Format verwendet und die Anwendung korrekt implementiert, muss man am Ende nicht in einem beschädigten Zustand landen.
      Mit XML/Zip lässt sich Ähnliches zwar durch einen Umbenennungs-Trick erreichen, aber SQLite bietet das in einer einzelnen Datei auf der Festplatte. Wenn man SQLite ohnehin schon als Speichermodell verwendet, gibt es keinen Grund, es nicht auch als Disk-/Übertragungsformat zu nutzen. An diesem Punkt ist es praktisch gratis.
      Das Problem der Dateigröße ließe sich vermutlich mit VACUUM lösen.