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.

Ảnh chụp sơ đồ nền tối minh họa kiểu động và type affinity trong container sqlite 3.45.3. Phần trên: một cột khai INTEGER chứa năm giá trị khác loại 42 integer, chuỗi xin chao text, 3.14 real, blob X00FF, NULL null, chú thích kiểu đi theo GIÁ TRỊ không theo cột, có đúng năm storage class NULL INTEGER REAL TEXT BLOB mà typeof cho biết. Phần dưới: chèn cùng chuỗi 123 vào năm cột khác affinity cho năm kết quả typeof khác nhau integer text real integer text, nhấn mạnh affinity quyết định giá trị được lưu thành gì

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.

Ảnh chụp sơ đồ nền tối hai phần đo trong container sqlite 3.45.3. Phần trên bảng năm quy tắc affinity: kiểu chứa INT thành INTEGER, chứa CHAR TEXT CLOB thành TEXT, chứa REAL FLOA DOUB thành REAL, BLOB hoặc không khai thành BLOB không ép, còn lại thành NUMERIC; dòng bẫy BOOLEAN và DATE đều thành NUMERIC lưu thành integer vì SQLite không có kiểu bool hay date thật. Phần dưới bẫy so sánh: cột TEXT chứa 2 10 9 sắp xếp ra 10 2 9 vì so chuỗi, cột INTEGER ra 2 9 10; chuỗi 10 nhỏ hơn 2 bằng 1 còn số 10 nhỏ hơn 2 bằng 0; và bảng STRICT từ 3.37 từ chối chèn TEXT vào cột INTEGER với lỗi 19 trong khi bảng thường nhận và lưu thành text

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.

Phần sau