开发人员喜欢使用 JSON,当他们存储数据时,通常不会对其进行规范化。 因此,在数据库中遇到 JSON(b) 字段并不罕见。 对于数据分析师、数据科学家和机器学习工程师来说,取消嵌套或扁平化,数据往往是后续分析的先决条件。
整理数据库中的 JSON 列可能具有挑战性。 通常目标是将 JSON 数组扩展到新行并将 JSON 对象取消嵌套到新列中。 幸运的是,Postgres 有几个内置函数,可以组合这些函数来处理数据库中复杂 JSON 的规范化。
1 数据集
- 下表说明了一个常见的场景,其中一个表(我们将其称为城市)包含一个 JSON 或 JSON(b) 列(即坐标)。
1 2 3 4 5 6 7 8 9 10
| +---+-------------------+-------+---------------------------------+ |idx|name |country|coordinates | +---+-------------------+-------+---------------------------------+ |1 |Sant Julià de Lòria|AD |{"lat": 42.46372, "lng": 1.49129}| |2 |Pas de la Casa |AD |{"lat": 42.54277, "lng": 1.73361}| |3 |Ordino |AD |{"lat": 42.55623, "lng": 1.53319}| |4 |les Escaldes |AD |{"lat": 42.50729, "lng": 1.53414}| |5 |la Massana |AD |{"lat": 42.54499, "lng": 1.51483}| |6 |Encamp |AD |{"lat": 42.53474, "lng": 1.58014}| +---+-------------------+-------+---------------------------------+
|
- 虽然数据存储为 JSON 对象,但数据分析师、数据科学家或 ML 工程师更喜欢将 lat 和 lng 规范化为列。 幸运的是,我们可以使用 Postgres 内置的
jsonb_to_record
函数来构造 JSON 对象。
2 使用 jsonb_to_record 展平 JSON 对象
-
如果相关列包含 JSON(b) 对象,你可以使用内置函数 jsonb_to_record
(或 json_to_record
)将键值对规范化为列。 该函数接受一个 JSON 对象或包含 JSON 对象的列,并返回一条记录。 通过在 AS 表达式中匹配用户提供的复合类型来扩展记录。 复合类型表达式只是 JSON 对象键名(区分大小写)和用于它的 Postgres 数据类型。 JSON 对象可以包含比您在复合类型中定义的更多的键值对,它们将被排除。 如果复合类型定义了 JSON 对象中缺少的键,则它的值为 null。
-
Postgres 文档显示了如何在给定 JSON 对象的情况下使用 json_to_record
:
1 2 3
| SELECT * FROM json_to_record('{"a":1,"b":[1,2,3],"c":"bar"}') AS x(a int, b text, d text)
|
1 2 3
| a | b | d ---+---------+--- 1 | [1,2,3] |
|
-
在此示例中,该函数接收一个 JSON 对象 {“a”:1,“b”:[1,2,3],“c”:“bar”}。 用户定义的复合类型 x 定义键 a、b 和 d 的映射。 因为 JSON 对象包含键 a 和 b,并且数据类型与复合类型中的数据类型匹配,所以它们被提取为记录的一部分。 复合类型没有为 c 定义映射,所以它不是从对象中提取的。 此外,复合类型定义了对象中缺少的 d,因此未被提取。
-
文档中的示例很好,但通常我们希望在表列而不是字符串中取消嵌套 JSON。 幸运的是,我们可以在包含 JSON 对象的列上使用相同的 json_to_record 和 jsonb_to_record 函数。
-
以上面的城市表为例,我们可以将坐标列中的 JSON 对象展平。
1 2 3 4 5 6 7 8 9
| SELECT city.idx, city."name", city.country, coord.lat, coord.lng FROM city, jsonb_to_record(coordinates) AS coord(lat numeric, lng numeric);
|
1 2 3 4 5 6 7 8 9 10
| +---+-------------------+-------+--------+-------+ |idx|name |country|lat |lng | +---+-------------------+-------+--------+-------+ |1 |Sant Julià de Lòria|AD |42.46372|1.49129| |2 |Pas de la Casa |AD |42.54277|1.73361| |3 |Ordino |AD |42.55623|1.53319| |4 |les Escaldes |AD |42.50729|1.53414| |5 |la Massana |AD |42.54499|1.51483| |6 |Encamp |AD |42.53474|1.58014| +---+-------------------+-------+--------+-------+
|
- 在此示例中,我们在坐标列上使用 jsonb_to_record 将对象展平为两列。 复合类型坐标匹配对象键和数据类型。 然后,我们可以在 SELECT 语句中引用与复合类型匹配的记录,以将 lat 和 lng 作为单独的列返回。
3 使用 jsonb_to_recordset 取消嵌套 JSON 数组
-
如果你的数据是 JSON 数组而不是 JSON 对象,则不能使用 jsonb_to_record
对其进行规范化。 相反,您想使用相关函数 jsonb_to_recordset
。
-
内置函数 json_to_recordset
和 jsonb_to_recordset
与对应的 json_to_record
和 jsonb_to_record
非常相似。 不同之处在于 *_to_recordset
函数对 JSON 数组而不是 JSON 对象进行操作。
-
为了说明这一点,请考虑下表(称为国家/地区)。 该表与上面的城市表类似,只是每个城市一行,每个国家一行。 cities 列包含一个 JSON 数组,其中每个条目都是一个城市,存储为 JSON 对象。
1 2 3 4 5 6 7 8 9 10
| +---+------------+---------------------------------------------------------------+ |idx|country_name|cities | +---+------------+---------------------------------------------------------------+ |1 |AU |[{"name": "York", "coordinates": {"lat": -31.88809, "lng": 1...| |2 |AT |[{"name": "Neu-Guntramsdorf", "coordinates": {"lat": 48.0642...| |3 |AR |[{"name": "Zárate", "coordinates": {"lat": -34.09814, "lng":...| |4 |AG |[{"name": "Saint John’s", "coordinates": {"lat": 17.12096, "...| |5 |AO |[{"name": "Saurimo", "coordinates": {"lat": -9.66078, "lng":...| |6 |AQ |[{"name": "McMurdo Station", "coordinates": {"lat": -77.846,...| +---+------------+---------------------------------------------------------------+
|
- 在 cities 列上使用 jsonb_to_recordset 函数,我们可以将 JSON 数组扩展为单独的行。 和以前一样,我们提供了一个包含键名和数据类型的复合类型。
1 2 3 4 5 6 7
| SELECT idx, country_name, city.name, city.coordinates FROM country, jsonb_to_recordset(cities) AS city(name text, coordinates jsonb);
|
1 2 3 4 5 6 7 8 9 10
| +---+------------+---------+------------------------------------+ |idx|country_name|name |coordinates | +---+------------+---------+------------------------------------+ |1 |AU |York |{"lat": -31.88809, "lng": 116.7678} | |1 |AU |Yanchep |{"lat": -31.54678, "lng": 115.63171}| |1 |AU |Yallingup|{"lat": -33.64592, "lng": 115.03514}| |1 |AU |Wundowie |{"lat": -31.76163, "lng": 116.3799} | |1 |AU |Wooroloo |{"lat": -31.8038, "lng": 116.31311} | |1 |AU |Woodville|{"lat": -34.88333, "lng": 138.55} | +---+------------+---------+------------------------------------+
|
- 请注意每个城市的 idx 值(国家/地区索引列)重复。
4 展平嵌套的 JSON
- 在前面的示例中,复合类型使用 jsonb 类型作为坐标键。 因此,查询将坐标作为 JSONB 列返回。 鉴于我们的目标是扁平化 JSON 数据,我们可以在之前的查询的基础上构建以返回单独的 lat 和 lng 列。
1 2 3 4 5 6 7 8 9
| SELECT idx, country_name, city.name, coord.lat, coord.lng FROM country, jsonb_to_recordset(cities) AS city(name text, coordinates jsonb), jsonb_to_record(coordinates) AS coord(lat numeric, lng numeric);
|
1 2 3 4 5 6 7 8 9 10
| +---+------------+---------+---------+---------+ |idx|country_name|name |lat |lng | +---+------------+---------+---------+---------+ |1 |AU |York |-31.88809|116.7678 | |1 |AU |Yanchep |-31.54678|115.63171| |1 |AU |Yallingup|-33.64592|115.03514| |1 |AU |Wundowie |-31.76163|116.3799 | |1 |AU |Wooroloo |-31.8038 |116.31311| |1 |AU |Woodville|-34.88333|138.55 | +---+------------+---------+---------+---------+
|
- 同时使用
jsonb_to_recordset
和 jsonb_to_record
,我们能够展平 JSON 数据,以便每个城市有一行,重复国家数据。
5 结束语
在数据库中争论 JSON 的数据分析师、数据科学家和 ML 工程师可以使用 Postgres 的内置函数来规范化数据。 通过将这些函数组合在一起,可以将复杂的 JSON 扩展为新的行和列,以便它们可以在下游用于数据探索、数据分析和构建模型。
原文链接:Flattening JSON(b) in Postgres