选择 打开 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2
历史版本PostgreSQL 9.4 已于 2020 年 2 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本

8.14. JSON 类型 #

JSON 数据类型用于存储 JSON(JavaScript Object Notation)数据,如 RFC 7159 所定义。这类数据也可以存储为 text,但 JSON 数据类型的优势在于会强制每个存储值都符合 JSON 规则。此外,对于存储在 这些数据类型中的数据,还提供了各种 JSON 专用的函数和操作符;见 第 9.15 节

有两种 JSON 数据类型:jsonjsonb。它们接受的输入值集合几乎相同。实际使用中的主要区别是效率。json 数据类型保存输入文本的精确副本,处理函数每次执行时都必须重新解析;而 jsonb 数据以分解后的二进制格式存储,额外的转换开销使输入稍慢,但无需重新解析,因此处理速度明显更快。jsonb 还支持索引,这可能带来显著优势。

由于 json 类型存储的是输入文本的精确副本,因此它会保留标记 之间在语义上无关紧要的空白,以及 JSON 对象内部键的顺序。此外,如果值中 的某个 JSON 对象包含同一个键多次,所有键/值对都会被保留下来(处理函数会 将最后一个值视为生效值)。相比之下,jsonb 不保留空白,不保留 对象键的顺序,也不保留重复的对象键。如果输入中指定了重复的键,则只保留 最后一个值。

一般而言,大多数应用都应优先将 JSON 数据存储为 jsonb, 除非存在相当特殊的需求,例如遗留系统对对象键顺序的假设。

PostgreSQL 的每个数据库只允许使用一种字符集编码。因此,除非数据库编码为 UTF8,否则 JSON 类型无法严格遵循 JSON 规范。直接包含数据库编码无法表示的字符会失败;反过来,数据库编码能够表示但 UTF8 无法表示的字符则会被允许。

RFC 7159 允许 JSON 字符串包含以 \uXXXX 表示的 Unicode 转义序列。json 类型的输入函数允许 Unicode 转义,而不管数据库使用什么编码,并且只检查语法是否正确(即 \u 后面是否有四位十六进制数字)。但 jsonb 的输入函数更严格:除非数据库编码为 UTF8,否则不允许非 ASCII 字符(大于 U+007F 的字符)的 Unicode 转义。jsonb 类型也会拒绝 \u0000(因为它无法在 PostgreSQLtext 类型中表示),并要求正确使用 Unicode 代理对来表示 Unicode 基本多文种平面之外的字符。有效的 Unicode 转义会转换为等价的 ASCII 或 UTF8 字符进行存储,包括将代理对合并为单个字符。

注意

第 9.15 节 中描述的许多 JSON 处理函数会将 Unicode 转义转换为普通字符,因此即使输入的类型为 json 而非 jsonb,也会抛出上述相同类型的错误。json 输入函数不做这些检查,可以视为历史遗留行为,不过它确实允许在非 UTF8 数据库编码下简单地存储 JSON Unicode 转义,而不进行处理。一般而言,应尽可能避免将 JSON 中的 Unicode 转义与非 UTF8 数据库编码混用。

当把文本形式的 JSON 输入转换为 jsonb 时, RFC 7159 描述的基本类型会有效映射到原生的 PostgreSQL 类型上,如 表 8.23 所示。因此,什么样的数据构成 有效的 jsonb 会有一些额外但较小的限制,这些限制不适用于 json 类型,也不适用于抽象意义上的 JSON;它们对应于底层 数据类型可表示范围的限制。特别地,jsonb 会拒绝超出 PostgreSQL numeric 数据类型 范围的数字,而 json 不会。RFC 7159 允许这种由实现定义的限制。不过在实践中,这类问题更可能出现在其他实现中, 因为通常会把 JSON 的 number 基本类型表示为 IEEE 754 双精度浮点数(RFC 7159 明确预见并允许了这一点)。 当把 JSON 用作与这类系统交换数据的格式时,应考虑与原先由 PostgreSQL 存储的数据相比丢失数值精度的风险。

另一方面,正如表中所指出的那样,JSON 基本类型的输入格式还有一些轻微限制, 而对应的 PostgreSQL 类型并没有这些限制。

表 8.23. JSON 基本类型及其对应的 PostgreSQL 类型

JSON 基本类型 PostgreSQL 类型 说明
string text 不允许 \u0000;如果数据库编码不是 UTF8,也不允许非 ASCII 字符的 Unicode 转义
number numeric 不允许 NaNinfinity
boolean boolean 只接受小写拼写 truefalse
null (无) SQL NULL 是不同的概念

8.14.1. JSON 输入和输出语法 #

JSON 数据类型的输入/输出语法遵循 RFC 7159。

