SQLite
 sql >> Datenbank >  >> RDS >> SQLite

SQLite-Fallen und Fallstricke

SQLite ist eine beliebte relationale Datenbank, die Sie in Ihre Anwendung einbetten. Es gibt jedoch viele Fallen und Fallstricke, die Sie vermeiden sollten. Dieser Artikel behandelt mehrere Fallstricke (und wie man sie vermeidet), wie z. B. die Verwendung von ORMs, die Rückgewinnung von Speicherplatz, die Beachtung der maximalen Anzahl von Abfragevariablen, Spaltendatentypen und den Umgang mit großen Ganzzahlen.

Einführung

SQLite ist ein beliebtes relationales Datenbanksystem (DB) . Es hat einen sehr ähnlichen Funktionsumfang wie seine größeren Brüder wie MySQL , die Client/Server-basierte Systeme sind. SQLite ist jedoch ein eingebettetes Datenbank . Sie kann als statische (oder dynamische) Bibliothek in Ihr Programm eingebunden werden. Dies vereinfacht die Bereitstellung , da kein separater Serverprozess notwendig ist. Bindungen und Wrapper-Bibliotheken ermöglichen Ihnen den Zugriff auf SQLite in den meisten Programmiersprachen .

Ich habe während der Entwicklung von BSync im Rahmen meiner Doktorarbeit intensiv mit SQLite gearbeitet. Dieser Artikel ist eine (zufällige) Liste von Fallen und Fallstricken, auf die ich während der Entwicklung gestoßen bin . Ich hoffe, dass Sie sie nützlich finden und vermeiden, die gleichen Fehler zu machen, die ich einmal gemacht habe.

Fallen und Fallstricke

Verwenden Sie ORM-Bibliotheken mit Vorsicht

Object-Relational Mapping (ORM)-Bibliotheken abstrahieren die Details von konkreten Datenbank-Engines und ihrer Syntax (z. B. spezifische SQL-Anweisungen) zu einer objektorientierten API auf hoher Ebene. Es gibt viele Bibliotheken von Drittanbietern (siehe Wikipedia). ORM-Bibliotheken haben einige Vorteile:

  • Sie sparen Zeit bei der Entwicklung , weil sie Ihren Code/Ihre Klassen schnell auf DB-Strukturen abbilden,
  • Sie sind häufig plattformübergreifend , d. h. Ersetzung der konkreten DB-Technologie (z. B. SQLite durch MySQL) ermöglichen,
  • Sie bieten Hilfscode für die Schemamigration an .

Allerdings haben sie auch mehrere schwerwiegende Nachteile Folgendes sollten Sie beachten:

  • Sie lassen das Arbeiten mit Datenbanken erscheinen einfach . In Wirklichkeit haben DB-Engines jedoch komplizierte Details, die Sie einfach kennen müssen . Wenn etwas schief geht, z. Wenn die ORM-Bibliothek Ausnahmen auslöst, die Sie nicht verstehen, oder wenn die Laufzeitleistung nachlässt, wird die Entwicklungszeit, die Sie durch die Verwendung von ORM eingespart haben, schnell durch den Aufwand für die Fehlersuche aufgebraucht . Zum Beispiel, wenn Sie nicht wissen, welche Indizes sind, hätten Sie Schwierigkeiten, Leistungsengpässe zu beheben, die durch das ORM verursacht werden, wenn es nicht automatisch alle erforderlichen Indizes erstellt hat. Kurz gesagt:Es gibt kein kostenloses Mittagessen.
  • Aufgrund der Abstraktion des konkreten DB-Anbieters ist anbieterspezifische Funktionalität entweder schwer zugänglich oder überhaupt nicht zugänglich .
  • Es gibt einen gewissen Rechenaufwand im Vergleich zum direkten Schreiben und Ausführen von SQL-Abfragen. Ich würde jedoch sagen, dass dieser Punkt in der Praxis strittig ist, da es üblich ist, dass Sie an Leistung verlieren, wenn Sie zu einer höheren Abstraktionsebene wechseln.

Letztendlich ist die Verwendung einer ORM-Bibliothek eine Frage der persönlichen Präferenz. Wenn Sie dies tun, seien Sie einfach darauf vorbereitet, dass Sie sich mit den Macken relationaler Datenbanken (und anbieterspezifischen Vorbehalten) vertraut machen müssen, sobald unerwartetes Verhalten oder Leistungsengpässe auftreten.

