JSON 数据类型用于存储 JSON(JavaScript 对象表示法)数据,如 RFC 7159 中所述。此类数据也可以存储为 text
,但 JSON 数据类型具有确保每个存储值都根据 JSON 规则有效的好处。对于存储在这些数据类型中的数据,还有各种特殊的 JSON 函数和运算符可用;参见 第 9.16 节。
PostgreSQL提供两种用于存储 JSON 数据的类型:json
和 jsonb
。为了实现针对这些数据类型的有效查询机制,PostgreSQL 还提供了 第 8.14.7 节 中描述的 jsonpath
数据类型。
json
和 jsonb
数据类型接受作为输入的 几乎 相同的值集。主要实际差异是效率。json
数据类型存储输入文本的确切副本,而处理函数必须在每次执行时重新解析该文本;而 jsonb
数据以分解的二进制格式存储,这使得其输入速度稍慢,因为增加了转换开销,但处理速度明显快,因为不需要重新解析。jsonb
还支持索引,这可能是一个显著优势。
由于 json
类型存储输入文本的确切副本,因此它将保留标记之间的语义无关空白以及 JSON 对象中键的顺序。此外,如果值内的一个 JSON 对象包含同一个键两次,将会保留所有键/值对。(处理函数将最后一个值视为应用值。)相比之下,jsonb
不保留空白,不保留对象键的顺序,也不保留重复的对象键。如果在输入中指定了重复键,则只会保留最后一个值。
一般来说,大多数应用程序应该更喜欢将 JSON 数据存储为 jsonb
,除非有非常特殊的需要,例如关于对象键顺序的传统假设。
RFC7159 规定 JSON 字符串应该用 UTF8 编码。因此,除非数据库编码是 UTF8,否则 JSON 类型不可能严格符合 JSON 规范。尝试直接包含无法在数据库编码中表示的字符将失败;相反,可以使用数据库编码表示但不能用 UTF8 表示的字符。
RFC7159 允许 JSON 字符串包含由 \u
表示的 Unicode 转义序列。在 XXXX
json
类型的输入函数中,无论数据库编码如何,都允许 Unicode 转义,并且仅检查其语法是否正确(即 \u
后面有四个十六进制数字)。但是,jsonb
的输入函数更加严格:它不允许将 Unicode 转义用于无法在数据库编码中表示的字符。 jsonb
类型还拒绝 \u0000
(因为无法在 PostgreSQL 的 text
类型中表示它),并且它坚持认为任何使用 Unicode 代理对来表示 Unicode 基本多语言平面上之外的字符都必须是正确的。有效的 Unicode 转义将被转换为等效的单个字符以供存储;这包括将代理对折叠成单个字符。
在 第 9.16 节中 描述的许多 JSON 处理函数会将 Unicode 转义转换为常规字符,因此即使它们的输入类型为 json
而不是 jsonb
,它们也会引发前面描述的相同类型的错误。 json
输入函数没有进行这些检查的事实可能会被视为历史遗留,虽然它确实允许在不支持所表示字符的数据库编码中简单存储(不处理)JSON Unicode 转义。
在将文本 JSON 输入转换为 jsonb
时,RFC7159 所述的基本类型实际上映射到了本机 PostgreSQL 类型,如 表 8.23 中所示。因此,对于构成有效 jsonb
数据(但对 json
类型和 JSON 本身并不适用)存在一些额外的次要约束,这对应于对底层数据类型可以表示什么的限制。值得注意的是, jsonb
会拒绝超出 PostgreSQL numeric
数据类型范围的数字,而 json
不会。这样的实现定义限制由RFC7159 允许。但是,在实践中,此类问题在其他实现中更有可能发生,因为通常将 JSON 的 number
基本类型表示为 IEEE 754 双精度浮点数(RFC7159 明确预期并允许)。当将 JSON 用作与此类系统的交换格式时,应该考虑与 PostgreSQL 最初存储的数据相比损失数字精度的风险。
相反地,正如表中所述,JSON 原始类型输入格式有一些次要限制,不适用于相应的 PostgreSQL 类型。
表 8.23. JSON 原始类型和相应的 PostgreSQL 类型
JSON 原始类型 | PostgreSQL 类型 | 注释 |
---|---|---|
string |
text |
\u0000 不允许使用,并且 Unicode 转义字符也不能表示数据库编码中不可用的字符 |
number |
numeric |
NaN 和 infinity 值不允许使用 |
boolean |
boolean |
仅支持用小写书写的 true 和 false 拼写 |
null |
(无) | SQL NULL 是一个不同的概念 |
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
会保留尾随小数的 0,如本示例所示,尽管这些 0 对于诸如相等性检查之类的用途而言毫无意义。
有关用于构造和处理 JSON 值的内置函数和运算符的列表,请参见 第 9.16 节。
将数据表示为 JSON 比传统的关系数据模型要灵活得多,这在需求不稳定的环境中是很有说服力的。这两种方法完全有可能在同一个应用程序内并存并相互补充。然而,即使对于需要最大灵活性的应用程序,仍然建议 JSON 文档具有一定的固定结构。此类结构原则上不会强制执行(尽管可以从陈述上执行某些业务规则),但拥有可预测的结构会更容易编写查询,以便对表中的一组 “文档”(datum)进行有用的汇总。
当存储在表中时,JSON 数据会受到与任何其他数据类型一样的并发控制考虑。虽然存储大型文档是可行的,但请记住任何更新都会对整行获取行级锁。考虑将 JSON 文档限制为可管理的大小,以减少更新事务之间的锁争用。理想情况下,每个 JSON 文档都应表示一个原子 datum,业务规则指示该 datum 不能合理地进一步细分为可以独立修改的更小 datum。
jsonb
约束和存在 #测试 包含 是 jsonb
的一项重要功能。json
类型没有一组并行设施。包含测试一个 jsonb
文档是否包含有另一个。以下示例返回 true,除非有说明
-- 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
值的顶层作为一个对象键或数组元素出现。以下示例返回 true,除非有说明
-- 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 包含是嵌套的,因此适当的查询可以跳过子对象的显式选择。例如,假设我们有一个包含顶级对象(其中大多数对象包含包含子对象数组的 tags
字段)的 doc
列。此查询将查找出现同时包含 "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 运算符和函数以及包括各种包含和存在运算符在内的其他 JSON 运算符和函数在 第 9.16 节 中记录。
jsonb
索引 #GIN 索引可以用来有效查找出现在大量 jsonb
文档(数据集)中的键或键值对。提供了两种 GIN “运算符类”,它们提供不同的性能和灵活性权衡。
用于 jsonb
的默认 GIN 运算符类支持带有键存在运算符 ?
、?|
和 ?&
、包含运算符 @>
和 jsonpath
匹配运算符 @?
和 @@
的查询。(有关这些运算符实施语义的详细信息,请参见 表 9.46。)创建具有此运算符类的索引的示例如下
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
的表中,位于名为 jdoc
的 jsonb
列中。如果在此列上创建 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';
然而,通过适当使用表达式索引,上述查询可以使用索引。如果通常查询 "标签"
键中的特定项,则定义此类索引可能是值得的
CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags'));
现在,WHERE
子句 jdoc -> '标签' ? 'qui'
将被识别为已编制索引表达式 jdoc -> '标签'
上可编制索引运算符 ?
的应用程序。(可以在 第 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
列中每个键和值的副本,而前一个示例的表达式索引仅存储在 标签
键下找到的数据。虽然简单索引方法更灵活(因为它支持对任何键的查询),但目标表达式索引可能比简单索引更小且搜索速度更快。
GIN 索引还支持执行 jsonpath
匹配的 @?
和 @@
运算符。示例包括
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @? '$.tags[*] ? (@ == "qui")';
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';
对于这些运算符,GIN 索引会从 jsonpath
模式中抽出表单
的子句,并基于这些子句中提到的键和值执行索引搜索。访问器链可能包含 访问器链
== 常量
.
、键
[*]
和 [
访问器。 索引
]jsonb_ops
运算符类还支持 .*
和 .**
访问器,但 jsonb_path_ops
运算符类不支持。
尽管 jsonb_path_ops
运算符类仅支持使用 @>
、@?
和 @@
运算符的查询,但它比默认运算符类 jsonb_ops
具有显着的性能优势。 jsonb_path_ops
索引通常远小于相同数据上的 jsonb_ops
索引,并且搜索的针对性更强,尤其是在查询包含频繁出现在数据中的键时。因此,搜索操作的通常性能优于默认运算符类。
jsonb_ops
和 jsonb_path_ops
GIN 索引的技术区别在于,前者为数据中的每个键和值创建独立的索引项目,而后者仅为数据中的每个值创建索引项目。[7] 基本上,每个 jsonb_path_ops
索引项目都是值和导致该值的键的哈希值;例如,要索引 {"foo": {"bar": "baz"}}
,将创建单个索引项目,该项目将 foo
、bar
和 baz
全部合并到哈希值中。因此,寻找此结构的包含查询将导致极其具体的索引搜索;但完全没有办法找出 foo
是否显示为键。另一方面, jsonb_ops
索引将创建分别表示 foo
、bar
和 baz
的三个索引项目;然后,为了完成包含查询,它会查找包含这三个项目中所有项目的行。虽然 GIN 索引可以相当高效地执行这种 AND 搜索,但它仍然不如等效的 jsonb_path_ops
搜索那样具体且慢,尤其是在包含三个索引项目中的任何一个的行的数量非常多的情况下。
jsonb_path_ops
方法的一个缺点是,它不会为不包含任何值的 JSON 结构产生索引条目,例如 {"a": {}}
。如果请求搜索包含此类结构的文档,则需要进行全索引扫描,这非常慢。因此, jsonb_path_ops
不适用于经常执行此类搜索的应用程序。
jsonb
还支持 btree
和 hash
索引。如果检查完整 JSON 文档的相等性非常重要,则通常只有这些情况下才有用。 type">jsonb
数据项的 btree
排序很少引起很大兴趣,但为了完整起见,它是
Object
>Array
>Boolean
>Number
>String
>Null
Object with n pairs
>object with n - 1 pairs
Array with n elements
>array with n - 1 elements
包含相等数量键值对的对象按照以下顺序比较
key-1
,value-1
,key-2
...
请注意,以存储顺序比较对象键;特别是,由于较短的键存储在较长的键之前,这样可能导致结果违反直觉,例如
{ "aa": 1, "c": 1} > {"b": 1, "d": 1}
类似地,包含相等数量元素的数组按以下顺序比较
element-1
,element-2
...
使用与底层 PostgreSQL 数据类型相同的比较规则比较原始 JSON 值。使用默认数据库整理规则比较字符串。
jsonb
下标 #jsonb
数据类型支持数组样式下标表达,用于提取和修改元素。嵌套值可以通过链接下标表达来表示,遵循 jsonb_set
函数中的 path
参数所遵循的规则。如果 jsonb
值是一个数组,则从零开始进行数字下标,而负整数从数组的最后一个元素开始向后计数。不支持切片表达式。下标表达的结果总是 jsonb
数据类型。
UPDATE
语句可以在 SET
子句中使用下标来修改 jsonb
值。对于所有受影响的值,必须可以在其存在的情况下遍历下标路径。例如,如果每个 val
、val['a']
和 val['a']['b']
都是对象,则该路径 val['a']['b']['c']
可以一直遍历到 c
。如果任何 val['a']
或 val['a']['b']
未定义,它将被创建为一个空对象并根据需要填充。但是,如果任何 val
本身或其中值定义为非对象(如字符串、数字或 jsonb
null
),则无法进行遍历,因此会引发错误并中止事务。
下标语法的一个示例
-- Extract object value by key SELECT ('{"a": 1}'::jsonb)['a']; -- Extract nested object value by key path SELECT ('{"a": {"b": {"c": 1}}}'::jsonb)['a']['b']['c']; -- Extract array element by index SELECT ('[1, "2", null]'::jsonb)[1]; -- Update object value by key. Note the quotes around '1': the assigned -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; -- This will raise an error if any record's jsonb_field['a']['b'] is something -- other than an object. For example, the value {"a": 1} has a numeric value -- of the key 'a'. UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"';
通过下标进行 jsonb
赋值与通过 jsonb_set
处理一些特殊情况的方式不同。如果源 jsonb
值为 NULL
,则通过下标进行赋值将继续进行,就好像它是下标键暗示的类型(对象或数组)的空 JSON 值
-- Where jsonb_field was NULL, it is now {"a": 1} UPDATE table_name SET jsonb_field['a'] = '1'; -- Where jsonb_field was NULL, it is now [1] UPDATE table_name SET jsonb_field[0] = '1';
如果为元素太少的数组指定索引,则会追加 NULL
元素,直到索引可访问并且可以设置值为止。
-- Where jsonb_field was [], it is now [null, null, 2]; -- where jsonb_field was [0], it is now [0, null, 2] UPDATE table_name SET jsonb_field[2] = '2';
jsonb
值将接受对不存在的下标路径的赋值,只要要遍历的最后一个现有元素是对象或数组,如相应的下标所暗示的(路径中最后一个下标指示的元素没有被遍历,并且可以是任何东西)。嵌套数组和对象结构将被创建,在前一种情况下,将按照下标路径指定的填充 null
-,直到可以放置分配的值。
-- Where jsonb_field was {}, it is now {"a": [{"b": 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; -- Where jsonb_field was [], it is now [null, {"a": 1}] UPDATE table_name SET jsonb_field[1]['a'] = '1';
有其他扩展可以为不同过程语言的 jsonb
类型实现转换。
PL/Perl 的扩展名为 jsonb_plperl
和 jsonb_plperlu
。如果您使用它们,则会根据需要将 jsonb
值映射到 Perl 数组、哈希和标量。
PL/Python 的扩展名为 jsonb_plpython3u
。如果您使用该扩展名,则 jsonb
值将被映射到 Python 字典、列表和标量(视具体情况而定)。
在这些扩展名中,jsonb_plperl
被认为是 “可信赖”的,也就是说,它可以由那些在当前数据库上拥有 CREATE
权限的非超级用户进行安装。其他扩展名需要超级用户权限才能安装。
jsonpath
类型在 PostgreSQL 中实现了对 SQL/JSON 路径语言的支持,从而可以高效地查询 JSON 数据。它提供了解析好的 SQL/JSON 路径表达式的二进制表示形式,该表达式指定路径引擎从 JSON 数据中检索的项目,以便进一步使用 SQL/JSON 查询函数进行处理。
SQL/JSON 路径谓词和运算符的语义通常遵循 SQL。与此同时,为了提供一种操作 JSON 数据的自然方法,SQL/JSON 路径语法使用了某些 JavaScript 惯例。
句点 (.
) 用于成员访问。
方括号 ([]
) 用于数组访问。
与从 1 开始的常规 SQL 数组不同,SQL/JSON 数组从 0 开始。
在 SQL/JSON 路径表达式中,数字文字遵循 JavaScript 规则,在一些小细节上不同于 SQL 和 JSON。例如,SQL/JSON 路径允许使用 .1
和 1.
,而在 JSON 中它们是非法的。支持非十进制整数文字和下划线分隔符,例如 1_000_000
、0x1EEE_FFFF
、0o273
、0b100101
。在 SQL/JSON 路径(以及在 JavaScript 中,但不在 SQL 本身中)中,基数前缀后面一定不能有下划线分隔符。
SQL/JSON 路径表达式通常在 SQL 查询中直接写成 SQL 字符串字面量,因此必须用单引号引起来,值中任何所需的单引号必须加倍(请参阅
路径表达式由路径元素序列组成,可以是以下任何一种
JSON 原始类型的路径字面量:Unicode 文本、数字、true、false 或 null。
在 表 8.24 中列出的路径变量。
在 表 8.25 中列出的访问器运算符。
jsonpath
运算符和方法,在 第 9.16.2.3 节 中列出。
括号,可用于提供筛选表达式或定义路径评估顺序。
有关将 jsonpath
表达式与 SQL/JSON 查询函数结合使用的详细信息,请参阅 第 9.16.2 节。
表 8.24. jsonpath
变量
变量 | 描述 |
---|---|
$ |
表示正被查询的 JSON 值的变量(上下文项)。 |
$varname |
命名变量。它的值可以通过多个 JSON 处理函数的参数 vars 进行设置;有关详细信息,请参阅 表 9.49。 |
@ |
表示筛选表达式中路径评估结果的变量。 |
表 8.25. jsonpath
访问器
访问器运算符 | 描述 |
---|---|
|
成员访问器,返回具有指定键的对象成员。如果键名与从 |
|
通配符成员访问器,返回位于当前对象顶层的成员的值。 |
|
递归通配符成员访问器,处理 JSON 层次结构的所有层级并返回所有成员值,无论嵌套层级如何。这是 SQL/JSON 标准的 PostgreSQL 扩展。 |
|
与 |
|
数组元素访问器。 指定的 |
|
通配符数组元素访问器,返回所有数组元素。 |