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只允许每个数据库用一种字符集编码。 因此,使JSON严格符合规范是不可能的,除非数据库编码是UTF8。 试图直接包含不能在数据库编码中表示的字符将会失败;相反的, 能在数据库编码中表示但是不在UTF8中的字符是被允许的。

RFC 7159允许JSON字符串包含以\uXXXX 表示的Unicode逃逸序列。在json类型的输入函数中,允许Unicode逃逸, 无视服务器编码,并且只检查语法的正确性(也就是,跟随\u的四个十六进制位)。 不过,jsonb的输入函数更加严格:它不允许非ASCII字符(那些在U+007F 之上的)的Unicode逃逸,除非数据库编码是UTF8。jsonb类型也拒绝\u0000 (因为\u0000不能以PostgreSQLtext类型表示), 并且它坚持任何在Unicode Basic Multilingual Plane之外使用Unicode代理对指定字符的都是正确的。 有效的Unicode逃逸为了存储被转换为相等的ASCII或UTF8字符;这包括折叠代理对为单个字符。

注意: 许多在第 9.15 节函数和操作符章节描述的JSON处理函数将转换 Unicode逃逸为普通字符,并且将因此抛出刚刚描述的相同类型的错误, 即使它们的输入是json类型而不是jsonb类型。 json输入函数并不做这些检查的事实可以被认为是一个历史遗留, 尽管它确实允许在非UTF8数据库编码中简单的存储(不处理)JSON Unicode逃逸。通常, 如果可以,最好避免在非UTF8的数据库编码中混合Unicode逃逸到JSON。

当转换文本JSON输入到jsonb时,RFC 7159 描述的基本类型有效的映射到本地PostgreSQL类型, 就如表 8-23中显示的那样。因此, 在没有应用到json类型、也没有应用到抽象的JSON的组成有效的jsonb 数据上有一些小的额外的约束,相当于限制可以通过底层数据类型表示的数据。特别的, jsonb拒绝超出PostgreSQL numeric 数据类型范围的数字,而json不拒绝。这样的实现定义限制是RFC 7159允许的。 不过,在实际中,这样的问题更有可能在其他实现中发生,因为它通常代表JSON的number 原始类型,就是IEEE 754双精度浮点数那样(这是RFC 7159明确预期和允许的)。 当使用JSON作为这样系统的交换格式时,应该考虑和PostgreSQL 原始存储的数据相比,丢失数值精度的风险。

相反的,就像在表中指出的那样,在JSON原始类型的输入格式上有一些小的限制, 没有应用到相应的PostgreSQL类型。

表 8-23. JSON原始类型和相应的PostgreSQL类型

JSON原始类型PostgreSQL类型说明
stringtext\u0000是不允许的,如果数据库编码不是UTF8, 那么非ASCII Unicode逃逸也一样
numbernumeric不允许NaNinfinity
booleanboolean只接受小写的truefalse
null(none)SQL NULL 是一个不同的概念

8.14.1. JSON输入和输出语法

JSON数据类型的输入/输出语法是在RFC 7159中指定的。

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

-- 简单的标量/原始值
-- 原始值可以是数字、加引号的字符串、true、false或null
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类型的行为输出。 在实际中这意味着带着E符号输入的数字在输出时将没有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文档有一个固定的结构。通常不强制使用哪种结构 (尽管有可能强制一些业务规则声明),但是有一个可以预见的结构使得它更容易书写查询, 有效的在一个表中总结一组"文档"(datums)。

当存储到一个表时,JSON数据和任何其他数据类型一样受到相同的并发性控制的考虑。 尽管存储大文档是可行的,但是要记住任何更新都在整个行上获得一个行级锁。 考虑限制JSON文档到一个易管理的尺寸,以便减少更新事务上的锁竞争。理想上, JSON文档应该每个代表一个原子数据, 原子数据是业务规则命令不能再合理的细分为更小的可以被独立修改的数据。

8.14.3. jsonb包含和存在

测试包含jsonb的一个重要能力。没有json 类型的平行设置的设施。包含测试一个jsonb文档是否包含在另一个当中。 这些示例除了注释了返回假之外的都返回真:

-- 简单的标量/原始值只包含相同的值:
SELECT '"foo"'::jsonb @> '"foo"'::jsonb;

-- 右侧的数组包含在左侧的数组当中:
SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb;

-- 数组元素的顺序不重要,所以这个也为真:
SELECT '[1, 2, 3]'::jsonb @> '[3, 1]'::jsonb;

-- 重复的数组元素也不重要:
SELECT '[1, 2, 3]'::jsonb @> '[1, 2, 2]'::jsonb;

-- 右侧只包含一对值的对象包含在左侧的对象中:
SELECT '{"product": "PostgreSQL", "version": 9.4, "jsonb":true}'::jsonb @> '{"version":9.4}'::jsonb;

-- 认为右侧的数组包含在左侧的数组中,尽管一个相似的数组嵌套在它里面
SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb;  -- 产生假

-- 但是有一个嵌套,它就被包含了:
SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb;

-- 相似的,包含在这里也不报告
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"bar": "baz"}'::jsonb;  -- 产生假

一般原理是被包含的对象必须在结构和数据内容上匹配包含对象, 可能是在包含对象中丢弃一些不匹配的数组元素或对象键/值对之后。但是要记住, 当做一个包含匹配时,数组元素的顺序是不重要的,并且重复的数组元素实际上只考虑一次。

作为一般原则中结构必须匹配的一个特别例外,一个数组可以包含一个原始值:

-- 这个数组包含这个原始字符串值:
SELECT '["foo", "bar"]'::jsonb @> '"bar"'::jsonb;

-- 这个例外反过来不可以 -- 在这里没有报告包含:
SELECT '"bar"'::jsonb @> '["bar"]'::jsonb;  -- 产生假

jsonb还有一个存在操作符,它是包含的一个变种: 它测试一个字符串(作为text值给出) 是否作为一个对象键或者数组元素中顶级的jsonb值出现。 这些示例除了注释为假的之外都返回真:

-- 字符串作为数组元素存在:
SELECT '["foo", "bar", "baz"]'::jsonb ? 'bar';

-- 字符串作为对象键存在:
SELECT '{"foo": "bar"}'::jsonb ? 'foo';

-- 不考虑对象值:
SELECT '{"foo": "bar"}'::jsonb ? 'bar';  -- 产生假

-- 和包含一样,存在必须在顶级匹配:
SELECT '{"foo": {"bar": "baz"}}'::jsonb ? 'bar'; -- 产生假

-- 如果字符串匹配一个原始JSON字符串,那么认为它存在:
SELECT '"foo"'::jsonb ? 'foo';

当包含许多键或元素时,JSON对象比数组更适合测试包含或存在, 因为JSON对象不像数组那样为了查找而内部优化,并且不需要线性搜索。

各种包含和存在操作符,还有所有其他JSON操作符和函数都在 第 9.15 节中记录。

8.14.4. jsonb 索引

GIN索引可以用来有效的搜索出现在大量jsonb文档(datums) 中的键或者键/值对。提供了两个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索引,那么像下面这样的查询可以利用该索引:

-- 在键"company"有值"Magnafone"的地方找到文档
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"company": "Magnafone"}';

不过,该索引不能用于像下面这样的查询,因为虽然操作符?是可索引的, 但是它没有直接应用到索引的字段jdoc上:

-- 在键"tags"包含键或数组元素"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 节中找到。)

另外一个查询的方式是利用包含,例如:

-- 在键 "tags" 包含数组元素 "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 索引之间技术上的不同是前者为数据中的每个键和值创建独立的索引项, 而后者只为数据中的每个值创建索引项。 [1] 从根本上来说,每个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文档的等价性重要时通常有用。很少关心 jsonb datums的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 ...

原始的JSON值是使用和底层PostgreSQL 数据类型一样的比较规则比较的。字符串使用默认的数据库排序规则比较。

备注

[1]

为了这个目的,术语"value"包括数组元素,尽管JSON术语有时认为数组元组和对象中的值不同。