STRICT-Tabellen in SQLite: wenn „drei“ nicht mehr in eine INTEGER-Spalte passt
SQLite nimmt in einer INTEGER-Spalte klaglos den Text „drei“ entgegen und speichert ihn als Text. Das ist kein Fehler, sondern Absicht – und trotzdem der Grund für Auswertungen, die still falsche Zahlen liefern. Ein Schlüsselwort schaltet es ab.
Das hier ist kein erfundenes Beispiel, sondern der Grund, warum ich diesen Beitrag schreibe:
CREATE TABLE lose (id INTEGER PRIMARY KEY, menge INTEGER);
INSERT INTO lose (menge) VALUES ('drei');
SELECT menge, typeof(menge) FROM lose;
-- drei | textKein Fehler, keine Warnung. In einer Spalte, die INTEGER heißt, steht Text. SQLite versucht zu konvertieren; klappt das verlustfrei ('42' → 42), wird konvertiert – klappt es nicht, wird der ursprüngliche Wert genommen. Das nennt sich type affinity und ist ausdrücklich so gebaut.
Der Ärger kommt später: SUM(menge) zählt den Textwert als 0, ORDER BY menge sortiert gemischt, und irgendwo in einer Auswertung steht eine Zahl, die um genau diesen einen Datensatz danebenliegt. Nichts stürzt ab, nichts meldet sich – die Zahl ist einfach falsch.
Ein Wort schaltet es ab
Seit SQLite 3.37.0 (November 2021) gibt es STRICT-Tabellen. Das Schlüsselwort steht hinter der schließenden Klammer:
CREATE TABLE lose (
id INTEGER PRIMARY KEY,
menge INTEGER NOT NULL,
notiz TEXT
) STRICT;
INSERT INTO lose (menge) VALUES ('drei');
-- Error: cannot store TEXT value in INTEGER column lose.menge
INSERT INTO lose (menge) VALUES ('42');
SELECT menge, typeof(menge) FROM lose;
-- 42 | integer ← verlustfrei konvertierbar, also erlaubtDer Unterschied ist genau der richtige: '42' wird weiterhin sauber zu einer Zahl, weil dabei nichts verloren geht. 'drei' wird abgewiesen, statt still als Text zu landen.
Was STRICT sonst noch verlangt
Jede Spalte braucht einen Typ. CREATE TABLE t (id INTEGER PRIMARY KEY, x) STRICT scheitert mit „missing datatype for t.x".
Nur sechs Typnamen sind erlaubt: INT, INTEGER, REAL, TEXT, BLOB und ANY. Alles andere fliegt raus – auch die aus anderen Datenbanken gewohnten:
CREATE TABLE t (id INTEGER PRIMARY KEY, name VARCHAR(50)) STRICT;
-- Error: unknown datatype for t.name: "VARCHAR(50)"Das wirkt zunächst wie ein Rückschritt, ist aber ehrlicher: VARCHAR(50) hat in SQLite noch nie auf 50 Zeichen begrenzt. Der Name hat eine Prüfung versprochen, die es nicht gab. Wenn du eine Längenbegrenzung willst, schreib sie hin, wo sie auch greift:
name TEXT NOT NULL CHECK (length(name) <= 50)ANY ist der Notausgang. Eine Spalte mit ANY nimmt in einer STRICT-Tabelle weiterhin alles an, ganz ohne Umwandlung – praktisch für ein Feld, in dem tatsächlich Verschiedenes liegt.
Jede PRIMARY-KEY-Spalte ist automatisch NOT NULL. In gewöhnlichen Tabellen ist das aus historischen Gründen nicht so, und auch das ist eine dieser Fallen, die man einmal im Leben sucht.
Die Falle beim Umstellen
INTEGER PRIMARY KEY ist ein Alias für die interne rowid. INT PRIMARY KEY ist es nicht – obwohl beide Typnamen erlaubt sind und fast gleich aussehen. Wer beim Aufräumen aus Versehen das kürzere schreibt, bekommt eine Tabelle, die sich anders verhält als vorher. Schreib INTEGER aus.
Und: STRICT gilt pro Tabelle, nicht pro Datenbank. Du kannst also neue Tabellen streng anlegen und bestehende erst einmal lassen. Ein PRAGMA integrity_check prüft in STRICT-Tabellen zusätzlich die Typen des vorhandenen Inhalts – ein guter erster Schritt, bevor du eine alte Tabelle umziehst.
Lohnt sich das?
Für mich ja, und zwar ausnahmslos bei allem Neuen. Alle Projekte hinter bymw.de laufen auf SQLite – das CMS, die Wetter-App, die Spiel-Backends. Eine Datenbank, die einen falschen Wert zurückweist, statt ihn schweigend anzunehmen, erspart genau die Sorte Fehler, die man erst in einer Auswertung bemerkt und dann nicht mehr rückwärts auflösen kann.
Der Preis sind ein paar CHECK-Bedingungen mehr und der Verzicht auf Typnamen, die ohnehin nur Dekoration waren. Das ist ein guter Tausch.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
Wie viel mehr als gestern? Differenz zur Vorzeile mit LAG() in SQL
„Wie viele Besucher heute im Vergleich zu gestern?" – der Reflex ist ein Self-Join auf die Vorzeile. Mit der Fensterfunktion LAG() bekommst du den Wert der vorherigen Zeile direkt daneben, ohne die Tabelle ein zweites Mal anzufassen.
Vergleich zur Vorzeile mit LAG() – wie stark hat sich ein Wert verändert
„Wie viele Besucher mehr als gestern?" ist in SQL erstaunlich fummelig, wenn man es mit einem Self-Join löst. Mit der Fensterfunktion LAG() greifst du direkt auf die vorherige Zeile zu – und rechnest die Differenz in einer einzigen, lesbaren Abfrage.
RETURNING in SQLite – die geschriebene Zeile sofort zurückbekommen
Nach einem INSERT oder UPDATE noch einmal SELECTen, um zu sehen, was drinsteht? Muss nicht sein. RETURNING liefert die betroffenen Zeilen direkt aus dem schreibenden Statement – ohne zweite Abfrage und ohne die Lücke dazwischen.