PostgreSQL處理JSON入門

背景環境

做爲一種簡單易用的非結構化數據,JSON格式的應用場景很是普遍。在當前的大數據環境下,處理非結構化數據的需求愈來愈頻繁,咱們是否是必須用MongoDB這一類NoSQL的解決方案?強大的PostgreSQL數據庫,在RDBMS的基礎上提供了對JSON的完善支持,不須要MongoDB也能夠玩轉JSON。sql

PostgreSQL-9.2中引入了對JSON類型的支持,通過幾個大版本的進化,目前對JSON數字類型的支持已經比較完善。在PG中對JSON格式信息的CRUD操做,針對具體的節點創建索引,這些均可以很容易的實現。數據庫

本次咱們測試在PG中使用JSON的常見場景,軟件環境以下express

CentOS 7 x64json

PostgreSQL 11.1數組

兩種數據類型

PG中提供了兩種不一樣的數據類型,分別是JSONJSONB。顧名思義,JSON是存儲字符串的原始格式,而JSONB是二進制編碼版本。JSON須要存儲包括空格等原始格式,因此在每次查詢的時候都會有解析過程。而JSONB查詢時不須要實時解析,因此更高效。函數

簡而言之,JSON 爲了準確存儲,插入快查詢慢;JSONB 爲了高效查詢,插入慢檢索快。測試

若是沒有特殊理由,最好使用JSONB類型。大數據

-- 使用 JSONB 字段類型(無特殊需求不要使用JSON類型)
drop table if exists demo.j_waybill;
create table demo.j_waybill (id int primary key, data jsonb);

insert into demo.j_waybill(id, data) values(1,
	' { "waybill": 2019000000, "project": "測試項目", "pay_org_name": "ABC製造廠", "driver": { "name": "張三", "mobile": 13800000000 }, "line": { "from": {"province":"河北省", "city":"唐山市", "district":"豐潤區"}, "to": {"province":"四川省", "city":"綿陽市", "district":"市轄區"} }, "payment": { "oil_amount": 1234, "cash_amount": 5678 } } '
);
複製代碼

數據查詢

格式化輸出

-- jsonb_pretty() 函數,打印更可讀的JSON輸出
select jsonb_pretty(w.data) from demo.j_waybill w where w.id = 1;
           jsonb_pretty
-----------------------------------
 {                                +
     "line": {                    +
         "to": {                  +
             "city": "綿陽市",    +
             "district": "市轄區",+
             "province": "四川省" +
         },                       +
         "from": {                +
             "city": "唐山市",    +
             "district": "豐潤區",+
             "province": "河北省" +
         }                        +
     },                           +
     "driver": {                  +
         "name": "張三",          +
         "mobile": 13800000000    +
     },                           +
     "payment": {                 +
         "oil_amount": 1234,      +
         "cash_amount": 5678      +
     },                           +
     "project": "測試項目",       +
     "waybill": 2019000000,       +
     "pay_org_name": "ABC製造廠"  +
 }
(1 row)
複製代碼

提取對象成員

PG提供了兩種類型的查詢語法,分別是用於提取頂級成員的 -> ,和提取嵌套成員的#> 語法。若是僅想取出文本內容,使用 ->> 或 #>> 便可。ui

-- 提取頂級成員, 注意 -> 和 ->> 的區別,後者取出的是文本值
select
     w.data->'waybill' as waybill,
     w.data->'project' as project,
     w.data->>'project' as project_text
 from demo.j_waybill w where w.id = 1;

waybill   |  project   | project_text
------------+------------+--------------
 2019000000 | "測試項目" | 測試項目
(1 row)
複製代碼
-- 指定節點的路徑來提取嵌套成員,仍然有 #> 和 #>> 的區別
select 
	w.data#>'{driver}' as driver, 
	w.data#>>'{driver, name}' as driver_name, 
	w.data#>'{driver, mobile}' as mobile 
from demo.j_waybill w where w.id = 1;

                 driver                  | driver_name |   mobile
-----------------------------------------+-------------+-------------
 {"name": "張三", "mobile": 13800000000} | 張三        | 13800000000
(1 row)
複製代碼

條件篩選

