SQLite 完全指南 / 06 - 数据类型
06 - 数据类型:动态类型、类型亲和性、JSON 与 BLOB
6.1 SQLite 的动态类型系统
与大多数关系数据库不同,SQLite 使用动态类型系统。值有类型,但列没有固定类型——任何列可以存储任何类型的值。
6.1.1 存储类(Storage Classes)
SQLite 中每个值都有以下五种存储类之一:
| 存储类 | 说明 | 示例 |
|---|---|---|
| NULL | 空值 | NULL |
| INTEGER | 有符号整数(1/2/3/4/6/8 字节) | 42, -1, 0 |
| REAL | 浮点数(8 字节 IEEE 754) | 3.14, -0.5 |
| TEXT | 文本字符串(UTF-8/UTF-16) | 'hello', '你好' |
| BLOB | 二进制大对象 | X'4F2B' |
-- 验证存储类
SELECT typeof(42); -- integer
SELECT typeof(3.14); -- real
SELECT typeof('hello'); -- text
SELECT typeof(NULL); -- null
SELECT typeof(X'DEADBEEF'); -- blob
-- 同一列可以存储不同类型的值
CREATE TABLE demo (value);
INSERT INTO demo VALUES (42);
INSERT INTO demo VALUES ('hello');
INSERT INTO demo VALUES (3.14);
INSERT INTO demo VALUES (NULL);
INSERT INTO demo VALUES (X'CAFE');
SELECT value, typeof(value) FROM demo;
6.1.2 INTEGER 的内部存储
SQLite 根据整数值的大小自动选择最紧凑的存储格式:
| 值范围 | 存储大小 |
|---|---|
| 0 | 0 字节(特殊优化) |
| -128 ~ 127 | 1 字节 |
| -32768 ~ 32767 | 2 字节 |
| -8388608 ~ 8388607 | 3 字节 |
| -2147483648 ~ 2147483647 | 4 字节 |
| -140737488355328 ~ 140737488355327 | 6 字节 |
| 其他 | 8 字节 |
-- 验证整数存储
CREATE TABLE int_test (val INTEGER);
INSERT INTO int_test VALUES (0), (127), (128), (32768), (2147483648);
SELECT val, typeof(val), length(CAST(val AS TEXT)) AS text_len
FROM int_test;
6.2 类型亲和性(Type Affinity)
虽然列没有固定类型,但 SQLite 通过"类型亲和性"来影响数据的存储方式。
6.2.1 亲和性规则
| # | 规则 | 亲和性 |
|---|---|---|
| 1 | 类型名包含 “INT” | INTEGER |
| 2 | 类型名包含 “CHAR”, “CLOB”, “TEXT” | TEXT |
| 3 | 类型名包含 “BLOB” 或未指定类型 | BLOB |
| 4 | 类型名包含 “REAL”, “FLOA”, “DOUB” | REAL |
| 5 | 其他(包括 “NUMERIC”, “DECIMAL”, “BOOLEAN”, “DATE”) | NUMERIC |
6.2.2 亲和性行为详解
CREATE TABLE affinity_demo (
a TEXT, -- TEXT 亲和性
b NUMERIC, -- NUMERIC 亲和性
c INTEGER, -- INTEGER 亲和性
d REAL, -- REAL 亲和性
e BLOB, -- BLOB 亲和性
f -- BLOB 亲和性(未指定)
);
-- 测试 1:插入整数值
INSERT INTO affinity_demo (a,b,c,d,e,f) VALUES (42, 42, 42, 42, 42, 42);
SELECT typeof(a), typeof(b), typeof(c), typeof(d), typeof(e), typeof(f)
FROM affinity_demo WHERE rowid = 1;
-- text|integer|integer|real|integer|integer
-- 测试 2:插入实数值
DELETE FROM affinity_demo;
INSERT INTO affinity_demo (a,b,c,d,e,f) VALUES (3.14, 3.14, 3.14, 3.14, 3.14, 3.14);
SELECT typeof(a), typeof(b), typeof(c), typeof(d), typeof(e), typeof(f)
FROM affinity_demo WHERE rowid = 1;
-- text|real|real|real|real|real
-- 测试 3:插入文本
DELETE FROM affinity_demo;
INSERT INTO affinity_demo (a,b,c,d,e,f) VALUES ('hello', 'hello', 'hello', 'hello', 'hello', 'hello');
SELECT typeof(a), typeof(b), typeof(c), typeof(d), typeof(e), typeof(f)
FROM affinity_demo WHERE rowid = 1;
-- text|text|text|text|text|text
6.2.3 NUMERIC 亲和性的特殊行为
NUMERIC 亲和性是最复杂的:
CREATE TABLE numeric_demo (val NUMERIC);
INSERT INTO numeric_demo VALUES ('3.14'); -- 存储为 REAL
INSERT INTO numeric_demo VALUES ('3'); -- 存储为 INTEGER
INSERT INTO numeric_demo VALUES ('hello'); -- 存储为 TEXT
INSERT INTO numeric_demo VALUES (3.0); -- 存储为 INTEGER(值为 3)
SELECT val, typeof(val) FROM numeric_demo;
| 输入值 | 存储为 | 说明 |
|---|---|---|
'3.14' | REAL | 可以无损转为 REAL |
'3' | INTEGER | 可以无损转为 INTEGER |
'3.0' | INTEGER | 可以无损转为 INTEGER(去掉小数点) |
'hello' | TEXT | 无法转为数字 |
3.0 | REAL → INTEGER | 值为 3 时转为 INTEGER |
6.3 常用数据类型选择
| 用途 | 推荐类型 | 说明 |
|---|---|---|
| 主键 | INTEGER PRIMARY KEY | rowid 等价,性能最佳 |
| 布尔 | INTEGER DEFAULT 0 | 0=false, 1=true |
| 日期/时间 | TEXT(ISO 8601) | '2025-01-15T12:00:00+08:00' |
| 时间戳 | INTEGER(Unix 时间戳) | 1737001200 |
| 金额 | INTEGER(以分为单位) | 避免浮点精度问题 |
| 大文本 | TEXT | 无大小限制(实用上限 1GB) |
| 文件/图片 | BLOB 或 TEXT(路径) | 小文件存 BLOB,大文件存路径 |
| JSON | TEXT | 配合 json_* 函数 |
| UUID | TEXT | '550e8400-e29b-41d4-a716-446655440000' |
| 枚举 | TEXT CHECK(...) | status TEXT CHECK(status IN ('a','b')) |
| IP 地址 | TEXT | '192.168.1.1' |
| 坐标 | REAL + REAL | latitude REAL, longitude REAL |
6.3.1 布尔值
SQLite 没有专门的布尔类型:
CREATE TABLE settings (
key TEXT PRIMARY KEY,
enabled INTEGER DEFAULT 0 CHECK(enabled IN (0, 1))
);
INSERT INTO settings VALUES ('dark_mode', 1);
INSERT INTO settings VALUES ('notifications', 0);
SELECT key, CASE enabled WHEN 1 THEN '开启' ELSE '关闭' END AS status
FROM settings;
6.3.2 日期与时间
-- 方式 1:TEXT(ISO 8601,推荐)
CREATE TABLE events_v1 (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
event_date TEXT NOT NULL -- '2025-06-15T14:30:00+08:00'
);
-- 可以直接比较和排序
SELECT * FROM events_v1 WHERE event_date > '2025-06-01';
-- 方式 2:INTEGER(Unix 时间戳)
CREATE TABLE events_v2 (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
event_time INTEGER NOT NULL -- Unix 秒级时间戳
);
-- 方式 3:REAL(Julian Day Number)
CREATE TABLE events_v3 (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
event_jd REAL NOT NULL -- Julian Day Number
);
| 方式 | 优点 | 缺点 |
|---|---|---|
| TEXT (ISO 8601) | 可读、可排序、时区信息 | 存储较大(约 26 字节) |
| INTEGER (Unix) | 紧凑、计算快 | 不可读、无时区信息 |
| REAL (Julian) | 可用内置日期函数 | 不直观 |
6.3.3 金额处理
-- ⚠️ 错误方式:使用 REAL(浮点精度问题)
CREATE TABLE orders_bad (price REAL);
INSERT INTO orders_bad VALUES (0.1 + 0.2);
SELECT price, price = 0.3 FROM orders_bad; -- 可能返回 0!
-- ✅ 正确方式 1:INTEGER(以分为单位)
CREATE TABLE orders_int (price_cents INTEGER);
INSERT INTO orders_int VALUES (1999); -- 19.99 元
SELECT price_cents / 100.0 AS price FROM orders_int;
-- ✅ 正确方式 2:TEXT(字符串存储)
CREATE TABLE orders_text (price TEXT);
INSERT INTO orders_text VALUES ('19.99');
6.4 BLOB 使用
6.4.1 存储二进制数据
-- 创建存储文件的表
CREATE TABLE files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
data BLOB NOT NULL,
size INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
-- 插入 BLOB 数据
-- 方式 1:十六进制字面量
INSERT INTO files (filename, mime_type, data, size)
VALUES ('test.bin', 'application/octet-stream', X'DEADBEEF', 4);
-- 方式 2:使用 zeroblob 预分配空间(适合大文件)
INSERT INTO files (filename, mime_type, data, size)
VALUES ('large.bin', 'application/octet-stream', zeroblob(1024*1024), 1048576);
-- 方式 3:使用 randomblob 生成随机数据
INSERT INTO files (filename, mime_type, data, size)
VALUES ('random.bin', 'application/octet-stream', randomblob(256), 256);
6.4.2 BLOB 大小限制
-- 默认最大 BLOB 大小:1 GB
PRAGMA max_blob_size; -- 1073741824
-- 设置最大 BLOB 大小
PRAGMA max_blob_size = 104857600; -- 100 MB
💡 建议:大于几 MB 的文件建议存储文件路径而非 BLOB 数据。
6.4.3 BLOB vs 路径存储
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| BLOB 存储 | 原子性备份、单一文件 | 数据库膨胀、性能下降 | 小图标、缩略图 |
| 路径存储 | 数据库小、性能好 | 需要管理文件系统 | 文档、视频、大图片 |
6.5 STRICT 表(SQLite 3.37.0+)
STRICT 模式强制类型检查,更接近传统数据库的行为:
CREATE TABLE strict_demo (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER,
score REAL,
data BLOB
) STRICT;
-- 以下操作会报错:
INSERT INTO strict_demo VALUES (1, '张三', '25', 95.5, NULL);
-- Error: cannot store TEXT value in INTEGER column
-- 必须使用正确的类型
INSERT INTO strict_demo VALUES (1, '张三', 25, 95.5, NULL); -- OK
-- STRICT 表允许的类型关键字
-- INTEGER, REAL, TEXT, BLOB, ANY
6.5.1 STRICT vs 普通表
| 特性 | 普通表 | STRICT 表 |
|---|---|---|
| 类型检查 | 不检查(仅亲和性) | 严格检查 |
| 存储类 | 5 种都可用 | 必须匹配声明的类型 |
| 隐式转换 | 自动转换 | 不转换(报错) |
ANY 类型 | — | 可声明为 ANY 接受任何类型 |
| 混合使用 | 可以混用 STRICT 和非 STRICT 列 | 通过 ANY 实现 |
-- 混合使用 STRICT 和 ANY
CREATE TABLE mixed (
id INTEGER PRIMARY KEY STRICT, -- 严格整数
metadata ANY -- 任意类型
) STRICT;
INSERT INTO mixed VALUES (1, '{"key": "value"}'); -- OK
INSERT INTO mixed VALUES (2, 42); -- OK
INSERT INTO mixed VALUES (3, X'DEAD'); -- OK
6.6 类型转换
6.6.1 隐式转换规则
-- TEXT → INTEGER/REAL(比较时自动转换)
SELECT '42' + 8; -- 50
SELECT '3.14' * 2; -- 6.28
-- INTEGER → REAL
SELECT typeof(42 + 0.5); -- real
-- 不能转换的保持原样
SELECT typeof('hello' + 1); -- integer('hello' 转为 0)
SELECT 'hello' + 1; -- 1
6.6.2 CAST 显式转换
SELECT CAST('42' AS INTEGER); -- 42
SELECT CAST('3.14' AS REAL); -- 3.14
SELECT CAST(42 AS TEXT); -- '42'
SELECT CAST('not a number' AS INTEGER);-- 0
SELECT CAST(NULL AS TEXT); -- NULL
-- CAST 在比较中的应用
SELECT * FROM users WHERE CAST(age AS REAL) > 18.5;
6.6.3 类型比较规则
比较不同类型的值时的规则:
- NULL 最小——任何值与 NULL 比较(除 IS NULL 外)返回 NULL
- INTEGER/REAL 之间——直接数值比较
- TEXT vs INTEGER/REAL——数值 < 文本
- TEXT vs TEXT——逐字节比较
- BLOB vs BLOB——逐字节比较
- BLOB vs TEXT——BLOB > TEXT
SELECT 1 < '2'; -- 1(TEXT 与 INTEGER 比较时 TEXT 转为数字)
SELECT '2' < '10'; -- 0(TEXT 比较,'2' > '1')
SELECT 1 < 'hello'; -- 1(不能转为数字,INTEGER < TEXT)
SELECT X'01' < 'a'; -- 1(BLOB > TEXT 的规则,但实际取决于具体实现)
6.7 表达式类型
-- typeof() 返回值的存储类
SELECT typeof(42); -- integer
SELECT typeof(42.0); -- real
SELECT typeof('42'); -- text
SELECT typeof(NULL); -- null
SELECT typeof(X'42'); -- blob
-- CASE 表达式的类型
SELECT typeof(CASE WHEN 1 THEN 42 ELSE 'hello' END); -- integer
SELECT typeof(CASE WHEN 0 THEN 42 ELSE 'hello' END); -- text
-- 聚合函数的返回类型
SELECT typeof(SUM(1, 2, 3)); -- integer
SELECT typeof(SUM(1, 2.5, 3)); -- real
SELECT typeof(GROUP_CONCAT('a','b')); -- text
⚠️ 注意事项
typeof()返回的是存储类——不是声明的列类型- 金额不要用 REAL——浮点精度问题会导致财务计算错误
- 布尔值用 INTEGER——0 为假,非 0 为真(但约定用 1)
- 日期用 TEXT (ISO 8601)——可直接排序和比较
- STRICT 表需要 SQLite 3.37.0+——低版本不支持
- BLOB 之间无法直接比较大小——只能用
memcmp语义
💡 技巧
- 使用
INTEGER PRIMARY KEY替代INTEGER PRIMARY KEY AUTOINCREMENT——除非需要严格单调递增 - 存储 UUID 时用 TEXT——SQLite 没有原生 UUID 类型
zeroblob()预分配空间——适合流式写入大 BLOBtypeof()函数在调试数据类型问题时非常有用- STRICT 表适合新项目——类型更安全,减少 bug
📌 业务场景
场景一:灵活的产品属性
使用 JSON 存储产品的动态属性:
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL, -- 以分为单位
attributes TEXT -- JSON
);
INSERT INTO products VALUES
(1, '笔记本电脑', 599900, '{"cpu":"i7","ram":"16GB","weight":"1.5kg"}'),
(2, '机械键盘', 39900, '{"switch":"青轴","layout":"87键"}');
SELECT name, json_extract(attributes, '$.cpu') AS cpu
FROM products WHERE json_extract(attributes, '$.cpu') IS NOT NULL;
场景二:审计日志
使用 TEXT 存储 ISO 8601 时间戳:
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
entity TEXT NOT NULL,
entity_id INTEGER,
old_value TEXT,
new_value TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO audit_log (action, entity, entity_id, old_value, new_value)
VALUES ('UPDATE', 'users', 1, '{"name":"张三"}', '{"name":"张三丰"}');
🔗 扩展阅读
📖 下一章:07 - 索引 —— B-Tree、部分索引、表达式索引、覆盖索引