在mysql未支持json數據類型時,我們通常使用varchar、blob或text的數據類型存儲json字元串,對mysql來說,用戶插入的數據只是序列化後的一個普通的字元串,不會對JSON文檔本身的語法合法性做檢查,文檔的合法性需要用戶自己保證。在使用時需要先將整個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)
- value包含 object、array、literal、number、string和custom-data六種類型,與type的16種類型對應。
- object表示json對象類型,由6部分組成:
- 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. - value-entry與key-enter功能類似,不同之處在於,value-entry可能存儲真實的value值。
value-entry由兩部分組成:
value-entry ::= type offset-or-inlined-value
其中:
type表示value類型,如上文所示,支持16種基本類型,從而可以表示各種類型的嵌套。 - 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
我們來使用示意圖更清晰的展示它的結構:
舉例說明:
需要註意的是:
- 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> 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} =
- 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 總結
- JSON類型無須預定義欄位,適合拓展信息的存儲
- 單個JSON文檔的大小不能超過4G;單個KEY的大小不能超過兩個位元組,即64K
- JSON類型適合應用於不常更新的靜態數據
- 對搜索較頻繁的數據建議增加虛擬列並建立索引
作者:京東物流 王鳳輝
來源:京東雲開發者社區 自猿其說Tech