天天看點

MYSQL中JSON類型介紹

作者:京東雲開發者

1 json對象的介紹

在mysql未支援json資料類型時,我們通常使用varchar、blob或text的資料類型存儲json字元串,對mysql來說,使用者插入的資料隻是序列化後的一個普通的字元串,不會對JSON文檔本身的文法合法性做檢查,文檔的合法性需要使用者自己保證。在使用時需要先将整個json對象從資料庫讀取出來,在記憶體中完成解析及相應的計算處理,這種方式增加了資料庫的網絡開銷并降低處理效率。

從 MySQL 5.7.8 開始,MySQL 支援RFC 7159定義的全部json 資料類型,具體的包含四種基本類型(strings, numbers, booleans,and null)和兩種結構化類型(objects and arrays)。可以有效地通路 JSON文檔中的資料。與将 JSON 格式的字元串存儲在字元串列中相比,該資料類型具有以下優勢:

  • 自動驗證存儲在 JSON列中的 JSON 文檔。無效的文檔會産生錯誤。
  • 優化的存儲格式。存儲在列中的 JSON 文檔被轉換為允許快速讀取文檔元素的内部格式。當讀取 JSON 值時,不需要從文本表示中解析該值,使伺服器能夠直接通過鍵或數組索引查找子對象或嵌套值,而無需讀取文檔中它們之前或之後的所有值。

2 json類型的存儲結構

mysql為了提供對json對象的支援,提供了一套将json字元串轉為結構化二進制對象的存儲方式。json會被轉為二進制的doc對象存儲于磁盤中(在處理JSON時MySQL使用的utf8mb4字元集,utf8mb4是utf8和ascii的超集)。

doc對象包含兩個部分,type和value部分。其中type占1位元組,可以表示16種類型:大的和小的json object類型、大的和小的 json array類型、literal類型(true、false、null三個值)、number類型(int6、uint16、int32、uint32、int64、uint64、double類型、utf8mb4 string類型和custom data(mysql自定義類型),具體可以參考源碼json_binary.cc和json_binary.h進行學習。

下面進行簡單介紹:

type ::=

0x00 | // small JSON object

0x01 | // large JSON object

0x02 | // small JSON array

0x03 | // large JSON array

0x04 | // literal (true/false/null)

0x05 | // int16

0x06 | // uint16

0x07 | // int32

0x08 | // uint32

0x09 | // int64

0x0a | // uint64

0x0b | // double

0x0c | // utf8mb4 string

0x0f // custom data (any MySQL data type)

  1. value包含 object、array、literal、number、string和custom-data六種類型,與type的16種類型對應。
  2. object表示json對象類型,由6部分組成:
  3. object ::= element-count size key-entry value-entry key value

    其中:

    element-count表示對象中包含的成員(key)個數,在array類型中表示數組元素個數。

    size表示整個json對象的二進制占用空間大小。小對象用2Bytes空間表示(最大64K),大對象用4Bytes表示(最大4G)

    key-entry可以了解為一個用于指向真實key值的數組。本身用于二分查找,加速json字段的定位。

    key-entry由兩個部分組成:

    key-entry ::= key-offset key-length

    其中:

    key-offset:表示key值存儲的偏移量,便于快速定位key的真實值。

    key-length:表示key值的長度,用于分割不同key值的邊界。長度為2Bytes,這說明,key值的長度最長不能超過64kb.

  4. value-entry與key-enter功能類似,不同之處在于,value-entry可能存儲真實的value值。

    value-entry由兩部分組成:

    value-entry ::= type offset-or-inlined-value

    其中:

    type表示value類型,如上文所示,支援16種基本類型,進而可以表示各種類型的嵌套。

  5. offset-or-inlined-value:有兩層含義,如果value值足夠小,可以存儲于此,那麼就存儲資料本身,如果資料本身較大,則存儲真實值的偏移用于快速定位。

    key 表示key值的真實值,類型為:key ::= utf8mb4-data,這裡無需指定key值長度,因為key-entry中已經聲明了key的存儲長度。同時,在同一個json對象中,key值的長度總是一樣的。

array表示json數組,array類型主要包含4部分:

array ::= element-count size value-entry value

我們來使用示意圖更清晰的展示它的結構:

MYSQL中JSON類型介紹

舉例說明:

MYSQL中JSON類型介紹
MYSQL中JSON類型介紹
MYSQL中JSON類型介紹
MYSQL中JSON類型介紹

需要注意的是:

  • JSON對象的Key索引(圖中橙色部分)都是排序好的,先按長度排序,長度相同的按照code point排序;Value索引(圖中黃色部分)根據對應的Key的位置依次排列,最後面真實的資料存儲(圖中白色部分)也是如此
  • Key和Value的索引對存儲了對象内的偏移和大小,單個索引的大小固定,可以通過簡單的算術跳轉到距離為N的索引
  • 通過MySQL5.7.16源代碼可以看到,在序列化JSON文檔時,MySQL會動态檢測單個對象的大小,如果小于64KB使用兩個位元組的偏移量,否則使用四個位元組的偏移量,以節省空間。同時,動态檢查單個對象是否是大對象,會造成對大對象進行兩次解析,源代碼中也指出這是以後需要優化的點
  • 現在受索引中偏移量和存儲大小四個位元組大小的限制,單個JSON文檔的大小不能超過4G;單個KEY的大小不能超過兩個位元組,即64K
  • 索引存儲對象内的偏移是為了友善移動,如果某個鍵值被改動,隻用修改受影響對象整體的偏移量
  • 索引的大小現在是備援資訊,因為通過相鄰偏移可以簡單的得到存儲大小,主要是為了應對變長JSON對象值更新,如果長度變小,JSON文檔整體都不用移動,隻需要目前對象修改大小
  • 現在MySQL對于變長大小的值沒有預留額外的空間,也就是說如果該值的長度變大,後面的存儲都要受到影響
  • 結合JSON的路徑表達式可以知道,JSON的搜尋操作隻用反序列化路徑上涉及到的元素,速度非常快,實作了讀操作的高性能
  • MySQL對于大型文檔的變長鍵值的更新操作可能會變慢,可能并不适合寫密集的需求

3 json類型基本操作

3.1 json資料插入

json類型資料插入時有兩種方式,一種是基于字元串格式插入,另一種是基于json_object()函數,在使用json_object()函數隻需按k-v順序,以,符号隔開順序插入即可,MYSQL會自動驗證 JSON 文檔,無效的文檔會産生錯誤。

mysql> CREATE TABLE t1 (jdoc JSON);
Query OK, 0 rows affected (0.20 sec)

mysql> INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}');
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO t1 VALUES('[1, 2,');
ERROR 3140 (22032) at line 2: Invalid JSON text:
"Invalid value." at position 6 in value (or column)  '[1, 2,'.           

