索引三剑客之降序索引和不可见索引
千呼万唤始出来,MySQL 8.0索引三剑客之函数索引
双重密码,MySQL 8.0创新特性
sql_mode兼容性,MySQL 8.0 升级踩过的坑
警惕参数变化,MySQL 8.0 升级避免再次踩坑
独孤九剑,重剑无锋,大巧不工,通晓剑意,无所施而不可。三剑客之首,函数索引。
函数索引这个概念并不新颖,Oracle早在十年前的Oracle10g中就支持了函数索引,函数索引在Oracle数据库中使用相当广泛和成熟,而MySQL却一直没有开发相关的索引功能。不过好消息是,MySQL 终于在8.0版本引入了这一特性。真的是,千呼万唤始出来,不过好歹还是来了。
普通索引是对列值或列的前缀值进行索引,而MySQL 8.0.13之后支持函数索引,函数索引是对表中的列执行表达式计算后的结构进行索引,而不是对列或列前缀值。使用函数索引可以对未直接存储在表中的数据进行索引。
函数索引为应用程序提供了极大的便利和性能提升。
创建函数索引的语法跟普通索引差别并不大,唯一需要注意的是函数索引对应的表达式需要用()括起来。
首先, 函数索引可以对单列的表达式进行索引,如下:
# 创建单列表达式索引
alter table t_wang add index idx_func(date(col1));
其次,函数索引也可以对多列组合的表达式进行索引,如下:
# 创建多列组合表达式索引
alter table t_wang add index idx_func((col1 + col2));
然后,函数索引也可以将表达式和普通列组合一起构成组合索引,如下:
# 创建组合索引
alter table t_wang add index idx_func(col1, (date(col1)));
最后,函数索引还可以跟其他选项,如unique 和 asc、desc排序一起使用,如下:
# 创建表达式排序索引
alter table t_wang add unique index idx_func(col1, (date(col1)) desc);
函数索引可以对字段表达式进行索引,从而在SQL语句中包含表达式的情况下可以显著提升查询性能。
a) 创建测试表t_wang,导入一些测试数据,在时间列创建一个普通索引
# 查看表结构,测试表的时间列有个普通索引
MySQL [test]> show create table t_wang\G
*************************** 1. row ***************************
Table: t_wang
CREATE TABLE `t_wang` (
`id` int NOT NULL,
`name` char(30) NOT NULL,
`fmodify_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_normal_time` (`fmodify_time`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
b) 查询在4月份有修改过的人名;虽然时间列存在普通索引,但是查看执行计划为全表扫描。
# 测试查询4月份有修改过的人名
MySQL [test]> explain select name from t_wang where month(fmodify_time) = 4;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t_wang | NULL | ALL | NULL | NULL | NULL | NULL | 10 | 100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
c) 在测试表上添加一个函数索引,表达式为month(fmodify_time)
# 添加函数索引
alter table t_wang add index `idx_func_time`((month(fmodify_time)));
MySQL [test]> show create table t_wang\G
*************************** 1. row ***************************
Table: t_wang
Create Table: CREATE TABLE `t_wang` (
`id` int NOT NULL,
`name` char(30) NOT NULL,
`fmodify_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_normal_time` (`fmodify_time`),
KEY `idx_func_time` ((month(`fmodify_time`)))
) ENGINE=InnoDB DEFAULT CHARSET=latin1
d) 再次查询4月份有修改过的人名;执行计划为索引扫描
MySQL [test]> explain select name from t_wang where month(fmodify_time) = 4;
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | t_wang | NULL | ref | idx_func_time | idx_func_time | 4 | const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+---------------+---------------+---------+-------+------+----------+-------+
MySQL 8.0 函数索引实际上是基于5.7版本引入的虚拟列(virtual generated columns)来实现的。实际上在MySQL 8.0中创建函数索引时,MySQL会自动在表上创建一个隐藏的
虚拟列,然后在虚拟列上创建索引。这里注意:虚拟列是不实际占用空间的,但是函数索引是需要实际占据空间的。
隐藏的虚拟列我们可以通过以下方式查看,Extra列有个明显的VIRTUAL GENERATED标识。
# 查看隐藏虚拟列
MySQL [test]> show extended columns from t_wang;
+----------------------------------+----------+------+-----+---------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+----------------------------------+----------+------+-----+---------+-------------------+
| id | int | NO | PRI | NULL | |
| fmodify_time | datetime | YES | MUL | NULL | |
| name | char(30) | YES | | wang | |
| bd47a6cb20076f31a47803e887053c31 | date | YES | MUL | NULL | VIRTUAL GENERATED |
| DB_TRX_ID | | NO | | NULL | |
| DB_ROLL_PTR | | NO | | NULL | |
+----------------------------------+----------+------+-----+---------+-------------------+
既然函数索引是基于虚拟列来实现的,那么虚拟列的一些限制在函数索引同样适用。
前缀索引,即只对字段的前几个字符进行索引,在优化字段查询效率的同时减小索引长度。
# 创建前缀索引
alter table t_wang add index `idx_prefix` (name(4));
MySQL 8.0引入的函数索引同样可以实现这个能力,使用SUBSTRING()函数取字段的前几个字符作为表达式建立索引。
# 创建函数索引
alter table t_wang add index `idx_substring` ((substring(name, 1, 4));
前缀索引和函数索引,同样都可以对字段前缀进行索引,但是要想利用函数索引提升查询性能,需要在查询语句的谓词部分使用与函数定义相同的函数才行。也就是说,这里如果我们想让执行计划走到函数索引,查询语句需要这样写:select * from t_wang where substring(name, 1, 4) = ‘wang’ 。而前缀索引并没有这个限制。
不过借助SUBSTRING()函数,函数索引甚至可以实现对字段的任意子集,甚至是对不同字段的交集、并集等建立索引,使用上更加灵活,可以适用各种不同场景。
函数索引为应用程序提供了很大的便利,我们可以通过调整查询条件来优化查询性能、缩小结果集、减少数据传输等。函数索引同样在 JSON 数据存取方面同样可以有类似效果。我们可以在JSON列创建函数索引来简化键值对的查询。
不过这里要注意:JSON的操作符->>对应的是JSON_UNQUOTE()函数,该函数返回的是带有排序规则 utf8mb4_bin 的字符串; 而使用CAST()函数返回的是带有排序规则 utf8mb4_0900_ai_ci(系统默认排序规则) 的字符串。所以,在查询时需要进行转义以利用到函数索引。
譬如,直接检查JSON的key->value键值对:
# 方式1:
CREATE TABLE employees (
data JSON,
INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin))
);
SELECT * FROM employees WHERE data->>'$.name' = 'James';
# 或者方式2:
CREATE TABLE employees (
data JSON,
INDEX idx ((CAST(data->>"$.name" AS CHAR(30))))
);
SELECT * FROM employees WHERE CAST(data->>'$.name' AS CHAR(30)) = 'James';
# 我们使用上面例子中的表结构
CREATE TABLE `t_wang` (
`id` int NOT NULL,
`name` char(30) NOT NULL,
`fmodify_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
我们可以在时间列上加上普通索引,然后将系统上线使用以来的每年4月数据都遍历一下。
# 在fmodify_time列加个普通索引
alter table t_wang add index idx_normal_time(fmodify_time);
# 对应查询语句
select name from t_wang where (fmodify_time between '2015-04-01 00:00:00' and '2015-05-01 00:00:00') or (fmodify_time between '2016-04-01 00:00:00' and '2016-05-01 00:00:00') or ... or (fmodify_time between '2022-04-01 00:00:00' and '2022-05-01 00:00:00');
我们可以仍然使用MySQL 5.7的虚拟列来优化查询,在表上添加一个虚拟列,然后在虚拟列添加一个普通索引。
# 添加虚拟列和对应索引
alter table t_wang add column `ftime_generated` int GENERATED ALWAYS AS (month(fmodify_time));
alter table t_wang add index `idx_generated_time` (ftime_generated);
# 对应查看语句
select name from t_wang where ftime_generated = 4;
我们可以使用MySQL 8.0引入的函数索引,在时间列上添加一个函数索引。
# 添加函数索引
alter table t_wang add index `idx_functional_time` ((month(fmodify_time)));
# 对应查询语句
select name from t_wang where month(fmodify_time) = 4;
实际上我们还可以有另外一种方式来优化查询,同样是在MySQL 8.0还引入了另外一个特性,表达式默认值。
MySQL 8.0.13开始,字段的DEFAULT 子句中指定的默认值可以是常量或表达式。将基于列的表达式计算值作为默认值,可以实现类似虚拟列的能力。
# 向表中添加一列,将时间列的表达式作为该列的默认值;然后再在该列添加一个普通索引。
alter table t_wang add column `ftime_default` int NOT NULL DEFAULT (month(fmodify_time));
alter table t_wang add index `idx_default_time` (ftime_default);
# 对应查询语句
select name from t_wang where ftime_default = 4;
MySQL 8.0引入了函数索引这一新特性,提高了业务程序的便利性和性能。有了函数索引,业务不需要手动在表上添加虚拟列,就能够享受虚拟列带来的性能提升。同其他类似实现方式相比,譬如虚拟列、表达式默认值,函数索引更加简洁和易于维护。函数索引还可以用于JSON数据的查询。MySQL 8.0在索引方面引入的新特性可不止函数索引这一项,还有倒序索引和不可见索引,这些特性对业务查询也是大有裨益的。
喜欢的同学麻烦点个关注和点赞