PG提供了特殊的存在判斷符號 ?。這種語法和 is not null 是等價的。編碼

-- 判斷是否存在指定的頂級key
select count(1) from demo.j_waybill w where w.data ? 'waybill';
 count
-------
     1
(1 row)

-- 上一句的等價語句以下
select count(1) from demo.j_waybill w where w.data->'waybill' is not null ;


-- 判斷嵌套中的key是否存在
select count(1) from demo.j_waybill w where w.data->'driver' ? 'mobile';
 count
-------
     1
(1 row)

複製代碼

?| 和 ?& 對 ? 的功能進行擴展,等價於 or 和 and 操做。

-- 多個條件的判斷 ?| 表示or, ?& 表示and
select count(1) from demo.j_waybill w where w.data->'driver' ?| '{"mobile", "addr"}';
複製代碼

除了檢查key的存在以外,還能夠用 @> 符號檢查key:value。

-- ? 僅用來檢查 key 存在,那麼 @> 能夠檢查子串的功能
select count(1) from demo.j_waybill w where w.data @> '{"waybill":2019000000, "project":"測試項目"}';
 count
-------
     1
(1 row)

-- 上一句的等價語句以下
-- PS:數字參數要用to_jsonb(),字符串要用 ->> 提取
select count(1) from demo.j_waybill w 
	where w.data->'waybill' = to_jsonb(2019000000) 
	and w.data->>'project' = '測試項目' ;
	
-- 也可使用類型轉換
select count(1) from demo.j_waybill w 
	where (w.data->'waybill')::numeric = 2019000000 
	and w.data->>'project' = '測試項目' ;
複製代碼

數據更新

新增/合併

-- 合併操做符 || 用來增長新的節點,演示以下
select 
	jsonb_pretty(w.data#>'{line}' || '{"new_line":"增長的"}') as new_line,
	jsonb_pretty(w.data || '{"new_key":"增長的"}') as new_key
from demo.j_waybill w where w.id = 1;

           new_line            |              new_key
-------------------------------+-----------------------------------
 {                            +| {                                +
     "to": {                  +|     "line": {                    +
         "city": "綿陽市",    +|         "to": {                  +
         "district": "市轄區",+|             "city": "綿陽市",    +
         "province": "四川省" +|             "district": "市轄區",+
     },                       +|             "province": "四川省" +
     "from": {                +|         },                       +
         "city": "唐山市",    +|         "from": {                +
         "district": "豐潤區",+|             "city": "唐山市",    +
         "province": "河北省" +|             "district": "豐潤區",+
     },                       +|             "province": "河北省" +
     "new_line": "增長的"     +|         }                        +
 }                             |     },                           +
                               |     "driver": {                  +
                               |         "name": "張三",          +
                               |         "mobile": 13800000000    +
                               |     },                           +
                               |     "new_key": "增長的",         +
                               |     "payment": {                 +
                               |         "oil_amount": 1234,      +
                               |         "cash_amount": 5678      +
                               |     },                           +
                               |     "project": "測試項目",       +
                               |     "waybill": 2019000000,       +
                               |     "pay_org_name": "ABC製造廠"  +
                               | }
(1 row)
複製代碼
-- 操做符能夠用在update語法中
update demo.j_waybill 
	set data = data || '{"new_key":"增長的"}' ;
複製代碼

刪除

-- 刪除整個頂級成員
update demo.j_waybill 
	set data = data-'driver'  ;
	
-- 刪除指定路徑下的成員 
update demo.j_waybill 
	set data = data#-'{driver, mobile}'  ;	
	
-- 同時刪除多個成員 
update demo.j_waybill 
	set data = data#-'{driver, mobile}'#-'{line, to}'  ;		
複製代碼

修改

jsonb_set() 就是設計用來更新單一路徑節點值。參數含義以下:

  1. 第一個就是你要修改的 JSONB 數據類型字段;
  2. 第二個是一個文本數組,用來指定修改的路徑;
  3. 第三個參數是要替換值(能夠是 JSON);
  4. 若是給的路徑不存在,json_set() 默認會建立他;若是想要禁用這個行爲,那就把第四個參數設置成 false;
-- 字符串,要使用雙引號
update demo.j_waybill set data = jsonb_set(data, '{"project"}', '"變動的"' );

-- 數字,要使用to_jsonb()
update demo.j_waybill set data = jsonb_set(data, '{"waybill"}', to_jsonb(100) );

-- 新增簡單元素
update demo.j_waybill set data = jsonb_set(data, '{"new_simple"}', to_jsonb(999) );

-- 增長複雜元素
update demo.j_waybill set data = jsonb_set(data, '{"new_complex"}', '{"foo":"bar", "foo1": 123}');
複製代碼

索引

PG自帶的gin類型索引,能夠支持除了範圍查詢以外的全部JSON操做。咱們用一些例子來進行說明。

-- 創建樣例表
drop table if exists demo.j_cargo;
create table demo.j_cargo (id int primary key, data jsonb);

insert into demo.j_cargo(id, data)
select v.waybill_id, to_jsonb(v)
from (
	select b.waybill_create_time, c.*
		from dwd_lhb.wb_cargo_info as c, dwd_lhb.wb_base_info as b 
	where c.waybill_id = b.waybill_id 
	limit 100000
) as v
;
複製代碼

默認模式

gin有兩種使用模式,默認不帶任何參數。建立index以下

-- 支持除範圍查詢之外的全部查詢
drop index if exists idx_jc_non_ops ;
create index idx_jc_non_ops on demo.j_cargo using gin (data);
複製代碼

判斷指定KEY是否存在的 ?操做,以下

-- 查看執行計劃確認用到索引
explain select * from demo.j_cargo j where j.data ? 'cargo_name';
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=16.77..389.25 rows=100 width=803)
   Recheck Cond: (data ? 'cargo_name'::text)
   ->  Bitmap Index Scan on idx_jc_non_ops  (cost=0.00..16.75 rows=100 width=0)
         Index Cond: (data ? 'cargo_name'::text)
