强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 根据整数值的大小自动选择最紧凑的存储格式:

值范围存储大小
00 字节(特殊优化)
-128 ~ 1271 字节
-32768 ~ 327672 字节
-8388608 ~ 83886073 字节
-2147483648 ~ 21474836474 字节
-140737488355328 ~ 1407374883553276 字节
其他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.0REAL → INTEGER值为 3 时转为 INTEGER

6.3 常用数据类型选择

用途推荐类型说明
主键INTEGER PRIMARY KEYrowid 等价,性能最佳
布尔INTEGER DEFAULT 00=false, 1=true
日期/时间TEXT(ISO 8601)'2025-01-15T12:00:00+08:00'
时间戳INTEGER(Unix 时间戳)1737001200
金额INTEGER(以分为单位)避免浮点精度问题
大文本TEXT无大小限制(实用上限 1GB)
文件/图片BLOB 或 TEXT(路径)小文件存 BLOB,大文件存路径
JSONTEXT配合 json_* 函数
UUIDTEXT'550e8400-e29b-41d4-a716-446655440000'
枚举TEXT CHECK(...)status TEXT CHECK(status IN ('a','b'))
IP 地址TEXT'192.168.1.1'
坐标REAL + REALlatitude 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 类型比较规则

比较不同类型的值时的规则:

  1. NULL 最小——任何值与 NULL 比较(除 IS NULL 外)返回 NULL
  2. INTEGER/REAL 之间——直接数值比较
  3. TEXT vs INTEGER/REAL——数值 < 文本
  4. TEXT vs TEXT——逐字节比较
  5. BLOB vs BLOB——逐字节比较
  6. 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

⚠️ 注意事项

  1. typeof() 返回的是存储类——不是声明的列类型
  2. 金额不要用 REAL——浮点精度问题会导致财务计算错误
  3. 布尔值用 INTEGER——0 为假,非 0 为真(但约定用 1)
  4. 日期用 TEXT (ISO 8601)——可直接排序和比较
  5. STRICT 表需要 SQLite 3.37.0+——低版本不支持
  6. BLOB 之间无法直接比较大小——只能用 memcmp 语义

💡 技巧

  1. 使用 INTEGER PRIMARY KEY 替代 INTEGER PRIMARY KEY AUTOINCREMENT——除非需要严格单调递增
  2. 存储 UUID 时用 TEXT——SQLite 没有原生 UUID 类型
  3. zeroblob() 预分配空间——适合流式写入大 BLOB
  4. typeof() 函数在调试数据类型问题时非常有用
  5. 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、部分索引、表达式索引、覆盖索引