选择 打开 改范围 完整检索页
受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10
当前 PostgreSQL 版本不在支持生命周期内。
您可以参阅当前版本的对应页面,或其他在上面列出的活跃大版本。

8.14. JSON 类型 #

JSON 数据类型用于存储 JSON(JavaScript Object Notation)数据,如 RFC 7159 所定义。这类数据也可以存储为 text,但 JSON 数据类型的优势在于会强制每个存储值都符合 JSON 规则。此外,对于存储在 这些数据类型中的数据,还提供了各种 JSON 专用的函数和操作符;见 Section 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 字符进行存储,包括将代理对合并为单个字符。

Note

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

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

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

Table 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

-- A top-level key and an empty object is contained:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"foo": {}}'::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 还有一个 存在操作符,它可看作 包含的一种变体:它测试某个字符串(以 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 对象比数组更适合用于测试包含或存在,因为对象 与数组不同,内部已针对搜索做了优化,不需要进行线性搜索。

Tip

由于 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 操作符和函数,均见 Section 9.15

8.14.4. jsonb 索引 #

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

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

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'。(表达式索引的更多信息见 Section 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数据类型相同的规则进行比较。字符串使用数据库的默认排序规则进行比较。

8.14.5. 转换

还提供了一些附加扩展,为不同过程语言实现了 jsonb 类型的转换。

PL/Perl 的扩展名为 jsonb_plperljsonb_plperlu。如果使用它们, jsonb 值会按适当方式映射为 Perl 数组、哈希和标量。

PL/Python 的扩展分别名为 jsonb_plpythonujsonb_plpython2ujsonb_plpython3u(关于 PL/Python 的命名约定请见 Section 46.1)。如果使用它们,jsonb 值会按适当方式映射为 Python 字典、列表和标量。



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