(4 rows)
複製代碼

判斷指定Key:Value是否相等的 @> 操做,以下

-- 判斷值相等,用到索引
explain select * from demo.j_cargo j where j.data @> '{"cargo_name":"尿素"}' ;
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=28.77..401.25 rows=100 width=803)
   Recheck Cond: (data @> '{"cargo_name": "尿素"}'::jsonb)
   ->  Bitmap Index Scan on idx_jc_non_ops  (cost=0.00..28.75 rows=100 width=0)
         Index Cond: (data @> '{"cargo_name": "尿素"}'::jsonb)
(4 rows)
複製代碼

OR操做的值相等判斷

-- PS:多個值or操做也用到索引
explain select * from demo.j_cargo j where j.data @> '{"cargo_name":"尿素"}' or j.data @> '{"cargo_name":"白酒"}';
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=57.60..775.81 rows=200 width=803)
   Recheck Cond: ((data @> '{"cargo_name": "尿素"}'::jsonb) OR (data @> '{"cargo_name": "白酒"}'::jsonb))
   ->  BitmapOr  (cost=57.60..57.60 rows=200 width=0)
         ->  Bitmap Index Scan on idx_jc_non_ops  (cost=0.00..28.75 rows=100 width=0)
               Index Cond: (data @> '{"cargo_name": "尿素"}'::jsonb)
         ->  Bitmap Index Scan on idx_jc_non_ops  (cost=0.00..28.75 rows=100 width=0)
               Index Cond: (data @> '{"cargo_name": "白酒"}'::jsonb)
(7 rows)
複製代碼

jsonb_path_ops 模式

帶有jsonb_path_ops的gin索引,效率比默認高。

-- jsonb_path_ops只支持@>操做符,可是效率高
drop index if exists idx_jc_ops ;
create index idx_jc_ops on demo.j_cargo using gin (data jsonb_path_ops);
複製代碼

查看執行計劃,肯定使用了更高效的索引 idx_jc_ops

explain select * from demo.j_cargo j where j.data @> '{"cargo_name":"尿素"}' ;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=16.77..389.25 rows=100 width=803)
   Recheck Cond: (data @> '{"cargo_name": "尿素"}'::jsonb)
   ->  Bitmap Index Scan on idx_jc_ops  (cost=0.00..16.75 rows=100 width=0)
         Index Cond: (data @> '{"cargo_name": "尿素"}'::jsonb)