當一個字元串被解析并發現是一個有效的 JSON 文檔時,它也會被規範化:具有與文檔中先前找到的鍵重複的鍵的成員被丢棄(即使值不同)。以下第一個sql中通過 JSON_OBJECT()調用生成的對象值不包括第二個key1元素,因為該鍵名出現在值的前面;第二個sql中隻保留了x第一次出現的值:

mysql> SELECT JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def');
+------------------------------------------------------+
| JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def') |
+------------------------------------------------------+
| {"key1": 1, "key2": "abc"}                           |
+------------------------------------------------------+

mysql> INSERT INTO t1 VALUES
     >     ('{"x": 17, "x": "red"}'),
     >     ('{"x": 17, "x": "red", "x": [3, 5, 7]}');

mysql> SELECT c1 FROM t1;
+-----------+
| c1        |
+-----------+
| {"x": 17} |
| {"x": 17} |
+-----------+           

3.2 json合并

MySQL 5.7支援JSON_MERGE()的合并算法,多個對象合并時産生一個對象。

可将多個數組合并為一個數組:

mysql> SELECT JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]');
+-----------------------------------------------------+
| JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]') |
+-----------------------------------------------------+
| [1, 2, "a", "b", true, false]                       |
+-----------------------------------------------------+           

當合并數組與對象時,會将對象轉換為新數組進行合并:

mysql> SELECT JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}');
+------------------------------------------------+
| JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}') |
+------------------------------------------------+
| [10, 20, {"a": "x", "b": "y"}]                 |
+------------------------------------------------+           

如果多個對象具有相同的鍵,則生成的合并對象中該鍵的值是包含鍵值的數組

mysql> SELECT JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}');
+----------------------------------------------------+
| JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}') |
+----------------------------------------------------+
| {"a": [1, 4], "b": 2, "c": 3}                      |
+----------------------------------------------------+           

MySQL 8.0.3(及更高版本)支援兩種合并算法,由函數 JSON_MERGE_PRESERVE()和 JSON_MERGE_PATCH(). 它們在處理重複鍵的方式上有所不同:JSON_MERGE_PRESERVE()保留重複鍵的值(與5.7版本的JSON_MERGE()相同),而 JSON_MERGE_PATCH()丢棄除最後一個值之外的所有值。具體的

  • JSON_MERGE_PRESERVE() 函數接受兩個或多個 JSON 文檔并傳回組合結果。如果參數為兩個object,相同的key将會把value合并為array(即使value也相同,也會合并為array),不同的key則直接合并。如果其中一個參數為json array,則另一個json object整體作為一個元素,加入array結果。
  • JSON_MERGE_PATCH()函數接受兩個或多個 JSON 文檔并傳回組合結果。如果參數為兩個object,相同的key的value将會被後面參數的value覆寫,不同的key則直接合并。如果合并的是數組,将按照“最後一個重複鍵獲勝”邏輯僅保留最後一個參數。
