《PostgreSQL 开发指南》第 31 篇 JSON 文档存储
JSON(JavaScript Object Notation、JavaScript 对象表示法)是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。JSON 易于阅读和编写,同时也方便机器解析和生成,并且能够有效地提升网络传输效率。在网络数据传输领域,JSON 已成为了 XML 强有力的替代者。
如果想要学习和了解 JSON,推荐 JSON 官方网站 。
JSON 数据类型
PostgreSQL 提供了两种 JSON 数据类型:JSON 以及 JSONB。这两种类型主要的区别在于数据存储格式,JSONB 使用二进制格式存储数据,更易于处理。
PostgreSQL 推荐优先选择 JSONB 数据类型。
下表描述了两种数据类型之间的区别:
功能 | JSON | JSONB |
---|---|---|
存储格式 | 字符串原文存储 | 解析后的二进制 |
全文索引 | 不支持 | 支持 |
保留空白符 | 保留 | 不保留 |
保留键的顺序 | 保留 | 不保留 |
保留重复键 | 保留 | 不保留 |
由于存储格式的不同,JSONB 输入时稍微慢一些(需要转换),但是查询时快很多。
接下来的内容主要使用 JSONB 数据类型,但是大部分功能也可以使用 JSON 数据类型。
定义 JSON 字段
首先创建一个产品表 product:
CREATE TABLE product (
id INTEGER NOT NULL PRIMARY KEY,
product_name VARCHAR(100),
attributes JSONB
);
产品表 product 中包含一个 JSONB 类型的字段 attributes,用于存储产品的属性。
JSON 字段赋值
我们可以直接使用字符串为 JSON 字段赋值,但是要求数据必须是有效的 JSON 格式,否则将会返回错误。
执行以下语句插入一条产品记录:
INSERT INTO product (id, product_name, attributes)
VALUES (1, '椅子','{"color":"棕色", "material":"实木", "height":"60cm"}');
接下来我们插入一条不符合 JSON 格式的数据:
INSERT INTO product (id, product_name, attributes)
VALUES (2, '沙发椅', '"color":"白色:50cm}');
SQL 错误 [22P02]: ERROR: invalid input syntax for type json
详细:Expected end of input, but found ":".
位置:71
在位置:JSON data, line 1: "color":...
Error position: line: 2 pos: 70
以下语句插入了一条包含 JSON 数组的产品信息:
INSERT INTO product (id, product_name, attributes)
VALUES (
'桌子',
'{"color":"黑色", "material":"金属", "drawers":[{"side":"左侧", "height":"30cm"}, {"side":"右侧", "height":"40cm"}]}'
);
我们使用同样的方法再创建一条记录:
INSERT INTO product (id, product_name, attributes)
VALUES (
'茶几',
'{"color":"棕色", "material":["金属", "实木"]}'
);
以上方法虽然使用简单,但是输入比较麻烦。为此,PostgreSQL 提供了一些方便生产 JSON 数据的函数。
jsonb_build_object 函数可以通过一系列输入创建二进制的 JSON 对象,例如:
SELECT jsonb_build_object('color', '黑色', 'material', '塑料');
jsonb_build_object |
---------------------------------+
{"color": "黑色", "material": "塑料"}|
我们可以利用该函数插入数据,而不需要手动输入方括号、逗号、冒号等 JSON 符号。例如:
INSERT INTO product (id, product_name, attributes)
VALUES (4, '小型桌子', JSONB_BUILD_OBJECT('color', '黑色', 'material', '塑料'));
其他常用的构建 JSON 数据的函数如下:
- json_build_object
- to_json 以及 to_jsonb
- array_to_json
- row_to_json
- json_build_array 以及 jsonb_build_array
- json_object 以及 jsonb_object
查询 JSON 字段数据
JSON 字段的查询和普通字段没有什么区别,例如:
SELECT id, product_name, attributes
FROM product;
id|product_name|attributes |
--+------------+------------------------------------------------------------------------------------------------------------------+
1|椅子 |{"color": "棕色", "height": "60cm", "material": "实木"} |
2|桌子 |{"color": "黑色", "drawers": [{"side": "左侧", "height": "30cm"}, {"side": "右侧", "height": "40cm"}], "material": "金属"}|
3|茶几 |{"color": "棕色", "material": ["金属", "实木"]} |
4|小型桌子 |{"color": "黑色", "material": "塑料"} |
获取单个属性
我们不仅可以查询整个 JSON 字段,也可以提取 JSON 数据中指定节点的属性值。例如:
SELECT id, product_name, attributes -> 'color' AS color
FROM product;
id|product_name|color|
--+------------+-----+
1|椅子 |"棕色" |
2|桌子 |"黑色" |
3|茶几 |"棕色" |
4|小型桌子 |"黑色" |
运算符 -> 可以通过指定节点的键获取相应的数据。这种方法返回的数据仍然是 JSON 类型,使用双引号包含。
如果想要以字符串形式返回节点中的数据值,可以使用运算符 ->>。例如:
SELECT id, product_name, attributes -> 'color' AS color
FROM product;
id|product_name|color|
--+------------+-----+
1|椅子 |棕色 |
2|桌子 |黑色 |
3|茶几 |棕色 |
4|小型桌子 |黑色 |
如果查询的 JSON 节点不存在,将会返回空值:
SELECT id, product_name, attributes ->> 'height' AS height
FROM product;
id|product_name|height|
--+------------+------+
1|椅子 |60cm |
2|桌子 | |
3|茶几 | |
4|小型桌子 | |
获取数组属性
属性 drawers 是一个 JSON 数组,我们同样可以查询它的内容:
SELECT id, product_name, attributes ->> 'drawers' AS drawers
FROM product
WHERE id = 2;
id|product_name|drawers |
--+------------+--------------------------------------------------------------------+
2|桌子 |[{"side": "左侧", "height": "30cm"}, {"side": "右侧", "height": "40cm"}]|
属性 drawers 包含了 2 个元素,每个元素代表一个抽屉,每个抽屉都拥有 2 个属性。
如果我们想要查看属性 drawers 中的第一个元素,可以多次使用 -> 运算符:
SELECT id, product_name, attributes -> 'drawers' -> 0 AS drawer1
FROM product
WHERE id = 2;
id|product_name|drawer1 |
--+------------+--------------------------------+
2|桌子 |{"side": "左侧", "height": "30cm"}|
第一个 -> 运算符返回了 drawers 属性,第二个 -> 运算符返回了该属性中的第 1 个数组元素(数组下标从 0 开始)。
我们也可以使用另外两个运算符获取嵌套的属性,例如:
SELECT id, product_name,
attributes #> '{drawers, 1}' AS drawer1,
attributes #>> '{drawers, 1}' AS drawer1_text
FROM product
WHERE id = 2;
id|product_name|drawer1 |drawer1_text |
--+------------+--------------------------------+--------------------------------+
2|桌子 |{"side": "右侧", "height": "40cm"}|{"side": "右侧", "height": "40cm"}|
运算符 #> 以及 #>> 可以通过指定 JSON 节点的路径获取嵌套属性,路径可以包含键的名称或者数组元素下标,返回类型分别为 JSON 和字符串。
基于 JSON 数据的过滤
如果我们想要查看颜色为棕色、材料为实木、高度为 60cm 的椅子,可以尝试使用以下查询语句:
SELECT id, product_name, attributes
FROM product
WHERE attributes = '{"color":"棕色", "material":"实木", "height":"60cm"}';
id|product_name|attributes |
--+------------+---------------------------------------------------+
1|椅子 |{"color": "棕色", "height": "60cm", "material": "实木"}|
查询返回了我们期望的结果,因为查询条件中的字符串和 attributes 字段完全匹配,我们提供了完整的属性信息。
如果我们只想要基于某个属性(例如颜色)查找产品,这种方法就无法返回正确的数据了。例如:
SELECT id, product_name, attributes
FROM product
WHERE attributes = '{"color":"棕色"}';
id|product_name|attributes|
--+------------+----------+
这种情况下我们可以使用前文查询属性的方法,例如:
SELECT id, product_name, attributes
FROM product
WHERE attributes ->> 'color' = '棕色';
id|product_name|attributes |
--+------------+---------------------------------------------------+
1|椅子 |{"color": "棕色", "height": "60cm", "material": "实木"}|
3|茶几 |{"color": "棕色", "material": ["金属", "实木"]} |
我们使用了 ->> 运算符,而不是 -> 运算符,因为前者返回的是字符串类型,后者返回的则是 JSON 数据类型。
JSON 转换为数据行
PostgreSQL 支持将 JSON 字段转换为数据行格式。例如,jsonb_each 函数可以将每个键值对转换为一个记录:
SELECT id, product_name, jsonb_each(attributes)
FROM product;
id|product_name|jsonb_each |
--+------------+------------------------------------------------------------------------------------------------+
1|椅子 |(color,"""棕色""") |
1|椅子 |(height,"""60cm""") |
1|椅子 |(material,"""实木""") |
2|桌子 |(color,"""黑色""") |
2|桌子 |(drawers,"[{""side"": ""左侧"", ""height"": ""30cm""}, {""side"": ""右侧"", ""height"": ""40cm""}]")|
2|桌子 |(material,"""金属""") |
3|茶几 |(color,"""棕色""") |
3|茶几 |(material,"[""金属"", ""实木""]") |
4|小型桌子 |(color,"""黑色""") |
4|小型桌子 |(material,"""塑料""") |
与此类似的函数还有:
- jsonb_each_text
- json_each 以及 json_each_text
- json_array_elements 以及 json_array_elements_text
- jsonb_array_elements 以及 jsonb_array_elements_text
我们还可以使用 json_object_keys 或者 jsonb_object_keys 函数获取 JSON 字段中的所有键的名称:
SELECT id, product_name, jsonb_object_keys(attributes)
FROM product;
id|product_name|jsonb_object_keys|
--+------------+-----------------+
1|椅子 |color |
1|椅子 |height |
1|椅子 |material |
2|桌子 |color |
2|桌子 |drawers |
2|桌子 |material |
3|茶几 |color |
3|茶几 |material |
4|小型桌子 |color |
4|小型桌子 |material |
判断属性是否存在
PostgreSQL 还提供了一些用于判断 JSON 属性是否存在的运算符,例如 ? 运算符。
以下语句可以查找拥有 drawers 属性的产品:
SELECT id, product_name, attributes
FROM product
WHERE attributes ? 'drawers' = true;
id|product_name|attributes |
--+------------+------------------------------------------------------------------------------------------------------------------+
2|桌子 |{"color": "黑色", "drawers": [{"side": "左侧", "height": "30cm"}, {"side": "右侧", "height": "40cm"}], "material": "金属"}|
其他存在性运算符可以参考 官方文档 。
更新 JSON 字段数据
使用 UPDATE 语句更新 JSON 字段时,可以通过 || 运算符将新的键值增加到已有 JSON 数据。例如:
UPDATE product
SET attributes = attributes || '{"width":"100cm"}'
WHERE id = 1;
SELECT *
FROM product
WHERE id = 1;
id|product_name|attributes |
--+------------+---------------------------------------------------------------------+
1|椅子 |{"color": "棕色", "width": "100cm", "height": "60cm", "material": "实木"}|
新的属性 width 被添加到了数据的中间,因为 JSONB 数据类型不会保留键的顺序。
另外一种方法就是利用 jsonb_insert 方法,例如:
UPDATE product
SET attributes = jsonb_insert(attributes, '{"weight"}', '"1kg"')
WHERE id = 3;
SELECT *
FROM product
WHERE id = 3;
id|product_name|attributes |
--+------------+----------------------------------------------------------+
3|茶几 |{"color": "棕色", "weight": "1kg", "material": ["金属", "实木"]}|
如果想要更新已有键的数值,可以使用 jsonb_set 函数。例如:
UPDATE product
SET attributes = jsonb_set(attributes, '{height}', '"75cm"')
WHERE id = 1;
SELECT *
FROM product
WHERE id = 1;
id|product_name|attributes |
--+------------+---------------------------------------------------------------------+
1|椅子 |{"color": "棕色", "width": "100cm", "height": "75cm", "material": "实木"}|
以上语句将 id 等于 1 的产品的属性 height 修改为 75cm。
删除 JSON 字段数据
删除整个 JSON 字段数据可以简单地将其设置为 NULL,例如:
UPDATE product
SET attributes = NULL
WHERE id = 4;
SELECT *
FROM product
WHERE id = 4;
id|product_name|attributes|
--+------------+----------+
4|小型桌子 | |
删除 JSON 字段中的某个属性可以使用 - 运算符,例如:
UPDATE product
SET attributes = attributes - 'height'
WHERE id = 1;
SELECT *
FROM product
WHERE id = 1;
id|product_name|attributes |
--+------------+---------------------------------------------------+
1|椅子 |{"color": "棕色", "width": "100cm", "material": "实木"}|
产品 1 中的 height 属性已经被删除了。
另一种删除 JSON 属性的方法是利用 jsonb_set_lax 函数,例如
UPDATE product
SET attributes = jsonb_set_lax(attributes, '{"width"}', null, false, 'delete_key')
WHERE id = 1;
SELECT *
FROM product
WHERE id = 1;
id|product_name|attributes |
--+------------+---------------------------------+
1|椅子 |{"color": "棕色", "material": "实木"}|
函数中的第三个参数表示将 width 属性设置为空,第四个参数表示属性不存在时不创建新的属性,第五个参数表示删除被设置为空的属性。
全文索引
当我们基于 JSON 属性查询数据时,可能会存在性能问题。我们生成一批产品数据:
WITH RECURSIVE t AS (
SELECT 10 n, '产品'||10 product_name, '{"color": "棕色", "height": "60cm", "material": "实木"}' attr
UNION ALL
SELECT n+1, '产品'||n+1, '{"color": "棕色", "height": "60cm", "material": "实木"}'
FROM t WHERE t.n<10000
INSERT INTO product
SELECT n, product_name, attr::jsonb FROM t;
然后查看以下语句的执行计划:
EXPLAIN
SELECT id, product_name, attributes
FROM product
WHERE attributes @> '{"color":"黑色"}';
QUERY PLAN |
--------------------------------------------------------+
Seq Scan on product (cost=0.00..258.93 rows=1 width=78)|
Filter: (attributes @> '{"color": "黑色"}'::jsonb) |
以上语句表示查找拥有黑色的产品。执行计划显示使用了全表顺序扫描,数据量很小的时候没有问题。但是对于数据量大的表,查询速度可能会很慢。
为此,PostgreSQL 提供了支持 JSON 字段的全文索引,可以优化查询的性能。这种索引的类型为 GIN(通用倒排索引),通常用于搜索引擎。
我们可以基于 JSON 字段创建一个全文索引:
CREATE INDEX idx_product_attributes ON product USING GIN(attributes);
关键字 USING GIN 用于指定索引类型。
再次查看执行计划:
EXPLAIN
SELECT id, product_name, attributes
FROM product
WHERE attributes @> '{"color":"黑色"}';