(4 rows)
複製代碼

btree索引 - 數字

由於gin索引不支持範圍查詢,因此咱們把有這種需求的字段提出來創建btree索引。在建立的時候,必須進行顯式的類型轉換,以下

-- 支持範圍查詢,把範圍查詢的類型提取出來,建立btree表達式索引
drop index if exists idx_jc_btree_num ;
create index idx_jc_btree_num on demo.j_cargo ( ((data->>'price')::numeric) ); 
複製代碼

使用索引的時候也須要執行類型轉換,以下

explain select * from demo.j_cargo j where (j.data->>'price')::numeric between 10 and 100;
                                                                QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=13.42..1673.22 rows=500 width=803)
   Recheck Cond: ((((data ->> 'price'::text))::numeric >= '10'::numeric) AND (((data ->> 'price'::text))::numeric <= '100'::numeric))
   ->  Bitmap Index Scan on idx_jc_btree_num  (cost=0.00..13.29 rows=500 width=0)
         Index Cond: ((((data ->> 'price'::text))::numeric >= '10'::numeric) AND (((data ->> 'price'::text))::numeric <= '100'::numeric))
(4 rows)
複製代碼

btree索引 - 時間戳

重要:若是直接建立timestamp類型的btree索引,會由於默認的字符串轉時間戳函數不知足IMMUTABLE特性而報錯,錯誤以下

-- Timestamp 錯誤!!! 由於默認的字符串轉時間戳函數不知足immutable
create index idx_jc_btree_ts on demo.j_cargo ( ((data->>'waybill_create_time')::timestamp) );
ERROR:  functions in index expression must be marked IMMUTABLE
複製代碼

正確的作法是,建立一個IMMUTABLE函數進行類型轉換,以下

-- 自定義immutable函數處理時間戳
drop function if exists demo.to_timestamp  ;
create or replace function demo.to_timestamp(text) returns timestamp as $$  
  select $1::timestamp;  
$$ language sql strict immutable;  

--
drop index if exists idx_jc_btree_ts ;
create index idx_jc_btree_ts on demo.j_cargo ( demo.to_timestamp(data->>'waybill_create_time') ); 
複製代碼

在SQL中也須要使用自定義函數才能用到索引,演示以下

-- 自定義函數用到索引
explain select * from demo.j_cargo j where  demo.to_timestamp(j.data->>'waybill_create_time') between '2015-06-27' and '2015-06-28';
                                                                                                                          QUERY PLAN

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------
 Bitmap Heap Scan on j_cargo j  (cost=13.42..1918.22 rows=500 width=803)
   Recheck Cond: ((demo.to_timestamp((data ->> 'waybill_create_time'::text)) >= '2015-06-27 00:00:00'::timestamp without time zone) AND (demo.to_timestamp((data ->> 'waybill_create_time'::text)) <= '201
5-06-28 00:00:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_jc_btree_ts  (cost=0.00..13.29 rows=500 width=0)
         Index Cond: ((demo.to_timestamp((data ->> 'waybill_create_time'::text)) >= '2015-06-27 00:00:00'::timestamp without time zone) AND (demo.to_timestamp((data ->> 'waybill_create_time'::text)) <=
'2015-06-28 00:00:00'::timestamp without time zone))
(4 rows)
複製代碼
-- 不用自定義函數的時候,使用的是filter操做
explain select * from demo.j_cargo j where (j.data->>'waybill_create_time')::timestamp between '2015-06-27' and '2015-06-28';
                                                                                                                                    QUERY PLAN

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------
 Gather  (cost=1000.00..13167.00 rows=500 width=803)
   Workers Planned: 2
   ->  Parallel Seq Scan on j_cargo j  (cost=0.00..12117.00 rows=208 width=803)
         Filter: ((((data ->> 'waybill_create_time'::text))::timestamp without time zone >= '2015-06-27 00:00:00'::timestamp without time zone) AND (((data ->> 'waybill_create_time'::text))::timestamp w
ithout time zone <= '2015-06-28 00:00:00'::timestamp without time zone))
(4 rows)
複製代碼
相關文章
相關標籤/搜索