以下都是有效的 json(或 jsonb)表达式:

-- Simple scalar/primitive value
-- Primitive values can be numbers, quoted strings, true, false, or null
SELECT '5'::json;

-- Array of zero or more elements (elements need not be of same type)
SELECT '[1, 2, "foo", null]'::json;

-- Object containing pairs of keys and values
-- Note that object keys must always be quoted strings
SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json;

-- Arrays and objects can be nested arbitrarily
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 类型的行为输出。在实践中,这意味着使用 E 记数法输入的数字在输出时将不再使用这种写法,例如:

SELECT '{"reading": 1.230e-5}'::json, '{"reading": 1.230e-5}'::jsonb;
         json          |          jsonb
-----------------------+-------------------------
 {"reading": 1.230e-5} | {"reading": 0.00001230}
(1 row)

不过,正如这个例子所示,jsonb 会保留小数部分末尾的零, 尽管对于等值检查之类的用途来说,这些零在语义上并不重要。

8.14.2. 有效地设计 JSON 文档 #

以 JSON 形式表示数据,可能比传统的关系数据模型灵活得多,这在需求变化较 大的环境中尤其有吸引力。这两种方法完全可能在同一个应用中共存并互为补充。 但是,即使对于追求最大灵活性的应用,也仍然建议 JSON 文档拥有某种相对固定 的结构。这种结构通常并不受强制约束(尽管也可以用声明式方式强制某些业务规则), 但具有可预测的结构会让编写查询更容易,从而能够有效地汇总表中一组 文档(数据项)。

当 JSON 数据存储在表中时,它与任何其他数据类型一样,都要面对相同的并发控 制考量。虽然存储大型文档是可行的,但要记住,任何更新都会在整行上获取一个 行级锁。应考虑将 JSON 文档限制在可管理的大小,以减少更新事务之间的锁争用。 理想情况下,每个 JSON 文档都应表示一个原子数据项,按照业务规则,它不应被 合理地进一步拆分为更小且可独立修改的数据项。

8.14.3. jsonb 包含与存在 #

测试 包含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

一般原则是,被包含对象在结构和数据内容上都必须与包含对象匹配;必要时, 可以从包含对象中丢弃某些不匹配的数组元素或对象键/值对后再进行这种匹配。 但要记住,在进行包含匹配时,数组元素的顺序并不重要,重复的数组元素实际 上也只会被考虑一次。

对于结构必须匹配这一一般原则,有一个特殊例外:数组可以包含一个基本值:

-- 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 还有一个 存在操作符,它可看作 包含的一种变体:它测试某个字符串(以 text 值给出) 是否在 jsonb 值的顶层作为对象键或数组元素出现。除特别说明 外,下面这些示例都返回真:

-- 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 对象比数组更适合用于测试包含或存在,因为对象 与数组不同,内部已针对搜索做了优化,不需要进行线性搜索。

提示

由于 JSON 包含是嵌套的,因此适当的查询可以跳过对子对象的显式选择。例如, 假设我们有一个 doc 列,其顶层是对象,而且大 多数对象都带有 tags 字段,该字段中包含子对象数组。下面 这个查询会找出那些包含同时带有 "term":"paris""term":"food" 的子对象的项,同时忽略 tags 数组之外的任何此类键:

SELECT doc->'site_name' FROM websites
  WHERE doc @> '{"tags":[{"term":"paris"}, {"term":"food"}]}';

例如,也可以用下面的写法完成同样的事情:

SELECT doc->'site_name' FROM websites
  WHERE doc->'tags' @> '[{"term":"paris"}, {"term":"food"}]';

但这种做法的灵活性较差,而且通常效率也更低。

另一方面,JSON 的存在操作符并不是嵌套的:它只会在 JSON 值的顶层查找指定 的键或数组元素。

各种包含和存在操作符,以及所有其他 JSON 操作符和函数,均见 第 9.15 节

8.14.4. jsonb 索引 #

GIN 索引可用于高效搜索大量 jsonb 文档(数据项)中出现的键 或键/值对。提供了两种 GIN 操作符类,它们在性能和灵活性 之间提供不同的权衡。

对于jsonb,默认 GIN 操作符类支持使用顶层键存在操作符??&?|以及路径/值存在操作符@>的查询。(这些操作符所实现语义的详情,参见表 9.41。)使用此操作符类创建索引的示例如下:

CREATE INDEX idxgin ON api USING GIN (jdoc);

非默认的 GIN 操作符类jsonb_path_ops仅支持为@>操作符建立索引。使用此操作符类创建索引的示例如下:

CREATE INDEX idxginp ON api USING GIN (jdoc jsonb_path_ops);

