MySQL 5.7 的 JSON 類型

原文: http://nullwy.me/2019/06/mysq...
若是以爲個人文章對你有用,請隨意讚揚

2015 年 8 月,MySQL 5.7.8 開始提供對 JSON 的原生支持 [doc1, doc2 ]。MySQL 對 JSON 的支持能夠說是千呼萬喚始出來。2009 年開始 NoSQL 逐漸流行起來,相繼出現了鍵值對數據庫、文檔數據庫、列族數據庫、圖數據庫等各種 NoSQL,解決經典關係型數據庫沒法解決的痛點。其中,對靈活存儲半結構化數據的需求,使得相似 MongoDB 這類文檔數據庫涌現出來。各大主流關係型數據庫也在響應趨勢,開始支持半結構化數據。早在 2012 年,PostgreSQL 9.2 就已經添加了 JSON 數據類型 [ref ]。Oracle 也在 2014 年 7 月發佈 12c Release 1 後開始支持 JSON [ref1, ref2 ]。Facebook 在 MySQL 5.7 沒發佈以前,對 5.6 版本的 MySQL 添加了存儲 JSON 功能,這個特性被 Facebook 命名爲 DocStore (Document Database for MySQL at Facebook) [doc, slides ]。另外,SQL 標準組織行動也很快,在 2014 年 3 月已經完成了 SQL/JSON 標準草案(DM32.2 SQL/JSON Proposals, part1, part2 [slides ]。完整的草案在 2016 年 12 月正式被採納爲標準,即 SQL:2016php

瀏覽 SQL/JSON 標準草案能夠發現,所有做者共有 9 人,這些做者來自兩個公司,Oracle 和 IBM,而排前面的做者如 Jim Melton, Fred Zemke, Beda Hammerschmidt 都 Oracle 的專家。正由於 SQL:2016 主要就是 Oracle 參與制定的,目前,Oracle 數據庫對 SQL:2016 的支持也是最全的 [ref ]。html

MySQL 對 JSON 的支持,設計文檔主要是 WL#7909: Server side JSON functions,另外還有 WL#8132: JSON datatype and binary storage format、WL#8249: JSON comparator、WL#8607: Inline JSON path expressions in SQL 等。在 MySQL 開始 WL#7909 之時,SQL/JSON 標準草案已經公開,WL#7909 中也說起了這份標準,可是若是拿 MySQL 提供 JSON 的功能與 SQL:2016 比較,能夠發現 MySQL 雖然融入了部分的設計,但並無徹底參考標準,定義的 JSON 函數多數有區別。mysql

回到正題,下面來看下 MySQL 5.7 的 JSON 的用法。git

JSON 函數列表

MySQL 官方列出 JSON 相關的函數,完整列表以下 [doc ]:github

分類 函數 描述
json 建立函數 json_array() 建立 json 數組
json_object() 建立 json 對象
json_quote() 用雙引號包裹 json 文檔
json 查詢函數 json_contains() 判斷是否包含某個 json 值
json_contains_path() 判斷某個路徑下是否包 json 值
json_extract() 提取 json 值
column->path json_extract() 的簡潔寫法,5.7.9 開始支持
column->>path json_unquote(json_extract()) 的簡潔寫法,5.7.13 開始支持
json_keys() 把 json 對象的頂層的所有鍵提取爲 json 數組
json_search() 按給定字符串關鍵字搜索 json,返回匹配的路徑
json 修改函數 json_append() 5.7.9 廢棄,更名爲 json_array_append
json_array_append() 在 josn 文檔末尾添加數組元素
json_array_insert() 在 josn 數組中插入元素
json_insert() 插入值(只插入新值,不替換舊值)
json_merge() 5.7.22 廢棄,與 json_merge_preserve() 同義
json_merge_patch() 合併 json 文檔,重複鍵的值將被替換掉
json_merge_preserve() 合併 json 文檔,保留重複鍵
json_remove() 刪除 json 文檔中的數據
json_replace() 替換值(只替換舊值,不插入新值)
json_set() 設置值(替換舊值,或插入新值)
json_unquote() 移除 json 值的雙引號包裹
json 屬性函數 json_depth() 返回 json 文檔的最大深度
json_length() 返回 json 文檔的長度
json_type() 返回 json 值的類型
json_valid() 判斷是否爲合法 json 文檔
json 工具函數 json_pretty() 美化輸出 json 文檔,5.7.22 新增
json_storage_size() 返回 json 文檔佔用的存儲空間,5.7.22 新增

官方文檔對所有函數都做了充分解釋並提供必定的示例代碼。下文挑選了部分函數,演示它們的使用方法。web

建立與插入 JSON

-- 建立 tbl 表,字段 data 爲 json 類型
mysql> create table tbl (data JSON);
Query OK, 0 rows affected (0.17 sec)

-- 插入 json 對象
mysql> insert into tbl values ('{"id": 1, "name": "Will"}');
Query OK, 1 row affected (0.04 sec)

-- 插入 json 數組
mysql> insert into tbl values ('[1, 42, 1024]');
Query OK, 1 row affected (0.01 sec) 

-- 使用 json_object() 建立 json 對象
mysql> insert into tbl values (json_object('id', 2, 'name', 'Joe'));
Query OK, 1 row affected (0.02 sec)

-- 使用 json_array() 建立 json 數組
mysql> insert into tbl values (json_array(1, "abc", null, true, curtime()));
Query OK, 1 row affected (0.02 sec)

-- 查詢 tbl 表數據
mysql> select * from tbl;
+-------------------------------------------+
| data                                      |
+-------------------------------------------+
| {"id": 1, "name": "Will"}                 |
| [1, 42, 1024]                             |
| {"id": 2, "name": "Andy"}                 |
| [1, "abc", null, true, "20:27:41.000000"] |
+-------------------------------------------+
4 rows in set (0.00 sec)

上面的 SQL 示例簡單驗演示了建立 JSON 列以及寫入並查詢 JSON 數據,比較簡單,就不作解釋了。sql

查詢 JSON

json_extract() 與 -> 操做符

若是要查詢 JSON 文檔中內容,提取 JSON 中的值,可使用 json_extract() 函數。函數定義以下:shell

json_extract(json_doc, path[, path] ...)

先來看下 SQL 示例:數據庫

-- 使用 json_extract() 函數查詢 json 對象
mysql> select json_extract('{"id": 1, "name": "Will"}', '$.name');
+-------------------------------------------------------+
| json_extract('{"id": 1, "name": "Will"}', '$.name')   |
|-------------------------------------------------------|
| "Will"                                                |
+-------------------------------------------------------+
1 row in set (0.01 sec)

示例中的 $.name,使用的是 JSON 路徑語法,用來提取 JSON 文檔的內容。JSON 路徑語法,源自 Stefan Goessner 的 JsonPath,不過 MySQL 做了簡化。路徑語法使用 $ 開頭來表示整個 JSON 文檔。若是要提取部分 JSON 文檔,能夠在路徑後面添加選擇符:express

  • 在路徑 path 後上追加對象的鍵名稱,能夠獲取這個鍵下成員。若是加鍵名稱後,路徑表達式非法,須要對鍵名稱用雙引號包裹(好比,鍵名稱中包含空格的狀況)
  • 在路徑 path 後加上追加 [N],用於選擇數組的第 N 個元素。數組索引從 0 開始。若是 path 下並非數組,path[0] 獲取結果就是 path 自己。
  • 路徑能夠包含 *** 通配符:

    • .[*] 用於獲取 JSON 對象的所有成員。
    • [*] 用於獲取 JSON 數組的所有元素。
    • prefix**suffix 表示所有以 prefix 開始,以 suffix 結尾的路徑。
  • 若是路徑在 JSON 文檔中不存在數據,將返回 NULL

假設 $ 引用的是以下 JSON 數組:

[3, {"a": [5, 6], "b": 10}, [99, 100]]

$[0] 獲取到的值爲 3,$[1] 獲取到 {"a": [5, 6], "b": 10}$[2] 獲取到 [99, 100]$[3] 獲取到 NULL(由於不存在第 4 個元素)。

由於 $[1]$[2] 獲取的並不是純量(nonscalar),它們能夠進一步使用路徑訪問到內嵌的值,好比:$[1].a 獲取到 [5, 6]$[1].a[1] 獲取到 6$[1].b 獲取到 10$[2][0] 獲取到 99

上文提到,若是追加鍵值名後,路徑表達式非法,須要對鍵名稱用雙引號包裹。假設 $ 引用的是以下 JSON 對象:

{"name 1": "Will", "name 2": "Andy"}

兩個鍵都包含空格,須要加上雙引號,才能使用路徑表達式訪問。$."name 1" 將獲取到 Will,而 $."name 2" 將獲取到 Andy

如今來看下通配符的示例,假設 JSON 對象以下:

{"a": {"b": 1}, "c": {"b": 2}, "d": [3, 4, 5]}

使用 $.* 將獲取到 [{"b": 1}, {"b": 2}, [3, 4, 5]]
使用 $.d[*] 將獲取到 [3, 4, 5]
使用 $**.b(對應 $.a.b$.c.b)將獲取到 [1, 2]

MySQL 5.7.9 開始,官方支持 json_extract(column, path) 的簡潔寫法,內聯 JSON 路徑表達式 column->pathWL#8607)。示例以下:

-- 使用內聯 json 路徑表達式,查詢 json 對象
mysql> select * from tbl where data -> '$.id' = 2;  
+---------------------------+
| data                      |
+---------------------------+
| {"id": 2, "name": "Andy"} |
+---------------------------+
1 row in set (0.00 sec)

本質上,這種寫法是語法糖,column->path 等價於 json_extract(column, path),內聯 JSON 路徑表達式會在語法解析階段被轉換爲 json_extract() 調用。另外,column->path,存在如下限制 [ref ]

限制

即,1. 數據源必須是表字段,2. 路徑表達式必須爲字符串,3. SQL 語句中最多隻支持一個。

如今來試驗下這個限制,若是使用內聯 JSON 路徑表達式查詢 MySQL 變量,將會報語法錯誤:

mysql> set @j = '["a", "b"]';

-- 語法錯誤
mysql> select @j -> '$[0]';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '-> '$[0]'' at line 1

json_unquote() 與 ->> 操做符

假設數據以下:

mysql> select * from tbl;
+-----------------------------------------------+
| data                                          |
+-----------------------------------------------+
| {"id": 1, "name": "Will"}                     |
| {"id": 2, "name": "printf(\"hello world\");"} |
+-----------------------------------------------+
2 rows in set (0.00 sec)

來看下使用 -> 提取得到 JSON 值:

mysql> select data -> '$.id', data -> '$.name', substr(data -> '$.name', 1, 1) from tbl;
+----------------+----------------------------+--------------------------------+
| data -> '$.id' | data -> '$.name'           | substr(data -> '$.name', 1, 1) |
+----------------+----------------------------+--------------------------------+
| 1              | "Will"                     | "                              |
| 2              | "printf(\"hello world\");" | "                              |
+----------------+----------------------------+--------------------------------+
2 rows in set (0.00 sec)

mysql> create table tmp (id int, name varchar(50));
mysql> insert tmp select data -> '$.id', data -> '$.name' from tbl;
mysql> select *, substr(name, 1, 1) from tmp;
+------+----------------------------+--------------------+
| id   | name                       | substr(name, 1, 1) |
+------+----------------------------+--------------------+
|    1 | "Will"                     | "                  |
|    2 | "printf(\"hello world\");" | "                  |
+------+----------------------------+--------------------+
2 rows in set (0.01 sec)

能夠看到,對於 string 類型的 JSON 值,使用 json_extract()-> 獲取的都是被雙引號包裹的字符串。MySQL 提供 json_unquote() 函數,用於去掉雙引號包裹。另外,MySQL 支持 column->>path 語法,經過 ->> 操做符獲取純量(scalar)。column->>path 寫法等價於 json_unquote( json_extract(column, path) ) 或者 json_unquote(column -> path)。來看下 SQL 示例:

mysql> select data ->> '$.id' as id, data -> '$.name' as name,
    ->    data ->> '$.name' as name, json_unquote(data -> '$.name') as name from tbl;
+------+----------------------------+------------------------+------------------------+
| id   | name                       | name                   | name                   |
+------+----------------------------+------------------------+------------------------+
| 1    | "Will"                     | Will                   | Will                   |
| 2    | "printf(\"hello world\");" | printf("hello world"); | printf("hello world"); |
+------+----------------------------+------------------------+------------------------+
2 rows in set (0.00 sec)

MySQL 這種區分 ->->> 的寫法,懷疑是源自 Postgres。由於 Postgres 也分別提供了 ->->> 操做符,-> 也是保留雙引號(get JSON object field by key),而 ->> 才能獲取實際的字符串值(get JSON object field as text) [doc, stackoverflow ]。

在筆者看來,這種須要經過 json_unquote() 才能獲取實際字符串值的寫法徹底沒有必要,由於很難想到有須要保留雙引號的使用場景,而就獲取實際的字符串值纔是多數狀況。實際上,SQLite 的開發者也持有相同的想法。2015 年 10 月,SQLite 3.9 發佈,開始支持 JSON 類型 [infoq, doc ]。簡單對比下,能夠發現 SQLite 提供的 JSON 函數和 MySQL 極其類似,不少函數同名而且同語義。SQLite 也提供了 json_extract() 函數,與 MySQL 不一樣,SQLite 返回的是移除雙引號後的字符串(the dequoted text for a JSON string value)。看下示例:

sqlite> select json_extract('{"id": 1, "name": "Will"}', '$.name');
Will
sqlite> select json_extract('{"code": "printf(\"hello world\");"}', '$.code');
printf("hello world");

對於提取 JSON 文檔中的純量(scalar),SQL 標準定義了的 json_value() 函數,MySQL 沒有支持,但 OracleMariaDBMSSQL 都有支持。MariaDB 在兼容 MySQL 的同時也支持 SQL 標準,json_extract() 和 json_value() 在 MariaDB 下均可用。來看下 SQL 示例:

MariaDB [testdb]> select * from tbl;
+-----------------------------------------------+
| data                                          |
+-----------------------------------------------+
| {"id": 1, "name": "Will"}                     |
| {"id": 2, "name": "printf(\"hello world\");"} |
+-----------------------------------------------+
2 rows in set (0.00 sec)

-- 使用 json_extract() 提取 JSON 值,string 類型的值保留雙引號
MariaDB [testdb]> select json_extract(data, '$.id'), json_extract(data, '$.name') from tbl;
+----------------------------+------------------------------+
| json_extract(data, '$.id') | json_extract(data, '$.name') |
+----------------------------+------------------------------+
| 1                          | "Will"                       |
| 2                          | "printf(\"hello world\");"   |
+----------------------------+------------------------------+
2 rows in set (0.00 sec)

-- 使用 json_value() 提取 JSON 值,string 類型的值自動移除雙引號
MariaDB [testdb]> select json_value(data, '$.id'), json_value(data, '$.name') from tbl;
+--------------------------+----------------------------+
| json_value(data, '$.id') | json_value(data, '$.name') |
+--------------------------+----------------------------+
| 1                        | Will                       |
| 2                        | printf("hello world");     |
+--------------------------+----------------------------+
2 rows in set (0.00 sec)

其餘查詢函數

除了上文的 json_extract() 函數,查詢 JSON 文檔相關的還有其餘函數,如 json_contains()、json_contains_path()、json_keys()、json_search()。示例以下:

mysql> set @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
Query OK, 0 rows affected (0.00 sec)

-- 使用 json_contains() 函數判斷是否存在某 JSON 值
mysql> select json_contains(@j, '{"a": 1}');
+-------------------------------+
| json_contains(@j, '{"a": 1}') |
+-------------------------------+
|                            1  |
+-------------------------------+
1 row in set (0.00 sec)

-- 使用 json_contains_path() 函數判斷是否存在某 JSON 路徑
mysql> select json_contains_path(@j, 'one', '$.a', '$.e');
+-----------------------------------------------+
| json_contains_path(@j, 'one', '$.a', '$.e')   |
|-----------------------------------------------|
| 1                                             |
+-----------------------------------------------+
1 row in set (0.00 sec)

-- 使用 json_contains_path() 函數判斷是否存在某 JSON 路徑
mysql> select json_contains_path(@j, 'all', '$.a', '$.e');
+-----------------------------------------------+
| json_contains_path(@j, 'all', '$.a', '$.e')   |
|-----------------------------------------------|
| 0                                             |
+-----------------------------------------------+
1 row in set (0.00 sec)

函數的完整定義和用法能夠參考官方文檔,本文再也不一一舉例說明。

修改 JSON

對於 MySQL 的 JSON 類型的數據,若要修改數據,可使用相似以下的 SQL:

mysql> select * from tbl where data->'$.id' = 2;
+---------------------------+
| data                      |
+---------------------------+
| {"id": 2, "name": "Will"} |
+---------------------------+
1 row in set (0.00 sec)

-- 對 data 整個字段修改
mysql> update tbl set data = '{"id": 2, "name": "Andy"}' where data->'$.id' = 2;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> select * from tbl where data->'$.id'= 2;
+---------------------------+
| data                      |
+---------------------------+
| {"id": 2, "name": "Andy"} |
+---------------------------+
1 row in set (0.00 sec)

若是要修改 JSON 內部數據,是否能夠經過 JSON 路徑表達式直接賦值呢?答案是,不行,MySQL 不支持。

-- 語法錯誤,不支持經過 JSON 路徑表達式賦值,修改 JSON 數據
mysql> update tbl set data->'$.name' = 'Andy' where data->'$.id' = 2;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '->'$.name' = 'Andy' where data->'$.id' = 2' at line 1

MySQL 提供了數個函數來修改 JSON 數據。咱們先來看看 json_replace()、json_set() 和 json_insert() 這三個函數:

  • json_replace():替換值。替換舊值,但不插入新值
  • json_set():設置值。替換舊值,或插入新值
  • json_insert():插入值。只插入新值,不替換舊值

json_insert() 只能插入數據, json_replace() 只能更新數據,json_set() 能更新或插入數據。

替換值,json_replace() 示例:

-- 使用 json_replace() 函數
-- 把 {"id": 2, "name": "Will"} 修改成 {"id": 2, "name": "Andy"}
-- 路徑 $.name 指向的值存在,舊值被替換爲新值
mysql> update tbl
    -> set data = json_replace(data, '$.name', 'Andy')
    -> where data->'$.id' = 2;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1  Changed: 1  Warnings: 0

設置值,json_set() 示例:

-- 使用 json_set() 函數
-- 把 {"id": 2, "name": "Will"} 修改成 {"id": 2, "city": "北京", "name": "Bill"}
-- 路徑 $.name 指向的值存在,舊值被替換爲新值;路徑 $.city 指向的值不存在,將插入新值
mysql> update tbl
    -> set data = json_set(data, '$.name', 'Bill', '$.city', '北京')
    -> where data->'$.id'= 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from tbl where data->'$.id'= 2;
+---------------------------------------------+
| data                                        |
+---------------------------------------------+
| {"id": 2, "city": "北京", "name": "Bill"}   |
+---------------------------------------------+
1 row in set (0.00 sec)

插入值,json_insert() 示例:

-- 使用 json_set() 函數
-- 把 {"id": 2, "name": "Will"} 修改成 {"id": 2, "address": "故宮"}
-- 路徑 $.name 指向的值存在,將不替換這個舊值;路徑 $.address 指向的值不存在,將插入新值
mysql> update tbl
    -> set data = json_insert(data, '$.name', 'Bill', '$.address', '故宮')
    -> where data->'$.id'= 2;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from tbl where data->'$.id'= 2;
+---------------------------------------------------------------------+
| data                                                                |
+---------------------------------------------------------------------+
| {"id": 2, "name": "Will", "address": "故宮"}        |
+---------------------------------------------------------------------+
1 row in set (0.00 sec)

如今,咱們來看下修改 JSON 數組的兩個函數,json_array_insert() 和 json_array_append(),函數定義以下:

json_array_insert(json_doc, path, val[, path, val] ...)
json_array_append(json_doc, path, val[, path, val] ...)

json_array_insert(),參數 path 必須指向 JSON 數組某個位置的元素,若該位置存在值,將會把 val 插入該位置,而後其餘元素向右移動;若該位置超出數組大小範圍,將會把 val 插入到數組末尾。SQL 示例以下:

mysql> set @j = '["a", {"b": [1, 2]}, [3, 4]]';

-- 在數組的索引 1 的位置上插入值 5,本來索引 1 位置上的 {"b": [1, 2]} 被擠到後邊
mysql> select json_array_insert(@j, '$[1]', 5);
+------------------------------------+
| json_array_insert(@j, '$[1]', 5)   |
|------------------------------------|
| ["a", 5, {"b": [1, 2]}, [3, 4]]    |
+------------------------------------+
1 row in set (0.00 sec)

-- 插入位置超出數組大小範圍,將會把值插入到數組末尾
mysql> select json_array_insert(@j, '$[100]', 5);
+--------------------------------------+
| json_array_insert(@j, '$[100]', 5)   |
|--------------------------------------|
| ["a", {"b": [1, 2]}, [3, 4], 5]      |
+--------------------------------------+
1 row in set (0.00 sec)

-- path 指向不是 JSON 數組的元素,SQL 執行報錯
mysql> select json_array_insert(@j, '$[1].b', 5);
(3165, 'A path expression is not a path to a cell in an array.')

json_array_append(),若是參數 path 指向的 JSON 是數組,將在數組末尾添加元素;若是參數 path 指向的 JSON 是值或對象,該值或對象將被包裹爲數組,而後在這個數組末尾添加元素。

mysql> set @j = '["a", {"b": [1, 2]}, [3, 4]]';

-- path 指向的 JSON 是數組,將在數組末尾添加元素
mysql> select json_array_append(@j, '$', 5);
+---------------------------------+
| json_array_append(@j, '$', 5)   |
+---------------------------------+
| ["a", {"b": [1, 2]}, [3, 4], 5] |
+---------------------------------+
1 row in set (0.00 sec)

-- path 指向的 JSON 是值或對象,該值或對象將被包裹爲數組,而後在這個數組末尾添加元素
mysql> select json_array_append(@j, '$[1]', 5);
+-----------------------------------+
| json_array_append(@j, '$[1]', 5)  |
+-----------------------------------+
| ["a", [{"b": [1, 2]}, 5], [3, 4]] |
+-----------------------------------+
1 row in set (0.00 sec)

除了上文提到的函數,還有 json_merge_patch()、json_merge_preserve()、json_remove() 這個些函數,能夠參考官方文檔的介紹,本文再也不一一舉例說明。

索引 JSON:生成列

如今來看下根據 JSON 列查詢表數據的執行計劃,以下:

mysql> explain select * from tbl where data -> "$.id" = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tbl
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

能夠看到,由於沒有加索引,訪問類型是全表掃描 type: ALL。來試下在 JSON 類型的 data 列上添加索引,會提示以下錯誤:

mysql> alter table tbl add index (data);
ERROR 3152 (42000): JSON column 'data' cannot be used in key specification.

對於索引 JSON 類型列問題,MySQL 文檔有以下闡述 [doc ]:

JSON columns, like columns of other binary types, are not indexed directly; instead, you can create an index on a generated column that extracts a scalar value from the JSON column. See Indexing a Generated Column to Provide a JSON Column Index, for a detailed example.

就是說,不能直接在 JSON 列上建立索引;替代方式是,先建立提取 JSON 純量的生成列(generated column),而後在這個生成列上建立索引。回過頭來,ERROR 3152,這個報錯提示信息其實讓人有點困惑,對沒仔細閱讀文檔的人來講,可能會誤覺得 MySQL 不支持索引 JSON 列(Bug #81364)。因而,在 MySQL 8.0 錯誤提示信息優化爲

ERROR 3152 (42000): JSON column '%s' supports indexing only via generated columns on a specified JSON path.

生成列以及在生成列上建立索引,是 MySQL 5.7 開始支持的新特性。但其實,在 SQL:2003 標準中,生成列就早已經被定義爲可選特性,「Optional Features of SQL/Foundation:2003, T175 Generated columns」。這個特性在其餘 DBMS 中很早就有支持。2007 年 9 月發佈的 Oracle Database 11g 開始支持生成列,不過它們稱之爲稱之爲虛擬列(virtual column)。2008 年 8 月發佈的 SQL Server 2008 開始支持計算列(computed column),實現的就是 SQL 標準中的生成列。在相近的時間點,MySQL 建立了WL#411: Computed virtual columns as MS SQL server has。以後,MySQL 的社區貢獻者 Andrey Zhakov 實現了 WL#411 描述的特性,併發布了實現的代碼補丁 [ref, blog, doc ]。惋惜的是 MySQL 官方很長一段時間都沒把這個補丁合併進來,直到 2015 年的 MySQL 5.7(7年後)才官方實現 WL#411,同時 WL#411 的標題也被更新爲符合 SQL 標準術語的 「Generated columns」。與之相對比的是,2010 年 4 月發佈的 MariaDB 5.2 就開始支持虛擬列,實現上一樣也是基於 Andrey Zhakov 貢獻的代碼 [ref ]。關於生成列或虛擬列,wikipedia 總結了各大 DBMS 的支持狀況,能夠參閱。總結下,標準 SQL 定義生成列的語法和 SQL Server 200八、Oracle 11g、MariaDB、MySQL 的區別 [ref1, ref2 ]:

Standard             MSSQL 2008      Oracle 11g           MariaDB 10.1           MySQL 5.7               
--------             -----------     ----------           ------------           ---------               
column_name          column_name     column_name          column_name            column_name             
[data type]                          [data type]          data_type              data type               
GENERATED ALWAYS AS  AS              GENERATED ALWAYS AS  [GENERATED ALWAYS] AS  [GENERATED ALWAYS] AS   
(expression)         (expression)    (expression)         (expression)           (expression)           
                     [PERSISTENT]    [VIRTUAL]            [VIRTUAL | PERSISTENT] [VIRTUAL | STORED]     
[constraints]        [constraints]   [constraints]        [constraints]          [constraints]          
                                                          [COMMENT 'string']     [COMMENT 'string']

回到正題,咱們如今來試試 MySQL 的生成列:

-- 添加生成列
mysql> alter table tbl add id int as (data -> "$.id");
Query OK, 0 rows affected (0.15 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> select * from tbl;
+-----------------------------------------------+------+
| data                                          | id   |
+-----------------------------------------------+------+
| {"id": 1, "name": "Will"}                     |    1 |
| {"id": 2, "name": "printf(\"hello world\");"} |    2 |
+-----------------------------------------------+------+
2 rows in set (0.00 sec)

上面的示例,建立生成列 id,生成列對應的表達式是 data -> "$.id"。如今再試試在生成列 id 上,建立索引:

-- 在生成列上建立索引 idx_id
mysql> create index idx_id on tbl (id);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

-- 執行計劃
mysql> explain select * from tbl where id  = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tbl
   partitions: NULL
         type: ref
possible_keys: idx_id
          key: idx_id
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

-- 執行計劃
mysql> explain select * from tbl  where data -> "$.id" = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tbl
   partitions: NULL
         type: ref
possible_keys: idx_id
          key: idx_id
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

從上面的執行計劃能夠看到,查詢條件用 id 或者 data -> "$.id" 都能使用索引 idx_id

JSON 二進制格式

內部實現上,保存到數據庫的 JSON 數據並不是以 JSON 文本存儲,而是二進制格式,具體能夠參見,WL#8132: JSON datatype and binary storage format,固然也能夠直接閱讀源碼 json_binary.hjson_binary.ccdoxygen)。

MySQL 的 JSON 二進制格式,其中有一點比較值得注意,WL#8132 提到:

The keys are sorted, so that lookup can use binary search to locate the key quickly.

就是,爲了能利用二分搜索快速定位鍵,存入數據庫的JSON 對象的鍵是被排序過的。來看下下面的 SQL:

mysql> truncate tbl;
mysql> insert into tbl values ('{"b": "c", "a": {"y": 1, "x": 2}}');
Query OK, 1 row affected (0.02 sec)

mysql> select * from tbl;
+-----------------------------------+
| data                              |
+-----------------------------------+
| {"a": {"x": 2, "y": 1}, "b": "c"} |
+-----------------------------------+
1 row in set (0.00 sec)

上面的 SQL 能夠看到,insert 寫入時鍵並無按次序排列,而用 select 將 JSON 數據反序列化讀出,發現實際保存的鍵是有序的。排序規則是,先按字符串長度排序,若長度相同按字母排序。一樣的,鍵關聯的值,按鍵排序後的次序排列。對鍵排序,顯然只能針對 JSON 對象,若要存儲 JSON 數組,值按索引位置排序。

MySQL 5.7.22 新增 json_storage_size() 函數,用於返回 json 文檔二進制表示佔用的存儲空間。先來看下 SQL 示例:

mysql> select json_storage_size('"abc"');
+----------------------------+
| json_storage_size('"abc"') |
+----------------------------+
|                          5 |
+----------------------------+
1 row in set (0.00 sec)

mysql> select json_storage_size('[42, "xy", "abc"]');
+----------------------------------------+
| json_storage_size('[42, "xy", "abc"]') |
+----------------------------------------+
|                                     21 |
+----------------------------------------+
1 row in set (0.00 sec)

mysql> select json_storage_size('{"b": 42, "a": "xy"}');
+-------------------------------------------+
| json_storage_size('{"b": 42, "a": "xy"}') |
+-------------------------------------------+
|                                        24 |
+-------------------------------------------+
1 row in set (0.00 sec)

WL#8132 給出了 JSON 二進制格式的 BNF 語法描述。參考這個語法定義,能夠推算出上文示例中的 "abc"[42, "xy", "abc"]{"b": 42, "a": "xy"} 對應的二進制表示。先來看下 "abc" 純量(scalar),語法推導過程以下:

doc
  => type value                     // 使用產生式 doc ::= type value
  => 0x0c value                     // 使用產生式 type ::= 0x0c (utf8mb4 string 類型)
  => 0x0c string                    // 使用產生式 value ::= string
  => 0x0c data-length utf8mb4-data  // 使用產生式 string ::= data-length utf8mb4-data
  => 0x0c 0x03 utf8mb4-data         // 使用產生式 data-length ::= uint8*
  => 0x0c 0x03 0x61 0x62 0x63

對應的二進制值,共 5 個字節,依次爲 0x0c 0x03 0x61 0x62 0x63,其中 0x61 0x62 0x63,就是 16 進製表示的字符串 abc。佔用 5個字節,與 json_storage_size() 函數返回的結果一致。相應的語法樹以下:

mysql-jsonb-syntax-tree-w350

從二進制的角度看,純量 "abc" 的 JSON 二進制表示以下:

mysql-jsonb-scalar-w350

[42, "xy", "abc"] 的推導過程,以下:

doc 
  => type value                          // 使用產生式 doc ::= type value
  => 0x02 array                          // 使用產生式 type ::= 0x02 (small JSON array 類型)
  => 0x02 element-count size value-entry* value*  // 使用產生式 array ::= element-count size value-entry* value*
  => 0x02 0x03 0x00 size value-entry* value*  // 使用產生式 element-count ::= uint16 (使用 little-endian)
  => 0x02 0x03 0x00 0x14 0x00 value-entry* value*  // 使用產生式 size ::= uint16 (使用 little-endian)
  => 0x02 0x03 0x00 0x14 0x00 type offset-or-inlined-value value-entry* value* // 使用產生式 value-entry ::= type offset-or-inlined-value
  => 0x02 0x03 0x00 0x14 0x00 0x06 offset-or-inlined-value value-entry* value* // 使用產生式 type ::= 0x06 (uint16 類型)
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 value-entry* value*  // 使用產生式 offset-or-inlined-value ::= uint16
  ... 省略
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 value*
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 string value  // 使用產生式 value ::= string
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 data-length utf8mb4-data value  // 使用產生式 string ::= data-length utf8mb4-data
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 0x02 utf8mb4-data value // 使用產生式 data-length ::= uint8*
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 0x02 0x78 0x78 value
  ... 省略
  => 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 0x02 0x78 0x79 0x03 0x61 0x62 0x63

[42, "xy", "abc"] 對應的二進制表示,共 21 個字節,依次爲 0x02 0x03 0x00 0x14 0x00 0x06 0x2a 0x00 0x0c 0x0d 0x00 0x0c 0x10 0x00 0x02 0x78 0x79 0x03 0x61 0x62 0x63。以下圖:

mysql-jsonb-array

相對來講,產生式 array ::= element-count size value-entry* value*,是整個JSON 數組二進制表示語法的核心。element-count,表示元素個數。上圖中,第 四、5 個字節是 size 字段,十進制值爲 20(0x14),是完整二進制表示去掉開頭 type 字段後的大小(文檔沒有明確這個字段的含義,不過經過源碼推斷出來)。另外,value-entrytypeoffset-or-inlined-value 字段組成。type 很好理解,不作解釋。offset-or-inlined-value 字段,官方文檔給出了含義,含義以下:

// This field holds either the offset to where the value is stored,
// or the value itself if it is small enough to be inlined (that is,
// if it is a JSON literal or a small enough [u]int).
offset-or-inlined-value ::=
uint16 |   // if used in small JSON object/array
uint32     // if used in large JSON object/array

就是說,若是實際要保存的值足夠小,將直接內聯在這個字段中,不然將保存偏移量(offset),也就是指向實際值的指針。在示例中,保存 xy 對應的 offset 值爲 13(0x0d),指向的相對地址是 14。由於這裏的 offset 並非以相對地址 0 爲基準地址,是以相對地址 1 爲基準地址(圖中箭頭 B 指向的位置),因此偏移量是 13 而不是 14(這個字段的明確含義也是從源碼推斷而來)。相似的,保存 abc 對應的 offset 值爲 16(0x10),指向的相對地址是 17。

閱讀文檔容易發現,element-countsizeoffset 字段佔用的字節大小是固定的,小 JSON(64KB 之內)是 2 字節,大 JSON 是 4 字節。因此,若要查找 JSON 數組的第 pos 個元素的 value-entry 的偏移量,可使用下面的式子快速定位:

entry_offset = offset_size * 2 + (1 + offset_size) * pos

JSON 數組二進制表示的其餘字段比較容易理解,文檔都有解釋,就不展開闡述了。

如今來看下,JSON 對象 {"b": 42, "a": "xy"} 的二進制表示,以下圖:

mysql-jsonb-object

對於 JSON 對象二進制表示的語法,核心的產生式是 object ::= element-count size key-entry* value-entry* key* value*element-countsizevalue-entry 字段,在 JSON 數組中也有,再也不贅述。而 key-entry 字段,相似於 value-entrykey-entry 中的 key-offset 保存的是偏移量,是指向鍵的指針。另外,正如上文提到的 MySQL 會對 JSON 鍵排序,因此上圖示例的第 20 和 21 個字節值分別是 0x610x62,即 ab,而非 ba。一樣的,鍵關聯的值,按鍵排序後的次序排列,依次是 "xy"42

參考資料

  1. MySQL 5.7 Reference Manual, 12 Data Types, 12.6 The JSON Data Type http://dev.mysql.com/doc/refm...
  2. MySQL 5.7 Reference Manual, 13 Functions and Operators, 13.16 JSON Functions https://dev.mysql.com/doc/ref...
  3. 2015-08 MySQL 5.7 Release Notes, Changes in MySQL 5.7.8 https://dev.mysql.com/doc/rel...
  4. 2015-04 JSON Labs Release: Native JSON Data Type and Binary Format http://mysqlserverteam.com/js...
  5. 2015-04 JSON Labs Release: JSON Functions, Part 1 — Manipulation JSON Data http://mysqlserverteam.com/js...
  6. 2015-04 JSON Labs Release: JSON Functions, Part 2 — Querying JSON Data http://mysqlserverteam.com/my...
  7. 2015-10 JSON in MariaDB 10.2 https://lists.launchpad.net/m...
  8. What is the difference between ->> and -> in Postgres SQL? https://stackoverflow.com/a/4...
  9. 2018-04 How to Use JSON in MySQL Wrong https://www.slideshare.net/bi...
  10. 2014-10 Generated Columns in MySQL 5.7.5 https://mysqlserverteam.com/g...
  11. 2016-03 Indexing JSON documents via Virtual Columns https://mysqlserverteam.com/i...
  12. 2016-02 Generated columns in MariaDB and MySQL https://planet.mysql.com/entr...
  13. 2017-06 What's New in SQL:2016 https://modern-sql.com/blog/2...
相關文章
相關標籤/搜索