【PostgreSQL】从零开始:(二十九)数据类型-JSON类型

发布时间:2023年12月25日

JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于JavaScript的一个子集,采用键值对的形式来表示数据。JSON被广泛应用于Web应用程序中,用于数据的传输和存储。

JSON的特点包括:

简洁和易读:JSON使用简洁的文本格式,易于人类阅读和编写。
轻量级:JSON的数据格式较小,传输和解析效率高。
跨平台:JSON可以被多种编程语言读取和解析,可以在不同的平台间进行数据交换。

JSON 数据类型用于存储 RFC 7159 中指定的 JSON(JavaScript 对象表示法)数据。此类数据也可以存储为 ,但 JSON 数据类型的优点是根据 JSON 规则强制每个存储的值都有效。还有各种特定于 JSON 的函数和运算符可用于存储在这些数据类型中的数据;

和数据类型接受几乎相同的值集作为输入。主要的实际区别在于效率。数据类型存储输入文本的精确副本,处理函数必须在每次执行时重新解析该文本;虽然数据以分解的二进制格式存储,但由于增加了转换开销,因此输入速度略慢,但处理速度明显更快,因为不需要重新解析。 还支持索引,这可能是一个显着的优势。

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

通常,大多数应用程序应更愿意将 JSON 数据存储为jsonb ,除非有非常特殊的需求,例如关于对象键排序的传统假设。

RFC 7159 指定 JSON 字符串应以 UTF8 编码。因此,除非数据库编码为 UTF8,否则 JSON 类型不可能严格符合 JSON 规范。尝试直接包含无法在数据库编码中表示的字符将失败;相反,允许使用数据库编码但不能用 UTF8 表示的字符。

RFC 7159 允许 JSON 字符串包含用 表示的 Unicode 转义序列。在类型的输入函数中,无论数据库编码如何,都允许使用 Unicode 转义,并且仅检查语法正确性(即后面有四个十六进制数字)。但是,的输入函数更严格:它不允许对无法在数据库编码中表示的字符进行 Unicode 转义。该类型也拒绝(因为这不能在 PostgreSQL 的类型中表示),并且它坚持认为任何使用 Unicode 代理项对来指定 Unicode 基本多语言平面之外的字符都是正确的。有效的 Unicode 转义将转换为等效的单个字符进行存储;这包括将代理项对折叠成一个字符。

将文本 JSON 输入转换为 时,RFC 7159 描述的原始类型会有效地映射到本机 PostgreSQL 类型,如下表所示。因此,对于有效数据的构成,存在一些小的附加约束,这些约束不适用于类型,也不适用于抽象的 JSON,对应于基础数据类型可以表示的内容的限制。值得注意的是,将拒绝超出 PostgreSQL 数据类型范围的数字,而不会。RFC 7159 允许此类实现定义的限制。然而,在实践中,此类问题更有可能发生在其他实现中,因为通常将 JSON 的原始类型表示为 IEEE 754 双精度浮点(RFC 7159 明确预测并允许)。当使用JSON作为与此类系统的交换格式时,应考虑与PostgreSQL最初存储的数据相比失去数字精度的危险。jsonbjsonbjsonjsonbnumericjsonnumber

相反,如表中所述,对 JSON 基元类型的输入格式有一些小限制,这些限制不适用于相应的 PostgreSQL 类型。

|JSON 基元类型|PostgreSQL 类型|笔记|
|string|text|\u0000不允许,表示数据库编码中不可用的字符的 Unicode 转义也是如此|
|number|numeric|NaN和值是不允许的infinity|
|boolean|boolean |只接受小写字母和拼写truefalse|
|null |(无)|SQL是一个不同的概念NULL|