假设有一个表,用于存储从第三方 Web 服务检索到的 JSON 文档,而且该服务的 模式定义已有文档说明。一个典型文档如下:

{
    "guid": "9c36adc1-7fb5-4d5b-83b4-90356a46061a",
    "name": "Angela Barton",
    "is_active": true,
    "company": "Magnafone",
    "address": "178 Howard Place, Gulf, Washington, 702",
    "registered": "2009-11-07T08:53:22 +08:00",
    "latitude": 19.793713,
    "longitude": 86.513373,
    "tags": [
        "enim",
        "aliquip",
        "qui"
    ]
}

我们把这些文档存储在名为 api 的表中,存放于 名为 jdocjsonb 列里。 如果在该列上创建了 GIN 索引,那么下面这样的查询就可以利用这个索引:

-- Find documents in which the key "company" has value "Magnafone"
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"company": "Magnafone"}';

但是,类似下面这样的查询就无法使用该索引,因为虽然操作符 ? 可索引,但它并未直接应用到被索引的列 jdoc 上:

-- Find documents in which the key "tags" contains key or array element "qui"
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc -> 'tags' ? 'qui';

不过,只要适当地使用表达式索引,上述查询也可以利用索引。如果经常查询 "tags" 键中的特定项,那么定义如下索引可能是值得的:

CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags'));

现在,WHERE 子句 jdoc -> 'tags' ? 'qui' 会被识别为把可索引操作符 ? 应用于被索引表达式 jdoc -> 'tags'。(表达式索引的更多信息见 第 11.7 节。)

另一种查询方法是利用包含,例如:

-- Find documents in which the key "tags" contains array element "qui"
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qui"]}';

jdoc 列上的简单 GIN 索引可以支持这个查询。 但要注意,这样的索引会存储 jdoc 列中每个键和 值的副本,而前一个例子中的表达式索引只存储 tags 键下 出现的数据。虽然简单索引方法灵活得多(因为它支持对任意键的查询),但有针 对性的表达式索引通常会比简单索引更小,搜索起来也更快。

虽然 jsonb_path_ops 操作符类只支持使用 @> 操作符的查询,但与默认操作符类 jsonb_ops 相比,它具有显著的性能优势。对于相同数据,jsonb_path_ops 索引通常比 jsonb_ops 索引小得多,搜索的针对性也更强,特别是当查询包含数据中频繁出现的键时。因此,搜索操作的性能通常优于默认操作符类。

jsonb_opsjsonb_path_ops GIN 索引之间的技术差异在于,前者会为数据中的每个键和值分别创建独立的 索引项,而后者只会为数据中的每个值创建索引项。 [6] 基本上,每个 jsonb_path_ops 索引项都是该值连同 通向该值的键一起计算出的哈希。例如,要索引 {"foo": {"bar": "baz"}},会创建一个单独的索引项, 其哈希值中同时纳入 foobarbaz 这三者。因此,查找这一结构的包含查询会得到一次 非常精确的索引搜索;但完全没有办法据此找出 foo 是否 作为键出现。另一方面,jsonb_ops 索引会分别创建三个 索引项来表示 foobarbaz;然后为了执行包含查询,它会查找包含这三个项的行。 尽管 GIN 索引可以相当高效地执行这种 AND 搜索,但它仍然会比等效的 jsonb_path_ops 搜索更不精确、也更慢,尤其是在包含这 三个索引项中任意一个的行数非常多时。

jsonb_path_ops 方法的一个缺点是,它不会为不包含任何值 的 JSON 结构产生索引项,例如 {"a": {}}。如果请求查 找包含此类结构的文档,就需要执行一次全索引扫描,这会相当慢。因此, jsonb_path_ops 并不适合经常执行此类搜索的应用。

jsonb也支持btreehash索引。通常只有在需要检查完整 JSON 文档是否相等时,这些索引才有用。对于btree排序,jsonb数据的顺序很少值得关注,但为求完整,列出如下:

Object > Array > Boolean > Number > String > Null

Object with n pairs > object with n - 1 pairs

Array with n elements > array with n - 1 elements

键值对数量相等的对象按以下顺序比较:

键-1, 值-1, 键-2 ...

注意,对象键按其存储顺序比较;尤其是,较短的键存储在较长的键之前,因此可能产生不直观的结果,例如:

{ "aa": 1, "c": 1} > {"b": 1, "d": 1}

类似地,元素数量相等的数组按以下顺序比较:

元素-1, 元素-2 ...

JSON 基本值使用与其底层PostgreSQL数据类型相同的规则进行比较。字符串使用数据库的默认排序规则进行比较。



[6] 在这里,术语 也包括数组元素,尽管在 JSON 术语中, 有时会把数组元素与对象中的值区分开来。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。