PostgreSQL把SQL查询结果转换为JSON类型
背景
数据仓库在DWS层对数据进行汇总的时候,经常会出现数据一对多的情况。这个时候会使用父子表的形式存储数据。但是在构造Data API的过程中,往往会使用JSON格式,把多条子信息整合到父信息上,最终形成一条JSON Document。
传统的做法是,使用Python,Java等程序语言对父子数据进行查询,然后在中间层转换封装,构造对应的JSON数据。
因为PG对JSON有很完善的支持,所以我们可以采用另一种方案,直接在父表里面增加JSONB格式的字段。这种方案在ETL过程中进行最终数据构造,隐藏了查询过程的复杂性,一次构造,多次使用。
参考 PostgreSQL 11 官方 API 文档 : JSON Functions and Operators
构造原始数据
我们模拟一个场景
- 父表是车辆基础信息表,一辆车有唯一的一条记录;
- 子表是车辆运输过的线路统计,一辆车加一条线路有唯一的一条记录;
- 最终返回JSON对象,一辆车一条数据,线路信息作为子元素嵌套;
-- 车辆表
drop table if exists t_2js_truck;
create table demo.t_2js_truck (id int, vehicle_no varchar(50), update_time timestamp) ;
insert into demo.t_2js_truck values(1, 'A001', '2019-01-01 01:02:03');
insert into demo.t_2js_truck values(2, 'A002', '2019-01-02 01:02:03');
-- 线路表,逻辑外键是 truck_id
drop table if exists t_2js_truck_route;
create table demo.t_2js_truck_route (id int, truck_id int, route_from varchar(50), route_to varchar(50), cnt int);
insert into demo.t_2js_truck_route values(10, 1, '四川省成都市', '云南省昆明市', 9);
insert into demo.t_2js_truck_route values(11, 1, '四川省广汉市', '云南省昆明市', 5);
insert into demo.t_2js_truck_route values(12, 1, '陕西省西安市', '四川省广汉市', 2);
DB2JSON场景
单表单行转JSON对象
使用row_to_json()和jsonb_build_object()把查询结果集一比一转换成JSON
-- row_to_json() 必须把所有字段都转换
select row_to_json(t.*) as json_obj from demo.t_2js_truck t;
-- jsonb_build_object() 有选择的转换一部分字段
select jsonb_build_object('id', t.id, 'vehicle_no', t.vehicle_no, 'update_time', t.update_time ) as json_obj from demo.t_2js_truck t;
-- 上面两条SQL都转换了所有字段,所以结果是一样的
{"id":1,"vehicle_no":"A001","update_time":"2019-01-01T01:02:03"}
{"id":2,"vehicle_no":"A002","update_time":"2019-01-02T01:02:03"}
(2 rows)
单表多行转JSON数组
我们需要把线路表中同一车辆的所有线路信息,转换成一个json数组。结合使用jsonb_agg()和jsonb_build_object()实现。
-- 多条记录整合成 json array
SELECT
jsonb_agg (
jsonb_build_object ( 'route_from', r.route_from, 'route_to', r.route_to, 'cnt', r.cnt )
) as json_arr
FROM
demo.t_2js_truck_route r;
[{"route_from" : "四川省成都市", "route_to" : "云南省昆明市", "cnt" : 9}, {"route_from" : "四川省广汉市", "route_to" : "云南省昆明市", "cnt" : 5}, {"route_from" : "陕西省西安市", "route_to" : "四川省广汉市", "cnt" : 2}]
(1 row)
子表多行转主表JSON字段
我们需要把子表中记录的多条线路信息,集中整合到对应的车辆信息中。这个案例里面,我们保持数据集的表格结构,仅仅是把线路信息作为表格的一个json字段。
-- 通过嵌套子查询,把车辆信息和多条线路信息整合成一条记录
-- 结果格式是表格,只是带有json格式的字段
-- coalesce()保证了即使没有对应的线路信息,也会生成一个空数组
select
t.id, t.vehicle_no, t.update_time,
(select coalesce(jsonb_agg(json_build_object('route_from', r.route_from, 'route_to', r.route_to, 'cnt', r.cnt)) , '[]')
from demo.t_2js_truck_route r where r.truck_id= t.id
) as routes
from demo.t_2js_truck t ;
id | vehicle_no | update_time | routes
1 | A001 | 2019-01-01 01:02:03 | [{"cnt": 9, "route_to": "云南省昆明市", "route_from": "四川省成都市"}, {"cnt": 5, "route_to": "云南省昆明市", "route_from": "四川省广汉市"}, {"cnt": 2, "route_to": "四川省广汉市", "route_from": "陕西省西安市"}]
2 | A002 | 2019-01-02 01:02:03 | []
(2 rows)
主表子表整合转JSON对象
上一个场景中,我们最后得到的结果是表格结构,但在对外提供数据的时候需要使用更方便的JSON对象。使用jsonb_build_object()对刚才的结果做一些简单的调整即可。
-- 通过嵌套子查询,把车辆信息和线路信息整合成一条记录
-- 结果格式json对象
select
jsonb_build_object(
'id', t.id, 'vehicle_no', t.vehicle_no, 'update_time', t.update_time,
'routes' , (select coalesce(jsonb_agg(jsonb_build_object('route_from', r.route_from, 'route_to', r.route_to, 'cnt', r.cnt)) , '[]')
from demo.t_2js_truck_route r where r.truck_id= t.id )
) as json_obj
from demo.t_2js_truck t
;
{"id": 1, "routes": [{"cnt": 9, "route_to": "云南省昆明市", "route_from": "四川省成都市"}, {"cnt": 5, "route_to": "云南省昆明市", "route_from": "四川省广汉市"}, {"cnt": 2, "route_to": "四川省广汉市", "route_from": "陕西省西安市"}], "vehicle_no": "A001", "update_time": "2019-01-01T01:02:03"}
{"id": 2, "routes": [], "vehicle_no": "A002", "update_time": "2019-01-02T01:02:03"}
(2 rows)
JSON2DB场景
如果PG中存储了嵌套JSON格式的数据,我们也可以转换成标准的表格结构。
drop table if exists demo.t_2js_truck_stat;
create table demo.t_2js_truck_stat (id int, vehicle_no varchar(50), update_time timestamp, routes jsonb) ;
-- 我们把刚才查询的结果保存到表t_2js_truck_stat里面
insert into demo.t_2js_truck_stat
select
t.id, t.vehicle_no, t.update_time,
(select coalesce(jsonb_agg(jsonb_build_object('route_from', r.route_from, 'route_to', r.route_to, 'cnt', r.cnt)) , '[]')
from demo.t_2js_truck_route r where r.truck_id= t.id
) as routes
from demo.t_2js_truck t
;
拆分JSON数组为多行
使用jsonb_array_elements()来把json列表拆分成多条数据,并且可以和非json字段的内容进行类似于join的操作。比如下面的例子,我们模拟了车辆A001关联对应的三条线路数据。
select ts.id, ts.vehicle_no,
jsonb_array_elements(ts.routes) as route
from demo.t_2js_truck_stat ts ;
复制代码
id | vehicle_no | route
----+------------+----------------------------------------------------------------------
1 | A001 | {"cnt": 9, "route_to": "云南省昆明市", "route_from": "四川省成都市"}
1 | A001 | {"cnt": 5, "route_to": "云南省昆明市", "route_from": "四川省广汉市"}
1 | A001 | {"cnt": 2, "route_to": "四川省广汉市", "route_from": "陕西省西安市"}
(3 rows)
注意:这种方式只能模拟inner join。如上图结果中,因为车辆A002的routes字段json array为空,所以最后结果中没有显示A002的数据。
获取JSON内容
使用 -> 和 ->> 直接把json字段内容取出来
-- 更进一步,使用表格返回全部字段
select ts.id, ts.vehicle_no,
(jsonb_array_elements(ts.routes)->'cnt')::int as cnt ,
jsonb_array_elements(ts.routes)->>'route_from' as route_from ,
jsonb_array_elements(ts.routes)->>'route_to' as route_to
from demo.t_2js_truck_stat ts ;
id | vehicle_no | cnt | route_from | route_to
----+------------+-----+--------------+--------------
1 | A001 | 9 | 四川省成都市 | 云南省昆明市
1 | A001 | 5 | 四川省广汉市 | 云南省昆明市
1 | A001 | 2 | 陕西省西安市 | 四川省广汉市
(3 rows)
评论