mysql> SELECT JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}');
+-------------------------------------------------------+
| JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}') |
+-------------------------------------------------------+
| {"a": [1, 3], "b": 2, "c": 3}                         |
+-------------------------------------------------------+
1 row in set (0.01 sec)
mysql> SELECT JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}');
+----------------------------------------------------+
| JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}') |
+----------------------------------------------------+
| {"a": 3, "b": 2, "c": 3}                           |
+----------------------------------------------------+
1 row in set (0.02 sec)

mysql> SELECT JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}');
+-----------------------------------------------------------+
| JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}') |
+-----------------------------------------------------------+
| ["a", 1, "a", {"key": "value"}]                           |
+-----------------------------------------------------------+
1 row in set (0.00 sec)
mysql> SELECT JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') ;
+--------------------------------------------------------+
| JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') |
+--------------------------------------------------------+
| {"key": "value"}                                       |
+--------------------------------------------------------+
1 row in set (0.01 sec)           

3.3 json資料查詢

MySQL 5.7.7+本身提供了很多原生的函數以及路徑表達式來友善使用者通路JSON資料。

JSON_EXTRACT()函數用于解析json對象,->符号是就一種JSON_EXTRACT()函數的等價模式。例如查詢上面t1表中 jdoc字段中key值為x的值

SELECT jdoc->'$.x' FROM t1;
SELECT JSON_EXTRACT(jdoc,'$.x') FROM t1;           

JSON_EXTRACT傳回值會帶有” “,如果想擷取原本的值可以使用JSON_UNQUOTE

mysql> SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name');
+---------------------------------------------------------+
| JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name') |
+---------------------------------------------------------+
| "Aztalan"                                               |
+---------------------------------------------------------+

mysql> SELECT JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name'));;
+-----------------------------------------------------------------------+
| JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name')) |
+-----------------------------------------------------------------------+
| Aztalan                                                               |
+-----------------------------------------------------------------------+           

json路徑的文法:

pathExpression:
    scope[(pathLeg)*]

pathLeg:
    member | arrayLocation | doubleAsterisk

member:
    period ( keyName | asterisk )

arrayLocation:
    leftBracket ( nonNegativeInteger | asterisk ) rightBracket

keyName:
    ESIdentifier | doubleQuotedString

doubleAsterisk:
    '**'

period:
    '.'

asterisk:
    '*'

leftBracket:
    '['

rightBracket:
    ']'           

以json { “a”: [ [ 3, 2 ], [ { “c” : “d” }, 1 ] ], “b”: { “c” : 6 }, “one potato”: 7, “b.c” : 8 } 為例:

$.a[1] 擷取的值為 [ { “c” : “d” }, 1 ]

$.b.c 擷取的值為 6

$.”b.c” 擷取的值為 8(因為鍵名包含不合法的表達式是以需要使用引号)

MYSQL中JSON類型介紹
mysql>  select json_extract('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c');
+-------------------------------------------------------------------------------------------------------------------+
| JSON_EXTRACT('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c') |
+-------------------------------------------------------------------------------------------------------------------+
| ["d", 6]                                                                                                          |
+-------------------------------------------------------------------------------------------------------------------+           

$**.c 比對到了兩個路徑 :

$.a[1].c 擷取的值是”d”

$.b.c 擷取的值為 6

3.4 json資料更新

一些函數采用現有的 JSON 文檔,以某種方式對其進行修改,然後傳回結果修改後的文檔。路徑表達式訓示在文檔中進行更改的位置。例如,JSON_SET()、 JSON_INSERT()和 JSON_REPLACE()函數各自采用現有的 JSON 文檔,加上一個或多個路徑和值對,來描述修改文檔和要更新的值。這些函數在處理文檔中現有值和不存在值的方式上有所不同。

具體如下

mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]';           

JSON_SET()替換存在的路徑的值并添加不存在的路徑的值:

mysql> SELECT JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2);
+--------------------------------------------+
| JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2) |
+--------------------------------------------+
| ["a", {"b": [1, false]}, [10, 20, 2]]      |
+--------------------------------------------+           

在這種情況下,路徑$[1].b[0]選擇一個現有值 ( true),該值将替換為路徑參數 ( 1) 後面的值。該路徑$[2][2]不存在,是以将相應的值 ( 2) 添加到 選擇的值中$[2]。

JSON_INSERT()添加新值但不替換現有值:

mysql> SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2);
+-----------------------------------------------+
| JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2) |
+-----------------------------------------------+
| ["a", {"b": [true, false]}, [10, 20, 2]]      |
+-----------------------------------------------+           

