Chapter 04 · SQLite’s Type System, Affinity, STRICT Tables, and Value Representation

Declared Types and Type Affinity Rules

Derive SQLite column affinity from declared type names, observe insertion-time conversions, reason about comparisons, and separate affinity from explicit CAST operations.

Beginner90–110 minutesAffinity matrix + comparison labSQLite 3.53.4 baselineSTRICT requires SQLite 3.37.0+Last reviewed: August 2026

Learning outcomes

Lesson 1 established that runtime values have storage classes. Now we add the other half of SQLite’s ordinary typing model: column affinity. An affinity is a preference SQLite derives from the declared type name. It can convert values during insertion and can influence comparisons, but it is not the same as a rigid constraint.

01

Derive INTEGER, TEXT, REAL, BLOB, or NUMERIC affinity from an ordinary column’s declared type using the documented ordered rules.

02

Predict the affinity of familiar-looking declarations such as BOOLEAN, DATE, VARCHAR, DECIMAL, and custom names.

03

Observe insertion-time conversions with typeof() rather than inferring them from displayed text.

04

Reason about small comparisons where TEXT and numeric affinity lead to different results.

05

Use CAST as an explicit expression conversion and distinguish it from persistent column affinity.

A declared type name is input to an ordered rule set

For ordinary non-STRICT tables, SQLite does not maintain a catalog of every SQL type name used by other products. Instead, it examines the text of the declared type and assigns one of five affinities. The rules are applied in order; the first match wins.

Rule orderDeclared type contains…Affinity
1INTINTEGER
2CHAR, CLOB, or TEXTTEXT
3BLOB, or no declared typeBLOB
4REAL, FLOA, or DOUBREAL
5Anything elseNUMERIC

The matching is based on substrings, not on a semantic dictionary. That is why unusual names can produce surprising affinities. The order also matters: a name containing both CHAR and INT receives INTEGER affinity because the INT rule is checked first.

BLOB affinity means “no preferred conversion”

In ordinary SQLite terminology, BLOB affinity is the fifth affinity category even though a BLOB-affinity column can store NULL, INTEGER, REAL, TEXT, or BLOB values. It does not force values to become BLOBs.

Familiar type names may not mean what they mean elsewhere

Type names such as BOOLEAN, DATE, and DECIMAL(10,2) are accepted in ordinary SQLite DDL, but accepting the spelling does not create dedicated Boolean, date, or exact-decimal storage classes. Their spelling falls through to NUMERIC affinity. VARCHAR contains CHAR, so it gets TEXT affinity.

Declared typeRule that matchesDerived affinityImportant consequence
BOOLEANNo earlier substring matchNUMERICThe name does not create a Boolean storage class.
DATENo earlier substring matchNUMERICThe name does not create a date storage class.
VARCHAR(40)Contains CHARTEXTNumeric inputs tend to be converted to text when possible.
DECIMAL(10,2)No earlier substring matchNUMERICNo fixed-point decimal representation is created by the declaration.
POINTContains INTINTEGERA custom-looking word accidentally triggers INTEGER affinity.
FIELDNOTE_CODENo earlier substring matchNUMERICCustom type names can silently receive NUMERIC affinity.
BLOBContains BLOBBLOBNo preferred storage-class conversion.
DOUBLE PRECISIONContains DOUBREALNumeric input tends toward REAL representation.

Inspect the declaration, then derive affinity yourself

PRAGMA table_xinfo reports the declared type string. It does not add a separate “affinity” column for you. Therefore an inspection tool or migration review must apply SQLite’s affinity rules to that declaration if it needs the derived affinity.

sql · metadata exposes declared names, not a ready-made affinity label
DROP TABLE IF EXISTS affinity_names;CREATE TABLE affinity_names (    as_boolean BOOLEAN,    as_date    DATE,    as_varchar VARCHAR(40),    as_decimal DECIMAL(10,2),    as_point   POINT,    as_custom  FIELDNOTE_CODE,    as_blob    BLOB,    as_double  DOUBLE PRECISION);PRAGMA table_xinfo('affinity_names');

