Menu

SQLite JSON 函数:json_extract、json_set 与 json_each

SQLite 如何存储和查询 JSON:提取字段、更新值、用 json_each 展开数组,以及给 JSON 路径建索引来提速。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

SQLite 没有 JSON 类型,这没关系

SQLite 没有专门的 JSON 列类型。JSON 存放在普通的 TEXT 列里,由一组内置函数(统称为 JSON1 扩展)负责解析、查询和修改。所有现代 SQLite 版本都自带 JSON1,所以什么都不用安装。

可以这样理解:把文档存成文本,用函数去查看里面的内容。

两行数据,每行都在一个普通文本列里存放一个 JSON 文档。现在我们需要办法深入这些文档内部。

用 json_extract 和 ->> 提取字段

json_extract(column, path) 从 JSON 文档中取出一个值。路径以 $(根)开头,用 .field 表示对象的键,用 [i] 表示数组下标。

到处写 json_extract(data, '$.name') 很快就会让人厌烦,所以 SQLite 提供了两个运算符:

  • -> 返回 JSON 编码的值(字符串会带着引号)。
  • ->> 返回 SQL 值(文本或数字,不带引号)。

name_json 返回的是 "Ada"(仍是 JSON),name_text 返回的是 Ada。要拿值做比较或显示时用 ->>。要把结果交给另一个 JSON 函数时用 ->。

按 JSON 字段过滤

能提取,就能过滤。表达式和其他条件一样写在 WHERE 子句中:

这样可以运行,但只要表有点规模就会很慢:为了判断条件,每一行都得解析一遍。稍后我们会用索引解决这个问题。

构建 JSON:json_object 和 json_array

反过来,你也可以在查询中构造 JSON:

json_object('k1', v1, 'k2', v2, ...) 构建一个对象。json_array(v1, v2, ...) 构建一个数组。直接在 SQL 中拼装 API 响应时它们很方便,而且可以随意嵌套:

更新 JSON:json_set、json_insert、json_replace

三个密切相关的函数可以修改 JSON 文档并返回新版本:

  • json_set(doc, path, value):设置路径的值,路径不存在就创建,存在就覆盖。
  • json_insert(doc, path, value):只在路径不存在时插入。
  • json_replace(doc, path, value):只在路径已存在时更新。

这些函数不会就地修改,而是返回一个新文档,通常你要用 UPDATE 把它写回去:

注意,json_set 一次调用就可以接收多组路径和值。要删除一个键,用 json_remove(doc, path)。

用 json_each 展开数组

json_each 是一个表值函数:它接收一个 JSON 数组(或对象),为每个元素返回一行。这样一来,“找出带有 admin 标签的用户”这种用普通 SQL 很别扭的需求,就变成了一个普通的连接:

users 中的每一行都会和它的 tags 数组的元素做连接。json_each 提供了一些有用的列,包括 key、value、type 和 fullkey。它的兄弟函数 json_tree 会递归遍历整个文档,包括每一个嵌套节点,适合在结构未知的文档中搜索。

给 JSON 字段建索引

上面的 WHERE data ->> '$.active' = 1 查询可以运行,但 SQLite 必须解析每一行才能判断条件。对于经常查询的字段,可以建一个表达式索引:

索引必须使用和查询完全相同的表达式。如果索引里用的是 json_extract(data, '$.email'),查询里用的却是 data ->> '$.email',两者就对不上,索引会闲置不用:选定一种写法并坚持下去。

对于需要频繁查询的字段,用生成列会更易读:

对写查询的人来说,email 就像一个普通列,但它会自动和 JSON 保持同步。

校验 JSON

json_valid(text) 在文本能被解析为 JSON 时返回 1,否则返回 0。配合 CHECK 约束,就能在写入时拒绝错误的数据:

第一条插入成功;第二条会因约束错误而失败。如果没有这个检查,格式错误的 JSON 会安静地躺在表里,直到几个月后某次 json_extract 调用突然报错。

JSON 与 JSONB

从 SQLite 3.45 开始,有了一种叫 JSONB 的二进制表示:数据相同,但预先解析成紧凑的二进制形式,这样函数就不用每次调用都重新解析。jsonb_* 系列函数(jsonb_extract、jsonb_set、jsonb_object 等)返回的是 JSONB 而不是文本,JSONB 列也可以用同样的运算符查询。

如果你希望文档在转储中便于阅读、方便检查,就用普通 JSON(文本)。当表很大、查询频繁,并且解析开销确实在性能分析中显现出来时,再考虑 JSONB。不要默认就切换:调试时,普通 JSON 的可读性价值很大。

什么时候适合用 JSON

JSON 列在这些情况下很出色:

  • 每行数据的结构各不相同(比如事件负载、审计日志、集成 webhook)。
  • 你在缓存外部 API 的响应,想完整保留它。
  • 某个字段很少被查询,几乎从不用来过滤。

在这些情况下就不太合适:

  • 你用 JSON 来逃避设计表结构。如果每一行都有相同的字段,那它们就应该是列。
  • 你需要经常按某个值过滤或连接。带索引的真实列每次都会比 JSON 路径查找更快。
  • 你本来会用到外键。JSON 没有关系完整性。

最理想的做法是两者结合:用标量列存放驱动查询和约束的字段,再加一个 JSON 列存放长尾的可变数据。

下一步:全文搜索

JSON 让你在存储方面更加灵活。下一页讲的是 FTS5,即 SQLite 的全文搜索引擎,它提供带排序和高亮的真正文本搜索,远远超出 LIKE 的能力。

常见问题

SQLite 是怎么存储 JSON 的?

SQLite 没有专门的 JSON 类型,JSON 以普通的 TEXT 存储。内置的 JSON1 扩展(从 3.38 起默认编译在内)提供了 json_extract、json_set 和 json_each 等函数,用来解析和操作这些文本。从 3.45 起,还有一种二进制的 JSONB 格式,适合需要反复访问的场景,速度更快。

怎么在 SQLite 中查询 JSON 列?

使用 json_extract(column, '$.path') 或简写的 ->> 运算符。例如,SELECT data ->> '$.name' FROM users 会从存放在 data 中的 JSON 文档里取出 name 字段。路径中 $ 表示根,.field 表示对象的键,[i] 表示数组下标。

SQLite 可以给 JSON 字段建索引吗?

可以:在提取出的路径上创建表达式索引:CREATE INDEX idx_user_email ON users(json_extract(data, '$.email'))。在 WHERE 子句中使用同一表达式的查询就会用到这个索引。对于经常查询的字段,用生成列加索引通常更清晰。

SQLite 中 -> 和 ->> 有什么区别?

-> 返回 JSON 值(仍然是 JSON 编码的,字符串会带引号),而 ->> 返回 SQL 值(文本或数字,不带引号)。需要原始值用于显示或比较时用 ->>;要继续进行 JSON 操作时用 ->。

Coddy 编程语言插图

和 Coddy 一起学编程

开始