查询优化-1

优化策略

永远小表驱动大表。

IN

  1. SELECT * FROM A WHERE id IN (SELECT id FROM B)

等价于:

  1. for select id from B
  2. for select * from A where A.id = B.id

当A表的数据集大于B表的数据集时,用in优先exists。

EXISTS

  1. SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id)

等价于

  1. for select id from A
  2. for select * from B = B.id = A.id

当A表的数据集小于B表的数据集时,用exists优先in。

查询优化-2

优化策略

Order By关键字优化:

Order By子句,尽量使用Index方式排序,避免使用FileSort方式排序。

尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀。

如果不在索引列上,FileSort有两种算法:MySQL就要启动双路排序和单路排序。

案例

SQL脚本:

  1. CREATE TABLE `tb5_a` (
  2. `age` int NOT NULL,
  3. `birth` timestamp NOT NULL
  4. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  5. INSERT INTO tb5_a(age, birth) VALUES(17, NOW());
  6. INSERT INTO tb5_a(age, birth) VALUES(18, NOW());
  7. INSERT INTO tb5_a(age, birth) VALUES(19, NOW());
  8. CREATE INDEX idx_a_agebirth ON tb5_a(age, birth);

八个case:

  • EXPLAIN SELECT * FROM tb5_a WHERE age > 20 ORDER BY age; => 正常
  • EXPLAIN SELECT * FROM tb5_a WHERE age > 20 ORDER BY birth; => using filesort
  • EXPLAIN SELECT * FROM tb5_a WHERE age > 20 ORDER BY age, birth; => 正常
  • EXPLAIN SELECT * FROM tb5_a WHERE age > 20 ORDER BY birth,age; => using filesprt
  • EXPLAIN SELECT * FROM tb5_a WHERE birth > '2020-3:23 00:00:00' ORDER BY age; => 正常
  • EXPLAIN SELECT * FROM tb5_a WHERE birth > '2020-3:23 00:00:00' ORDER BY birth; => using filesort
  • EXPLAIN SELECT * FROM tb5_a WHERE birth > '2020-3:23 00:00:00' ORDER BY age,birth; => 正常
  • EXPLAIN SELECT * FROM tb5_a WHERE birth > '2020-3:23 00:00:00' ORDER BY birth,age; => using filesort
  • EXPLAIN SELECT * FROM tb5_a ORDER BY age ASC,birth ASC; => 正常
  • EXPLAIN SELECT * FROM tb5_a ORDER BY age DESC,birth DESC; => 正常
  • EXPLAIN SELECT * FROM tb5_a ORDER BY age ASC,birth DESC; => using filesort
  • EXPLAIN SELECT * FROM tb5_a ORDER BY age DESC,birth ASC; => using filesort

总结

总结:Order By满足两种情况,会使用Index方式排序:

  • Order By语句使用索引最左前列
  • 使用Where子句与Order By子句条件列组合满足索引最左前列

注意:如果不在索引列上,filesort有两种算法:

  • 双路排序:两次扫描磁盘获取数据,读取行指针和order by列,对它们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
  • 单路排序:从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
  • 单路问题:在单路排序中,要占用很多空间,因为需要把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排,从而导致多次I/O。
  • 优化策略:SQL服务器参数调优,增大sort_buffer_size参数的设置,增大max_length_for_sort_data参数的设置。

提高Order By的速度:

(1)Order By时select *是一个大忌,只查询需要字段,这点非常重要。

(2)尝试提高sort_buffer_size。

(3)尝试提高max_length_for_sort_data。

排序案例

  1. index a_b_c(a, b, c);
  2. -- 使用索引最左前缀
  3. ORDER BY a
  4. ORDER BY a, b
  5. ORDER BY a, b, c
  6. ORDER BY a DESC, b DESC, c DESC
  7. -- WHERE使用索引的最左前缀定义为常量
  8. WHERE a = const ORDER BY b, c
  9. WHERE a = const AND b = const ORDER BY c
  10. WHERE a = const ORDER BY b, c
  11. WHERE a = count AND b > const ORDER BY b, c
  12. -- 不能使用索引进行排序
  13. ORDER BY a ASC, b DESC, c DESC -- 排序不一致
  14. WHERE g = const ORDER BY b, c -- 丢失a索引
  15. WHERE a = const ORDER BY c -- 丢失b索引
  16. WHERE a = const ORDER BY a, d -- d不是索引的一部分
  17. WHERE a IN (...) ORDER BY b, c -- 对于排序来说,多个相等条件也是范围查询

查询优化-3

优化策略

Group By关键字优化:

优化策略与Order By相似。

Group By实质是先排序后进行分组,遵照索引建的最佳左前缀。

当无法使用索引列,增大max_length_for_sort_data参数设置,增大sort_buffer_size参数的设置。

where高于having,能卸载where限定的条件就不要去having限定了。

慢查询日志

简介

  • MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
  • 具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上的语句。
  • 默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。
  • 当然,如果不是调优需要的话,一般不建议启动该参数,因为开启漫长哈讯日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。

配置

查看是否开启慢查询日志和文件位置:

  1. mysql> SHOW VARIABLES LIKE '%SLOW_QUERY_LOG%';
  2. +---------------------+-------------------------------+
  3. | Variable_name | Value |
  4. +---------------------+-------------------------------+
  5. | slow_query_log | OFF |
  6. | slow_query_log_file | /var/lib/mysql/parak-slow.log |
  7. +---------------------+-------------------------------+

开启慢查询日志(只对本次生效,重启后失效):

  1. SET global slow_query_log = 1;

查看慢查询阈值(默认为10s):

  1. mysql> SHOW VARIABLES LIKE 'long_query_time%';
  2. +-----------------+-----------+
  3. | Variable_name | Value |
  4. +-----------------+-----------+
  5. | long_query_time | 10.000000 |
  6. +-----------------+-----------+

设置慢查询阈值(3s):

  1. SET global long_query_time = 3;

查看慢查询记录数量:

  1. SHOW global STATUS LIKE '%SLOW_QUERIES%';

永久生效需要修改配置文件/etc/my.cnf,需要在[mysqld]下增加或修改参数:

  1. slow_query_log = 1
  2. slow_query_log_file = /var/lib/mysql/<hostname>-slow.log
  3. long_query_time = <time>

命令行工具mysqldumpslow

参数:

  • s:表示按照何种方式排序
  • c:访问次数
  • l:锁定时间
  • r:返回记录
  • t:查询时间
  • al:平均锁定时间
  • ar:平均返回记录数量
  • at:平均查询时间
  • t:返回数据数量
  • g:正则匹配,大小写不敏感

得到返回记录最多的10个SQL:

  1. mysqldumpslow -s r -t 10 /var/lib/mysql/parak-slow.log

得到访问次数最多的10个SQL:

  1. mysqldumpslow -s c -t 10 /var/lib/mysql/parak-slow.log

得到按照时间排序的前10条里面含有左连接的查询语句:

  1. mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/parak-slow.log

建议在使用这些命令时结合 | 和 more使用,防止爆屏。

批量数据脚本

配置

变量log_bin_trust_function_creators:控制是否可以信任存储函数创建者,不会创建写入二进制日志引起不安全事件的存储函数。

查看是否开启:

  1. mysql> SHOW VARIABLES LIKE 'log_bin_trust_function_creators';
  2. +---------------------------------+-------+
  3. | Variable_name | Value |
  4. +---------------------------------+-------+
  5. | log_bin_trust_function_creators | OFF |
  6. +---------------------------------+-------+

开启(暂时性开启,永久性依然是修改my.cnf):

  1. SET global log_bin_trust_function_creators = 1;

数据准备

  1. CREATE TABLE `tb6_dept` (
  2. `id` int NOT NULL AUTO_INCREMENT,
  3. `deptno` mediumint NOT NULL DEFAULT '0',
  4. `dname` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  5. `loc` varchar(13) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  6. PRIMARY KEY (`id`)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  8. CREATE TABLE `tb6_emp` (
  9. `id` int NOT NULL AUTO_INCREMENT,
  10. `empno` mediumint NOT NULL DEFAULT '0',
  11. `ename` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  12. `job` varchar(9) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  13. `mgr` mediumint NOT NULL DEFAULT '0',
  14. `hiredate` date NOT NULL,
  15. `sal` decimal(7,2) NOT NULL,
  16. `comm` decimal(7,2) DEFAULT NULL,
  17. `deptno` mediumint NOT NULL DEFAULT '0',
  18. PRIMARY KEY (`id`)
  19. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

创建函数

  1. # 随机产生字符串
  2. DELIMITER $$
  3. CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
  4. BEGIN
  5. DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwsyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  6. DECLARE return_str VARCHAR(255) DEFAULT '';
  7. DECLARE i INT DEFAULT 0;
  8. WHILE i < n DO
  9. SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
  10. SET i = i + 1;
  11. END WHILE;
  12. RETURN return_str;
  13. END $$
  14. # 随机产生部门编号
  15. DELIMITER $$
  16. CREATE FUNCTION rand_num() RETURNS INT(5)
  17. BEGIN
  18. DECLARE i INT DEFAULT 0;
  19. SET i = FLOOR(100 + RAND() * 10);
  20. RETURN i;
  21. END $$

创建存储过程

  1. # 向tb6_dept表批量插入
  2. DELIMITER $$
  3. CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10))
  4. BEGIN
  5. DECLARE i INT DEFAULT 0;
  6. SET autocommit = 0;
  7. REPEAT
  8. SET i = i + 1;
  9. INSERT INTO dept(deptno,dname,loc) VALUES((START + i),rand_string(10),rand_string(8));
  10. UNTIL i = max_num
  11. END REPEAT;
  12. COMMIT;
  13. END $$
  14. # 向tb6_emp表批量插入
  15. DELIMITER $$
  16. CREATE PROCEDURE insert_emp(IN START INT(10),IN max_num INT(10))
  17. BEGIN
  18. DECLARE i INT DEFAULT 0;
  19. SET autocommit = 0;
  20. REPEAT
  21. SET i = i + 1;
  22. INSERT INTO emp(empno,ename,job,mgr,hiredata,sal,comm,deptno) VALUES((START + i),rand_string(6),'SALESMAN',0001,CURDATE(),2000,400,rand_num());
  23. UNTIL i = max_num
  24. END REPEAT;
  25. COMMIT;
  26. END $$

批量插入数据

  1. # 向tb6_dept中插入10条数据
  2. DELIMITER ;
  3. CALL insert_dept(100, 10);
  4. # 向tb6_emp中插入50万条数据
  5. DELIMITER ;
  6. CALL insert_emp(100001, 500000);

Show Profile

概述

MySQL提供的可以用来分析当前会话中语句执行的资源消耗情况,可以用于SQL调优的测量。

官网:https://dev.mysql.com/doc/refman/8.0/en/show-profile.html

默认情况下,参数处于关闭状态,并保存最近15次的运行结果。

配置

查看开启状态:

  1. mysql> SHOW VARIABLES LIKE 'profiling';
  2. +---------------+-------+
  3. | Variable_name | Value |
  4. +---------------+-------+
  5. | profiling | OFF |
  6. +---------------+-------+

开启:

  1. SET global profiling = 1;

MySQL8需要关闭依赖检测,即从sql_mode中移除ONLY_FULL_GROUP_BY

查看sql_mode:

  1. mysql> SELECT @@global.sql_mode;
  2. +-----------------------------------------------------------------------------------------------------------------------+
  3. | @@global.sql_mode |
  4. +-----------------------------------------------------------------------------------------------------------------------+
  5. | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
  6. +-----------------------------------------------------------------------------------------------------------------------+

移除ONLY_FULL_GROUP_BY

  1. SET global @@sql_mode = `STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION`;

相关命令

查看SQL:SHOW PROFILES

诊断SQL:SHOW PROFILE <type ...> FOR QUERY <Query_ID>

可选参数如下:

  1. | ALL -- 显示所有的开销信息
  2. | BLOCK IO -- 显示块IO相关开销
  3. | CONTEXT SWITCHES -- 上下文切换相关开销
  4. | CPU -- 显示CPU相关开销信息
  5. | IPC -- 显示内存相关开销信息
  6. | MEMORY -- 显示内存相关开销信息
  7. | PAGE FAULTS -- 显示页面错误相关开销信息
  8. | SOURCE -- 显示和Source_function, Source_file, Source_line相关的开销信息
  9. | SWAPS -- 显示交换次数相关开销的信息

日常开发需要注意

  • converting HEAP to MyISAM 查询结果太大,内存都不够用了王磁盘上搬了
  • Creating tmp table 拷贝数据到临时表,用完再删除
  • Copying to tmp table on disk 吧内存中临时表复制到磁盘,危险
  • locked 死锁

全局查询日志

永久启用

修改my.cnf,设置如下:

  1. # 开启
  2. general_log = 1
  3. # 记录日志文件的路径
  4. general_log_file = /<path>/<name>
  5. # 输出格式
  6. log_output = <.extension>

临时启用

  1. set global general_log = 1;
  2. set global log_output = 'TABLE';

注意

永远不要在生产环境开启这个功能。