Read each returned type value and run it through the ordered rules. This exercise is more durable than memorizing vendor-like names because the same technique works for arbitrary legacy schemas.

Affinity is applied when values are inserted

When TEXT that looks like a well-formed integer or real enters a NUMERIC- or INTEGER-affinity column, SQLite attempts to store it numerically. REAL affinity similarly prefers numeric representation, while TEXT affinity prefers text. NULL and BLOB values are not converted by these ordinary affinity rules.

sql · the same source text meets different affinities
DELETE FROM affinity_names;INSERT INTO affinity_names VALUES (    '00123', '00123', '00123', '00123',    '00123', '00123', '00123', '00123');SELECT    typeof(as_boolean), as_boolean,    typeof(as_varchar), as_varchar,    typeof(as_decimal), as_decimal,    typeof(as_point),   as_point,    typeof(as_blob),    as_blob,    typeof(as_double),  as_doubleFROM affinity_names;

Expected logical behavior: BOOLEAN, DECIMAL, POINT, and the custom FIELDNOTE_CODE declaration all prefer numeric storage and turn '00123' into INTEGER 123. VARCHAR preserves TEXT 00123. BLOB affinity leaves the incoming TEXT value as TEXT. DOUBLE PRECISION prefers REAL and stores a numeric representation such as 123.0. The leading zeros disappear wherever numeric conversion succeeds.

NUMERIC affinity can choose INTEGER or REAL after parsing text

NUMERIC affinity is not synonymous with REAL. If text represents an integer exactly, SQLite prefers INTEGER. If it represents a non-integer real value, SQLite uses REAL. This is observable with scientific notation too.

sql · NUMERIC affinity is conversion logic, not a storage class
DROP TABLE IF EXISTS numeric_probe;CREATE TABLE numeric_probe (value NUMERIC);INSERT INTO numeric_probe(value) VALUES    ('42'),    ('42.5'),    ('3.0e+5'),    ('not-a-number');SELECT rowid, quote(value), typeof(value)FROM numeric_probeORDER BY rowid;

Expected storage classes are INTEGER for '42', REAL for '42.5', INTEGER for '3.0e+5' because 300000 is exactly integral, and TEXT for 'not-a-number' because numeric conversion is not available. This is one reason a DECIMAL-looking declaration does not by itself guarantee fixed decimal scale.

Comparisons can apply affinity too

Affinity is not only an insertion concept. Before some comparisons, SQLite may apply an operand’s affinity to the other operand. Keep the example small: one column has TEXT affinity, one has NUMERIC affinity, and both receive the source text '2'.

sql · same characters, different comparison context
DROP TABLE IF EXISTS comparison_probe;CREATE TABLE comparison_probe (    text_value    TEXT,    numeric_value NUMERIC);INSERT INTO comparison_probe VALUES ('2', '2');SELECT text_value,       typeof(text_value),       text_value < 10 AS text_less_than_10,       text_value = 2  AS text_equals_2FROM comparison_probe;SELECT numeric_value,       typeof(numeric_value),       numeric_value < 10 AS numeric_less_than_10,       numeric_value = 2  AS numeric_equals_2FROM comparison_probe;

The TEXT-affinity value is stored as text. For text_value < 10, the numeric literal can be converted to text for the comparison, so lexical ordering makes '2' < '10' false. For equality, 2 can become text '2', so equality is true. The NUMERIC-affinity column stores INTEGER 2, making 2 < 10 and 2 = 2 both true.

Reasoning habit

Do not memorize isolated surprising comparisons. Ask: What storage class does each operand have? Does a column contribute affinity? What conversion rule is applied before comparison? Then verify with typeof() and a minimal query.

CAST is an explicit conversion on an expression

Column affinity is attached to a column and participates automatically in specific contexts. CAST(expression AS type-name) is different: it is an explicit SQL expression that requests conversion now and produces a converted result value.

