SQLite's Type System — Type Affinity, Not Strict Typing

SQLite

Chapter 3 · SQLite's Type System — Type Affinity, Not Strict Typing

This is the first item from sqlite1-1's own roadmap table, and it's genuinely the most surprising difference for anyone coming from mysql2/mysql3 or postgres1.

What "Type Affinity" Actually Means

MySQL and Postgres both use static, strict column typing — a column declared INTEGER can only ever store integer values (or NULL); inserting 'hello' into it raises a real, hard error at insert time.

SQLite works differently: it's dynamically typed at the value level, not the column level. A column's declared type is really just a hint — a type affinity — that SQLite uses to decide how to try to convert an incoming value, but it does not reject a value just because it doesn't match. A column declared INTEGER can still end up storing a text string, if SQLite's own conversion rules can't reasonably coerce the value — it's stored as-is, using whichever of SQLite's five actual storage classes (NULL, INTEGER, REAL, TEXT, BLOB) genuinely fits it.

There are five type affinities — TEXT, NUMERIC, INTEGER, REAL, BLOB — assigned to a column based on textual matching rules applied to its declared type (a declared type containing "INT" gets INTEGER affinity; "CHAR", "CLOB", or "TEXT" gets TEXT affinity, and so on).

A Concrete Demonstration

CREATE TABLE example (id INTEGER, quantity INTEGER);
INSERT INTO example VALUES (1, 'five');
SELECT * FROM example;
-- 1 | five

This succeeds in SQLite by default, even though 'five' is not a number — SQLite only tries to coerce a value toward its column's own affinity, and simply stores it as-is when coercion isn't reasonably possible, rather than rejecting the insert outright. The identical statement against a MySQL table in strict mode, or against a Postgres table, would fail immediately.

Why This Is a Real Design Trade-off, Not Just a Flaw

This isn't an accident or an oversight — it's documented, deliberate design, tracing directly back to sqlite1-1's own embedding material. SQLite was built as a general-purpose, embedded storage engine meant to work smoothly from dynamically-typed scripting languages, where a value of the "wrong" type shouldn't necessarily hard-fail an entire operation the way it might need to in a strict, statically-typed system serving many different, independently-written applications with different assumptions about the data.

This flexibility is genuinely useful for SQLite's own actual use cases — loosely-typed application data, rapid prototyping, embedded contexts where the application itself is the sole writer and already controls data quality directly. It's also, honestly, the single most commonly cited source of real bugs for anyone arriving from a strict-typing background. Both things are true at once — a real, double-edged design choice worth respecting rather than dismissing outright as a mistake.

STRICT Tables — The Modern Opt-In Fix

SQLite 3.37 (2021) added STRICT tables — an opt-in mechanism giving real, MySQL/Postgres-like enforcement to a specific table:

CREATE TABLE example (id INTEGER, quantity INTEGER) STRICT;
INSERT INTO example VALUES (1, 'five');
-- Error: cannot store TEXT value in INTEGER column quantity

It's worth naming explicitly that this is opt-in and per-table, not the default — most existing SQLite code, and most tutorials, still use the traditional, flexible typing described above. Understanding both modes matters, not just the newer, stricter one.

The Storage Classes Underneath

Regardless of a column's declared type or affinity, every value in SQLite ultimately has one of five actual storage classes: NULL, INTEGER, REAL, TEXT, or BLOB. Affinity influences which storage class a value ends up using — it doesn't guarantee a fixed class the way MySQL/Postgres's own strict types do.

Mixed storage classes produce genuinely surprising sort/comparison results
A column that ends up containing mixed storage classes — some rows genuinely numeric, some accidentally text, per this chapter's own demonstration — can produce surprising, inconsistent sorting and comparison behavior. SQLite orders different storage classes in a fixed sequence (NULL < INTEGER/REAL < TEXT < BLOB) rather than comparing mixed values as if they shared one type. This is the first genuinely SQLite-specific practical trap in this course — sqlite1-9 revisits it directly, per sqlite1-1's own roadmap.
STRICT tables are the direct fix for this warn-box's own gotcha
A STRICT table can never end up with mixed storage classes in a single column in the first place, since a value that doesn't genuinely match is rejected at insert time — closing the exact gap the warn-box above describes.

Hands-On Exercises

Exercise 1

Explain the difference between MySQL/Postgres's strict static column typing and SQLite's own type affinity system, using this chapter's own concrete INSERT example.

📄 View solution
Exercise 2

Explain the real, historical design reasoning behind SQLite's own flexible typing and its connection to being an embedded, general-purpose engine — why is this a genuine trade-off rather than simply a mistake?

📄 View solution
Exercise 3

Using this chapter's own warn-box, explain how a column containing mixed storage classes can produce surprising sort/comparison results, and explain how STRICT tables would prevent this specific problem from occurring in the first place.

📄 View solution

Chapter 3 Quick Reference

  • MySQL/Postgres — strict static column typing, rejects mismatched values at insert · SQLite (default) — type affinity, tries to coerce, stores as-is if it can't
  • Five type affinities (TEXT/NUMERIC/INTEGER/REAL/BLOB), five actual storage classes (NULL/INTEGER/REAL/TEXT/BLOB) — affinity influences, doesn't guarantee, the stored class
  • A real, documented design trade-off — flexible for embedded/scripting use, a real bug source for strict-typing backgrounds
  • STRICT tables (SQLite 3.37+, 2021) — opt-in, per-table, real MySQL/Postgres-like enforcement
  • Mixed storage classes in one column sort in a fixed NULL < INTEGER/REAL < TEXT < BLOB order — a real, common gotcha, revisited in sqlite1-9
  • Next chapter: Concurrency & Locking — contrasted with postgres1-9's own MVCC