JSON_REPLACE()替換現有值并忽略新值:

mysql> SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2);
+------------------------------------------------+
| JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2) |
+------------------------------------------------+
| ["a", {"b": [1, false]}, [10, 20]]             |
+------------------------------------------------+           

JSON_REMOVE()接受一個 JSON 文檔和一個或多個路徑,這些路徑指定要從文檔中删除的值。傳回值是原始文檔減去文檔中存在的路徑選擇的值:

mysql> SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]');
+---------------------------------------------------+
| JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]') |
+---------------------------------------------------+
| ["a", {"b": [true]}]                              |
+---------------------------------------------------+           

$[2]比對[10, 20] 并删除它。

$[1].b[1]比對 元素false中 的第一個執行個體b并将其删除。

不比對的第二個執行個體$[1].b[1]:該元素已被删除,路徑不再存在,并且沒有效果。

3.5 json比較與排序

JSON值可以使用=, <, <=, >, >=, <>, !=, <=>等操作符,BETWEEN, IN,GREATEST, LEAST等操作符現在還不支援。JSON值使用的兩級排序規則,第一級基于JSON的類型,類型不同的使用每個類型特有的排序規則。

JSON類型按照優先級從高到低為

BLOB
BIT
OPAQUE
DATETIME
TIME
DATE
BOOLEAN
ARRAY
OBJECT
STRING
INTEGER, DOUBLE
NULL           

優先級高的類型大,不用再進行其他的比較操作;如果類型相同,每個類型按自己的規則排序。具體的規則如下:

  • BLOB/BIT/OPAQUE: 比較兩個值前N個位元組,如果前N個位元組相同,短的值小
  • DATETIME/TIME/DATE: 按照所表示的時間點排序
  • BOOLEAN: false小于true
  • ARRAY: 兩個數組如果長度和在每個位置的值相同時相等,如果不想等,取第一個不相同元素的排序結果,空元素最小。例:[] < [“a”] < [“ab”] < [“ab”, “cd”, “ef”] < [“ab”, “ef”]
  • OBJECT: 如果兩個對象有相同的KEY,并且KEY對應的VALUE也都相同,兩者相等。否則,兩者大小不等,但相對大小未規定。例:{“a”: 1, “b”: 2} = {“b”: 2, “a”: 1}
  • STRING: 取兩個STRING較短的那個長度為N,比較兩個值utf8mb4編碼的前N個位元組,較短的小,空值最小。例:”a” < “ab” < “b” < “bc”;此排序等同于使用 collation 對 SQL 字元串進行排序utf8mb4_bin。因為 utf8mb4_bin是二進制排序規則,是以 JSON 值的比較區分大小寫:”A” < “a”
  • INTEGER/DOUBLE: 包括精确值和近似值的比較

4 JSON的索引

現在MySQL不支援對JSON列進行索引,官網文檔的說明是:

JSON columns cannot be indexed. You can work around this restriction by creating an index on a generated column that extracts a scalar value from the JSON column.

雖然不支援直接在JSON列上建索引,但MySQL規定,可以首先使用路徑表達式對JSON文檔中的标量值建立虛拟列,然後在虛拟列上建立索引。這樣使用者可以使用表達式對自己感興趣的鍵值建立索引。舉個具體的例子來說明:

ALTER TABLE features ADD feature_street VARCHAR(30) AS (JSON_UNQUOTE(feature->"$.properties.STREET"));
ALTER TABLE features ADD INDEX (feature_street);           

兩個步驟,可以對feature列中properties鍵值下的STREET鍵(feature->”$.properties.STREET”)建立索引。

其中,feature_street列就是新添加的虛拟列。之是以取名虛拟列,是因為與它對應的還有一個存儲列(stored column)。它們最大的差別為虛拟列隻修改資料庫的metadata,并不會存儲真實的資料在硬碟上,讀取過程也是實時計算的方式;而存儲列會把表達式的列存儲在硬碟上。兩者使用的場景不一樣,預設情況下通過表達式生成的列為虛拟列。

這樣虛拟列的添加和删除都會非常快,而在虛拟列上建立索引跟傳統的建立索引的方式并沒有差別,會提高虛拟列讀取的性能,減慢整體插入的性能。虛拟列的特性結合JSON的路徑表達式,可以友善的為使用者提供高效的鍵值索引功能。

5 總結

  1. JSON類型無須預定義字段,适合拓展資訊的存儲
  2. 單個JSON文檔的大小不能超過4G;單個KEY的大小不能超過兩個位元組,即64K
  3. JSON類型适合應用于不常更新的靜态資料
  4. 對搜尋較頻繁的資料建議增加虛拟列并建立索引

作者:京東物流 王鳳輝

來源:京東雲開發者社群 自猿其說Tech