Fügen Sie von Anfang an eine Migrationstabelle hinzu

Wenn Sie nicht sind Wenn Sie eine ORM-Bibliothek verwenden, müssen Sie sich um die Schemamigration der DB kümmern . Dazu gehört das Schreiben von Migrationscode, der Ihre Tabellenschemata ändert und die gespeicherten Daten auf irgendeine Weise transformiert. Ich empfehle Ihnen, eine Tabelle namens „Migrationen“ oder „Version“ mit einer einzigen Zeile und Spalte zu erstellen, die einfach die Schemaversion speichert, z. unter Verwendung einer monoton steigenden ganzen Zahl. Dadurch kann Ihre Migrationsfunktion erkennen, welche Migrationen noch angewendet werden müssen. Immer wenn ein Migrationsschritt erfolgreich abgeschlossen wurde, erhöht Ihr Migrationstoolcode diesen Zähler über ein UPDATE SQL-Anweisung.

Automatisch erstellte rowid-Spalte

Immer wenn Sie eine Tabelle erstellen, erstellt SQLite automatisch einen INTEGER Spalte namens rowid für dich – es sei denn, Sie haben den WITHOUT ROWID angegeben Klausel (aber wahrscheinlich kannten Sie diese Klausel noch nicht). Die rowid row ist eine Primärschlüsselspalte. Wenn Sie auch selbst eine solche Primärschlüsselspalte angeben (z. B. mit der Syntax some_column INTEGER PRIMARY KEY ) ist diese Spalte einfach ein Alias für rowid . Siehe hier für weitere Informationen, die das Gleiche mit ziemlich kryptischen Worten beschreiben. Beachten Sie, dass eine SELECT * FROM table Anweisung wird nicht schließen Sie rowid ein standardmäßig – Sie müssen nach der rowid fragen Spalte explizit.

Verifizieren Sie, dass PRAGMA Es funktioniert wirklich

Unter anderem PRAGMA -Anweisungen werden verwendet, um Datenbankeinstellungen zu konfigurieren oder verschiedene Funktionen aufzurufen (offizielle Dokumente). Es gibt jedoch nicht dokumentierte Nebenwirkungen, bei denen das Setzen einer Variablen manchmal tatsächlich keine Wirkung hat . Mit anderen Worten, es funktioniert nicht und schlägt stillschweigend fehl.

Wenn Sie beispielsweise die folgenden Anweisungen in der angegebenen Reihenfolge ausgeben, wird die letzte Anweisung wird nicht irgendeine Wirkung haben. Variable auto_vacuum hat immer noch den Wert 0 (NONE ), ohne triftigen Grund.

PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)

Sie können den Wert einer Variablen lesen, indem Sie PRAGMA variableName ausführen und das Gleichheitszeichen und den Wert weglassen.

Um das obige Beispiel zu beheben, verwenden Sie eine andere Reihenfolge. Die Verwendung der Zeilenreihenfolge 3, 1, 2 funktioniert wie erwartet.

Vielleicht möchten Sie solche Prüfungen sogar in Ihre Produktion einbeziehen Code, da diese Seiteneffekte von der konkreten SQLite-Version und deren Erstellung abhängen können. Die in der Produktion verwendete Bibliothek kann sich von der Bibliothek unterscheiden, die Sie während der Entwicklung verwendet haben.

Beanspruchung von Speicherplatz für große Datenbanken

