MySQL 8.0.21来了,该关心的不只是改专有术语名词

还有其他需要关注的新变化。

昨天(北京时间2020.7.13),MySQL如期推出8.0.21版本,这里是 release notes
可能不少人在调侃MySQL为了ZZ正确,修改了诸如master/slave/whitelist/blacklist等专有名词,白做思想真是害死人。所幸这些改动在当前并没太大影响,旧的名词还能继续用一阵子。
除此外,还有下面几个我认为也很重要的新功能或者性能提升点:
1. 可以全局关闭REDO(WL#13795),加速数据导入(用在例如做数据恢复或初始化期间)。
2. 优化lock_sys mutex(WL#10314),采用拆分+排队的方案(我理解为类似秒杀业务场景的优化思路,不肯定是否准确)。
3. 对UNDO表空间的DDL操作记入REDO LOG(WL#11819),增加ACID保证。
4. 增加CREATE TABLE…SELECT的原子性和crash safe支持(WL#13355),这样也能用在MGR场景中了(以前会被拒绝执行,因为被拆分成两个SQL,不支持原子性)。
5. 优化器新增开关prefer_ordering_index(WL#13929),修复个别场景下的错误LIMIT优化做法(bug#97001)。
6. 单表UPDATE、DELETE也增加semijoin优化支持(WL#6057)。

当然了,​其他没列出来的不代表不重要,更多的可以阅读完整release notes,或者这篇文章 The MySQL 8.0.21 Maintenance Release is Generally Available

延伸阅读

Enjoy MySQL 8.0 :)

全文完。


扫码加入叶老师的「MySQL核心优化课」,开启MySQL的修行之旅吧。

意想不到的MySQL复制延迟原因

导读

线上有个MySQL实例,存在严重的复制延迟问题,原因出乎意料。

线上有个MySQL 5.7版本的实例,从服务器延迟了3万多秒,而且延迟看起来好像还在加剧。

MySQL版本

Server version:     5.7.18-log MySQL Community Server (GPL)

看下延迟状况

yejr@imysql.com:mysql3306.sock : (none) > show slave status\G
              Master_Log_File: mysql-bin.013225
          Read_Master_Log_Pos: 1059111551
        Relay_Master_Log_File: mysql-bin.013161
          Exec_Master_Log_Pos: 773131396
                  Master_UUID: e7c35a95-ffb1-11e6-9620-90e2babb5b90

我们看到,binlog文件落后了64个,相当的夸张。

MySQL 5.7不是已经实现并行复制了吗,怎么还会延迟这么厉害?

先检查系统负载。
先看I/O子系统负载,没看到这方面存在瓶颈。

再看mysqld进程的CPU消耗。

可以看到mysqld进程的CPU消耗长时间超过100%。

再检查MySQL复制现场,确认了几个频繁更新的表都有主键,以及必要的索引,相应的DML操作,也几乎都是基于主键或唯一索引条件执行的,排除无主键、无合理索引方面的因素。

最后只能祭出perf top神器了。

perf top -p `pidof mysqld`

看到perf top最后的报告是这样的

Samples: 107K of event 'cycles', Event count (approx.): 29813195000                                                                                                                              
Overhead  Shared Object        Symbol                                                                                                                                                            
  56.19%  mysqld               [.] bitmap_get_next_set                                                                                                                                           
  16.18%  mysqld               [.] build_template_field                                                                                                                                          
   4.61%  mysqld               [.] ha_innopart::try_semi_consistent_read                                                                                                                         
   4.44%  mysqld               [.] dict_index_copy_types                                                                                                                                         
   4.16%  libc-2.12.so         [.] __memset_sse2                                                                                                                                                 
   2.92%  mysqld               [.] ha_innobase::build_template

我们看到, bitmap_get_next_set 这个函数调用占到了 56.19%,非常高,其次是 build_template_field 函数,占了 16.18%。

经过检查MySQL源码并请教MySQL内核开发专家,最后确认这两个函数跟启用表分区有关系。查询下当前实例有多少个表分区:

yejr@imysql.com:mysql3306.sock : (none) > select count(*) from partitions where partition_name is not null;
+----------+
| count(*) |
+----------+
|    32128 |
+----------+
1 row in set (11.92 sec)

我勒个去,竟然有3万多个表分区,难怪上面那两个函数调用那么高。

这个业务数据库几个大表采用每天一个分区方案,而且把直到当年年底所有分区也都给提前创建好了,所以才会有这么多。

不过,虽然有这么多表分区,在master服务器上却不存在这个瓶颈,看起来是在主从复制以及大量表分区的综合因素下才有这个瓶颈,最终导致主从复制延迟越来越严重。

知道问题所在,解决起来就简单了。把直到下个月底前,其余还用不到的表分区全部删除,之后约只剩下1.6万个分区。重启slave线程,问题解决,主从复制延迟很快就消失了。

最后,这个问题对应的bug(#85352)在此,已在5.6.38、5.7.20、8.0.3等版本修复了。

NOT NULL列用IS NULL也能查到数据?

导读

datetime列设置了NOT NULL约束,但查询条件IS NULL却能返回结果,奇怪吗?

测试表DDL

CREATE TABLE `t1` (
  `id` int(11) DEFAULT NULL,
  `dt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB

插入测试数据:

yejr@imysql.com> insert into t1(id) select 1;  --- 不指定dt列的值
yejr@imysql.com> insert into t1 select 2, now();  --- 指定dt列的值为now()
yejr@imysql.com> insert into t1(id) select 3;  --- 不指定dt列的值

查询数据:

yejr@imysql.com> select * from t1 where dt is null;
+------+---------------------+
| id   | dt                  |
+------+---------------------+
|    1 | 0000-00-00 00:00:00 |
|    3 | 0000-00-00 00:00:00 |
+------+---------------------+
2 rows in set (0.00 sec)

有没有觉得很奇怪,为什么查到了2条 dt 列值为 ‘0000-00-00 00:00:00’ 的记录?

先查看执行计划:

yejr@imysql.com> desc select * from t1 where dt is null\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t2
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 20.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

yejr@imysql.com> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `yejr`.`t1`.`id` AS `id`,`yejr`.`t2`.`dt` AS `dt` from `yejr`.`t1` where (`yejr`.`t1`.`dt` = '0000-00-00 00:00:00')

发现 IS NULL 条件被转换了,所以才能查到结果,这是为什么呢?
我尝试了调整SQL_MODE,发现并没什么卵用,最后还是在官方文档找到了答案:

For DATE and DATETIME columns that are declared as NOT NULL, you can find the special date ‘0000-00-00’ by using a statement like this:

SELECT * FROM tbl_name WHERE date_column IS NULL

This is needed to get some ODBC applications to work because ODBC does not support a ‘0000-00-00’ date value.

See Obtaining Auto-Increment Values, and the description for the FLAG_AUTO_IS_NULL option at Connector/ODBC Connection Parameters.

文档出自:12.3.2 Comparison Functions and Operators

最后的结论告诉我们,遇到问题时,查询官档是有多么重要。

Enjoy MySQL :)

全文完。


扫码加入叶老师的「MySQL核心优化课」,开启MySQL的修行之旅吧。

为何COUNT很慢却不写SLOW LOG

MySQL对COUNT(*)一直在优化。

1. 问题描述

某日,群友反馈问题对大表COUNT(*)很慢,但却不会记录到slow log中,这是为什么呢?
我自己根据他提供的信息,复现了这个问题:

# MySQL版本是8.0.20
[root@yejr.run]>\s
...
Server version:     8.0.20 MySQL Community Server - GPL
...

# 确认 long_query_time
[root@yejr.run]>select @@global.long_query_time, @@session.long_query_time;
+--------------------------+---------------------------+
| @@global.long_query_time | @@session.long_query_time |
+--------------------------+---------------------------+
|                 0.010000 |                  0.010000 |
+--------------------------+---------------------------+

# 执行 COUNT(*),耗时超过 0.01,但slow log没有记录
[root@yejr.run]>select count(*) from t1;
+----------+
| count(*) |
+----------+
|   799994 |
+----------+
1 row in set (0.27 sec)

这到底是为什么呢?

2. 问题排查

我们先检查所有和slow log相关的参数:

[root@yejr.run]>show global variables;
...
| log_slow_admin_statements              | OFF      |
| log_slow_extra                         | ON       |
| log_slow_slave_statements              | OFF      |
| long_query_time                        | 0.010000 |
| slow_query_log                         | ON       |
| slow_query_log_file                    | slow.log |
| log_output                             | FILE     |
| min_examined_row_limit                 | 100      |
| log_queries_not_using_indexes          | 1        |
| log_throttle_queries_not_using_indexes | 60       |
...

上面几个参数中,比较可疑有下面几个:

| min_examined_row_limit                 | 100      |
| log_queries_not_using_indexes          | 1        |
| log_throttle_queries_not_using_indexes | 60       |

先说 log_queries_not_using_indexes,这表示把没有使用索引的SQL也当成slow query记录下来,但在本例中,是有走索引的:

[root@yejr.run]>desc select count(*) from t1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 42760
     filtered: 100.00
        Extra: Using index

由此也顺便排除了参数 log_throttle_queries_not_using_indexes 的嫌疑。
那么,只剩下参数 min_examined_row_limit 的嫌疑,它表示当扫描行数少于设定值时,这个SQL也不会被当做slow query记录下来。
那么,本例中的COUNT(*)是否符合这种情况呢?
我们先把参数 min_examined_row_limit 值设置为 0,也就是默认值。

[root@yejr.run]>set global min_examined_row_limit=0;
[root@yejr.run]>set session min_examined_row_limit=0;
[root@yejr.run]>select @@global.min_examined_row_limit, @@session.min_examined_row_limit;
+---------------------------------+----------------------------------+
| @@global.min_examined_row_limit | @@session.min_examined_row_limit |
+---------------------------------+----------------------------------+
|                               0 |                                0 |
+---------------------------------+----------------------------------+

再执行一次 COUNT(*) 查询

[root@yejr.run]>select count(*) from t1;
+----------+
| count(*) |
+----------+
|    43462 |
+----------+
1 row in set (0.02 sec)

果然,这次被记录到slow log中了

# Query_time: 0.026083  Lock_time: 0.000110 Rows_sent: 1  Rows_examined: 0
...
select count(*) from t1;

注意到 Rows_examined 的值是 0,嗯,好像不太科学?

到这里,原因查明了,参数 min_examined_row_limit 的值设置大于 0 了,而本例中的 COUNT(*) 操作因为 Rows_examined=0,所以不会被记录到slow log中。

3. 问题解释

虽然知道问题原因了,但 Rows_examined 表示什么意思呢,文档中的解释如下:

• Rows_examined: 
The number of rows examined by the server layer (not counting any processing internal to storage engines).

可能字面意思上看起来不太好理解,换个思路,其实就是我们执行完一个SQL后,查看状态变量中名为 Handler_read_% 的几个指标即可,例如:

[root@yejr.run]> flush status;
[root@yejr.run]> select count(*) from t1;
...
[root@yejr.run]> show status like 'handler%read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 0     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+

可以看到以上几个值均为 0,因此 Rows_examined 也为 0,就不会被记录到slow log中了。

3.1 关于聚集索引并行读

说到这里,我还要隆重介绍MySQL 8.0的另一个新特性。
从8.0.14版本起,新增参数 innodb_parallel_read_threads,支持对聚集索引的并行扫描,需要满足以下几个条件:
– 参数 innodb_parallel_read_threads 值 > 0
– 只支持聚集索引
– 只支持无锁查询
– 不是INSERT…SELECT查询

主要用于加速以下两种场景:
– CHECK TABLE操作
– 不带WHERE条件的全表COUNT(*)

因此,COUNT(*)也是可以并行读聚集索引的,从error log中可以看到类似下面的信息:

[Note] [MY-011825] [InnoDB] Parallel scan: 4
[Note] [MY-011825] [InnoDB] ranges: 130 max_threads: 4 split: 128 depth: 1
[Note] [MY-011825] [InnoDB] n: 20914
[Note] [MY-011825] [InnoDB] n: 18066
[Note] [MY-011825] [InnoDB] n: 4482

从上述日志能看出来几点:
1. 设置了最高4个并行线程
2. 实际并行3个线程,实际并行数从1~4,不一定每次都跑最高并行
3. 分别扫描行数是 20914、18066、4482,即 COUNT() 结果总数是 43462

对t1表加上一个辅助索引后,再来看下面这个COUNT(*)

# 看起来这个查询是走辅助索引
[root@yejr.run]>desc select count(*) from t1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: index
possible_keys: NULL
          key: k1
      key_len: 5
          ref: NULL
         rows: 42760
     filtered: 100.00
        Extra: Using index

# 实际执行一把
[root@yejr.run]>select count(*) from t1;
+----------+
| count(*) |
+----------+
|    43462 |
+----------+
1 row in set (0.01 sec)

# 发现 Handler_read_% 的值还是 0
[root@yejr.run]>show status like 'handler%read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 0     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+        

且此时error log中依然有并行扫描的记录

[Note] [MY-011825] [InnoDB] Parallel scan: 4
[Note] [MY-011825] [InnoDB] ranges: 91 max_threads: 4 split: 88 depth: 1
[Note] [MY-011825] [InnoDB] n: 21493
[Note] [MY-011825] [InnoDB] n: 21486
[Note] [MY-011825] [InnoDB] n: 483

看到了么,实际上还是用到了聚集索引的并行扫描特性来加速。

提醒:上述error log中记录并行扫描聚集索引信息的功能在8.0.20中又被去掉了,上面之所以能看到这段日志是因为我排查到后面又回退到8.0.19版本了,有点费劲。。。
看下8.0.20 Release Notes

InnoDB: Unnecessary messages about parallel scans were printed to the error log. (Bug #30330448)

其实留着不挺好的嘛,搞不懂为毛要去掉的说。。。

提醒:MySQL 5.6版本里,无论基于主键还是辅助索引的全表无WHERE条件的COUNT(*),Rows_examined记录的是总行数,而不是像8.0那样值为0。

延伸阅读

Enjoy MySQL :)

全文完。


扫码加入叶老师的「MySQL核心优化课」,开启MySQL的修行之旅吧。

MySQL企业版之Firewall(SQL防火墙)

利用Friewall阻挡恶意SQL请求。

1. 关于Firewall插件

Friewall是MySQL企业版非常不错的功能插件之一,启用Firewall功能后,SQL的执行流程见下图示意:

2. Firewall插件的工作方式

Firewall插件的工作机制大概是这样的:
0.将某个账号Register(注册)到Firewall插件中,未注册的账号将不会被Firewall插件保护。
1.先将Firewall插件设置 recording(记录)模式,将各种SQL格式化/模式化之后,形成各种不同的SQL fingerprint(指纹)。例如下面的两条SQL,都会被格式化成一条:

# 原始SQL
a) SELECT * FROM t1 WHERE c1 = 1;
b) SELECT * FROM t1 WEHRE c1 = 1024;

# 格式化之后的SQL
SELECT * FROM t1 WHERE c1 = ?;

备注:在这个过程中,如果总有超长SQL的话,需要加大参数 max_digest_length 的设置,其默认值是1024。
2.Firewall插件会学习上述SQL,形成一个白名单。
3.经过一段时间的训练后,可以将Firewall插件工作模式切换为 protecting(保护)模式,开始工作。这时候就能自动判断有哪些SQL可能是恶意的,会被自动拒绝,并且记录到日志中,如果启用参数 mysql_firewall_trace的话。
4.如果还发生个别SQL被拒绝的情况,则可以将插件切换回 recording(记录)模式,继续学习训练一段时间再切换到工作状态。
5.此外,还有一种工作模式是detecting(探测),在这个模式下,符合白名单的会被放过执行,而其他SQL则会被记录到日志中,但并不会被拒绝执行,这就相当于正式开始工作前的灰度测试模式了。

简言之,就是在业务账号对外正式开放前,先自行模拟各种正常业务请求,使之完成前期必要的学习,正式上线后再开启保护模式。因为外网生产环境中坏人太多,任意时候都有可能有坏蛋提交各种恶意破坏的请求。

3. Firewall插件测试

接下来我们做个简单的测试场景。
首先,先尝试将一个新账号直接设置为 protecting 模式,这时候该账号还未学习任何规则,因此所有的SQL应该都会被拒绝才对。

[root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
+-------------------------------------------------------------------------+
| result                                                                  |
+-------------------------------------------------------------------------+
| ERROR: PROTECTING mode requested for yejr@% but the whitelist is empty. |
+-------------------------------------------------------------------------+

嗯,看来这个插件还挺聪明的,发现规则是空的,直接不让设置了,要不然可能会害的DBA直接下岗走人了吧,哈哈。

还是先设置为 recording 模式吧。

[root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'recording');
+-----------------------------------------------+
| read_firewall_whitelist(arg_userhost,FW.rule) |
+-----------------------------------------------+
| Imported users: 0
Imported rules: 0
          |
+-----------------------------------------------+
1 row in set (0.01 sec)

# 手动查询 mysql.firewall_users 表确认
[root@yejr.run]>select * from mysql.firewall_users;
+----------+-----------+
| USERHOST | MODE      |
+----------+-----------+
| yejr@%   | RECORDING |
+----------+-----------+
1 row in set (0.00 sec)

设置成功。

然后用这个新账号连接MySQL,执行一些合规的SQL操作后,再查看白名单规则表:

[root@yejr.run]>select * from mysql.firewall_whitelist;
+----------+--------------------------------------------------------+----+
| USERHOST | RULE                                                   | ID |
+----------+--------------------------------------------------------+----+
| yejr@%   | SHOW CREATE TABLE `t1`                                 |  8 |
| yejr@%   | SELECT * FROM `t1`                                     |  9 |
| yejr@%   | SELECT * FROM `t1` WHERE `id` >= ?                     | 10 |
| yejr@%   | SELECT * FROM `t1` WHERE `id` = `rand` ( ) * ?         | 11 |
| yejr@%   | SELECT * FROM `t1` WHERE `id` = `rand` ( ) * ? LIMIT ? | 12 |
| yejr@%   | SELECT * FROM `t1` WHERE `c1` >= ?                     | 13 |
+----------+--------------------------------------------------------+----+

再将该账号修改为 PROTECTING 模式:

[root@yejr.run]>CALL mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
Query OK, 6 rows affected (0.01 sec)

执行一些不合规的SQL后,看怎么报错的。

[yejr@yejr.run] [test]>select * from t1 order by rand() limit 1;
ERROR 1045 (28000): Statement was blocked by Firewall

error log中也记录了相应的行为:

2020-06-09T09:50:38.286878Z 26 [Note] [MY-011192] [Server] Plugin MYSQL_FIREWALL reported: 'ACCESS DENIED for 'yejr@%'. Reason: No match in whitelist. Statement: SELECT * FROM `t1` ORDER BY `rand` ( ) LIMIT ?'

再查看几个相关的全局状态参数:

[root@yejr.run] [mysql]>show global status like '%firewall%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Firewall_access_denied     | 10    | #拒绝次数
| Firewall_access_granted    | 11    | #通过次数
| Firewall_access_suspicious | 9     | #在DETECTING模式下不匹配白名单的次数
| Firewall_cached_entries    | 6     | #在cache中的白名单数量
+----------------------------+-------+

想要关闭该账号的Firewall规则,执行下面的指令即可:

[root@yejr.run] [mysql]>call mysql.sp_set_firewall_mode('yejr@%', 'RESET');

或者只是简单地设置为 OFF 也可以:

[root@yejr.run] [mysql]>call mysql.sp_set_firewall_mode('yejr@%', 'OFF');

二者的区别在于,设置为 RESET 时,除了关闭Firewall保护,同时也会将该账号之前训练学习的白名单全部清空,这样下次再想采用Firewall保护就需要重头开始了,除非再也不用了,否则不建议这么做。

当已经对一个账号启用Firewall保护后,此时有新增业务SQL不在白名单中,除了将模式改回 RECORDINGDETECTING 之外,其实还可以手动往 mysql.firewall_whitelist 表中插入格式化之后的SQL,再刷新使之生效即可,例如:

# 先确认一条SQL的格式化写法
[root@yejr.run]>SELECT normalize_statement('SELect *,sleep(1) from t1 where 1');
+----------------------------------------------------------+
| normalize_statement('SELect *,sleep(1) from t1 where 1') |
+----------------------------------------------------------+
| SELECT * , `sleep` (?) FROM `t1` WHERE ?                 |
+----------------------------------------------------------+

# 手动插入新规则
[root@yejr.run]>insert into mysql.firewall_whitelist select 'yejr@%', 'SELECT * , `sleep` (?) FROM `t1` WHERE ?', 1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

# 查询内存中的规则,此时为空
[root@yejr.run] [mysql]>SELECT * FROM INFORMATION_SCHEMA.MYSQL_FIREWALL_WHITELIST;
Empty set (0.00 sec)

# reload加载规则,使之生效
[root@yejr.run] [mysql]>CALL mysql.sp_reload_firewall_rules('yejr@%');
+--------+
| Result |
+--------+
| OK     |
+--------+
1 row in set (0.00 sec)

+--------------------------------------+
| Result                               |
+--------------------------------------+
| Imported users: 0
Imported rules: 1
 |
+--------------------------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

# 再次查询,此时就可以看到规则加载到内存中了,已生效
[root@yejr.run]>SELECT * FROM INFORMATION_SCHEMA.MYSQL_FIREWALL_WHITELIST;
+----------+------------------------------------------+
| USERHOST | RULE                                     |
+----------+------------------------------------------+
| yejr@%   | SELECT * , `sleep` (?) FROM `t1` WHERE ? |
+----------+------------------------------------------+
1 row in set (0.00 sec)

# 最后记得启用保护模式
[root@yejr.run]>call mysql.sp_set_firewall_mode('yejr@%', 'PROTECTING');
Query OK, 3 rows affected (0.00 sec)

4. 总结

简单测试下来,我认为Firewall插件至少有几个地方可以更完善:
1.对SQL进行格式化时,不支持正则匹配模式,建议增加。
2.设置RECORDING模式测试期间,我执行一个SQL后,又手动执行CTRL+C终止,结果记录了一条KILL QUERY ? 的规则,这个规则就不建议记录了。
3.存储过程 mysql.sp_reload_firewall_rules() 可以增加一个参数表示重载规则后,是否要顺便设置账号的规则,例如 CALL mysql.sp_reload_firewall_rules('yejr@%', 'PROTECTING') 表示重载完规则后,顺便将该账号设置为 PROTECTING 模式。
总的来说,Firewall插件工作方式还是比较简单的,直观感觉就是对规则的学习比较初级,真正在线上生产环境启用的话,可能需要记录大量的规则,这也势必会造成性能的下降,以后再做个性能测试吧。
更多关于Firewall插件资料请查看官方手册。

延伸阅读

Enjoy MySQL 8.0 :)

全文完。


扫码加入叶老师的「MySQL核心优化课」,开启MySQL的修行之旅吧。