sql · explicit conversion versus affinity
SELECT CAST('0042' AS INTEGER) AS n,       typeof(CAST('0042' AS INTEGER)) AS n_type;SELECT CAST(7.0 AS INT) AS as_int,       typeof(CAST(7.0 AS INT)) AS as_int_type,       CAST(7.0 AS NUMERIC) AS as_numeric,       typeof(CAST(7.0 AS NUMERIC)) AS as_numeric_type;

The first result is INTEGER 42. The second pair illustrates a documented distinction: casting 7.0 to an INT-like type produces INTEGER 7, while casting 7.0 to NUMERIC can preserve the REAL 7.0 representation. A CAST does not alter the table schema and does not change the affinity of a stored column.

Matrix lab: declaration, input, stored class, displayed value

Create a compact evidence table by inserting several source forms into columns with different affinities. Then reshape the observations with UNION ALL so each result row states the declaration you are testing.

sql · affinity observation matrix
DROP TABLE IF EXISTS affinity_lab;CREATE TABLE affinity_lab (    sample_id INTEGER PRIMARY KEY,    text_col    VARCHAR(20),    integer_col INTEGER,    numeric_col DECIMAL(10,2),    real_col    DOUBLE,    blob_col    BLOB);INSERT INTO affinity_lab VALUES    (1, '00123', '00123', '00123', '00123', '00123'),    (2, '12.50', '12.50', '12.50', '12.50', '12.50'),    (3, 'offline', 'offline', 'offline', 'offline', 'offline');SELECT sample_id, 'VARCHAR(20)' AS declaration,       typeof(text_col) AS stored_type, quote(text_col) AS valueFROM affinity_labUNION ALLSELECT sample_id, 'INTEGER', typeof(integer_col), quote(integer_col)FROM affinity_labUNION ALLSELECT sample_id, 'DECIMAL(10,2)', typeof(numeric_col), quote(numeric_col)FROM affinity_labUNION ALLSELECT sample_id, 'DOUBLE', typeof(real_col), quote(real_col)FROM affinity_labUNION ALLSELECT sample_id, 'BLOB', typeof(blob_col), quote(blob_col)FROM affinity_labORDER BY sample_id, declaration;

Before running the query, predict each typeof(). Then compare prediction with evidence. Record any mismatch in your notes and identify which ordered affinity rule or numeric conversion rule explains it.

Affinity checkpoint

Use the rules rather than product-name intuition.

  1. What affinity does BOOLEAN receive in an ordinary table?
  2. Why does VARCHAR(40) receive TEXT affinity?
  3. Why can POINT unexpectedly receive INTEGER affinity?
  4. Does DECIMAL(10,2) create an exact decimal storage class?
  5. How is CAST conceptually different from a column’s affinity?
Review the answers

BOOLEAN falls through to NUMERIC; VARCHAR contains CHAR; POINT contains the substring INT; DECIMAL also falls through to NUMERIC and creates no new storage class; and CAST explicitly converts an expression result rather than defining the persistent conversion preference of a column.

Failure patterns and safe corrections

FailureWhy it happensSafe correction
A designer assumes BOOLEAN is rigidly Boolean.Ordinary SQLite derives NUMERIC affinity from that name.Use an INTEGER/TEXT representation plus CHECK, or a STRICT table plus a domain CHECK.
A custom type POINT behaves numerically.Its spelling contains INT.Use ordinary SQLite type names intentionally; do not invent semantic type labels casually.
DECIMAL values lose textual scale.NUMERIC affinity converts numeric-looking text to INTEGER/REAL.Choose an exact representation strategy explicitly; Lesson 4 covers money.
A comparison gives a lexical result.TEXT affinity was applied in the comparison context.Inspect typeof(), derive affinity, and CAST only when an explicit conversion is semantically correct.
A migration checker compares only display output.Displayed 2 and 2.0 can hide different storage classes.Compare schema declarations and runtime typeof() where representation is part of the contract.

Summary and bridge to STRICT tables

Declared type names matter because they deterministically produce affinity. Affinity is a conversion preference, not a universal rigid constraint: values that cannot be converted may remain in another storage class. That flexibility is often useful, but some tables represent an application contract that should reject incompatible values. Lesson 3 applies strictness exactly where you choose it.

References

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.