欢迎光临

我们一直在努力
当前位置:首页 > 互联网 >

升级MySQL5.7,开发不得不注意的坑

日期:
后台-插件-广告管理-首页/栏目/内容广告位一(PC)
后台-插件-广告管理-首页/栏目/内容广告位一(手机)

前段时间,将线上MySQL数据库升级到了5.7。考虑到可能产生的不兼容性,在升级之前,确实也是战战兢兢,虽然测试环境,开发环境早在半年前就已提前升级。

基于前期的调研和朋友的反馈,与开发相关的主要有两点:

sql_mode

MySQL 5.6中,其默认值为"NO_ENGINE_SU BSTITUTION",可理解为非严格模式,譬如,对自增主键插入空字符串'',虽然提示warning,但并不影响自增主键的生成。

但在MySQL 5.7中,其就调整为了严格模式,对于上面这个,其不会提示warning,而是直接报错。

分组求最值

分组求最值的某些写法在MySQL5.7中得不到预期结果,这点,相对来说比较隐蔽。

其中,第一点是可控的,毕竟可以调整参数。而第二点,却是不可控的,没有参数与之相关,需要开发Review代码。

下面具体来看看

测试数据

mysql> select * from emp;
+-------+----------+--------+--------+
| empno | ename    | sal    | deptno |
+-------+----------+--------+--------+
|  1001 | emp_1001 | 100.00 |     10 |
|  1002 | emp_1002 | 200.00 |     10 |
|  1003 | emp_1003 | 300.00 |     20 |
|  1004 | emp_1004 | 400.00 |     20 |
|  1005 | emp_1005 | 500.00 |     30 |
|  1006 | emp_1006 | 600.00 |     30 |
+-------+----------+--------+--------+
6 rows in set (0.00 sec)

其中,empno是员工编号,ename是员工姓名,sal是工资,deptno是员工所在部门号。

业务的需求是,求出每个部门中工资最高的员工的相关信息。

在MySQL5.6中,我们可以通过下面这个SQL来实现,

SELECT
    deptno,ename,sal 
FROM
    ( SELECT * FROM emp ORDER BY sal DESC ) t 
GROUP BY
    deptno;

结果如下,可以看到,其确实实现了预期效果。

+--------+----------+--------+
| deptno | ename    | sal    |
+--------+----------+--------+
|     10 | emp_1002 | 200.00 |
|     20 | emp_1004 | 400.00 |
|     30 | emp_1006 | 600.00 |
+--------+----------+--------+

再来看看MySQL5.7的结果,竟然不一样。

+--------+----------+--------+
| deptno | ename    | sal    |
+--------+----------+--------+
|     10 | emp_1001 | 100.00 |
|     20 | emp_1003 | 300.00 |
|     30 | emp_1005 | 500.00 |
+--------+----------+--------+

实际上,在MySQL5.7中,对该SQL进行了改写,改写后的SQL可通过explain(extended) + show warnings查看。

mysql> explain select deptno,ename,sal from (select * from emp order by sal desc) t group by deptno;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `slowtech`.`emp`.`deptno` AS `deptno`,`slowtech`.`emp`.`ename` AS `ename`,`slowtech`.`emp`.`sal` AS `sal` from `slowtech`.`emp` group by `slowtech`.`emp`.`deptno`
1 row in set (0.00 sec)

从改写后的SQL来看,其消除了子查询,导致结果未能实现预期效果,官方也证实了这一点,https://bugs.mysql.com/bug.php?id=80131

很多人可能不以为然,认为没人会这样写,但在大名鼎鼎的stackoverflow中,该实现的点赞数就有116个-由此可见其受众之广,仅次于后面提到的“方法二”(点赞数206个)。 
https://stackoverflow.com/questions/12102200/get-records-with-max-value-for-each-group-of-grouped-sql-results

需要注意的是,该SQL在5.7中是不能直接运行的,其会提示如下错误:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 't.ename' which is not functionally dependent>三种实现方式的性能对比

因上面测试案例的数据量太小,三种实现方式的结果都是秒出,仅凭执行计划很难直观地看出实现方式的优劣。

下面换上数据量更大的测试数据,官方示例数据库employees中的dept_emp表,https://github.com/datacharmer/test_db

表的相关信息如下,其中emp_no是员工编号,dept_no是部门编号,from_date是入职日期。[!--empirenews.page--]

mysql> show create table dept_emp\G
*************************** 1. row ***************************
       Table: dept_emp
