本篇主要介绍 MySQL 的函数索引(也叫表达式索引)。mysql
一般来说,索引都是基于字段自己或者字段前缀(第 20 篇),而函数索引是基于字段自己加上函数、操做符、表达式等计算而来。若是将表达式或者操做符也看作函数的话,简单来讲,这样的索引就能够统称函数索引。sql
MySQL 的函数索引内部是基于虚拟列(generated columns)实现,不一样于直接定义虚拟列,函数索引自动建立的虚拟列自己实时计算结果,并不存储数据,只把函数索引自己存在磁盘上。express
MySQL 8.0.13 以前不支持函数索引,因此老版本包括如今主流的 MySQL 5.7 也不支持函数索引,须要手工模拟建立或者改 SQL。json
本章基于如下几点来说函数索引:segmentfault
函数索引最最经典的使用场景莫过于就是对日期的处理,特别是表中只定义了一个字段,后期对这个字段的查询都是基于部分结果。好比 “2100-02-02 08:09:09.123972” 包含了日期 “2100-02-02”,时间 “08:09:09”,小数位时间 “123972”,有可能会对这个值拆解后部分查询。session
举个简单例子,表 t1 有两个字段,一个主键,另一个时间字段,总记录数不到 40W。ide
<localhost|mysql>show create table t1\G *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `log_time` datetime(6) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_log_time` (`log_time`) ) ENGINE=InnoDB AUTO_INCREMENT=524268 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec) <localhost|mysql>select count(*) from t1; +----------+ | count(*) | +----------+ | 393216 | +----------+ 1 row in set (0.07 sec)
执行下面这条 SQL 1,把日期单独拿出来,执行了 0.09 秒。函数
# SQL 1 <localhost|mysql>select * from t1 where date(log_time) = '2100-02-02'; +--------+----------------------------+ | id | log_time | +--------+----------------------------+ | 524267 | 2100-02-02 08:09:09.123972 | +--------+----------------------------+ 1 row in set (0.09 sec)
看下它的执行计划,虽然走了索引,可是扫描行数为总记录数,至关于全表扫,这时候比全表扫还不理想,全表扫直接走聚簇索引还快点。优化
<localhost|mysql>explain select * from t1 where date(log_time) = '2100-02-02'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 partitions: NULL type: index possible_keys: NULL key: idx_log_time key_len: 9 ref: NULL rows: 392413 filtered: 100.00 Extra: Using where; Using index 1 row in set, 1 warning (0.00 sec)
这时最好的方法就是为列 log_time 加一新索引,基于函数 date 的函数索引。spa
<localhost|mysql>alter table t1 add key idx_func_index_1((date(log_time))); Query OK, 0 rows affected (2.76 sec) Records: 0 Duplicates: 0 Warnings: 0
再次执行上面的 SQL 1,瞬间执行完毕。
<localhost|mysql>select * from t1 where date(log_time) = '2100-02-02'; +--------+----------------------------+ | id | log_time | +--------+----------------------------+ | 524267 | 2100-02-02 08:09:09.123972 | +--------+----------------------------+ 1 row in set (0.00 sec)
接下来查看执行计划,结果显示走函数索引 idx_func_index_1 扫描记录数只有一行,执行计划达到最优。
<localhost|mysql>explain select * from t1 where date(log_time) = '2100-02-02'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 partitions: NULL type: ref possible_keys: idx_func_index_1 key: idx_func_index_1 key_len: 4 ref: const rows: 1 filtered: 100.00 Extra: NULL 1 row in set, 1 warning (0.00 sec)
若是想查看 MySQL 函数索引内部建立的列,直接 show create table 看是没有结果的,好比下面只看到一个新的索引。
<localhost|mysql>show create table t1\G ... KEY `idx_func_index_1` ((cast(`log_time` as date))) ) ENGINE=InnoDB AUTO_INCREMENT=524268 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec)
经过 MySQL 8.0 的新语句 show extended columns 查看隐藏的列,下面结果发现确实是新加了一个虚拟列。
<localhost|mysql>show extended columns from t1; ... | bbd3daff935e7a4d0991a8393ec03728 | date | YES | MUL | NULL | VIRTUAL GENERATED | ... 5 rows in set (0.03 sec)
好比须要遍历 JSON 类型的子串做为索引,直接用遍历操做符 ->> 报错。
<localhost|mysql>create table t2 (id int primary key, r1 json); Query OK, 0 rows affected (0.09 sec) <localhost|mysql>alter table t2 add key idx_func_index_2((r1->>'$.x')); ERROR 3757 (HY000): Cannot create a functional index on an expression that returns a BLOB or TEXT. Please consider using CAST.
操做符 ->> 表示从 JSON 串中遍历指定路径的 value,在 MySQL 内部转换为 json_unquote(jso_extract(...)),而函数 json_unquote 返回结果具备如下特性:
因此针对 JSON 字段来创建新的函数索引:
<localhost|mysql>alter table t2 add key idx_func_index_2((cast(r1->>'$.x' as char(1)) collate utf8mb4_bin)); Query OK, 0 rows affected (0.07 sec) Records: 0 Duplicates: 0 Warnings: 0
看下表结构,操做符 ->> 被转换为 json_unquote(json_extract(...)),而且排序规则为 utf8mb4_bin。
<localhost|mysql>show create table t2\G *************************** 1. row *************************** Table: t2 ... KEY `idx_func_index_2` (((cast(json_unquote(json_extract(`r1`,_utf8mb4'$.x')) as char(1) charset utf8mb4) collate utf8mb4_bin))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec)
接下来插入几条记录,看看这个函数索引的使用。
<localhost|mysql>select * from t2; +----+---------------------+ | id | r1 | +----+---------------------+ | 1 | {"x": "1", "y": 10} | | 2 | {"x": "2", "y": 20} | | 3 | {"x": "a", "y": 20} | | 4 | {"x": "A", "y": 20} | +----+---------------------+ 4 rows in set (0.00 sec)
执行下 SQL 2,而且看下执行计划,直接走了刚才建立的函数索引。
# SQL 2 <localhost|mysql>select * from t2 where r1->>'$.x'='a'; +----+---------------------+ | id | r1 | +----+---------------------+ | 3 | {"x": "a", "y": 20} | +----+---------------------+ 1 row in set (0.00 sec) <localhost|mysql>explain select * from t2 where r1->>'$.x'='a'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t2 partitions: NULL type: ref possible_keys: idx_func_index_2 key: idx_func_index_2 key_len: 7 ref: const rows: 1 filtered: 100.00 Extra: NULL 1 row in set, 1 warning (0.00 sec)
这里其实应该有个疑问,对函数索引的调用,必需要按照以前定义好的函数来执行,不然不会用到索引,那 SQL 2 怎么能够直接到用索引?
MySQL 在这块儿其实内部已经转换为正确的语句。查看下刚才 EXPLAIN 的 WARNINGS 信息。能够看到 SQL 2 被 MySQL 转换为遵照函数索引规则的正确语句。
<localhost|mysql>show warnings\G *************************** 1. row *************************** Level: Note Code: 1003 Message: /* select#1 */ select `ytt`.`t2`.`id` AS `id`,`ytt`.`t2`.`r1` AS `r1` from `ytt`.`t2` where ((cast(json_unquote(json_extract(`ytt`.`t2`.`r1`,_utf8mb4'$.x')) as char(1) charset utf8mb4) collate utf8mb4_bin) = 'a') 1 row in set (0.00 sec)
以前讲过前缀索引,可能会有这样的疑问。前缀索引能不能被函数索引替代?固然是不行的!函数索引要求查询条件严格按照函数索引的定义匹配,虽然有的场景下 MySQL 能够内部转换,可是 MySQL 没法为每一个函数都替换为最优化的写法。好比函数 substring,left,right 等。
下面例子用来模拟下是否能够用函数索引替代前缀索引。示例表 t3,一个前缀索引和两个函数索引实现的目的同样,可是实际查询的时候 SQL 语句并不同。
<localhost|mysql>show create table t3\G *************************** 1. row *************************** Table: t3 Create Table: CREATE TABLE `t3` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `r1` char(36) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`), KEY `idx_r1_prefix` (`r1`(8)), KEY `idx_func_index_3` ((left(`r1`,8))), KEY `idx_func_index_4` ((substr(`r1`,1,8))) ) ENGINE=InnoDB AUTO_INCREMENT=249 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec) 如下 SQL 3 、SQL 四、SQL 5 写法不同,查询结果同样,走的索引不同。 # SQL 3 select * from t3 where r1 like 'de45c7d9%'; # SQL 4 select * from t3 where left(r1,8) ='de45c7d9'; # SQL 5 select * from t3 where substring(r1,1,8) ='de45c7d9'; <localhost|mysql>select * from t3 where r1 like 'de45c7d9%'; +-----+--------------------------------------+ | id | r1 | +-----+--------------------------------------+ | 178 | de45c7d9-935c-11ea-8421-08002753f58d | +-----+--------------------------------------+ 1 row in set (0.00 sec) <localhost|mysql>select * from t3 where left(r1,8) ='de45c7d9'; +-----+--------------------------------------+ | id | r1 | +-----+--------------------------------------+ | 178 | de45c7d9-935c-11ea-8421-08002753f58d | +-----+--------------------------------------+ 1 row in set (0.00 sec) <localhost|mysql>select * from t3 where substring(r1,1,8) ='de45c7d9'; +-----+--------------------------------------+ | id | r1 | +-----+--------------------------------------+ | 178 | de45c7d9-935c-11ea-8421-08002753f58d | +-----+--------------------------------------+ 1 row in set (0.00 sec)
各自的查询计划,每条 SQL 走的不一样的索引。
<localhost|mysql>explain select * from t3 where r1 like 'de45c7d9%'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t3 partitions: NULL type: range possible_keys: idx_r1_prefix key: idx_r1_prefix key_len: 33 ref: NULL rows: 1 filtered: 100.00 Extra: Using where 1 row in set, 1 warning (0.00 sec) <localhost|mysql>explain select * from t3 where left(r1,8) ='de45c7d9'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t3 partitions: NULL type: ref possible_keys: idx_func_index_3 key: idx_func_index_3 key_len: 35 ref: const rows: 1 filtered: 100.00 Extra: Using where 1 row in set, 1 warning (0.00 sec) <localhost|mysql>explain select * from t3 where substring(r1,1,8) ='de45c7d9'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t3 partitions: NULL type: ref possible_keys: idx_func_index_4 key: idx_func_index_4 key_len: 35 ref: const rows: 1 filtered: 100.00 Extra: Using where 1 row in set, 1 warning (0.00 sec)
此时删除掉函数索引 idx_func_index_3, SQL 4 就没法走正确的索引。
<localhost|mysql>alter table t3 drop key idx_func_index_3; Query OK, 0 rows affected (0.05 sec) Records: 0 Duplicates: 0 Warnings: 0 <localhost|mysql>explain select * from t3 where left(r1,8) ='de45c7d9'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t3 partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 128 filtered: 100.00 Extra: Using where 1 row in set, 1 warning (0.00 sec)
查看 warnings,发现 MySQL 优化器转换后的 SQL,LEFT 函数仍是保持原样,可是表里没有基于 LEFT 函数的索引,只能全表扫。
<localhost|mysql>show warnings\G *************************** 1. row *************************** Level: Note Code: 1003 Message: /* select#1 */ select `ytt`.`t3`.`id` AS `id`,`ytt`.`t3`.`r1` AS `r1` from `ytt`.`t3` where (left(`ytt`.`t3`.`r1`,8) = 'de45c7d9') 1 row in set (0.00 sec)
函数索引是 MySQL 8.0.13 才有的。那在老的版本如何实现呢?
MySQL 5.7 自持虚拟列,只须要在虚拟列上建立一个普通索引就行。
MySQL 5.6 以及 MySQL 5.5 等,则须要本身定义一个冗余列,而后按期更新这列内容。固然最核心的是如何规划好按期更新内容这块。这块若是讨论起来,内容很是多,能够参考我以前写的关于表样例数据更新收集这块内容,MySQL 内部的作法。
关于 MySQL 的技术内容,大家还有什么想知道的吗?赶忙留言告诉小编吧!