PostgreSQL 自从支持 JSONB 到现在,已经有十余年,这十多年来,社区为 JSONB 提供了很多强大的功能。就我个人而言,其实最常用的还是匹配操作 @>
。
把JSON数据看作一个抽象语法树(AST)的话,这个操作符判断右参数是不是左参数的子图。
这里本来应该有个图示, 但是周末的时候临时有个数据集在处理,所以没有时间去找合适的工具了。简单举几个例子,下面这个例子得到true,这应该很好理解:
select '{"a": 1, "b": 2, "c": 3}'::jsonb @> '{"b":2}' ;
--------------
t
而它也可以匹配更复杂的情况,下面这个例子也是 true:
select '{"a": 1, "b": 2, "c": {"value": 3}}'::jsonb @> '{"c":{"value": 3}}';
?column?
----------
t
(1 row)
下面这个例子可能新用户会有点儿迷惑,但是其实也很好的契合了这个规则:
select '{"a": 1, "b": 2, "c": {"value": 3}}'::jsonb @> '{"c":{}}';
?column?
----------
t
(1 row)
但是应该注意的是,下面这个例子结果是 false:
select '{"a": 1, "b": 2, "c": {"value": 3}}'::jsonb @> '{"c":[]}';
?column?
----------
f
(1 row)
这也不难理解,{}
和 []
不相等。
下面这个例子比较有意思:
select '{"a": 1, "b": 2, "c": {"value": [1, 2, 3]}}'::jsonb @> '{"c":{"value": [2]}}';
?column?
----------
t
(1 row)
这里要注意的是,比较一个 JSON 数组是否匹配另一个时,它并不要求两个数组的顺序相等,只要右边是左边的真子集就可以:
select '{"a": 1, "b": 2, "c": {"value": [1, 2, 3]}}'::jsonb @> '{"c":{"value": [2]}}';
?column?
----------
t
(1 row)
select '{"a": 1, "b": 2, "c": {"value": [1, 2, 3]}}'::jsonb @> '{"c":{"value": [5, 2]}}';
?column?
----------
f
(1 row)
select '{"a": 1, "b": 2, "c": {"value": [1, 2, 3]}}'::jsonb @> '{"c":{"value": [3, 2]}}';
?column?
----------
t
(1 row)
这个规则契合了PostgreSQL的倒排索引,PostgreSQL的gin索引,JSONB 字段类型和匹配操作 @> 成为了一个非常有力的组合。在过去几年里,我习惯为一些重要的业务表加上一个类型为 JSONB 的meta 字段,并对其建立 gin 索引
create index idx_xxx_meta on xxx using(gin);
需要注意的是指定索引类型时的 create index 语法。
这样的设计可以解决很多传统上难以解决的问题,例如我可以给每个条目打上一个 tag 列表,取带有某几个 tag 的条目就是一个简单的匹配查询:
select xxx from data_table where meta @> '{"tags": ["tag1", "tagx", "tagy"]}'
因为有gin索引的帮助,这个搜索的性能足够常规的互联网应用所需。
甚至我的在 CSDN NLP 组的同事还挖掘出了新的用法。我们在一个存储树节点的表里,保存了一个 meta 字段,其中有一个 path 列表,存储当前字段在树中的路径,它的每一项都是 {"id": node_id, "title": something}
这样的结构,而我们搜索某一个节点下面的所有子节点,包括其隔代的子节点时,仅需要执行这样一个查询:
select xxx from tree_node where meta @> '{"path": [{"id": node_id}]}'
当然这个匹配操作也有它的限制,它在右边是左边的真子图的情况下才会匹配成功。例如我希望查找 tags 列表中包含我搜索项中的任何一个(即两者存在非空交集)的情况,用这种方法就不行了。此时我们需要另一个运算符 ?|
select '["tag1", "tag2", "tag3"]'::jsonb ?| '{tag2, tag3}';
?column?
----------
t
(1 row)
select '["tag1", "tag2", "tag3"]'::jsonb ?| '{tag2, tag3, tag5}';
?column?
----------
t
(1 row)
select '["tag1", "tag2", "tag3"]'::jsonb ?| '{tag5}';
?column?
----------
f
(1 row)
注意这几个例子,首先右边的运算符不再是jsonb,而必须是 text[]
,其次它其实是检查 key 值——也就是可以通过 gin 索引存储的值:
select '{"tag1":1, "tag2":2, "tag3":3}'::jsonb ?| '{tag5}';
?column?
----------
f
(1 row)
select '{"tag1":1, "tag2":2, "tag3":3}'::jsonb ?| '{tag3}';
?column?
----------
t
(1 row)
select '{"tag1":1, "tag2":2, "tag3":3}'::jsonb ?| '{tag3, tag1}';
?column?
----------
t
(1 row)
PostgreSQL 支持 JSON 和 JSONB 已经有十余年,每一个版本都在积极的增强其 JSON 数据处理能力,即使我近十年来的积极探索和学习,也没有全面的了解。这个交集运算也是近期在 NLP 组的工作过程中才注意到的。