Standardmäßig wächst die Größe einer SQLite-Datenbankdatei monoton . Das Löschen von Zeilen markiert nur bestimmte Seiten als frei , sodass sie zum INSERT verwendet werden können Daten in die Zukunft. Um tatsächlich Speicherplatz zurückzugewinnen und die Leistung zu beschleunigen, gibt es zwei Möglichkeiten:

  1. Führen Sie den VACUUM aus Erklärung . Dies hat jedoch mehrere Nebeneffekte:
    • Es sperrt die gesamte DB. Während des VACUUM können keine gleichzeitigen Operationen stattfinden Betrieb.
    • Es dauert sehr lange (bei größeren Datenbanken), da es intern neu erstellt wird die DB in einer separaten temporären Datei und löscht schließlich die ursprüngliche Datenbank und ersetzt sie durch diese temporäre Datei.
    • Die temporäre Datei verbraucht zusätzlich Speicherplatz, während der Vorgang ausgeführt wird. Daher ist es keine gute Idee, VACUUM auszuführen falls Sie wenig Speicherplatz haben. Sie könnten es immer noch tun, müssten aber regelmäßig überprüfen, ob (freeDiskSpace - currentDbFileSize) > 0 ist .
  2. Verwenden Sie PRAGMA auto_vacuum = INCREMENTAL beim Erstellen die DB. Machen Sie dieses PRAGMA die erste Anweisung nach dem Erstellen der Datei! Dies ermöglicht eine gewisse interne Verwaltung und hilft der Datenbank, Speicherplatz zurückzugewinnen, wenn Sie PRAGMA incremental_vacuum(N) aufrufen . Dieser Aufruf fordert bis zu N zurück Seiten. Die offiziellen Dokumente enthalten weitere Details und auch andere mögliche Werte für auto_vacuum .
    • Hinweis:Sie können bestimmen, wie viel freier Speicherplatz (in Byte) beim Aufruf von PRAGMA incremental_vacuum(N) gewonnen würde :Multiplizieren Sie den zurückgegebenen Wert mit PRAGMA freelist_count mit PRAGMA page_size .

Die bessere Option hängt von Ihrem Kontext ab. Für sehr große Datenbankdateien empfehle ich Option 2 , da Option 1 Ihre Benutzer mit minuten- oder stundenlangem Warten auf die Bereinigung der Datenbank ärgern würde. Option 1 eignet sich für kleinere Datenbanken . Sein zusätzlicher Vorteil ist, dass die Leistung der DB wird sich verbessern (was bei Option 2 nicht der Fall ist), da die Neuerstellung Nebenwirkungen der Datenfragmentierung beseitigt.

Beachten Sie die maximale Anzahl von Variablen in Abfragen

Standardmäßig ist die maximale Anzahl von Variablen („Hostparameter“), die Sie in einer Abfrage verwenden können, fest auf 999 codiert (siehe hier, Abschnitt Maximale Anzahl von Hostparametern in einer einzelnen SQL-Anweisung ). Dieses Limit kann variieren, da es sich um eine Kompilierzeit handelt Parameter, dessen Standardwert Sie (oder wer auch immer SQLite kompiliert hat) möglicherweise geändert haben.

Dies ist in der Praxis problematisch, da Ihre Anwendung nicht selten eine (beliebig große) Liste an die DB-Engine liefert. Zum Beispiel, wenn Sie Massen-DELETE möchten (oder SELECT ) Zeilen, die beispielsweise auf einer Liste von IDs basieren. Eine Anweisung wie

DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)Code language: SQL (Structured Query Language) (sql)

gibt einen Fehler aus und wird nicht abgeschlossen.

Um dies zu beheben, befolgen Sie die folgenden Schritte:

  • Analysieren Sie Ihre Listen und teilen Sie sie in kleinere Listen auf
  • Wenn eine Teilung erforderlich war, stellen Sie sicher, dass Sie BEGIN TRANSACTION verwenden und COMMIT um die Unteilbarkeit zu emulieren, die eine einzelne Anweisung gehabt hätte .
  • Berücksichtigen Sie auch andere ? Variablen, die Sie möglicherweise in Ihrer Abfrage verwenden, die sich nicht auf die Eingangsliste beziehen (zB ? Variablen, die in einem ORDER BY verwendet werden Bedingung), sodass die Gesamtsumme Anzahl der Variablen überschreitet nicht das Limit.

Eine alternative Lösung ist die Verwendung temporärer Tabellen. Die Idee ist, eine temporäre Tabelle zu erstellen, die Abfragevariablen als Zeilen einzufügen und diese temporäre Tabelle dann in einer Unterabfrage zu verwenden, z. B.

DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)Code language: SQL (Structured Query Language) (sql)

Achten Sie auf die Typaffinität von SQLite