JSON常见的数据类型:

  1. 字符串(String):用于表示文本数据,使用双引号包裹起来。 数字(Number):用于表示数值,可以是整数或小数。
  2. 布尔值(Boolean):表示真或假,只有两个取值:true和false。
  3. 数组(Array):表示一组有序的数据,使用方括号包裹起来,每个元素用逗号分隔。
  4. 对象(Object):表示一组键值对的集合,使用大括号包裹起来,每个键值对使用冒号分隔,键名为字符串类型。
    例如,以下是一个JSON对象的示例:
{
  "name": "John",
  "age": 30,
  "isStudent": false,
  "hobbies": ["reading", "running", "coding"],
  "address": {
    "street": "123 Main St",
    "city": "New York"
  }
}

以上示例中,name、age和isStudent是对象的属性,hobbies是数组类型的属性,address是对象类型的属性。

PostgreSQL 提供了两种类型来存储 JSON 数据:json和jsonb.为了实现这些数据类型的高效查询机制,PostgreSQL 还提供了jsonpath数据类型。

JSON输入和输出语法

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

以下是所有有效(或)表达式:

-- 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 值,然后在没有任何额外处理的情况下打印时,输出的文本与输入的文本相同,同时不保留语义上无关紧要的细节,例如空格。例如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中,数字将根据基础类型的行为进行打印。在实践中,这意味着使用符号输入的数字将不打印,例如:

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

但是,将保留尾随的小数零,如本示例所示,即使这些零在语义上对于相等性检查等目的无关紧要。

检查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还有一个存在运算符,它是包含主题的变体:它测试字符串(作为值给出)是否在值的顶层显示为对象键或数组元素。除非另有说明,否则这些示例返回 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 对象比数组更适合测试包含或存在,因为与数组不同,它们内部针对搜索进行了优化,并且不需要线性搜索。

jsonb索引

GIN 索引可用于有效地搜索大量文档(数据)中出现的键或键/值对。提供了两个 GIN“运算符类”,提供了不同的性能和灵活性权衡。

