今天介紹一個MySQL中的資料型別-JSON,相信大家對JSON都不陌生,在日常工作中使用到的頻率也很高,話不多說,直接開始。
何謂JSON
看下RFC文件對於JSON的描述
1.基於 JavaScript 語言的輕量級的資料交換格式
2.基於文字
3.語言無關
JSON應用場景
我大概使用過以下兩類:
1.介面的資料交換,比如ajax請求時的application/json、rpc呼叫時的JSON序列化\反序列化;
2.以JSON格式儲存資料,我接觸過以下兩種:
2.1 以Mongodb為代表的文件型資料庫,很好的支援JSON格式的資料儲存;
2.2 以MySQL為代表的關係型資料庫,5.7.8之前沒有JSON這種資料型別,只能以varchar或者text形式變相的支援JSON,存取鍵值極不方便;5.7.8開始有JSON資料型別,有專門語法支援鍵值的存取,易用性得到很大提升。
接下來重點聊聊MySQL中如何存取JSON以及存在的一些問題。
MySQL 儲存JSON
熟悉關係型資料庫的同學都知道,資料儲存在表中,得先有表才能插資料,看一條普通的SQL insert語句
insert into user(id,name,age) values(1,'jack',10);
代表的語義是往user表中插入一條資料,這條資料有三個屬性,分別是id、name、age,各自對應MySQL user表中的三個列,如果我們向user表中插入一個不存在的列salary,MySQL會報錯
Error Code: 1054. Unknown column 'salary' in 'field list'
結論是要往MySQL表中插入資料,必須提前定義好表結構,表結構包括表名、表字符集、表包含的欄位、欄位名、欄位型別等等。
有什麼辦法能不給表加物理欄位就可以為資料增加屬性呢?
給表預置一個擴充套件欄位是一種解決思路,比如extdata,裡面儲存JSON形式的鍵值對,形如:
extdata
{"salary":1000,
"sex":'女',
"其他key":'其他值'
}
至於存哪些key完全由使用方決定,key的數量不限,value的型別也不限,是不是有很好的擴充套件性,不管業務怎麼變,底層儲存都是支援的。
這也就是為什麼要在MySQL中存取JSON的目的,主要是為了追求擴充套件性。
具體到MySQL中怎麼實現,前面提到MySQL 5.7.8之前是不支援JSON的,要支援JSON語義,只能以字串形式來變相實現,比如要修改extdata中的salary為2000,是沒有辦法直接修改的,需要先在應用層將extdata讀出然後反序列化為JSON物件,通過JSON物件的Api來修改salary的值,修改完以後將新的JSON物件序列化為新JSON串,最後整體修改user表中的extdata欄位為新JSON串,用程式碼實現大體如下:
1.result = db.execute("select extdata from user where id = xxx");
2.JSONObj = JSONUtil.parse(result.get("extdata"));
3.JSONObj.put("salary",2000);
4.extdata_str = JSONObj.toJSONString();
5.db.execute("update user set extdata=extdata_str where id=xxx");
這一套更新操作繁瑣且效能低,讀取操作也存在類似問題,由於沒有原生Api的支援,這一切感覺有點糟糕。
到了MySQL 5.7.8開始,MySQL開始支援JSON這種資料型別,看下官方文件的介紹:
MySQL新增加的原生JSON型別比在字串列中儲存 JSON 格式的字串相比有兩個優點:
1.自動的資料校驗,對於JSON型別的列MySQL會校驗其合法性;
2.提供了更方便的Api用於存取,避免了繁瑣的應用層操作。
看下基於MySQL 5.7.8,如何優雅的存取JSON型別中的鍵值,依然以修改extdata中的salary為例:
update user set extdata = JSON_SET(user.extdata, '$.salary',2000) where id =1;
讀取salary的值:
select JSON_EXTRACT(user.extdata, '$.salary') from user where id =1;
藉助JSON_SET和JSON_EXTRACT這兩個Api,極大的降低了存取的複雜度,想深入瞭解MySQL JSON用法的請參考文章最後的推薦閱讀內容。
說到這兒,藉助MySQL的原生JSON型別以及相關的Api存取擴充套件資料在易用性方面已經沒什麼問題了,接下來從效能角度思考下是否有待提升。
/*找出salary等於2000的user*/
select * from user where JSON_EXTRACT(user.extdata, '$.salary') =2000;
在我自己的pc機上,user表中共300萬條資料,執行這條SQL花費接近3秒,不談快慢,就論是否有優化空間,貼個執行計劃出來
面對大名鼎鼎的全表掃描如何優化呢?
優化JSON查詢
按照過往的思路,我們只要設計合理的索引就能避免全表掃描,但這次面對JSON似乎有點黔驢技窮了,別擔心,大名鼎鼎的MySQL早已幫你做了既生瑜又生亮的美事,看看官方怎麼說。
-
JSON型別列無法直接索引;
-
可以基於JSON建立一個生成列,然後基於生成列建立索引,從而達到對JSON型別列加索引的效果。
接著看下何謂生成列
生成列的值在插入資料時不需要設定,MySQL會根據生成列關聯的表示式自動計算填充,生成列的定義方式如下:
col_name data_type [GENERATED ALWAYS] AS (expr)
[VIRTUAL | STORED] [NOT NULL | NULL]
[UNIQUE [KEY]] [[PRIMARY] KEY]
[COMMENT 'string']
AS (expr)指示生成列並定義用於計算列值的表示式,可以在前面加上GENERATED ALWAYS明確的表示這是一個生成列。
迴歸到我們的場景中,分三步進行優化:
1.建立一個生成列v_salary,計算列值表示式為extdata->"$.salary",代表提取extdata中的salary值
ALTER TABLE `user` ADD COLUMN `v_salary` DECIMAL(10,2) as (extdata->"$.salary") AFTER `extdata`;
2.針對v_salary建立索引
ALTER TABLE `user` ADD INDEX `idx_salary` (`v_salary`) ;
3.替換查詢語句中JSON_EXTRACT(user.extdata, '$.salary')為v_salary;
select * from user where v_salary =2000
select * from user where v_salary =2000,執行耗時為0.047s,這個優化效果非常顯著。
看下現在的執行計劃已經使用了索引
總結
任何新技術的引入一定要有一個比較全面的認識,充分理解其利弊,不能只看到其光鮮的一面,而忽略其帶來的弊端,對於弊端要有應對措施,知己知彼。
推薦閱讀
https://dev.mysql.com/doc/refman/5.7/en/json.html
https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-set
https://dev.mysql.com/doc/refman/5.7/en/create-table-secondary-indexes.html
https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html