SQLite-Spalten sind nicht streng typisiert, und Konvertierungen erfolgen nicht unbedingt wie erwartet. Die von Ihnen bereitgestellten Typen sind nur Hinweise . SQLite speichert oft Daten von beliebigen geben Sie das Original ein Typ, und konvertieren Sie Daten nur dann in den Typ der Spalte, wenn die Konvertierung verlustfrei ist. Sie können zum Beispiel einfach ein "hello" einfügen Zeichenfolge in ein INTEGER Säule. SQLite wird sich nicht beschweren oder Sie vor Typkonflikten warnen. Umgekehrt erwarten Sie möglicherweise nicht, dass Daten von einem SELECT zurückgegeben werden Anweisung eines INTEGER Spalte ist immer ein INTEGER . Diese Typhinweise werden in der SQLite-Sprache als „Typaffinität“ bezeichnet, siehe hier. Achten Sie darauf, diesen Teil des SQLite-Handbuchs genau zu studieren, um die Bedeutung der Spaltentypen besser zu verstehen, die Sie beim Erstellen neuer Tabellen angeben.

Achten Sie auf große Ganzzahlen

SQLite unterstützt signiert 64-Bit-Ganzzahlen , die es speichern oder mit denen es Berechnungen durchführen kann. Mit anderen Worten, nur Zahlen von -2^63 zu (2^63) - 1 werden unterstützt, da zur Darstellung des Vorzeichens ein Bit benötigt wird!

Das heißt, wenn Sie erwarten, mit größeren Zahlen zu arbeiten, z. 128-Bit-Ganzzahlen (mit Vorzeichen) oder 64-Bit-Ganzzahlen ohne Vorzeichen, Sie müssen Wandeln Sie die Daten in Text um vor dem Einsetzen .

Der Horror beginnt, wenn Sie dies ignorieren und einfach größere Zahlen (als Ganzzahlen) einfügen. SQLite wird sich nicht beschweren und eine gerundete speichern Nummer statt! Wenn Sie beispielsweise 2^63 einfügen (was bereits außerhalb des unterstützten Bereichs liegt), wird SELECT ed-Wert ist 9223372036854776000 und nicht 2^63=9223372036854775808. Je nach verwendeter Programmiersprache und Bindungsbibliothek kann sich das Verhalten jedoch unterscheiden! Zum Beispiel prüft die sqlite3-Bindung von Python auf solche Integer-Überläufe!

Verwenden Sie nicht REPLACE() für Dateipfade

Stellen Sie sich vor, Sie speichern relative oder absolute Dateipfade in einem TEXT Spalte in SQLite, z.B. um Dateien im eigentlichen Dateisystem zu verfolgen. Hier ist ein Beispiel mit drei Zeilen:

foo/test.txt
foo/bar/
foo/bar/x.y

Angenommen, Sie möchten das Verzeichnis „foo“ in „xyz“ umbenennen. Welchen SQL-Befehl würden Sie verwenden? Dieses hier?

REPLACE(path_column, old_path, new_path) Code language: SQL (Structured Query Language) (sql)

Das habe ich getan, bis seltsame Dinge passierten. Das Problem mit REPLACE() ist, dass es alle ersetzen wird Vorkommnisse. Wenn es eine Zeile mit dem Pfad „foo/bar/foo/“ gab, dann REPLACE(column_name, 'foo/', 'xyz/') wird Chaos anrichten, da das Ergebnis nicht „xyz/bar/foo/“, sondern „xyz/bar/xyz/“ lautet.

Eine bessere Lösung ist so etwas wie

UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"Code language: SQL (Structured Query Language) (sql)

Die 4 spiegelt die Länge des alten Pfads wider (in diesem Fall ‚foo/‘). Beachten Sie, dass ich GLOB verwendet habe statt LIKE um nur die Zeilen zu aktualisieren, die beginnen mit ‚foo/‘.

Schlussfolgerung

SQLite ist eine fantastische Datenbank-Engine, bei der die meisten Befehle wie erwartet funktionieren. Bestimmte Feinheiten, wie die, die ich gerade vorgestellt habe, erfordern jedoch immer noch die Aufmerksamkeit eines Entwicklers. Lesen Sie zusätzlich zu diesem Artikel auch die offizielle SQLite-Dokumentation zu Vorbehalten.

Sind Sie in der Vergangenheit auf andere Vorbehalte gestoßen? Wenn ja, lass es mich in den Kommentaren wissen.