默认的 GIN 运算符类支持使用 key-exists 运算符 、 和 、 包含运算符 、 匹配运算符 和 进行查询。(有关这些运算符实现的语义的详细信息,请参阅表 9.46。使用此运算符类创建索引的示例如下:jsonb??|?&@>jsonpath@?@@

CREATE INDEX idxgin ON api USING GIN (jdoc);

非默认 GIN 运算符类不支持 key-exists 运算符,但支持 、 和 。使用此运算符类创建索引的示例如下: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 的表中,jsonb在名为jdoc的列中。如果在此列上创建了 GIN 索引,则如下所示的查询可以使用该索引:

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

但是,索引不能用于如下所示的查询,因为尽管运算符是可索引的,但它不直接应用于索引列:

-- 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'));

现在,该子句将被识别为可索引运算符对索引表达式的应用。

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

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

列上的简单 GIN 索引可以支持此查询。但请注意,这样的索引将存储列中每个键和值的副本,而上一个示例的表达式索引仅存储在键下找到的数据。虽然简单索引方法要灵活得多(因为它支持对任何键的查询),但与简单索引相比,目标表达式索引可能更小,搜索速度更快。
GIN 索引还支持执行匹配的 and 运算符。例如:@?@@jsonpath

SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @? '$.tags[*] ? (@ == "qui")';
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';

对于这些运算符,GIN 索引从模式中提取表单子句,并根据这些子句中提到的键和值执行索引搜索。访问器链可以包括 、 和 访问器。operator 类还支持 和访问器,但 operator 类不支持。accessors_chain = constantjsonpath.key[][index]jsonb_ops..**jsonb_path_ops

尽管运算符类仅支持使用 和 运算符的查询,但与默认运算符类相比,它具有显着的性能优势。索引通常比相同数据的索引小得多,并且搜索的特异性更好,尤其是当查询包含数据中频繁出现的键时。因此,搜索操作通常比使用默认运算符类的性能更好。jsonb_path_ops@>@?@@jsonb_opsjsonb_path_opsjsonb_ops

GIN索引和GIN索引之间的技术区别在于,前者为数据中的每个键和值创建独立的索引项,而后者仅为数据中的每个值创建索引项。jsonb_opsjsonb_path_ops[7]基本上,每个索引项都是值和导致它的键的哈希值;例如,要索引,将创建一个索引项,将 、 和 这三个项合并到哈希值中。因此,查找此结构的包含查询将导致极其具体的索引搜索;但是根本没有办法确定是否显示为密钥。另一方面,索引将创建三个索引项,分别表示 、 和 ;然后,要执行包含查询,它将查找包含所有这三个项的行。虽然 GIN 索引可以相当有效地执行此类 AND 搜索,但它仍然不如等效搜索具体且速度慢,尤其是在有大量行包含三个索引项中的任何一个时。jsonb_path_ops{“foo”: {“bar”: “baz”}}foobarbazfoojsonb_opsfoobarbazjsonb_path_ops

该方法的缺点是,它不会为不包含任何值的 JSON 结构生成索引条目,例如 .如果请求搜索包含此类结构的文档,则需要进行全索引扫描,这非常慢。 因此,不适合经常执行此类搜索的应用程序。jsonb_path_ops{“a”: {}}jsonb_path_ops

jsonb还支持和索引。仅当检查完整 JSON 文档的相等性很重要时,这些通常才有用。基准的排序很少引起人们的极大兴趣,但为了完整起见,它是:

btreehashbtreejsonb

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 数据类型。jsonbpathjsonb_setjsonb

UPDATE语句可以在子句中使用下标来修改值。对于所有受影响的值,只要它们存在,下标路径必须是可遍历的。例如,路径可以一直遍历到 if 每个 , , 和一个对象。如果有或未定义,它将被创建为空对象,并根据需要填充。但是,如果任何本身或中间值之一被定义为非对象(如字符串、数字或),则遍历无法继续,因此会引发错误并中止事务。SETjsonbval[‘a’][‘b’][‘c’]cvalval[‘a’]val[‘a’][‘b’]val[‘a’]val[‘a’][‘b’]valjsonbnull

下标语法示例:

-- 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通过下标分配处理一些边缘情况的方式与 不同。当源值为 时,通过下标进行赋值将继续进行,就好像它是下标键所隐含的类型(对象或数组)的空 JSON 值一样:jsonb_setjsonbNULL

-- 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';

如果为包含太少元素的数组指定了索引,则将追加元素,直到可访问索引并可以设置值。

-- 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';

只要要遍历的最后一个现有元素是对象或数组,值就会接受对不存在的下标路径的赋值,正如相应的下标所暗示的那样(路径中最后一个下标指示的元素不会遍历,可以是任何东西)。将创建嵌套数组和对象结构,在前一种情况下为 -padded,由下标路径指定,直到可以放置分配的值。

-- 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 的扩展称为 和 。如果使用它们,则值将根据需要映射到 Perl 数组、哈希和标量。jsonb_plperljsonb_plperlujsonb

PL/Python 的扩展称为 .如果使用它,则值将根据需要映射到 Python 字典、列表和标量。jsonb_plpython3ujsonb

在这些扩展中,被认为是“受信任的”,也就是说,它可以由对当前数据库具有权限的非超级用户安装。其余的需要超级用户权限才能安装。jsonb_plperlCREATE

jsonpath类型

该类型在 PostgreSQL 中实现了对 SQL/JSON 路径语言的支持,以有效地查询 JSON 数据。它提供了已解析的 SQL/JSON 路径表达式的二进制表示形式,该表达式指定路径引擎要从 JSON 数据中检索的项目,以便使用 SQL/JSON 查询函数进行进一步处理。jsonpath

SQL/JSON 路径谓词和运算符的语义通常遵循 SQL。同时,为了提供一种自然的方式来处理 JSON 数据,SQL/JSON 路径语法使用了一些 JavaScript 约定:

点 () 用于成员访问。.

方括号 () 用于数组访问。[]

SQL/JSON 数组是相对于 0 的,这与从 1 开始的常规 SQL 数组不同。

SQL/JSON 路径表达式中的数值文字遵循 JavaScript 规则,这些规则在一些小细节上与 SQL 和 JSON 不同。例如,SQL/JSON 路径允许 和 ,这在 JSON 中无效。支持非十进制整数文字和下划线分隔符,例如 、 、 、 。在 SQL/JSON 路径中(在 JavaScript 中,但在 SQL 中则不然),基数前缀后面不得有下划线分隔符。.11.1_000_0000x1EEE_FFFF0o2730b100101

SQL/JSON 路径表达式通常以 SQL 字符串文字的形式写入 SQL 查询,因此它必须用单引号括起来,并且值中所需的任何单引号都必须加倍(参见第 4.1.2.1 节)。某些形式的路径表达式需要其中的字符串文字。这些嵌入的字符串文字遵循 JavaScript/ECMAScript 约定:它们必须用双引号括起来,并且可以在其中使用反斜杠转义来表示其他难以键入的字符。特别是,在嵌入的字符串文字中编写双引号的方法是 ,而要编写反斜杠本身,必须编写 。其他特殊的反斜杠序列包括 JSON 字符串中识别的反斜杠序列:、、、反斜杠语法还包括 JSON 不允许的两种情况:仅用两个十六进制数字编写的字符代码,以及用 1 到 6 个十六进制数字编写的字符代码。"\\b\f\n\r\t\v\uNNNN\xNN\u{N…}

路径表达式由一系列路径元素组成,这些元素可以是以下任意元素:

  1. JSON 基元类型的路径文本:Unicode 文本、数字、true、false 或 null。
  2. 表 8.24 中列出的路径变量。
  3. 表 8.25 中列出的访问器运算符。
  4. jsonpath第 9.16.2.2 节中列出的运算符和方法。

括号,可用于提供筛选器表达式或定义路径计算的顺序。

有关将表达式用于 SQL/JSON 查询函数的详细信息,请参见Section 9.16.2。jsonpath

表 8.24.JSONPash路径变量

变量描述
$一个变量,表示正在查询的 JSON 值(上下文项)。
$varname命名变量。它的值可以通过几个JSON处理函数的参数vars来设置;
@一个变量,表示筛选器表达式中的路径计算结果。
访问器运算符描述
.key
.“$varname”
返回具有指定键的对象成员的成员的成员。如果键名称与某个以 JavaScript 开头的变量匹配或不符合标识符的 JavaScript 规则,则必须将其括在双引号中,以使其成为字符串文本。$
.*通配符成员访问器,返回位于当前对象顶层的所有成员的值。
.**递归通配符成员访问器,用于处理当前对象的 JSON 层次结构的所有级别,并返回所有成员值,而不考虑其嵌套级别。这是 SQL/JSON 标准的 PostgreSQL 扩展。
.{level}
.
{start_level to end_level}
与 类似,但仅选择 JSON 层次结构的指定级别。嵌套级别指定为整数。级别零对应于当前对象。若要访问最低嵌套级别,可以使用关键字。这是 SQL/JSON 标准的 PostgreSQL 扩展。.**last
[subscript, …]数组元素访问器。 可以采用两种形式给出:或 .第一种形式通过其索引返回单个数组元素。第二种形式按索引范围返回数组切片,包括与提供的start_index和end_index对应的元素。subscriptindexstart_index to end_index,指定的索引可以是整数,也可以是返回单个数值的表达式,该值会自动转换为整数。索引零对应于第一个数组元素。还可以使用关键字来表示最后一个数组元素,这对于处理未知长度的数组很有用。last
[*]返回所有数组元素的通配符数组元素访问器。

为此,术语“值”包括数组元素,尽管 JSON 术语有时将数组元素与对象中的值不同。

文章来源:https://blog.csdn.net/sinat_36528886/article/details/135174620
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。