- Wenn am Ende der Tabellendefinition
STRICT ergänzt wird, lassen sich Typfehler wie beliebiger Text in Integer-Spalten frühzeitig verhindern und so die Datenintegrität erhöhen
- Beim Einfügen und Aktualisieren werden Typen geprüft, Werte, die sich ohne Verlust umwandeln lassen, wie
'123', sind jedoch erlaubt; Spaltentypen sind auf INT, INTEGER, REAL, TEXT, BLOB, ANY beschränkt
- Für Spalten, die mehrere Typen enthalten müssen, kann
ANY angegeben werden, sodass strenge Validierung und flexible Speicherung in derselben Tabelle kombiniert werden können
- Bestehende Tabellen lassen sich nicht direkt in STRICT umwandeln; dafür sind das Erstellen einer neuen Tabelle und das Kopieren der Daten nötig, fehlerhafte Bestandsdaten müssen bereinigt oder typkonvertiert werden
- STRICT-Tabellen werden erst ab SQLite 3.37.0 unterstützt; es gibt zwar theoretischen Prüfaufwand, in inoffiziellen Experimenten wurden jedoch keine deutlichen Unterschiede bei Performance oder Dateigröße festgestellt
STRICT-Tabellen erstellen
- STRICT-Tabellen in SQLite wenden, ähnlich wie andere SQL-Engines, eine strengere Typprüfung an
- Dazu wird am Ende der
CREATE TABLE-Definition STRICT angehängt
CREATE TABLE people (name TEXT) STRICT;
Typprüfung beim Einfügen und Aktualisieren
- Normale SQLite-Tabellen können auch Text wie
'garbage' in einer INTEGER-Spalte speichern, STRICT-Tabellen behandeln eine Typabweichung jedoch als Fehler
CREATE TABLE people_nonstrict (age INTEGER);
INSERT INTO people_nonstrict (age) VALUES ('garbage');
-- 정상 처리됨
CREATE TABLE people_strict (age INTEGER) STRICT;
INSERT INTO people_strict (age) VALUES ('garbage');
-- 오류: cannot store TEXT value in INTEGER column
- Dieselbe Prüfung gilt auch für
UPDATE, sodass auch beim Ändern von Werten nach dem Speichern falsche Typen verhindert werden
- Wenn ein Wert jedoch ohne Verlust konvertiert werden kann, ist er auch in STRICT-Tabellen erlaubt
- Der String
'123' kann vollständig in den Integer 123 umgewandelt werden, daher werden die beiden folgenden Einfügungen gleich behandelt
INSERT INTO people_strict (age) VALUES ('123');
INSERT INTO people_strict (age) VALUES (123);
Typbeschränkungen bei der Tabellendefinition
- In normalen Tabellen können auch Typnamen in Spaltendeklarationen verwendet werden, die SQLite nicht unterstützt
GARBAGE, DATETIME, JSON, UUID und BLOBB werden alle akzeptiert
- Solche Deklarationen können Tippfehler sein oder aus einem Missverständnis der von SQLite unterstützten Datentypen entstehen
- STRICT-Tabellen lösen bereits beim Erstellen einen Fehler aus, wenn ein nicht unterstützter Typname verwendet wird
CREATE TABLE tbl (name GARBAGE) STRICT;
CREATE TABLE tbl (name DATETIME) STRICT;
CREATE TABLE tbl (name JSON) STRICT;
CREATE TABLE tbl (name UUID) STRICT;
CREATE TABLE tbl (name BLOBB) STRICT;
- Erlaubt sind nur die Spaltentypen
INT, INTEGER, REAL, TEXT, BLOB, ANY
- Für jede Spalte muss ein Typ angegeben werden;
CREATE TABLE tbl (name) ohne Typangabe ist daher nicht möglich
Flexibilität mit ANY beibehalten
- Wenn Daten mit uneinheitlichen Typen gespeichert werden müssen, kann eine
ANY-Spalte verwendet werden
- Auch innerhalb einer STRICT-Tabelle lassen sich in
ANY-Spalten alle Typen speichern, darunter Integer, Text, Real-Zahlen und BLOBs
CREATE TABLE tbl (value ANY) STRICT;
INSERT INTO tbl (value) VALUES (123);
INSERT INTO tbl (value) VALUES ('text');
INSERT INTO tbl (value) VALUES (12.34);
INSERT INTO tbl (value) VALUES (X'8647');
Migration bestehender Tabellen
- Es gibt keine Möglichkeit, eine bestehende nicht strikte Tabelle per
ALTER direkt in eine STRICT-Tabelle umzuwandeln; einfacher ist es daher, sie von Anfang an strikt zu erstellen
- Um eine bestehende Tabelle umzustellen, ist der Ablauf neue STRICT-Tabelle erstellen → Daten kopieren → bestehende Tabelle ersetzen nötig
CREATE TABLE new_people (name TEXT) STRICT;
INSERT INTO new_people SELECT * FROM people;
DROP TABLE people;
ALTER TABLE new_people RENAME TO people;
- Wenn in Integer-Spalten bestehender Daten etwa Text steht und die Typen damit falsch sind, kann beim Kopieren ein Fehler auftreten
- Vor der Migration müssen Daten gegebenenfalls bereinigt oder mit
CAST typkonvertiert werden
- Eine Strategie, STRICT nur auf neue Tabellen anzuwenden, ist ebenfalls möglich; dann unterscheidet sich jedoch die Strenge der Validierung je nach Tabelle, was das Verhalten schwerer vorhersehbar machen kann als bei ausschließlich lockeren Tabellen
Wann flexible Typisierung geeignet ist
- Die SQLite-Entwickler haben die Vorteile flexibler Typisierung separat zusammengefasst und nennen Fälle, in denen das Standardverhalten nützlich ist
- Nicht strikte Tabellen können für folgende Zwecke sinnvoll sein
- Ein reiner Key-Value-Store, der mehrere Typen unverändert aufnimmt
- Ein Bereich zum Speichern sonstiger Attribute mit unterschiedlichen Typen
- Wenn schmutzige CSV-Dateien direkt importiert werden und auch ungültige Werte ohne Verlust erhalten bleiben müssen
- Unerwartete Typen können jedoch subtile Bugs verursachen; bei gewöhnlichen Tabellen ist es daher vorteilhafter, sofort einen Fehler auszugeben, statt sie stillschweigend zu erlauben
- Im SQLite-Quellcode gibt es auch einen Kommentar, der nicht strikte Tabellen als
legacy bezeichnet, doch das taugt schwerlich als verlässlichere Grundlage als die offizielle Dokumentation
Versionskompatibilität
- STRICT-Tabellen wurden mit SQLite 3.37.0 eingeführt, das im November 2021 veröffentlicht wurde
- In älteren SQLite-Versionen können STRICT-Tabellen nicht verwendet werden
- Ältere Versionen können auch Datenbanken nicht lesen, die STRICT-Tabellen enthalten
- Wird mit einer aktuellen SQLite-Version eine STRICT-Tabelle erstellt und anschließend dieselbe Datenbank mit SQLite 3.36.0 geöffnet, tritt ein Fehler auf
Performance und Speicherplatz
- Da STRICT-Tabellen beim Einfügen und Aktualisieren Datentypen zusätzlich prüfen, können sie theoretisch langsamer sein
- In einem inoffiziellen Experiment, bei dem mehrere Millionen Zeilen in eine Tabelle mit 100 Spalten eingefügt wurden, zeigte sich auf mehreren Computern kein deutlicher Performance-Unterschied
- Auch die Größe der Datenbankdatei auf der Festplatte war gleich; da es sich aber nicht um einen gründlichen Benchmark handelte, könnten unentdeckte Unterschiede bestehen
- Es ist auch möglich, dass sich die Performance verbessert, weil versehentlich gespeicherte Werte verhindert werden, die nicht zur Spaltenaffinität (column affinity) passen; das wurde jedoch nicht separat überprüft
Worauf bei der Einführung zu achten ist
- STRICT-Tabellen lösen nicht alle Datenprobleme, reduzieren aber typbezogene Fehler und stärken die Datenintegrität
- Meist genügt es, der Tabellendefinition
STRICT hinzuzufügen, sodass die Einführung einfach ist
- Bei gewöhnlichen Tabellen überwiegen die Vorteile strenger Typvalidierung gegenüber dem Migrationsaufwand, der Inkompatibilität mit älteren Versionen und der eingeschränkten Flexibilität
1 Kommentare
Hacker-News-Kommentare
Da SQLite keine Funktion hat, um eine Tabelle per
ALTERin den Strict-Modus umzuwandeln und man die Daten einer nicht-strikten Tabelle stattdessen in eine strikte Tabelle kopieren muss, wurde in sqlite-utils 4.1 eine bidirektionale Konvertierungsfunktion ergänztIn der CLI kann man
uvx sqlite-utils transform data.db mytable --strictverwenden, in Pythondb.table("mytable").transform(strict=True)Die Release Notes gibt es unter https://sqlite-utils.datasette.io/en/stable/changelog.html#v..., die Python-API-Dokumentation unter https://sqlite-utils.datasette.io/en/stable/python-api.html#... und die CLI-Dokumentation unter https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
https://sqlite.org/flextypegood.html erklärt, warum flexible Typisierung der Standard ist und sich das wahrscheinlich auch künftig nicht ändern wird, aber das passt nicht zu praktischen Erfahrungen
Anders als die Erklärung nahelegt, dass man einen Kundennamen in
Customer.creditScorevom Typ Integer leicht entdecken und korrigieren könne, sind beschädigte Zeilen schwer wiederherzustellen und die Daten möglicherweise vollständig verlorenDer Logik, dass strenge Typprüfung nur leicht auffindbare Fehler verhindere und dadurch die Erkennung und Behebung von Bugs sogar erschwere, ist ebenfalls kaum zuzustimmen
In Postgres fügt man möglichst viele Prüfungen und Schutzmechanismen hinzu, damit man Fehler, die von vornherein nicht hätten auftreten dürfen, später gar nicht erst suchen muss
Das wirkt weniger wie ein strenges Engineering-Prinzip als wie ein Ergebnis der Zeit, in der SQLite entstand, und der starken Überzeugungen des Autors; nach viel Kritik entsteht der Eindruck, dass die Position um jeden Preis verteidigt wird
Mit Typen wie JSON oder HSTORE in Postgres lässt sich die gewünschte Flexibilität bieten, aber fast immer ist es besser, das bei Bedarf auszuwählen, statt unbegrenzte Typisierung standardmäßig zu erzwingen
STRICT als Standard wäre wünschenswert
Abgesehen davon sind die SQLite-Entwickler großartige Leute, die ein hervorragendes Werkzeug gebaut haben
PRAGMA foreign_keys = ON;verwenden mussDas größere Problem ist, dass es für strikte Tabellen kein entsprechendes pragma gibt und man daher an jedes
CREATE TABLEdas nicht standardisierteSTRICTanhängen mussEin globales STRICT-pragma wurde zwar erwogen, aber nicht umgesetzt; Näheres dazu unter https://sqlite.org/foreignkeys.html und https://sqlite.org/forum/forumpost/1b9d073a37ca5998
So soll vermieden werden, dass Software für SQLite 3.53 nach einem Upgrade auf 3.54 plötzlich mit
CREATE TABLEstrikte Tabellen erzeugt, Fehler auslöst und alles kaputtgehtMan fragt sich, ob SQLite zeitweise nur die Fremdschlüssel-Syntax unterstützt hat, ohne die Funktion tatsächlich zu implementieren
Es war überhaupt kein Vergnügen, Code zu bereinigen, der auf Tausenden von Geräten Strings
'1'und'0'in einer Booleschen Spalte gespeichert hatEs gibt auch keinen Timestamp-Typ, also muss man in einer Textspalte speichern, und selbst die Standard-Datums-/Zeitfunktionen erzeugen
yyyy-mm-dd HH:MM:SS, wobei UTC implizit angenommen wird, statt des ISO-Formatsyyyy-mm-ddTHH:MM:SSZSQLite ist wirklich ein großartiges Projekt, aber einige Designentscheidungen sind irritierend
Aus der Perspektive einer Enterprise-SQL-Umgebung habe ich SQLite nie ernsthaft in Betracht gezogen, weil Feldtypen standardmäßig nicht erzwungen werden, und selbst als es zur Grundlage für App-Metadaten auf Smartphones wurde, hat mich das überrascht.
Das erinnert an die alte Netzwerkgeschichte, in der man sich wegen niedriger Latenz und Einfachheit für UDP entscheidet und dann den Großteil der Zuverlässigkeitsfunktionen von TCP in der Anwendung selbst wieder nachbaut.
Aber es scheint nicht so, als entstünden vergleichbare Stärken, wenn man Typprüfungen selbst auf ein nicht-striktes SQLite aufsetzt.
Auch PostgreSQL ist in den Defaults auf schwache Systeme ausgelegt, sodass für optimale Leistung Konfiguration nötig ist.
Außerdem ist TCP streambasiert, daher haben viele Anwendungen Probleme mit HOL-Blocking; wenn man eine Zuverlässigkeitsschicht selbst über UDP baut, kann man nach viel Arbeit auch ein besseres Ergebnis als mit TCP erreichen.
Sie würden bemängeln, dass sie laut sind und sich schlecht fahren, aber sie beurteilen Produkte für einen anderen Einsatzzweck so, als wäre nur ihr eigener Use Case relevant.
Strikte Tabellen haben den Nachteil, dass sich einige Typen wie Date nicht verwenden lassen, sollten aber trotzdem der Standard sein.
Wenn mehrere Anwendungen sich eine Datenbank teilen, muss man deklarierten Typen vertrauen können; speichert eine davon Strings in einer Zahlenspalte, gehen alle anderen kaputt.
Andererseits wird die eingebettete Datenbank, der primäre Einsatzzweck von SQLite, meist nur von einer einzigen Anwendung verwendet; dort hat es auch Vorteile, das Schema weiterentwickeln zu können, statt eine neue Datenbank zu erzeugen und zu kopieren.
In diesem Fall weiß der Anwendungscode, was in jeder Spalte zu erwarten ist, auch bei gemischten Typen.
INTEGER,TEXT,BLOB,REAL,NUMERIC: https://sqlite.org/datatype3.htmlEXPLAINauf ein beliebigesSELECT-Query anzuwenden, um den formal angegebenen Typnamen zu erhalten.Deshalb lässt sich selbst bei Namen wie
DATEoderDATETIMEnicht einmal darauf schließen.Dateist kein echter Typ, sondern erzeugt nur eine Spalte mit numerischer Affinität.Der strikte Modus beschränkt die Schreibweise von Spaltentypen, sodass man keine aussagekräftigeren Namen verwenden kann, und verhindert damit eher ein strengeres Typsystem auf Anwendungsebene, weil der Code diese Namen beim Mapping zwischen Datenbanktypen und Anwendungstypen nicht nutzen kann.
Mehr dazu steht unter https://hn.algolia.com/?query=chrismorgan+strict+sqlite&type=comment, und wenn man mit Crates wie sqlx in Rust mit Datenbanken arbeitet, halte ich es für besser, den strikten Modus zu vermeiden.
Bei Datenbanken und RPC möchte ich strikte Typen, aber SQLite verfolgt einen etwas anderen Zweck; wenn man es direkt nutzt, versteht man vielleicht eher https://sqlite.org/flextypegood.html.
Das Ziel, dass auch irgendein beliebiges Skript, das nicht für SQLite geschrieben wurde, zufällig funktioniert, ist etwa eine Eigenschaft, die bei anderen DBMS normalerweise nicht mitgedacht wird.
Der Hauptvorteil flexibler Typisierung ist, dass sich Schemas leicht weiterentwickeln lassen.
Wenn sich die Anforderungen an eine eingebettete Datenbank ändern, kann man das Schema direkt an Ort und Stelle ändern, statt jedes Mal eine neue Datenbank zu erstellen und Daten zu migrieren; und wenn nur eine Anwendung liest und schreibt, ist es keine Überraschung, wenn zu einer Integer-Spalte auch Strings dazukommen.
Wenn dagegen mehrere Anwendungen denselben Datenbankbestand teilen und jeweils nach ihrem eigenen Zeitplan aktualisiert werden, dann ist das Schema ein Vertrag, und ein einzelner falscher Typ kann andere Anwendungen zerstören.
Wenn eine Anwendung eine Tabelle erstellt und eine andere sie nutzt, müssen die vereinbarten Datentypen strikt eingehalten werden.
Eine zusätzliche Prüfung zur Laufzeit bringt praktisch keinen Gewinn, und Code, bei dem die statische Analyse bestätigt hat, dass er einen String einfügt, mutiert nicht plötzlich dazu, einen Integer einzufügen.
SQLite ist nicht wie Postgres, wo mehrere Anwendungen Daten teilen und man Dritten vertrauen muss.
In einer Umgebung wie Postgres ist Laufzeitvalidierung essenziell, aber SQLite wurde grundsätzlich für eine Anwendung und eine Datenbank entworfen, daher muss man nur dem eigenen Code vertrauen, den man zur Compile-Zeit bewerten kann.
In dem untypischen Fall, dass mehrere Anwendungen eine Datei teilen, kann man strikte Tabellen aktivieren.
Dynamische Datentypen mögen für Zwecke wie einen einfachen Key-Value-Store passen, aber man sollte sich fragen, was eher dem Prinzip der geringsten Überraschung entspricht: dass
INTEGERohne Fehler'hello world'akzeptiert, oder dass man nur mit einem Keyword wieNONSTRICToder dem TypANYsolche Werte einfügen kann.Die meisten SQLite-Nutzer würden vermutlich nicht erwarten, dass Ersteres möglich ist.
Das passt auch zur Erklärung, dass Code für andere DBMS zufällig auch unter SQLite funktionieren soll.
Trotzdem ist es äußerst überraschend, dass ein Nutzer nicht etwa den Typ weggelassen hat, sondern explizit
INTEGERangibt und dennoch Text einfügen kann.Es gab einmal einen Fall, in dem eine UUID mit einem Wert wie
08123…begann und als Oktalzahl geparst wurde, wodurch ein Teil fälschlich in eine Zahl umgewandelt wurde.Das war verwirrend und lästig, ließ sich aber durch
STRICTund das komplette Neuerstellen der Tabelle beheben.SQLite kennt keinen Oktalzahl-Typ.
Standardmäßig
CREATE TABLE ... STRICT WITHOUT ROWIDverwenden; ich sehe keinen guten Grund, es anders zu machen