SQLite 完全指南 / 12 - JSON 支持
12 - JSON 支持:json 函数、索引与部分更新
12.1 JSON 函数总览
SQLite 3.9.0+ 提供完整的 JSON 支持,3.38.0+ 引入 -> 和 ->> 操作符。
12.1.1 函数分类
| 类别 | 函数 | 说明 |
|---|
| 创建 | json() | 验证并规范化 JSON |
| json_object() | 创建 JSON 对象 |
| json_array() | 创建 JSON 数组 |
| 提取 | json_extract() | 提取指定路径的值 |
| -> | 提取 JSON 值(带引号) |
| ->> | 提取 JSON 值(纯文本) |
| 修改 | json_set() | 设置值(不存在则创建) |
| json_insert() | 仅插入不存在的路径 |
| json_replace() | 仅替换已存在的路径 |
| json_remove() | 删除路径 |
| json_patch() | 合并两个 JSON 对象 |
| 展开 | json_each() | 展开 JSON 数组/对象(一行) |
| json_tree() | 递归展开嵌套 JSON |
| 工具 | json_valid() | 验证 JSON 是否有效 |
| json_type() | 返回值的类型 |
| json_array_length() | 数组长度 |
| json_group_array() | 聚合为 JSON 数组 |
| json_group_object() | 聚合为 JSON 对象 |
| json_quote() | 将值转为 JSON 字面量 |
12.2 创建 JSON
-- json_object():键值对创建对象
SELECT json_object('name', '张三', 'age', 25, 'active', json(1));
-- {"name":"张三","age":25,"active":true}
-- json_array():创建数组
SELECT json_array(1, 'hello', NULL, 3.14, json('true'));
-- [1,"hello",null,3.14,true]
-- json():验证并规范化
SELECT json('{"name":"张三","age":25}');
-- {"name":"张三","age":25}
-- 嵌套结构
SELECT json_object(
'name', '张三',
'address', json_object('city', '北京', 'zip', '100000'),
'hobbies', json_array('阅读', '编程', '旅行')
);
-- {"name":"张三","address":{"city":"北京","zip":"100000"},"hobbies":["阅读","编程","旅行"]}
-- json_quote():将值转为 JSON 字面量
SELECT json_quote('hello'); -- "hello"
SELECT json_quote(42); -- 42
SELECT json_quote(NULL); -- null
12.3 提取 JSON 值
SET @data = '{"name":"张三","age":25,"address":{"city":"北京"},"hobbies":["阅读","编程"]}';
-- 基本提取
SELECT json_extract('{"name":"张三","age":25}', '$.name'); -- 张三
SELECT json_extract('{"name":"张三","age":25}', '$.age'); -- 25
-- 嵌套路径
SELECT json_extract('{"address":{"city":"北京"}}', '$.address.city'); -- 北京
-- 数组索引
SELECT json_extract('["a","b","c"]', '$[0]'); -- a
SELECT json_extract('["a","b","c"]', '$[2]'); -- c
SELECT json_extract('["a","b","c"]', '$[#-1]'); -- c(最后一个)
-- 多路径提取(返回数组)
SELECT json_extract('{"a":1,"b":2,"c":3}', '$.a', '$.c'); -- [1,3]
-- 通配符(数组)
SELECT json_extract('[1,2,3,4]', '$[*]'); -- [1,2,3,4]
-- 路径语法
-- $.key — 对象成员
-- $[0] — 数组元素
-- $[#] — 数组长度
-- $[#-1] — 最后一个元素
-- $.a.b.c — 嵌套路径
-- $[*] — 所有元素
-- $.key[*] — 对象成员的所有子元素
12.3.2 -> 和 -» 操作符
-- -> 返回 JSON 值(保留类型和引号)
SELECT '{"name":"张三","age":25}' -> '$.name'; -- "张三"
SELECT '{"name":"张三","age":25}' -> '$.age'; -- 25
-- ->> 返回 SQL 值(去除引号)
SELECT '{"name":"张三","age":25}' ->> '$.name'; -- 张三
SELECT '{"name":"张三","age":25}' ->> '$.age'; -- 25
-- 实际使用对比
-- -> 适合嵌套 JSON 或继续使用 json 函数
-- ->> 适合直接比较和显示
SELECT * FROM products
WHERE info ->> '$.category' = '电子产品'
AND (info ->> '$.price')::REAL > 100;
12.4 修改 JSON
-- json_set():设置值(不存在则创建,存在则更新)
SELECT json_set('{"a":1}', '$.b', 2); -- {"a":1,"b":2}
SELECT json_set('{"a":1}', '$.a', 99); -- {"a":99}
SELECT json_set('{}', '$.name', '张三', '$.age', 25); -- {"name":"张三","age":25}
-- json_insert():仅在路径不存在时插入
SELECT json_insert('{"a":1}', '$.b', 2); -- {"a":1,"b":2}
SELECT json_insert('{"a":1}', '$.a', 99); -- {"a":1}(已存在,不修改)
-- json_replace():仅在路径存在时替换
SELECT json_replace('{"a":1}', '$.b', 2); -- {"a":1}(不存在,不创建)
SELECT json_replace('{"a":1}', '$.a', 99); -- {"a":99}
-- json_remove():删除路径
SELECT json_remove('{"a":1,"b":2,"c":3}', '$.b'); -- {"a":1,"c":3}
SELECT json_remove('["a","b","c"]', '$[1]'); -- ["a","c"]
-- json_patch():合并 JSON 对象
SELECT json_patch('{"a":1,"b":2}', '{"b":3,"c":4}'); -- {"a":1,"b":3,"c":4}
-- 第二个对象覆盖第一个的同名键
12.5 JSON 表函数
12.5.1 json_each()
-- 展开 JSON 数组
SELECT * FROM json_each('["苹果","香蕉","橙子"]');
-- key value type atom id parent fullkey path
-- 0 苹果 text 苹果 1 0 $[0] $
-- 1 香蕉 text 香蕉 3 0 $[1] $
-- 2 橙子 text 橙子 5 0 $[2] $
-- 展开 JSON 对象
SELECT * FROM json_each('{"name":"张三","age":25}');
-- key value type atom id parent fullkey path
-- name 张三 text 张三 1 0 $.name $
-- age 25 integer 25 3 0 $.age $
-- 实际使用:数组转行
SELECT
key AS idx,
value AS item
FROM json_each('["苹果","香蕉","橙子"]');
-- 展开嵌套数组
CREATE TABLE orders (id INTEGER PRIMARY KEY, items TEXT);
INSERT INTO orders VALUES (1, '[{"product":"苹果","qty":2},{"product":"香蕉","qty":3}]');
SELECT
o.id,
je.value ->> '$.product' AS product,
je.value ->> '$.qty' AS quantity
FROM orders o, json_each(o.items) je;
12.5.2 json_tree()
-- 递归展开嵌套 JSON
SELECT * FROM json_tree('{"a":{"b":1},"c":[2,3]}');
-- 只查看特定路径的子树
SELECT * FROM json_tree('{"a":{"b":{"c":1}}}', '$.a');
12.6 JSON 聚合函数
-- json_group_array():将行聚合为 JSON 数组
CREATE TABLE tags (article_id INTEGER, tag TEXT);
INSERT INTO tags VALUES (1, 'SQLite'), (1, '数据库'), (1, '教程');
INSERT INTO tags VALUES (2, 'Python'), (2, '编程');
SELECT article_id, json_group_array(tag) AS tags
FROM tags
GROUP BY article_id;
-- 1 ["SQLite","数据库","教程"]
-- 2 ["Python","编程"]
-- json_group_object():将行聚合为 JSON 对象
CREATE TABLE attrs (item_id TEXT, key TEXT, value TEXT);
INSERT INTO attrs VALUES ('item1', 'color', 'red');
INSERT INTO attrs VALUES ('item1', 'size', 'large');
INSERT INTO attrs VALUES ('item2', 'color', 'blue');
SELECT item_id, json_group_object(key, value) AS attrs
FROM attrs
GROUP BY item_id;
-- item1 {"color":"red","size":"large"}
-- item2 {"color":"blue"}
-- 组合使用
SELECT json_object(
'id', id,
'name', name,
'tags', (SELECT json_group_array(tag) FROM tags WHERE article_id = a.id)
) AS article_json
FROM articles a;
12.7 JSON 索引
12.7.1 表达式索引
-- 创建存储 JSON 的表
CREATE TABLE products (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL -- JSON 字符串
);
INSERT INTO products VALUES
(1, '{"name":"iPhone","category":"电子产品","price":5999}'),
(2, '{"name":"MacBook","category":"电子产品","price":12999}'),
(3, '{"name":"T恤","category":"服装","price":99}');
-- 为 JSON 字段创建索引
CREATE INDEX idx_products_category ON products(json_extract(data, '$.category'));
CREATE INDEX idx_products_price ON products(json_extract(data, '$.price'));
-- 使用索引查询
EXPLAIN QUERY PLAN
SELECT * FROM products
WHERE json_extract(data, '$.category') = '电子产品';
-- SEARCH TABLE products USING INDEX idx_products_category
-- 复合表达式索引
CREATE INDEX idx_products_cat_price ON products(
json_extract(data, '$.category'),
json_extract(data, '$.price')
);
12.7.2 JSON 部分索引
-- 只为电子产品创建价格索引
CREATE INDEX idx_electronics_price ON products(
json_extract(data, '$.price')
) WHERE json_extract(data, '$.category') = '电子产品';
12.7.3 -» 操作符索引
-- 使用 ->> 操作符创建索引
CREATE INDEX idx_products_name ON products(data ->> '$.name');
-- 查询
SELECT * FROM products WHERE data ->> '$.name' = 'iPhone';
12.8 JSON 实战模式
12.8.1 EAV 模式(实体-属性-值)
-- 使用 JSON 替代传统的 EAV 表
CREATE TABLE entities (
id INTEGER PRIMARY KEY,
type TEXT NOT NULL,
attributes TEXT NOT NULL -- JSON
);
INSERT INTO entities VALUES
(1, 'user', '{"name":"张三","age":25,"city":"北京"}'),
(2, 'user', '{"name":"李四","age":30,"city":"上海","phone":"13800138000"}'),
(3, 'product', '{"name":"iPhone","price":5999}');
-- 查询:用户的城市
SELECT id, attributes ->> '$.city' AS city
FROM entities WHERE type = 'user';
-- 筛选:年龄大于 25 的用户
SELECT * FROM entities
WHERE type = 'user' AND CAST(attributes ->> '$.age' AS INTEGER) > 25;
12.8.2 动态表单数据
CREATE TABLE form_submissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
form_name TEXT NOT NULL,
submitted_at TEXT DEFAULT (datetime('now')),
data TEXT NOT NULL -- JSON 表单数据
);
INSERT INTO form_submissions (form_name, data) VALUES
('registration', '{"name":"张三","email":"zs@example.com","plan":"pro"}'),
('registration', '{"name":"李四","email":"ls@example.com","plan":"free"}');
-- 查询特定计划的注册
SELECT * FROM form_submissions
WHERE form_name = 'registration'
AND data ->> '$.plan' = 'pro';
12.8.3 配置存储
CREATE TABLE app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO app_settings VALUES
('theme', '{"mode":"dark","primary":"#1976D2","font_size":14}'),
('notifications', '{"email":true,"push":false,"sms":true}');
-- 更新单个配置项
UPDATE app_settings
SET value = json_set(value, '$.font_size', 16)
WHERE key = 'theme';
-- 查询配置
SELECT value ->> '$.mode' FROM app_settings WHERE key = 'theme';
12.9 性能注意事项
12.9.1 JSON 函数的开销
| 操作 | 开销 | 说明 |
|---|
json_extract() | 中等 | 每次调用需解析 JSON |
->> | 中等 | 与 json_extract 类似 |
json_each() | 较高 | 需要解析并展开整个 JSON |
json_set() | 较高 | 需要解析、修改、重新序列化 |
| 表达式索引 | 查询快 | 索引避免了重复解析 |
12.9.2 何时使用 JSON vs 关系列
| 场景 | 推荐方式 |
|---|
| 需要查询/过滤的字段 | 关系列 |
| 需要 JOIN 的字段 | 关系列 |
| 灵活/动态属性 | JSON |
| 配置数据 | JSON |
| 日志/审计附加信息 | JSON |
| 需要外键约束的字段 | 关系列 |
| 数组类型数据 | JSON |
⚠️ 注意事项
- JSON 值以 TEXT 形式存储——数值提取后需要 CAST 转换
- 路径是大小写敏感的——
$.Name 和 $.name 是不同的路径 json_extract 返回 NULL 时路径不存在——不是错误- 大 JSON 的频繁解析会影响性能——考虑使用表达式索引
-> 返回 JSON 格式——比较时需要注意引号差异- JSON 数组索引从 0 开始——
$[0] 是第一个元素
💡 技巧
->> 用于 WHERE 和显示——返回纯文本值-> 用于嵌套提取——返回有效的 JSON 子文档json_each() + JOIN 可以将 JSON 数组转为关系数据- 表达式索引是 JSON 性能的关键——为经常查询的 JSON 字段创建索引
json_group_array() 可以将关系数据聚合为 JSON
📌 业务场景
场景一:商品属性系统
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL,
attrs TEXT DEFAULT '{}' -- JSON 动态属性
);
-- 创建属性索引
CREATE INDEX idx_products_brand ON products(json_extract(attrs, '$.brand'));
CREATE INDEX idx_products_color ON products(json_extract(attrs, '$.color'));
-- 插入不同类型的属性
INSERT INTO products (name, price, attrs) VALUES
('iPhone 15', 599900, '{"brand":"Apple","color":"黑色","storage":"256GB","5g":true}'),
('MacBook Pro', 1299900, '{"brand":"Apple","cpu":"M3","ram":"16GB","screen":"14寸"}');
-- 按品牌查询
SELECT * FROM products WHERE json_extract(attrs, '$.brand') = 'Apple';
-- 聚合属性
SELECT json_group_object(key, value)
FROM products, json_each(products.attrs)
WHERE id = 1;
场景二:Webhook 事件存储
CREATE TABLE webhook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
processed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
-- 存储事件
INSERT INTO webhook_events (event_type, payload) VALUES
('order.created', '{"order_id":12345,"amount":9999,"user_id":42}'),
('user.updated', '{"user_id":42,"changes":{"email":"new@example.com"}}');
-- 处理特定事件
SELECT * FROM webhook_events
WHERE event_type = 'order.created'
AND payload ->> '$.amount' > 5000
AND processed = 0;
🔗 扩展阅读
📖 下一章:13 - 备份恢复 —— .backup、VACUUM、在线备份 API