今天线上发生CPU使用率超过95%的报警, 登上RDS以后发现一堆的sending data状态的SQL,大致有3个问题SQL,因为这3个SQL导致了其他原本很快的SQL也被“拖慢了”,以下是其中的一个SQL,拿出来记录一下

    建表DDL:

    1. mysql> show create table task_log\G
    2. *************************** 1. row ***************************
    3. Table: task_log
    4. Create Table: CREATE TABLE `task_log` (
    5. `id` bigint(20) NOT NULL AUTO_INCREMENT,
    6. `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
    7. `task_type` int(1) NOT NULL DEFAULT '0' COMMENT '任务类型 1:每日任务 2:成长任务 3:签到任务',
    8. ....
    9. `user_id` varchar(24) NOT NULL DEFAULT '' COMMENT '用户ID',
    10. `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '县编码',
    11. `task_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '任务id',
    12. ....
    13. PRIMARY KEY (`id`),
    14. KEY `idx_create_at` (`created_at`) USING BTREE,
    15. KEY `idx_user_id_create_at` (`user_id`,`created_at`) USING BTREE
    16. ) ENGINE=InnoDB AUTO_INCREMENT=5549 DEFAULT CHARSET=utf8mb4 COMMENT='任务明细表'
    17. 1 row in set (0.00 sec)
    1. mysql> show create table account\G
    2. *************************** 1. row ***************************
    3. Table: account
    4. Create Table: CREATE TABLE `account` (
    5. `id` varchar(24) NOT NULL DEFAULT '' COMMENT '代码生成',
    6. `auto_pk` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    7. `nick_name` varchar(45) NOT NULL DEFAULT '' COMMENT '昵称',
    8. ....
    9. `forbidden` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否禁言(0:否,1:是)',
    10. `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    11. ....
    12. `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '县编码',
    13. `portrait_update_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '用户修改头像标记:0否 1是',
    14. PRIMARY KEY (`auto_pk`),
    15. UNIQUE KEY `uk_id` (`id`),
    16. KEY `idx_name_anonymous` (`nick_name`,`anonymous`),
    17. KEY `idx_nav_version` (`nav_version`),
    18. KEY `idx_unid_authType` (`union_id`(191),`auth_type`),
    19. KEY `idx_refCode_anonymous` (`ref_code`,`anonymous`),
    20. KEY `idx_updated_at` (`updated_at`),
    21. KEY `idx_authUid_authType` (`auth_uid`,`auth_type`),
    22. KEY `idx_snda_id` (`snda_id`)
    23. ) ENGINE=InnoDB AUTO_INCREMENT=16668 DEFAULT CHARSET=utf8mb4 COMMENT='客户端用户表'
    24. 1 row in set (0.00 sec)

    首先看下执行计划

    1. mysql> desc
    2. SELECT t1.id AS taskLogId,
    3. t4.maxCreateTimeByDay,
    4. t1.task_type,
    5. t1.event_name AS eventName,
    6. t1.completed AS completed,
    7. t1.finish_times AS finishTimes,
    8. t1.frequency,
    9. t1.experience,
    10. t1.integral,
    11. t1.status,
    12. t1.reason,
    13. t1.user_id AS userId,
    14. t2.auth_type AS authType,
    15. t2.mobile AS mobile,
    16. t2.nick_name AS nickName,
    17. t1.task_id AS taskId,
    18. t1.tenant_id AS tenantId,
    19. t4.date
    20. FROM(
    21. SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, DATE_FORMAT(created_at, '%Y-%m-%d') "date"
    22. FROM task_log
    23. GROUP BY user_id, task_id, date) t4 JOIN task_log t1 ON t4.user_id = t1.user_id AND t4.task_id=t1.task_id and t4.maxCreateTimeByDay= t1.created_at
    24. INNER JOIN account t2 ON t4.user_id= t2.ID AND t4.tenant_id= t2.tenant_id
    25. WHERE t1.tenant_id= 2;
    1. +----+-------------+------------+------------+--------+-------------------------------------+-----------------------+---------+----------------------------------+------+----------+---------------------------------+
    2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    3. +----+-------------+------------+------------+--------+-------------------------------------+-----------------------+---------+----------------------------------+------+----------+---------------------------------+
    4. | 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 5560 | 100.00 | Using where |
    5. | 1 | PRIMARY | t1 | NULL | ref | idx_create_at,idx_user_id_create_at | idx_user_id_create_at | 105 | t4.user_id,t4.maxCreateTimeByDay | 1 | 4.96 | Using where |
    6. | 1 | PRIMARY | t2 | NULL | eq_ref | uk_id | uk_id | 98 | t4.user_id | 1 | 10.00 | Using where |
    7. | 2 | DERIVED | task_log | NULL | ALL | NULL | NULL | NULL | NULL | 5560 | 100.00 | Using temporary; Using filesort |
    8. +----+-------------+------------+------------+--------+-------------------------------------+-----------------------+---------+----------------------------------+------+----------+---------------------------------+
    9. 4 rows in set, 1 warning (0.00 sec)

    从执行计划里可以看到,

    1. SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, DATE_FORMAT(created_at, '%Y-%m-%d') "date"
    2. FROM task_log
    3. GROUP BY user_id, task_id, date)

    的部分没有用到索引,并且在与表task log关联时也没有走索引,这里有点特殊的情况就是t4跟t1关联是3个字段关联

    建立以下索引

    1. mysql> create index idx_tenantid on task_log(tenant_id);
    2. Query OK, 0 rows affected (0.06 sec)
    3. Records: 0 Duplicates: 0 Warnings: 0
    1. mysql> alter table task_log add index idx_userid_taskid_create(user_id,task_id,created_at);
    2. Query OK, 0 rows affected (0.10 sec)
    3. Records: 0 Duplicates: 0 Warnings: 0

    再来看执行计划

    1. mysql> desc SELECT t1.id AS taskLogId, t4.maxCreateTimeByDay, t1.task_type, t1.event_name AS eventName, t1.completed AS completed, t1.finish_times AS finishTimes, t1.frequency, t1.experience, t1.integral, t1.status, t1.reason, t1.user_id AS userId, t2.auth_type AS authType, t2.mobile AS mobile, t2.nick_name AS nickName, t1.task_id AS taskId, t1.tenant_id AS tenantId, t4.date FROM( SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, DATE_FORMAT(created_at, '%Y-%m-%d') "date" FROM task_log GROUP BY user_id, task_id, date) t4 JOIN task_log t1 ON t4.user_id = t1.user_id AND t4.task_id=t1.task_id and t4.maxCreateTimeByDay= t1.created_at INNER JOIN account t2 ON t4.user_id= t2.ID AND t4.tenant_id= t2.tenant_id WHERE t1.tenant_id= 2;
    2. +----+-------------+------------+------------+--------+---------------------------------------------------------------------------+--------------+---------+----------------------------------------------------------------------+------+----------+---------------------------------+
    3. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    4. +----+-------------+------------+------------+--------+---------------------------------------------------------------------------+--------------+---------+----------------------------------------------------------------------+------+----------+---------------------------------+
    5. | 1 | PRIMARY | t1 | NULL | ref | idx_create_at,idx_user_id_create_at,idx_userid_taskid_create,idx_tenantid | idx_tenantid | 8 | const | 345 | 100.00 | NULL |
    6. | 1 | PRIMARY | t2 | NULL | eq_ref | uk_id | uk_id | 98 | xucl.t1.user_id | 1 | 100.00 | NULL |
    7. | 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 122 | xucl.t1.user_id,xucl.t1.task_id,xucl.t1.created_at,xucl.t2.tenant_id | 10 | 100.00 | NULL |
    8. | 2 | DERIVED | task_log | NULL | ALL | idx_userid_taskid_create | NULL | NULL | NULL | 5560 | 100.00 | Using temporary; Using filesort |
    9. +----+-------------+------------+------------+--------+---------------------------------------------------------------------------+--------------+---------+----------------------------------------------------------------------+------+----------+---------------------------------+
    10. 4 rows in set, 1 warning (0.00 sec)

    从执行计划看,问题就只剩下子查询的部分,如果这个部分能用到索引,那么这个SQL基本上就能达到要求了
    先看下运行时间

    1. 194 rows in set (0.09 sec)

    总共运行时间为90ms

    再来分析下子查询的部分

    1. SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, DATE_FORMAT(created_at, '%Y-%m-%d') "date"
    2. FROM task_log
    3. GROUP BY user_id, task_id, date)

    这个部分是根据user_id,task_id,date分组,而date又是把created_at字段格式化后取日期的部分,这个部分是没有办法直接用到索引的
    查询了下生产环境的RDS版本为5.7.18,可以用5.7的虚拟列完美解决这个问题呀!
    先来看下MySQL官方文档关于虚拟列咋写的
    【MySQL】MySQL5.7利用虚拟列优化 - 图1

    • 虚拟列分为VIRTUAL和STORED两种,两者的区别是VIRTUAL只是在读行的时候计算结果,但在物理上是不存储的,并且InnoDB引擎支持在虚拟列上建二级索引;

    • 而STORED则是当行数据进行插入或更新时计算并存储的,是需要占用物理空间的,并且也可以添加为索引列。

    但虚拟列在实际使用过程中也需要注意如下:

    1. 虚拟列支持使用内置函数和运算符,但不能使用 CONNECTION_ID(), CURRENT_USER(), NOW()

    2. 子查询、参数、变量、 存储函数和自定义函数都无法使用虚拟列可以参照其他的虚拟列,但是参照的虚拟列必须定义在前面,假如参照的列为非虚拟列,那么该列可以定义在虚拟列的前后

    3. 自增属性无法使用在虚拟列并且自增列不能作为基础列

    4. MySQL5.7.10版本,如果表达式计算造成截断或将错误数据传入函数,CREATE TABLE语句会造成错误并且DDL操作会被拒绝

    OK,大致了解了虚拟列,我们看下DDL语法

    1. col_name data_type [GENERATED ALWAYS] AS (expression)
    2. [VIRTUAL | STORED] [NOT NULL | NULL]
    3. [UNIQUE [KEY]] [[PRIMARY] KEY]
    4. [COMMENT 'string']

    回到正题上来,首先,增加虚拟列

    1. alter table task_log add date date generated always as (DATE_FORMAT(created_at, '%Y-%m-%d')) stored after created_at;

    对虚拟列建立索引

    1. create index idx_userid_taskid_crtime on task_log(user_id,task_id,date);

    再看下执行计划

    1. mysql> desc
    2. SELECT t1.id AS taskLogId,
    3. t4.maxCreateTimeByDay,
    4. t1.task_type,
    5. t1.event_name AS eventName,
    6. t1.completed AS completed,
    7. t1.finish_times AS finishTimes,
    8. t1.frequency,
    9. t1.experience,
    10. t1.integral,
    11. t1.status,
    12. t1.reason,
    13. t1.user_id AS userId,
    14. t2.auth_type AS authType,
    15. t2.mobile AS mobile,
    16. t2.nick_name AS nickName,
    17. t1.task_id AS taskId,
    18. t1.tenant_id AS tenantId,
    19. t4.date
    20. FROM(
    21. SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, DATE_FORMAT(created_at, '%Y-%m-%d') "date"
    22. FROM task_log
    23. GROUP BY user_id, task_id, date) t4 JOIN task_log t1 ON t4.user_id = t1.user_id AND t4.task_id=t1.task_id and t4.maxCreateTimeByDay= t1.created_at
    24. INNER JOIN account t2 ON t4.user_id= t2.ID AND t4.tenant_id= t2.tenant_id
    25. WHERE t1.tenant_id= 2;
    1. +----+-------------+------------+------------+--------+----------------------------------------------------------------------------------------------------+--------------------------+---------+----------------------------------------------------------------------+------+----------+-------+
    2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    3. +----+-------------+------------+------------+--------+----------------------------------------------------------------------------------------------------+--------------------------+---------+----------------------------------------------------------------------+------+----------+-------+
    4. | 1 | PRIMARY | t1 | NULL | ref | idx_create_at,idx_user_id_create_at,idx_userid_taskid_create,idx_tenantid,idx_userid_taskid_crtime | idx_tenantid | 8 | const | 345 | 100.00 | NULL |
    5. | 1 | PRIMARY | t2 | NULL | eq_ref | uk_id | uk_id | 98 | xucl.t1.user_id | 1 | 100.00 | NULL |
    6. | 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 122 | xucl.t1.user_id,xucl.t1.task_id,xucl.t1.created_at,xucl.t2.tenant_id | 10 | 100.00 | NULL |
    7. | 2 | DERIVED | task_log | NULL | index | idx_userid_taskid_crtime | idx_userid_taskid_crtime | 110 | NULL | 5472 | 100.00 | NULL |
    8. +----+-------------+------------+------------+--------+----------------------------------------------------------------------------------------------------+--------------------------+---------+----------------------------------------------------------------------+------+----------+-------+
    9. 4 rows in set, 2 warnings (0.00 sec)

    从执行计划上看,子查询的部分也已经用上了刚刚对虚拟列建立的索引,但是可以注意到有两个warnings

    1. mysql> show warnings;
    2. +---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    3. | Level | Code | Message |
    4. +---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    5. | Warning | 1052 | Column 'date' in group statement is ambiguous |
    6. | Note | 1003 | /* select#1 */ select `xucl`.`t1`.`id` AS `taskLogId`,`t4`.`maxCreateTimeByDay` AS `maxCreateTimeByDay`,`xucl`.`t1`.`task_type` AS `task_type`,`xucl`.`t1`.`event_name` AS `eventName`,`xucl`.`t1`.`completed` AS `completed`,`xucl`.`t1`.`finish_times` AS `finishTimes`,`xucl`.`t1`.`frequency` AS `frequency`,`xucl`.`t1`.`experience` AS `experience`,`xucl`.`t1`.`integral` AS `integral`,`xucl`.`t1`.`status` AS `status`,`xucl`.`t1`.`reason` AS `reason`,`xucl`.`t1`.`user_id` AS `userId`,`xucl`.`t2`.`auth_type` AS `authType`,`xucl`.`t2`.`mobile` AS `mobile`,`xucl`.`t2`.`nick_name` AS `nickName`,`xucl`.`t1`.`task_id` AS `taskId`,`xucl`.`t1`.`tenant_id` AS `tenantId`,`t4`.`date` AS `date` from (/* select#2 */ select `xucl`.`task_log`.`user_id` AS `user_id`,`xucl`.`task_log`.`task_id` AS `task_id`,max(`xucl`.`task_log`.`created_at`) AS `maxCreateTimeByDay`,`xucl`.`task_log`.`tenant_id` AS `tenant_id`,date_format(`xucl`.`task_log`.`created_at`,'%Y-%m-%d') AS `date` from `xucl`.`task_log` group by `xucl`.`task_log`.`user_id`,`xucl`.`task_log`.`task_id`,`xucl`.`task_log`.`date`) `t4` join `xucl`.`task_log` `t1` join `xucl`.`account` `t2` where ((`t4`.`maxCreateTimeByDay` = `xucl`.`t1`.`created_at`) and (`t4`.`task_id` = `xucl`.`t1`.`task_id`) and (`t4`.`tenant_id` = `xucl`.`t2`.`tenant_id`) and (`xucl`.`t2`.`id` = `xucl`.`t1`.`user_id`) and (`t4`.`user_id` = `xucl`.`t1`.`user_id`) and (`xucl`.`t1`.`tenant_id` = 2)) |
    7. +---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    8. 2 rows in set (0.00 sec)

    可以看到的是新增的date跟原先DATE_FORMAT(created_at, ‘%Y-%m-%d’)是重名的,这个地方也是本次优化一个需要注意的地方,虽然这里报了warning,但是我们可以巧妙地解决无需修改线上代码就能达到优化的结果

    看下执行结果

    1. mysql> show profiles;
    2. +----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    3. | Query_ID | Duration | Query |
    4. +----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    5. | 1 | 0.00081025 | desc SELECT t1.id AS taskLogId, t4.maxCreateTimeByDay, t1.task_type, t1.event_name AS eventName, t1.completed AS completed, t1.finish_times AS finishTimes, t1.frequency, t1.experience, t1.integral, t1.status, t1.reason, t1. |
    6. | 2 | 0.02203450 | SELECT t1.id AS taskLogId, t4.maxCreateTimeByDay, t1.task_type, t1.event_name AS eventName, t1.completed AS completed, t1.finish_times AS finishTimes, t1.frequency, t1.experience, t1.integral, t1.status, t1.reason, t1.user_ |
    7. +----------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    8. 2 rows in set, 1 warning (0.00 sec)
    9. mysql> show profile for query 2;
    10. +----------------------+----------+
    11. | Status | Duration |
    12. +----------------------+----------+
    13. | starting | 0.000245 |
    14. | checking permissions | 0.000007 |
    15. | checking permissions | 0.000003 |
    16. | checking permissions | 0.000006 |
    17. | Opening tables | 0.000041 |
    18. | init | 0.000093 |
    19. | System lock | 0.000011 |
    20. | optimizing | 0.000005 |
    21. | optimizing | 0.000005 |
    22. | statistics | 0.000020 |
    23. | preparing | 0.000021 |
    24. | Sorting result | 0.000016 |
    25. | statistics | 0.000113 |
    26. | preparing | 0.000014 |
    27. | executing | 0.000011 |
    28. | Sending data | 0.000105 |
    29. | executing | 0.000003 |
    30. | Sending data | 0.020828 |
    31. | end | 0.000023 |
    32. | query end | 0.000017 |
    33. | closing tables | 0.000006 |
    34. | removing tmp table | 0.000371 |
    35. | closing tables | 0.000017 |
    36. | freeing items | 0.000038 |
    37. | cleaning up | 0.000017 |
    38. +----------------------+----------+
    39. 25 rows in set, 1 warning (0.00 sec)

    整个执行效果在21ms左右,性能提升4.3倍。

    但是有点小小强迫症,让开发尽快修改代码,SQL修改如下

    1. mysql> desc
    2. SELECT t1.id AS taskLogId,
    3. t4.maxCreateTimeByDay,
    4. t1.task_type,
    5. t1.event_name AS eventName,
    6. t1.completed AS completed,
    7. t1.finish_times AS finishTimes,
    8. t1.frequency,
    9. t1.experience,
    10. t1.integral,
    11. t1.status,
    12. t1.reason,
    13. t1.user_id AS userId,
    14. t2.auth_type AS authType,
    15. t2.mobile AS mobile,
    16. t2.nick_name AS nickName,
    17. t1.task_id AS taskId,
    18. t1.tenant_id AS tenantId,
    19. t4.date
    20. FROM(
    21. SELECT user_id, task_id, MAX(created_at) "maxCreateTimeByDay", tenant_id, date
    22. FROM task_log
    23. GROUP BY user_id, task_id, date) t4 JOIN task_log t1 ON t4.user_id = t1.user_id AND t4.task_id=t1.task_id and t4.maxCreateTimeByDay= t1.created_at
    24. INNER JOIN account t2 ON t4.user_id= t2.ID AND t4.tenant_id= t2.tenant_id
    25. WHERE t1.tenant_id= 2;

    最后生产环境上线以后,由原先的378ms提升到36ms,提升了10.5倍,效果还是很明显的
    【MySQL】MySQL5.7利用虚拟列优化 - 图2
    【MySQL】MySQL5.7利用虚拟列优化 - 图3