Create Table: CREATE TABLE `dept_emp` (
  `emp_no` int(11) NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  KEY `dept_no` (`dept_no`,`from_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.09 sec)

mysql> select * from dept_emp limit 1;
+--------+---------+------------+------------+
| emp_no | dept_no | from_date  | to_date    |
+--------+---------+------------+------------+
|  10001 | d005    | 1986-06-26 | 9999-01-01 |
+--------+---------+------------+------------+
1 row in set (0.00 sec)

方法1

mysql> select d.dept_no,d.emp_no,d.from_date from dept_emp d, (select dept_no,max(from_date) max_hiredate from dept_emp group by dept_no) t where d.dept_no=t.dept_no and d.from_date=t.max_hiredate;
…
12 rows in set (0.00 sec)

mysql> explain select d.dept_no,d.emp_no,d.from_date from dept_emp d, (select dept_no,max(from_date) max_hiredate from dept_emp group by dept_no) t where d.dept_no=t.dept_no and d.from_date=t.max_hiredate;
+----+-------------+------------+------------+-------+---------------+---------+---------+--------------------------+------+----------+----------------------
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref                      | rows | filtered | Extra                
+----+-------------+------------+------------+-------+---------------+---------+---------+--------------------------+------+----------+----------------------
|  1 | PRIMARY     | <derived2> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL                     |    9 |   100.00 | Using where          
|  1 | PRIMARY     | d          | NULL       | ref   | dept_no       | dept_no | 19      | t.dept_no,t.max_hiredate |    5 |   100.00 | NULL                 
|  2 | DERIVED     | dept_emp   | NULL       | range | dept_no       | dept_no | 16      | NULL                     |    9 |   100.00 | Using index for group-by
+----+-------------+------------+------------+-------+---------------+---------+---------+--------------------------+------+----------+----------------------

方法2

mysql> explain select a.dept_no,a.emp_no,a.from_date from dept_emp a left join dept_emp b on a.dept_no=b.dept_no and a.from_date < b.from_date where b.from_date is null;
+----+-------------+-------+------------+------+---------------+---------+---------+--------------------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key     | key_len | ref                | rows   | filtered | Extra                    |
+----+-------------+-------+------------+------+---------------+---------+---------+--------------------+--------+----------+--------------------------+
|  1 | SIMPLE      | a     | NULL       | ALL  | NULL          | NULL    | NULL    | NULL               | 331008 |   100.00 | NULL                     |
|  1 | SIMPLE      | b     | NULL       | ref  | dept_no       | dept_no | 16      | slowtech.a.dept_no |  41376 |    19.00 | Using where; Using index |
+----+-------------+-------+------------+------+---------------+---------+---------+--------------------+--------+----------+--------------------------+
2 rows in set, 1 warning (0.00 sec)

方法3

mysql> select dept_no,emp_no,from_date from ( select dept_no,emp_no,from_date,last_value(from_date) over(partition by dept_no order by from_date rows between unbounded preceding and unbounded following) max_hiredate from dept_emp) a where from_date=max_hiredate;
…
12 rows in set (1.57 sec)

mysql> desc select dept_no,emp_no,from_date from ( select dept_no,emp_no,from_date,last_value(from_date) over(partition by dept_no order by from_date rows between unbounded preceding and unbounded following) max_hiredate from dept_emp) a where from_date=max_hiredate;
+----+-------------+------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
+----+-------------+------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
|  1 | PRIMARY     | <derived2> | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 331008 |   100.00 | Using where    |
|  2 | DERIVED     | dept_emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 331008 |   100.00 | Using filesort |
+----+-------------+------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
2 rows in set, 2 warnings (0.00 sec)[!--empirenews.page--]

从执行时间上看,

方法1的时间最短,在有复合索引(deptno, fromdate)的情况下,结果瞬间就出来了,即使在没有索引的情况下,也只消耗了0.75s。

方法2的时间最长,3个小时还是没出结果。同样的数据,同样的SQL,放到Oracle查,也消耗了87分49秒。

方法3的时间比较固定,无论是否存在索引,都维持在1.5s左右,比方法1的耗时要久。

这里,对之前提到的,MySQL 5.7中不再兼容的实现方式也做了个测试,在没有任何索引的情况下,其稳定在0.7s(性能并不弱,怪不得有人使用),而同等情况下,方法1稳定在0.5s(哈,MySQL 5.6竟然比8.0还快)。但与方法1不同的是,其无法通过索引进行优化。

从执行计划上看,

方法1, 先将group by的结果放到临时表中,然后再将该临时表作为驱动表,来和dept_emp表进行关联查询。驱动表小(只有9条记录),关联列又有索引,无怪乎,结果能秒出。

方法2, 两表关联。其犯了SQL优化中的两个大忌。

   1. 驱动表太大,其有331603条记录。

   2. 被驱动表虽然也有索引,但从执行计划上看,其只使用了复合索引  (dept_no, from_date)中的dept_no,而dept_no的选择率又太低,毕竟只有9个部门。

方法3, 先把分析的结果放到一个临时表中,然后再对该临时表进行处理。其进行了两次全表扫描,一次是针对dept_emp表,一次是针对临时表。

所以,对于分组求最值的需求,建议使用方法1,其不仅符合SQL规范,查询性能上也是最好的,尤其是在联合索引的情况下。

PS:

经大神指点,对之前提到的,MySQL 5.7中不再兼容的实现方式,实际可以通过调整optimizer_switch来加以规避

set optimizer_switch='derived_merge=off';

derived_merge是MySQL 5.7引入的,其会试图将Derived Table(派生表,from后面的子查询),视图引用,公用表表达式(Common table expressions)与外层查询进行合并。如,

SELECT *
  FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2
          ON t1.f2=derived_t2.f1
  WHERE t1.f1 > 0;

改写为

SELECT *
 FROM t1 JOIN (SELECT DISTINCT f1 FROM t2) AS derived_t2
         ON t1.f1=derived_t2.f1;
后台-插件-广告管理-首页/栏目/内容广告位二(PC)
后台-插件-广告管理-首页/栏目/内容广告位二(手机)
后台-插件-广告管理-内容广告位三(PC)
后台-插件-广告管理-内容广告位三(手机)

相关阅读

后台-插件-广告管理-内容广告位四(PC)
后台-插件-广告管理-内容广告位四(手机)

聚合标签

热门文章

后台-插件-广告管理-侧边广告位一(PC)
后台-插件-广告管理-侧边广告位一(手机)
  • Windows主机中localhost与127.0.0.1的区别是什么?

  • localhost与127.0.0.1的区别是什么? 相信有人会说是本地IP,曾有人说,用127.0.0.1比localhost好,可以减少一次解析。 这个理解是错误的,其实这两者是有区别的。 localhost也叫l
  • c盘满了怎么清理垃圾而不误删

  • 今天分享的主题是:c盘爆满发出警告如何清理又不误删系统文件。如果你也不会的话就看看下面的经验吧。 c盘满了怎么清理垃圾而不误删 1、很多人在清理c盘垃圾的时候会误删
  • steam怎么退款?

  • 有的时候我们在steam上买了游戏,但是却发现自己的电脑无法加载这款游戏,这时候我们就会想到退款,那么steam如何退款呢?下面小编就来给大家介绍一下。 steam怎么退款? 1、在ste
  • 电脑怎么录屏?如何录制电脑屏幕操作?

  • 如何录制电脑屏幕操作,相信很多朋友们遇到过这种类似的问题,你们对于这类问题如何解决呢?下面就给大家分享一下个人经验,希望可以帮助到大家。 电脑怎么录屏? 方法一:手机录制。
  • 手机如何投屏到电脑?(手机投屏电脑方法)

  • 每次都有新手机发布会,总会提到采用多少英寸的屏幕,但是手机在大的屏幕,也没有手机投屏到电脑、电视的体验爽,下面就一起来看看手机如何投屏到电脑? 手机投屏电脑方法 1、打开
后台-插件-广告管理-侧边广告位二(PC)
后台-插件-广告管理-侧边广告位二(手机)

最新文章

  • 升级MySQL5.7,开发不得不注意的坑

  • 前段时间,将线上MySQL数据库升级到了5.7。考虑到可能产生的不兼容性,在升级之前,确实也是战战兢兢,虽然测试环境,开发环境早在半年前就已提前升级。基于前期的调研和朋友的反馈,与
  • 如何优化MySQL数据库中的SQL语句性能?

  • 如何优化MySQL数据库中的SQL语句性能?概述: MySQL是目前最常用的关系型数据库管理系统之一,它的性能影响着许多应用程序的运行效率。在开发和维护MySQL数据库时,优化SQL语句的性
  • 如何使用SQL语句在MySQL中进行数据排序和分组?

  • 如何使用SQL语句在MySQL中进行数据排序和分组?在数据库中,我们经常需要对数据进行排序和分组以满足不同的需求。MySQL提供了强大的SQL语句来实现这些操作。本文将介绍如何使用
  • 如何使用SQL语句在MySQL中进行数据备份和恢复?

  • 如何使用SQL语句在MySQL中进行数据备份和恢复?在数据库中,数据备份和恢复是非常重要的操作,可以保证数据的安全性并且在遇到意外情况时能够迅速恢复数据。MySQL是一个非常常用
后台-插件-广告管理-侧边广告位三(PC)
后台-插件-广告管理-侧边广告位三(手机)