JSON 数据类型用于存储按照 RFC 7159 规范定义的 JSON(JavaScript 对象表示法)数据。此类数据亦可存储为普通 text 文本,但 JSON 数据类型具备在写入时强制核验 JSON 语法合规性的优势,并提供丰富的 JSON 专属函数与操作符(参见 Section 9.16)。
PostgreSQL 提供了两种存储 JSON 数据的内置类型:json 与 jsonb。为了实现对这些类型的高效检索,系统还提供了在 Section 8.14.7 中描述的 jsonpath 路径表达式数据类型。
json 与 jsonb 几乎接受完全相同的数据输入集合。它们的核心实际差异在于存储与处理效率。json 类型原样存储输入文本的精确物理副本,处理函数在每次执行时必须重复重新解析;而 jsonb 以分解后的紧凑二进制格式存储,虽然写入时稍有解析转换开销,但后续读取与过滤处理速度大幅提升,无需任何重复解析。此外,jsonb 原生支持 GIN 倒排索引,具备巨大性能优势。
由于 json 原样保存输入文本,它会忠实保留标记间的空白符、对象内部键的原始出现顺序,以及键名重复的对象。而 jsonb 不保留语义无关的空白符,不保留键的原始输入顺序,并在对象存在重复键时仅保留最后出现的键的值。
在绝大多数业务应用中,应当优先选用 jsonb 存储 JSON 数据,除非业务上对保留源码文本格式或重复键有非常强烈的硬性诉求。
RFC 7159 规范指出 JSON 字符串应使用 UTF-8 编码。在非 UTF-8 编码的数据库中,包含非 ASCII 字符的 JSON 数据将无法直接严格符合规范。
RFC 7159 允许 JSON 字符串中包含形如 \u 的 Unicode 转义序列。在输入为 XXXXjson 时系统原样保存转义序列;而在输入为 jsonb 时系统会将转义序列转换为等价的 UTF-8 物理字符。jsonb 同样拒绝 \u0000 字符并严格校验 Unicode 代理对。
Section 9.16 中描述的许多 JSON 处理函数会将 Unicode 转义序列转换为数据库编码字符,如果字符在当前编码中无法表示则会报错。
将文本 JSON 输入转换为 jsonb 时,RFC 7159 描述的基本类型将精确映射为原生 PostgreSQL 类型,详见 Table 8.21。
相反地,如表中所注,对输入的 JSON 基本格式存在若干约束,均严格遵循 PostgreSQL 内核物理范围规范。
Table 8.21. JSON 基本类型与对应的 PostgreSQL 类型
| JSON primitive type | PostgreSQL type | Notes |
|---|---|---|
string | text | \u0000 is disallowed, as are Unicode escapes
representing characters not available in the database encoding |
number | numeric | NaN and infinity values are disallowed |
boolean | boolean | Only lowercase true and false spellings are accepted |
null | (none) | SQL NULL is a different concept |
JSON 数据类型的输入与输出语法严格遵循 RFC 7159 规范。
以下均为完全合法的 json(或 jsonb)表达式:
-- 简单标量/原始值
SELECT '5'::json;
-- 包含零个或多个元素的数组
SELECT '[1, 2, "foo", null]'::json;
-- 包含键值对的对象
SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json;
-- 深度嵌套的数组与对象
SELECT '{"foo": [true, "bar"], "tags": {"a": 1, "b": null}}'::json;
如前所述,当输入一个 JSON 值并在不进行额外处理的情况下直接打印时,json 原样返回输入字符串,而 jsonb 则不保留语义无关的空白符:
SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::json;
json
-------------------------------------------------
{"bar": "baz", "balance": 7.77, "active":false}
(1 row)
SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::jsonb;
jsonb
--------------------------------------------------
{"bar": "baz", "active": false, "balance": 7.77}
(1 row)
在 jsonb 中,数值将按照底层 numeric 类型的行为输出格式化:
SELECT '{"reading": 1.230e-5}'::json, '{"reading": 1.230e-5}'::jsonb;
json | jsonb
-----------------------+-------------------------
{"reading": 1.230e-5} | {"reading": 0.00001230}
(1 row)
然而 jsonb 会保留末尾小数零,尽管它们在逻辑等值判断中是不影响语义的。
关于用于构造与处理 JSON 值的内置函数与操作符完整清单,请参阅 Section 9.16。
将数据表示为 JSON 比传统关系数据模型具有极大的灵活性,在面对频繁变动且属性高度异构的数据模式时表现极佳。但在多数场景中,两种模型可以完美互补。
存储在表中的 JSON 数据受与其他数据类型相同的并发控制约束。将大型文档存储在单一单元格中可能会放大整行更新时的锁争用与 I/O 成本。
测试包含性(Containment)是 jsonb 的核心重要特性。json 类型不具备等价的包含性操作符。包含性用于测试一个 jsonb 文档是否完整包含了另一个文档的内容。
-- Simple scalar/primitive values contain only the identical value:
SELECT '"foo"'::jsonb @> '"foo"'::jsonb;
-- The array on the right side is contained within the one on the left:
SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb;
-- Order of array elements is not significant, so this is also true:
SELECT '[1, 2, 3]'::jsonb @> '[3, 1]'::jsonb;
-- Duplicate array elements don't matter either:
SELECT '[1, 2, 3]'::jsonb @> '[1, 2, 2]'::jsonb;
-- The object with a single pair on the right side is contained
-- within the object on the left side:
SELECT '{"product": "PostgreSQL", "version": 9.4, "jsonb": true}'::jsonb @> '{"version": 9.4}'::jsonb;
-- The array on the right side is not considered contained within the
-- array on the left, even though a similar array is nested within it:
SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb; -- yields false
-- But with a layer of nesting, it is contained:
SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb;
-- Similarly, containment is not reported here:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"bar": "baz"}'::jsonb; -- yields false
-- A top-level key and an empty object is contained:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"foo": {}}'::jsonb;
基本原则是被包含的对象必须在结构和数据内容上与外层包含对象严格匹配,在对象中允许外层对象包含额外的未声明键值对:
-- 简单的键值匹配测试
SELECT '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb;
作为结构匹配原则的特例,数组可以包含原始标量元素:
SELECT '[1, 2, "a"]'::jsonb @> '"a"'::jsonb;
-- This array contains the primitive string value: SELECT '["foo", "bar"]'::jsonb @> '"bar"'::jsonb; -- This exception is not reciprocal -- non-containment is reported here: SELECT '"bar"'::jsonb @> '["bar"]'::jsonb; -- yields false
jsonb 同样提供了存在性(Existence)操作符(?、?| 与 ?&),用于测试字符串是否存在为顶层对象的键或数组元素:
SELECT '{"a":1, "b":2}'::jsonb ? 'b';
SELECT '["a", "b", "c"]'::jsonb ? 'b';
-- String exists as array element:
SELECT '["foo", "bar", "baz"]'::jsonb ? 'bar';
-- String exists as object key:
SELECT '{"foo": "bar"}'::jsonb ? 'foo';
-- Object values are not considered:
SELECT '{"foo": "bar"}'::jsonb ? 'bar'; -- yields false
-- As with containment, existence must match at the top level:
SELECT '{"foo": {"bar": "baz"}}'::jsonb ? 'bar'; -- yields false
-- A string is considered to exist if it matches a primitive JSON string:
SELECT '"foo"'::jsonb ? 'foo';
当存在大量键或元素时,JSON 对象比数组更适合测试包含性与存在性,因为对象在内部进行了哈希优化,检索复杂度为 O(1)。
由于 JSON 包含性支持深度嵌套,一条精巧的包含查询可以跳过显式提取子对象的繁琐路径,直接在任意嵌套层级完成匹配:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"bar": "baz"}'::jsonb;
另一方面,JSON 存在性操作符是非嵌套的:它只在顶层检索指定的键或数组元素。
关于各种包含性与存在性操作符及所有其他 JSON 函数的详细文档,请参阅 Section 9.16。
GIN 倒排索引可以用于在海量 jsonb 文档中高速检索键或键值对。系统为 jsonb 提供了两套标准 GIN 操作符类:默认的 jsonb_ops 以及为路径包含高度优化的 jsonb_path_ops。
默认的 jsonb_ops 操作符类支持键存在性操作符(?、?|、?&)、包含操作符(@>)以及 jsonpath 匹配操作符(@?、@@)。
以存储第三方 Web 服务的 JSON 文档表为例,可以为其创建标准 GIN 索引:
CREATE INDEX idxgin ON api USING gin (jdoc);
另一种查询方式是利用包含性操作符,例如:
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["postgres"]}';
GIN 索引同样原生支持使用 @? 与 @@ 操作符执行的高级 jsonpath 表达式查询:
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @? '$.tags[*] ? (@ == "postgres")'; SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "postgres" ';
尽管 jsonb_path_ops 操作符类仅支持包含操作符 @> 与 jsonpath 操作符,但其索引体积通常远小于默认的 jsonb_ops,且针对复杂深层包含检索的性能显著更高:
CREATE INDEX idxginp ON api USING gin (jdoc jsonb_path_ops);
jsonb_ops 与 jsonb_path_ops 的底层技术差异在于:前者为数据中的每个键和每个值分别建立独立的索引条目,而后者为每个值连同其完整的访问路径生成哈希索引条目 [6] 。
jsonb_path_ops 的局限在于它无法为不包含任何值的空对象或空数组建立索引项,因而无法用于匹配空集合检索。
jsonb 同样支持 B 树与 Hash 索引。但通常仅在需要对整个 JSON 文档进行全局唯一性去重(UNIQUE)或者显式按照整个文档执行 ORDER BY 排序时才使用 B 树索引。
jsonb 原生支持类似数组下标的直观表达式语法来提取和就地修改元素。提取对象的字段值可使用字符串键名作为下标,提取数组元素可使用整数下标(注意 JSON 数组下标从 0 开始计数)。
在 UPDATE 语句的 SET 子句中可以直接通过下标语法就地修改嵌套字段,系统会自动在后台重写对应的嵌套结构。
下标语法的典型使用范例如下:
-- 通过键名提取对象字段值
SELECT ('{"a": 1}'::jsonb)['a'];
-- 通过整数下标提取数组元素
SELECT ('[1, "2", null]'::jsonb)[1];
-- 链式深层下标抽取
SELECT ('{"a": [1, 2, 3]}'::jsonb)['a'][1];
系统还提供了扩展包,用于在不同的过程语言中实现 jsonb 的透明类型转换。
面向 PL/Perl 的扩展名为 jsonb_plperl 与 jsonb_plperlu。
面向 PL/Python 的扩展名为 jsonb_plpython3u。启用后,jsonb 在 Python 中会自动双向映射为原生 Python 字典和列表。
在这些扩展中,jsonb_plperl 被标记为受信任扩展,非超级用户同样可以直接创建使用。
jsonpath 类型实现了对 PostgreSQL 中 SQL/JSON 路径语言规范的原生支持,用于在 JSON 文档中执行复杂的模式导航、过滤与值抽取。
SQL/JSON 路径谓词和操作符的语义通常遵循标准 SQL 规范,同时采用类似 JavaScript 的语法进行嵌套成员导航:
点号(.)用于访问对象的成员字段。
方括号([])用于访问数组元素。
与常规 SQL 数组从 1 开始编号不同,SQL/JSON 数组下标从 0 开始计数。
SQL/JSON 路径表达式中的数值字面量遵循 JavaScript 规则,这与普通 SQL 和 JSON 标准存在细微差别。
SQL/JSON 路径表达式在 SQL 语句中通常书写为字符串字面常量,因而需包裹在单引号内部。
路径表达式由一系列路径元素构成,可以包含如下任意组合:
JSON 基本类型的字面常量:Unicode 文本、数字、true、false 或 null。
在 Table 8.22 中列出的路径变量。
在 Table 8.23 中列出的访问器操作符。
在 Section 9.16.2.3 中列出的 jsonpath 专属操作符与方法。
圆括号,用于提供过滤表达式或显式定义路径求值的先后顺序。
关于在 SQL/JSON 查询函数中结合使用 jsonpath 表达式的详细指南,请参阅 Section 9.16.2。
Table 8.22. jsonpath 变量
| Variable | Description |
|---|---|
$ | A variable representing the JSON value being queried (the context item). |
$varname |
A named variable. Its value can be set by the parameter
vars of several JSON processing functions;
see Table 9.51 for details.
|
@ | A variable representing the result of path evaluation in filter expressions. |
Table 8.23. jsonpath 访问器方法
| Accessor Operator | Description |
|---|---|
|
|
对象成员访问器,返回具有指定键的对象成员。若键名以美元符开头或包含特殊符号,需使用双引号将其包裹为标识符。 |
|
|
通配符成员访问器,返回当前对象顶层所有成员的值。 |
|
|
递归通配符成员访问器,递归遍历当前对象的所有嵌套层级并返回所有层级上的成员字段值。 |
|
|
类似于 |
|
|
数组元素访问器,支持单一整数下标或指定起止范围的切片下标。 指定的下标可以是一个整数常量,也可以是返回单一整数的表达式。 |
|
|
通配符数组访问器,返回数组中的所有元素。 |