Mọi CSDL quan hệ bạn từng dùng đều cưỡng chế kiểu cột: khai một cột INTEGER thì nó chỉ nhận số nguyên, chèn chuỗi vào là lỗi. SQLite thì không — và đây là một trong những đặc điểm gây bất ngờ nhất của nó. Kiểu trong SQLite là thuộc tính của giá trị, không phải của cột; một cột khai INTEGER vẫn vui vẻ chứa một chuỗi. Bài này đo cơ chế đó — gọi là kiểu động (manifest typing) và type affinity — bằng chính cách SQLite cất mỗi giá trị, trong container sqlite 3.45.3.
Kiểu đi theo giá trị, không theo cột
Tạo một cột khai INTEGER, rồi nhét vào đủ loại giá trị, và hỏi typeof() xem mỗi giá trị thực sự được lưu thành loại gì:
CREATE TABLE t(x INTEGER);
INSERT INTO t VALUES (42), ('xin chao'), (3.14), (x'00ff'), (NULL);
SELECT quote(x), typeof(x) FROM t;
giá trị lưu thành (storage class)
───────── ─────────
42 integer
'xin chao' text ← cột khai INTEGER vẫn chứa CHUỖI
3.14 real
X'00FF' blob
NULL null
Mỗi dòng lưu đúng loại của giá trị, bất kể cột khai gì. SQLite có đúng năm storage class: NULL, INTEGER, REAL, TEXT, BLOB — và typeof() cho biết một ô đang là loại nào. Cột không ép kiểu; nó chỉ gợi ý. Cái "gợi ý" đó tên là affinity.
Type affinity: cùng một giá trị, lưu khác nhau
Kiểu khai của cột cho nó một affinity — một xu hướng chuyển đổi giá trị khi chèn. Chèn cùng một chuỗi '123' vào năm cột có affinity khác nhau, rồi xem typeof:
CREATE TABLE aff(c_int INTEGER, c_text TEXT, c_real REAL, c_num NUMERIC, c_blob BLOB);
INSERT INTO aff VALUES ('123','123','123','123','123');
SELECT typeof(c_int), typeof(c_text), typeof(c_real), typeof(c_num), typeof(c_blob);
c_int c_text c_real c_num c_blob
integer text real integer text
Cùng chuỗi '123', năm kết quả khác nhau. Cột affinity INTEGER chuyển nó thành số nguyên 123; affinity TEXT giữ nguyên chuỗi; affinity REAL thành 123.0; NUMERIC thành integer; còn BLOB (không có affinity) giữ y nguyên chuỗi. Affinity quyết định dữ liệu của bạn được lưu thành gì — và đây là lý do đọc hiểu nó không phải chuyện học thuật.

Hình 1: Kiểu đi theo giá trị — một cột INTEGER chứa cả năm storage class (trên); và affinity của cột quyết định cùng chuỗi '123' được lưu thành integer, text hay real (dưới). Đo bằng typeof trong container sqlite 3.45.3.
Năm quy tắc affinity — và bẫy BOOLEAN, DATE
Affinity của một cột được suy ra từ chuỗi kiểu khai theo năm quy tắc, kiểm bằng cách chèn '7' vào và xem typeof:
kiểu khai affinity '7' lưu thành
───────────────────── ──────── ─────────────
INTEGER, INT, BIGINT INTEGER integer (chứa "INT")
VARCHAR, TEXT, CLOB TEXT text (chứa "CHAR/TEXT/CLOB")
REAL, DOUBLE, FLOAT REAL real (chứa "REAL/FLOA/DOUB")
BLOB, (không khai) BLOB text (giữ nguyên, không ép)
NUMERIC, DECIMAL NUMERIC integer (còn lại)
BOOLEAN, DATE NUMERIC integer ← BẪY
Bốn quy tắc đầu tìm chuỗi con trong tên kiểu (INT, CHAR/TEXT/CLOB, REAL/FLOA/DOUB, BLOB); không khớp cái nào thì rơi vào NUMERIC. Điều bất ngờ nằm ở dòng cuối: khai một cột BOOLEAN hay DATE thì nó nhận affinity NUMERIC và lưu '7' thành integer — vì SQLite không có kiểu boolean hay date thật sự. Boolean là integer 0/1; ngày tháng được quy ước lưu bằng TEXT ('2026-11-15'), số (unix time), hoặc real (Julian day) tùy bạn chọn. Khai DATE không tạo ra một kiểu ngày — nó chỉ là một nhãn mang affinity NUMERIC.
Hệ quả: bẫy so sánh, và cách ép kiểu thật
Vì kiểu đi theo giá trị, so sánh và sắp xếp phụ thuộc vào storage class thật. Một cột TEXT chứa các "số" sẽ so sánh theo thứ tự chuỗi, không phải giá trị:
cột TEXT chứa '2','10','9' → ORDER BY: 10, 2, 9 ('10' < '2' vì '1' < '2')
cột INTEGER chứa '2','10','9' → ORDER BY: 2, 9, 10 (đúng thứ tự số)
SELECT '10' < '2'; → 1 (chuỗi: đúng)
SELECT 10 < 2; → 0 (số: sai)
Lưu số vào cột affinity TEXT (hoặc không affinity) là một lỗi âm thầm: dữ liệu trông đúng nhưng sắp xếp và so sánh sai. Nếu bạn muốn SQLite cưỡng chế kiểu như CSDL khác, từ phiên bản 3.37 có bảng STRICT:
CREATE TABLE s(id INTEGER, ten TEXT) STRICT;
INSERT INTO s VALUES ('không phải số', 'x');
-- Error: cannot store TEXT value in INTEGER column s.id (19)
Bảng STRICT từ chối giá trị sai kiểu thay vì âm thầm chuyển hoặc lưu nhầm — cùng câu chèn đó vào bảng thường thì được nhận và lưu thành text. Với dữ liệu cần kỷ luật kiểu chặt, STRICT là lựa chọn nên dùng.

Hình 2: Năm quy tắc affinity với bẫy BOOLEAN/DATE (trên), và hệ quả — cột TEXT sắp xếp số sai cùng bảng STRICT cưỡng chế kiểu (dưới). Đo trong container sqlite 3.45.3.
Vì sao điều này quan trọng
Kiểu động là con dao hai lưỡi đặc trưng của SQLite. Nó cho sự linh hoạt tuyệt vời cho nguyên mẫu và dữ liệu không đồng nhất, nhưng cũng là nguồn của những lỗi âm thầm khó lần: một cột DATE không phải kiểu ngày, một cột TEXT chứa số sắp xếp sai, một phép so sánh cho kết quả tùy storage class. Hiểu affinity — và dùng STRICT khi cần kỷ luật — là cách biến đặc điểm này từ bẫy thành công cụ. Tới đây ta đã đi qua cách SQLite tổ chức, tra cứu, ghi an toàn và lưu kiểu dữ liệu. Phần sau quay lại chuyện vật lý của file: khi bạn xóa nhiều dữ liệu, file không tự nhỏ lại — các trang trống thành "freelist", và VACUUM mới thực sự thu hồi. Đo bằng chính kích thước file trước và sau.