需求
抓取网站多样
抓取元素不同
查询条件不固定
Mysql JSON介绍
JSON的格式非常简单:名称/键值。之前MySQL版本里面要实现这样的存储,要么用VARCHAR要么用TEXT大文本。 MySQL5.7发布后,专门设计了JSON数据类型以及关于这种类型的检索以及其他函数解析。 为什么JSON的原生支持
文档合法性
在MySQL5.7.7对JSON提供原生类型的支持之前,用户可以用TEXT或者BLOB类型来存储JSON文档。但对于MySQL来说,用户插入的数据只是序列化后的一个普通的字符串,不会对JSON文档本身的语法合法性做检查,文档的合法性需要用户自己保证。在引入新的JSON类型之后,插入语法错误的JSON文档,MySQL会提示错误,并在插入之后做归一化处理,保证每一个键对应一个值。
更有效的访问
MySQL 5.7.7+本身提供了很多原生的函数以及路径表达式来方便用户访问JSON数据。例如对于下面的JSON文档: { "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }
用户可以使用 $.a[1][0]获取{ "c" : "d" }, $.a[1]获取[ { "c" : "d" }, 1 ] 还可以使用通配符 * 和 ** 来进行模糊匹配,详见下一段。
性能优化
在MySQL提供JSON原生支持之前,如果用户需要获取或者修改某个JSON文档的键值,需要把TEXT或者BLOB整个字符串读出来反序列化成JSON对象,然后通过各种库函数访问JSON数据。显然这样是非常没有效率的,特别是对较大的文档。而原生JSON的性能,特别是读性能非常好。根据Oracle公司针对200K+数据文档做的性能测试表明,同样的数据用TEXT和JSON类型的查询性能差异达到两个数量级以上,而且用户还可以对经常访问的JSON键值做索引,进一步提升性能。JSON数据操作性能的提升是基于JSON数据本身的存储结构的,下文会进一步介绍。
JSON的操作接口及路径表达式
JSON的操作接口
JSON_APPEND()JSON_ARRAY_INSERT()JSON_UNQUOTE()JSON_ARRAY()JSON_REPLACE()JSON_CONTAINS()JSON_DEPTH()JSON_EXTRACT()JSON_INSERT()JSON_KEYS()JSON_LENGTH()JSON_VALID()JSON_MERGE()JSON_OBJECT()JSON_QUOTE()JSON_REMOVE()JSON_CONTAINS_PATH()JSON_SEARCH()JSON_SET()JSON_TYPE()
JSON路径表达式
为了更方便快速的访问JSON的键值,MySQL 5.7.7+提供了新的路径表达式语法支持。前文提到的$.a[1][0]就是路径表达式的一个具体的示例。完整的路径表达式语法为:
pathExpression>::= scope [ ( pathLeg )* ]scope::= [ columnReference ] dollarSigncolumnReference::= [ [ databaseIdentifier period ] tableIdentifier period ] columnIdentifierdatabaseIdentifier::= sqlIdentifiertableIdentifier::= sqlIdentifiercolumnIdentifier::= sqlIdentifierpathLeg::= member | arrayLocation | doubleAsteriskmember::= period ( keyName | asterisk )arrayLocation::= leftBracket ( non-negative-integer | asterisk ) rightBracketkeyName::= ECMAScript-identifier | double-quoted-string-literaldoubleAsterisk::= **
还是以
{ "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
JSON的存储结构及具体实现
在处理JSON时,MySQL使用的utf8mb4字符集,utf8mb4是utf8和ascii的超集。由于历史原因,这里utf8并非是我们常说的UTF-8 Unicode变长编码方案,而是MySQL自身定义的utf8编码方案,最长为三个字节。具体区别非本文重点,请大家自行Google了解。
MySQL在内存中是以DOM的形式表示JSON文档,而且在MySQL解析某个具体的路径表达式时,只需要反序列化和解析路径上的对象,而且速度极快。要弄清楚MySQL是如何做到这些的,我们就需要了解JSON在硬盘上的存储结构。有个有趣的点是,JSON对象是BLOB的子类,在其基础上做了特化。
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文档中的标量值建立虚拟列,然后在虚拟列上建立索引。这样用户可以使用表达式对自己感兴趣的键值建立索引。举个具体的例子来说明:
CREATETABLEfeatures (idINTNOTNULLAUTO_INCREMENT, featureJSONNOTNULL, PRIMARYKEY(id));
插入它的JSON数据的格式为:
{ "type":"Feature", "properties":{ "TO_ST":"0", "BLKLOT":"0001001", "STREET":"UNKNOWN", "FROM_ST":"0", "LOT_NUM":"001", "ST_TYPE":null, "ODD_EVEN":"E", "BLOCK_NUM":"0001", "MAPBLKLOT":"0001001"}}
使用:
ALTERTABLEfeaturesADDfeature_streetVARCHAR(30)AS(JSON_UNQUOTE(feature->"$.properties.STREET"));ALTERTABLEfeaturesADDINDEX(feature_street);
两个步骤,可以对feature列中properties键值下的STREET键(feature->"$.properties.STREET")创建索引。
其中,feature_street列就是新添加的虚拟列。之所以取名虚拟列,是因为与它对应的还有一个存储列(stored column)。它们最大的区别为虚拟列只修改数据库的metadata,并不会存储真实的数据在硬盘上,读取过程也是实时计算的方式;而存储列会把表达式的列存储在硬盘上。两者使用的场景不一样,默认情况下通过表达式生成的列为虚拟列。
这样虚拟列的添加和删除都会非常快,而在虚拟列上建立索引跟传统的建立索引的方式并没有区别,会提高虚拟列读取的性能,减慢整体插入的性能。虚拟列的特性结合JSON的路径表达式,可以方便的为用户提供高效的键值索引功能。
JSON比较与排序
JSON值可以使用=, <, <=, >, >=, <>, !=, <=>等操作符,BETWEEN, IN,GREATEST, LEAST等操作符现在还不支持。JSON值使用的两级排序规则,第一级基于JSON的类型,类型不同的使用每个类型特有的排序规则。
实践
创建表
CREATETABLE`user`(`uid`int(11)NOTNULLAUTO_INCREMENT,`info`jsonDEFAULTNULL, #注意desc字段类型为jsonPRIMARYKEY(`uid`))ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8;
插入数据
INSERT INTO `user`(`uid`,`info`) VALUES (1,'{\"mail\":\"[email protected]\",\"name\":\"David\",\"address\":\"Shangahai\"}'),(2,'{\"mail\":\"[email protected]\",\"name\":\"Amy\"}');
普通查询
selectuid,infofromuserwhereinfolike'%mail%'
key查询
SELECTuid,json_keys(info)as"keys"FROMuser;
value查询
SELECTuid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROMUSER;
条件查询
json表里面有个content字段数据如下面的Json格式 {"eq":{"f_1360040399":"admin","f_3038116851":"20","f_318208994":"[email protected]"}}
需要对其中的f开头的Json key值所对的value进行模糊查询,方法如下:
select * from table where content->'$.eq.f_1360040399' like '%min%'
SELECTuid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROMUSERwhereinfo->'$.name'="Amy";
SELECTuid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROMUSERwhereinfo->'$.name'like"%A%";
性能测试
100万条数据检索
创建随机字符串
select concat('{\"mail\":\"',rand_string(15),'@gmail.com\",\"name\":\"',rand_string(20),'\",\"address\":\"',rand_string(30),'\",\"company\":\"',rand_string(30),'\"}') from dual ;
造100万数据
DELIMITER $$USE `platform`$$DROPPROCEDUREIFEXISTS`autoinsert`$$CREATEPROCEDURE`autoinsert`(INIP_NUM INT)BEGINDECLAREiIPINTDEFAULT0 ;DECLARE json varchar(255);WHILE(iIP < IP_NUM)DOSETjson =concat('{\"mail\": \"',rand_string(15),'@gmail.com\", \"name\":\"',rand_string(20),'\", \"address\": \"',rand_string(30),'\", \"company\": \"',rand_string(30),'\"}'); insertintouser(info)values (json);SETiIP = iIP+1;ENDWHILE;END$$DELIMITER ;call `autoinsert`(1000000);
条件查询
SELECTuid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROMUSERwhereinfo->'$.name'like"%D%";
创建索引
ALTERTABLEuserADDnameVARCHAR(50)AS(JSON_UNQUOTE(info->"$.name"));ALTERTABLEuserADDINDEX(name);
条件查询(同上)
DurationTime统计表
数据量未加索引添加索引
110万0.005s0.005s
09:58:36SELECT uid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROM USER where info->'$.name'like"%D%"277070row(s)returned0.005sec /2.416sec10:09:05SELECT uid,json_extract(info,'$.mail')AS'mail',json_extract(info,'$.name')AS'name'FROM USER where info->'$.name'like"%A%"276982row(s)returned0.002sec /1.858sec
结论
本文介绍了Mysql JSON数据类型的特性,并实践测试JSON效率。
Json格式的灵活性满足需求中
抓取网站多样
不同网站对应不同json格式
抓取元素不同
查询条件不固定
查询条件可以先将json_keys(json)返回前端动态生成查询条件
100万数据量添加索引效果不明显 ,大数据量待测试
参考资料
生成 JSON 值的函数
MySQL5.7 JSON实现简介
MySQL 5.7 新特性 JSON 的创建,插入,查询,更新
mysql存储过程中(varchar)变量的问题
mysql concat 字符串拼接命令
关于mysql的Fetch Time 和 Duration Time
mysql 索引( mysql index )