天天看點

PgSQL · 特性分析· jsonb類型解析

pg 9.4版本裡面,增強了對json資料的支援,受到了很大關注。9.4之前,pg已經原生支援json資料類型了,但隻是用字元串的形式存儲和處理。這樣做天然有性能上的缺點:每次對json字元串裡面的資料進行查詢,一般需要全表掃描加字元串比對,效率很低。當然也可以在存儲json的字元串字段上建立gin索引,但需要對查詢中用到的json的key或value建立單獨索引,造成要被動維護很多索引。是以,這種json類型,隻适用于把pg單純作為資料存儲,隻讀入讀出資料,不對資料進行限定key或value查詢的場景。

pg 9.4中引入了jsonb類型。其特點是,将json資料中的key和value進行解析,轉換為pg的基本資料類型,包括數字,字元串和布爾類型等;同時,增加了對應的gin處理函數,可以将json中的所有key和value轉換為gin索引的key。這樣,隻用一個gin索引,即可實作對所有key或value的條件查詢。下面我們分析一下jsonb的使用方法和核心實作。

<b>使用</b>

建立含jsonb類型的表方法如下所示:

建立gin索引的方法如下:

可以使用下面的查詢得到含有&lt;product, progresql&gt;鍵值對的行:

<b>核心實作</b>

先分析一下jsonb是如何從字元串,變成特殊的二進制形式存入磁盤的。追蹤一下jsonb插入的過程,可以看到pg所調用的函數流程如下。

其中,pg_parse_json先把使用者輸入的字元串,通過編譯器轉換為一個樹形結構(每個節點的類型為jsonbvalue)。然後jsonbvaluetojsonb在這個結構基礎上,轉換為存入磁盤的格式。從convertjsonbobject函數可以看出,轉換為磁盤格式的政策為:從樹形結構的根部開始周遊,遞歸進行廣度優先周遊。對于同一父親下面的子鍵值,将所有鍵名(字元串)長度寫入buffer中預留的頭部,随後将鍵名依次寫入buffer中。最後再以相似的方式寫入鍵所對應的所有值(值如果是json對象,則遞歸調用)。這樣,讀入buffer的頭部,就可以周遊出所有鍵名的位置,得到鍵名。再從讀第一個鍵值開始,讀入對應的值或子鍵,最終得到整個樹(見jsonbiteratornext)。

采用這種存儲方式,jsonb所占用的存儲空間比原來支援的json類型要多一些。其實,jsonb的核心優勢在于快速和靈活的索引。從前面建立index的語句可以看到,jsonb支援兩種特有的gin索引jsonb_ops和jsonb_path_ops。我們知道,gin索引建立時,會先通過内建函數從表中每行資料的索引字段的值中,抽取鍵(key),一個字段值一般可抽取多個key。然後,将每個key與含有此key的所有行的id組成鍵值對,再将它們插入b樹索引供查詢。那麼這兩種gin索引有什麼差別呢?

它們的差別在于,生成gin key的方式不同。jsonb_ops調用gin_extract_jsonb函數生成key,這樣每個字段的json資料中的所有鍵和值都被轉成gin的key;而jsonb_path_ops使用函數gin_extract_jsonb_path抽取:如果将一個jsonb類型的字段值看做一顆樹,葉子節點為具體的值,中間節點為鍵,則抽取的每個鍵值實際上時每個從根節點到葉子節點的路徑對應的hash值。

不難推測,jsonb_path_ops索引的key的數目和jsonb的葉子節點數有關,用葉子節點的路徑做查詢條件時會比較快(這也是這種索引唯一支援的查詢方式);而jsonb_ops索引的key的數目與jsonb包含的鍵和值(即樹形結構的所有節點)的總數有關,可以用于路徑查詢之外的其他查詢。