zoukankan      html  css  js  c++  java
  • MySQL优化

    优化SQL步骤

    在应用的的开发过程中,由于初期数据量小,开发人员写 SQL 语句时更重视功能上的实现。

    但是当应用系统正式 上线后,随着生产数据量的急剧增长,很多 SQL 语句开始逐渐显露出性能问题,对生产的影响也越来越大,此时这些有问题的 SQL 语句就成为整个系统性能的瓶颈因此我们必须要对它们进行优化

    本章将详细介绍在 MySQL 中优化 SQL 语句的方法。

    当面对一个有 SQL 性能问题的数据库时,我们应该从何处入手来进行系统的分析,使得能够尽快定位问题 SQL 并 尽快解决问题。

    查看SQL执行频率

    MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信息。

    show [session|global] status 可以根据需要加上参数“session”或者“global”来显示 session 级(当前连接)的计结果和 global 级(自数据库上次启动至今)的统计结果。如果不写,默认使用参数是“session”。

    下面的命令显示了当前 session 中所有统计参数的值:

    mysql> show status like 'Com_______';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | Com_binlog    | 0     |
    | Com_commit    | 0     |
    | Com_delete    | 0     |
    | Com_import    | 0     |
    | Com_insert    | 5     |
    | Com_repair    | 0     |
    | Com_revoke    | 0     |
    | Com_select    | 10    |
    | Com_signal    | 0     |
    | Com_update    | 0     |
    | Com_xa_end    | 0     |
    +---------------+-------+
    11 rows in set (0.02 sec)
    
    mysql> show status like 'Innodb_rows_%';
    +----------------------+-------+
    | Variable_name        | Value |
    +----------------------+-------+
    | Innodb_rows_deleted  | 0     |
    | Innodb_rows_inserted | 15    |
    | Innodb_rows_read     | 21    |
    | Innodb_rows_updated  | 3     |
    +----------------------+-------+
    4 rows in set (0.00 sec)
    

    Com_xxx 表示每个 xxx 语句执行的次数,我们通常比较关心的是以下几个统计参数

    参数 含义
    Com_select 执行 select 操作的次数,一次查询只累加 1。
    Com_insert 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次。
    Com_update 执行 UPDATE 操作的次数。
    Com_delete 执行 DELETE 操作的次数。
    Innodb_rows_read select 查询返回的行数。
    Innodb_rows_inserted 执行 INSERT 操作插入的行数。
    Innodb_rows_updated 执行 UPDATE 操作更新的行数。
    Innodb_rows_deleted 执行 DELETE 操作删除的行数。
    Connections 试图连接 MySQL 服务器的次数。
    Uptime 服务器工作时间。
    Slow_queries 慢查询的次数。

    tips

    • Com_*** : 这些参数对于所有存储引擎的表操作都会进行累计
    • Innodb_*** : 这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同。

    定位低效率执行SQL

    可以通过以下两种方式定位执行效率较低的 SQL 语句。

    • 慢查询日志:通过慢查询日志定位那些执行效率较低的 SQL 语句

    用--log-slow-queries[=file_name]选项启 动时,mysqld 写一个包含所有执行时间超过 long_query_time 秒的 SQL 语句的日志文件。具体可以查看本书第 26 章中日志管理的相关部分。

    • show processlist : 查看当前MySQL在进行的线程,包括线程的状态、是否 锁表等

    慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢查询 日志并不能定位问题,可以使用show processlist命令查看当前MySQL在进行的线程,包括线程的状态、是否 锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化。

    mysql> show processlist;
    +-----+-----------------+-----------+---------+---------+---------+------------------------+------------------+
    | Id  | User            | Host      | db      | Command | Time    | State                  | Info             |
    +-----+-----------------+-----------+---------+---------+---------+------------------------+------------------+
    |   5 | event_scheduler | localhost | NULL    | Daemon  | 1020421 | Waiting on empty queue | NULL             |
    | 916 | root            | localhost | demo_01 | Sleep   |    1738 |                        | NULL             |
    | 917 | root            | localhost | demo_01 | Query   |       0 | init                   | show processlist |
    +-----+-----------------+-----------+---------+---------+---------+------------------------+------------------+
    3 rows in set (0.00 sec)
    
    • id列,用户登录mysql时,系统分配的"connection_id",可以使用函数connection_id()查看
    • user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句
    • host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户
    • db列,显示这个进程目前连接的是哪个数据库
    • command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接 (connect)等
    • time列,显示这个状态持续的时间,单位是秒
    • state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一 个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成
    • info列,显示这个sql语句,是判断问题语句的一个重要依据

    explain分析执行计划

    通过以上步骤查询到效率低的 SQL 语句后,可以通过 EXPLAIN或者 DESC命令获取 MySQL如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。

    查询SQL语句的执行计划:

    mysql> explain select * from goods_innodb where id = 1;
    +----+-------------+--------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    | id | select_type | table        | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
    +----+-------------+--------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | goods_innodb | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
    +----+-------------+--------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    

    字段含义:

    字段 含义
    id select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。
    select_type 表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、 PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语 句)、SUBQUERY(子查询中的第一个 SELECT)等
    table 输出结果集的表
    type 表示表的连接类型,性能由好到差的连接类型为( system ---> const -----> eq_ref ------> ref -------> ref_or_null----> index_merge ---> index_subquery -----> range -----> index ------> all )
    possible_keys 表示查询时,可能使用的索引
    key 表示实际使用的索引
    key_len 索引字段的长度
    rows 扫描行的数量
    extra 执行情况的说明和描述

    数据准备

    image-20210508142328034

    创建表

    CREATE TABLE `t_role` (
      `id` varchar(32) NOT NULL,
      `role_name` varchar(255) DEFAULT NULL, `role_code` varchar(255) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`),
      UNIQUE KEY `unique_role_name` (`role_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `t_user` (
      `id` varchar(32) NOT NULL,
      `username` varchar(45) NOT NULL,
      `password` varchar(96) NOT NULL,
      `name` varchar(45) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `unique_user_username` (`username`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `user_role` (
      `id` int(11) NOT NULL auto_increment ,
      `user_id` varchar(32) DEFAULT NULL,
      `role_id` varchar(32) DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `fk_ur_user_id` (`user_id`),
      KEY `fk_ur_role_id` (`role_id`),
      CONSTRAINT `fk_ur_role_id` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ON
    DELETE NO ACTION ON UPDATE NO ACTION,
      CONSTRAINT `fk_ur_user_id` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`) ON
    DELETE NO ACTION ON UPDATE NO ACTION
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    插入数据

    -- user数据
    insert into `t_user` (`id`, `username`, `password`, `name`) values('1','super','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe',' 超级管理员');
    insert into `t_user` (`id`, `username`, `password`, `name`) values('2','admin','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe',' 系统管理员');
    insert into `t_user` (`id`, `username`, `password`, `name`) values('3','itcast','$2a$10$8qmaHgUFUAmPR5pOuWhYWOr291WJYjHelUlYn07k5ELF8ZCrW0Cui', 'test02');
    insert into `t_user` (`id`, `username`, `password`, `name`) values('4','stu1','$2a$10$pLtt2KDAFpwTWLjNsmTEi.oU1yOZyIn9XkziK/y/spH5rftCpUMZa','学 生1');
    insert into `t_user` (`id`, `username`, `password`, `name`) values('5','stu2','$2a$10$nxPKkYSez7uz2YQYUnwhR.z57km3yqKn3Hr/p1FR6ZKgc18u.Tvqm','学 生2');
    insert into `t_user` (`id`, `username`, `password`, `name`) values('6','t1','$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe','老师 1');
    
      -- role数据
    INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('5','学 生','student','学生');
    INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('7','老 师','teacher','老师');
    INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('8','教 学管理员','teachmanager','教学管理员');
    INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('9','管 理员','admin','管理员');
    INSERT INTO `t_role` (`id`, `role_name`, `role_code`, `description`) VALUES('10','超 级管理员','super','超级管理员');
    
    -- user_role
    INSERT INTO user_role(id,user_id,role_id) VALUES(NULL, '1', '5'),(NULL, '1', '7'),(NULL, '2', '8'),(NULL, '3', '9'),(NULL, '4', '8'),(NULL, '5', '10') ;
    
    

    explain 之 id

    id 字段是 select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。

    id有三种情况:

    1. id 相同表示加载表的顺序是从上到下。
    mysql> explain select * from t_role r, t_user u, user_role ur where r.id = ur.role_id and u.id = ur.user_id ;
    +----+-------------+-------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+-------------+
    | id | select_type | table | partitions | type   | possible_keys               | key           | key_len | ref                | rows | filtered | Extra       |
    +----+-------------+-------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+-------------+
    |  1 | SIMPLE      | r     | NULL       | ALL    | PRIMARY                     | NULL          | NULL    | NULL               |    5 |   100.00 | NULL        |
    |  1 | SIMPLE      | ur    | NULL       | ref    | fk_ur_user_id,fk_ur_role_id | fk_ur_role_id | 99      | demo_01.r.id       |    1 |   100.00 | Using where |
    |  1 | SIMPLE      | u     | NULL       | eq_ref | PRIMARY                     | PRIMARY       | 98      | demo_01.ur.user_id |    1 |   100.00 | NULL        |
    +----+-------------+-------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+-------------+
    3 rows in set, 1 warning (0.00 sec)
    
    1. id 不同id值越大,优先级越高,越先被执行。
    mysql> EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1'));
    +----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
    | id | select_type | table     | partitions | type  | possible_keys        | key                  | key_len | ref   | rows | filtered | Extra       |
    +----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
    |  1 | PRIMARY     | t_role    | NULL       | const | PRIMARY              | PRIMARY              | 98      | const |    1 |   100.00 | NULL        |
    |  2 | SUBQUERY    | user_role | NULL       | ref   | fk_ur_user_id        | fk_ur_user_id        | 99      | const |    1 |   100.00 | Using where |
    |  3 | SUBQUERY    | t_user    | NULL       | const | unique_user_username | unique_user_username | 137     | const |    1 |   100.00 | Using index |
    +----+-------------+-----------+------------+-------+----------------------+----------------------+---------+-------+------+----------+-------------+
    3 rows in set, 1 warning (0.00 sec)
    
    1. id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越 大,优先级越高,越先执行。
    
    mysql> EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ;
    +----+-------------+-------------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+------------------------------+
    | id | select_type | table       | partitions | type   | possible_keys               | key           | key_len | ref                | rows | filtered | Extra                        |
    +----+-------------+-------------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+------------------------------+
    |  1 | PRIMARY     | <derived2>  | NULL       | ALL    | NULL                        | fk_ur_user_id | NULL    | const              |    2 |   100.00 | Using where                  |
    |  1 | PRIMARY     | r           | NULL       | eq_ref | PRIMARY                     | PRIMARY       | 98      | demo_01.ur.role_id |    1 |   100.00 | NULL                         |
    |  1 | DERIVED     | ur          | NULL       | ref    | fk_ur_user_id               | PRIMARY       | 99      | demo_01.ur.role_id |    1 |   100.00 | Using index condition        |
    +----+-------------+-------------+------------+--------+-----------------------------+---------------+---------+--------------------+------+----------+------------------------------+
    2 rows in set, 1 warning (0.00 sec)
    

    explain 之 select_type

    表示 SELECT 的类型,常见的取值,如下表所示:

    select_type 含义
    SIMPLE 简单的select查询,查询中不包含子查询或者UNION
    PRIMARY 查询中若包含任何复杂的子查询,最外层查询标记为该标识
    SUBQUERY 在SELECT 或 WHERE 列表中包含了子查询
    DERIVED 在FROM 列表中包含的子查询,被标记为 DERIVED(衍生) MYSQL会递归执行这些子查 询,把结果放在临时表中
    UNION 若第二个SELECT出现在UNION之后,则标记为UNION ; 若UNION包含在FROM子句的子 查询中,外层SELECT将被标记为 : DERIVED
    UNION RESULT 从UNION表获取结果的SELECT

    explain 之 table

    type 显示的是访问类型,是较为重要的一个指标,可取值为:

    type 含义
    NULL MySQL不访问任何表,索引,直接返回结果
    system 表只有一行记录(等于系统表),这是const类型的特例,一般不会出现
    const 表示通过索引一次就找到了,const 用于比较primary key 或者 unique 索引。因为只匹配一行数 据,所以很快。如将主键置于where列表中,MySQL 就能将该查询转换为一个常亮。const于将 "主键" 或 "唯一" 索引的所有部分与常量值进行比较
    eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询,关联查询出的记录只有一条。常见于主键或唯一索引扫描
    ref 非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,返回所有匹配某个单独值的所有行(多个)
    range 只检索给定返回的行,使用一个索引来选择行。 where 之后出现 between , < , > , in 等操作。
    index index 与 ALL的区别为 index 类型只是遍历了索引树, 通常比ALL 快, ALL 是遍历数据文件。
    all 将遍历全表以找到匹配的行

    结果值从最好到最坏以此是:

    NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
    
    system > const > eq_ref > ref > range > index > ALL
    

    一般来说, 我们需要保证查询至少达到 range 级别, 最好达到ref 。

    explain 之 key

    key 含义
    possible_keys 显示可能应用在这张表的索引, 一个或多个
    key 实际使用的索引, 如果为NULL, 则没有使用索引。
    key_len 表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前 提下, 长度越短越好 。

    explain 之 rows

    扫描行的数量。

    explain 之 extra

    其他的额外的执行计划信息,在该列展示

    extra 含义
    using filesort 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取, 称为 “文件排序”, 效率低。
    using temporary 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于 order by 和 group by; 效率低
    using index 表示相应的select操作使用了覆盖索引, 避免访问表的数据行, 效率不错。

    show profile分析SQL

    Mysql从5.0.37版本开始增加了对 show profiles 和 show profile 语句的支持。show profiles 能够在做SQL优化时

    帮助我们了解时间都耗费到哪里去了。

    通过 have_profiling 参数,能够看到当前MySQL是否支持profile:

    mysql> select @@have_profiling;
    +------------------+
    | @@have_profiling |
    +------------------+
    | YES              |
    +------------------+
    1 row in set, 1 warning (0.00 sec)
    

    默认profiling是关闭的,可以通过set语句在Session级别开启profiling:

    mysql> select @@profiling;
    +-------------+
    | @@profiling |
    +-------------+
    |           0 |
    +-------------+
    1 row in set, 1 warning (0.00 sec)
    
    -- 开启profiling开关
    mysql> set profiling=1;
    Query OK, 0 rows affected, 1 warning (0.00 sec)
    

    通过profile,我们能够更清楚地了解SQL执行的过程。

    首先,我们可以执行一系列的操作,如下图所示:

    mysql> show databases;
    mysql> use demo_01;
    mysql> show tables;
    mysql> select * from t_user where id < 5;
    mysql> select count(*) from t_user;
    

    执行完上述命令之后,再执行show profiles 指令, 来查看SQL语句执行的耗时:

    mysql> show profiles;
    +----------+------------+-----------------------------------+
    | Query_ID | Duration   | Query                             |
    +----------+------------+-----------------------------------+
    |        1 | 0.00018425 | select @@profiling                |
    |        2 | 0.00068000 | show databases                    |
    |        3 | 0.00016000 | SELECT DATABASE()                 |
    |        4 | 0.00111225 | show tables                       |
    |        5 | 0.00031550 | select * from t_user where id < 5 |
    |        6 | 0.02122775 | select count(*) from t_user       |
    +----------+------------+-----------------------------------+
    6 rows in set, 1 warning (0.00 sec)
    

    通过show profile for query query_id 语句可以查看到该SQL执行过程中每个线程的状态和消耗的时间:

    mysql> show profile for query 6;
    +--------------------------------+----------+
    | Status                         | Duration |
    +--------------------------------+----------+
    | starting                       | 0.000069 |
    | Executing hook on transaction  | 0.000006 |
    | starting                       | 0.000010 |
    | checking permissions           | 0.000009 |
    | Opening tables                 | 0.000032 |
    | init                           | 0.000008 |
    | System lock                    | 0.000011 |
    | optimizing                     | 0.000007 |
    | statistics                     | 0.000018 |
    | preparing                      | 0.000018 |
    | executing                      | 0.020961 |
    | end                            | 0.000018 |
    | query end                      | 0.000007 |
    | waiting for handler commit     | 0.000012 |
    | closing tables                 | 0.000012 |
    | freeing items                  | 0.000017 |
    | cleaning up                    | 0.000015 |
    +--------------------------------+----------+
    17 rows in set, 1 warning (0.00 sec)
    

    tips:

    • Sending data 状态表示MySQL线程开始访问数据行并把结果返回给客户端,而不仅仅是返回个客户端。由于在Sending data状态下,MySQL线程往往需要做大量的磁盘读取操作,所以经常是整各查询中耗时最长的状态。

    在获取到最消耗时间的线程状态后,MySQL支持进一步选择all、cpu、block io 、context switch、page faults等 明细类型类查看MySQL在使用什么资源上耗费了过高的时间。例如,选择查看CPU的耗费时间:

    mysql> show profile cpu for query 6;
    +--------------------------------+----------+----------+------------+
    | Status                         | Duration | CPU_user | CPU_system |
    +--------------------------------+----------+----------+------------+
    | starting                       | 0.000069 | 0.000021 |   0.000042 |
    | Executing hook on transaction  | 0.000006 | 0.000002 |   0.000004 |
    | starting                       | 0.000010 | 0.000004 |   0.000007 |
    | checking permissions           | 0.000009 | 0.000003 |   0.000005 |
    | Opening tables                 | 0.000032 | 0.000010 |   0.000021 |
    | init                           | 0.000008 | 0.000003 |   0.000005 |
    | System lock                    | 0.000011 | 0.000004 |   0.000008 |
    | optimizing                     | 0.000007 | 0.000002 |   0.000005 |
    | statistics                     | 0.000018 | 0.000006 |   0.000011 |
    | preparing                      | 0.000018 | 0.000006 |   0.000012 |
    | executing                      | 0.020961 | 0.008725 |   0.000000 |
    | end                            | 0.000018 | 0.000013 |   0.000000 |
    | query end                      | 0.000007 | 0.000006 |   0.000000 |
    | waiting for handler commit     | 0.000012 | 0.000013 |   0.000000 |
    | closing tables                 | 0.000012 | 0.000011 |   0.000000 |
    | freeing items                  | 0.000017 | 0.000017 |   0.000000 |
    | cleaning up                    | 0.000015 | 0.000015 |   0.000000 |
    +--------------------------------+----------+----------+------------+
    17 rows in set, 1 warning (0.00 sec)
    
    字段 含义
    Status sql语句执行的状态
    Duration sql执行过程中每一个步骤的耗时
    CPU_user 当前用户占有的CPU
    CPU_system 系统占有CPU

    trace分析优化器执行计划

    MySQL5.6提供了对SQL的跟踪trace, 通过trace文件能够进一步了解为什么优化器选择A计划, 而不是选择B计划。

    打开trace , 设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。

    SET optimizer_trace="enabled=on",end_markers_in_json=on;
    SET optimizer_trace_max_mem_size=1000000;
    

    执行sql语句

    mysql> select * from t_user where id < 4;
    

    最后, 检查information_schema.optimizer_trace就可以知道MySQL是如何执行SQL的

    mysql> select * from information_schema.optimizer_traceG;
    *************************** 1. row ***************************
                                QUERY: select * from t_user where id < 4
                                TRACE: {
      "steps": [
        {
          "join_preparation": {
            "select#": 1,
            "steps": [
              {
                "expanded_query": "/* select#1 */ select `t_user`.`id` AS `id`,`t_user`.`username` AS `username`,`t_user`.`password` AS `password`,`t_user`.`name` AS `name` from `t_user` where (`t_user`.`id` < 4)"
              }
            ] /* steps */
          } /* join_preparation */
        },
        {
          "join_optimization": {
            "select#": 1,
            "steps": [
              {
                "condition_processing": {
                  "condition": "WHERE",
                  "original_condition": "(`t_user`.`id` < 4)",
                  "steps": [
                    {
                      "transformation": "equality_propagation",
                      "resulting_condition": "(`t_user`.`id` < 4)"
                    },
                    {
                      "transformation": "constant_propagation",
                      "resulting_condition": "(`t_user`.`id` < 4)"
                    },
                    {
                      "transformation": "trivial_condition_removal",
                      "resulting_condition": "(`t_user`.`id` < 4)"
                    }
                  ] /* steps */
                } /* condition_processing */
              },
              {
                "substitute_generated_columns": {
                } /* substitute_generated_columns */
              },
              {
                "table_dependencies": [
                  {
                    "table": "`t_user`",
                    "row_may_be_null": false,
                    "map_bit": 0,
                    "depends_on_map_bits": [
                    ] /* depends_on_map_bits */
                  }
                ] /* table_dependencies */
              },
              {
                "ref_optimizer_key_uses": [
                ] /* ref_optimizer_key_uses */
              },
              {
                "rows_estimation": [
                  {
                    "table": "`t_user`",
                    "range_analysis": {
                      "table_scan": {
                        "rows": 6,
                        "cost": 2.95
                      } /* table_scan */,
                      "potential_range_indexes": [
                        {
                          "index": "PRIMARY",
                          "usable": true,
                          "key_parts": [
                            "id"
                          ] /* key_parts */
                        },
                        {
                          "index": "unique_user_username",
                          "usable": true,
                          "key_parts": [
                            "username"
                          ] /* key_parts */
                        }
                      ] /* potential_range_indexes */,
                      "setup_range_conditions": [
                      ] /* setup_range_conditions */,
                      "group_index_range": {
                        "chosen": false,
                        "cause": "not_group_by_or_distinct"
                      } /* group_index_range */,
                      "skip_scan_range": {
                        "chosen": false,
                        "cause": "disjuntive_predicate_present"
                      } /* skip_scan_range */
                    } /* range_analysis */
                  }
                ] /* rows_estimation */
              },
              {
                "considered_execution_plans": [
                  {
                    "plan_prefix": [
                    ] /* plan_prefix */,
                    "table": "`t_user`",
                    "best_access_path": {
                      "considered_access_paths": [
                        {
                          "rows_to_scan": 6,
                          "access_type": "scan",
                          "resulting_rows": 6,
                          "cost": 0.85,
                          "chosen": true
                        }
                      ] /* considered_access_paths */
                    } /* best_access_path */,
                    "condition_filtering_pct": 100,
                    "rows_for_plan": 6,
                    "cost_for_plan": 0.85,
                    "chosen": true
                  }
                ] /* considered_execution_plans */
              },
              {
                "attaching_conditions_to_tables": {
                  "original_condition": "(`t_user`.`id` < 4)",
                  "attached_conditions_computation": [
                  ] /* attached_conditions_computation */,
                  "attached_conditions_summary": [
                    {
                      "table": "`t_user`",
                      "attached": "(`t_user`.`id` < 4)"
                    }
                  ] /* attached_conditions_summary */
                } /* attaching_conditions_to_tables */
              },
              {
                "finalizing_table_conditions": [
                  {
                    "table": "`t_user`",
                    "original_table_condition": "(`t_user`.`id` < 4)",
                    "final_table_condition   ": "(`t_user`.`id` < 4)"
                  }
                ] /* finalizing_table_conditions */
              },
              {
                "refine_plan": [
                  {
                    "table": "`t_user`"
                  }
                ] /* refine_plan */
              }
            ] /* steps */
          } /* join_optimization */
        },
        {
          "join_execution": {
            "select#": 1,
            "steps": [
            ] /* steps */
          } /* join_execution */
        }
      ] /* steps */
    }
    MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
              INSUFFICIENT_PRIVILEGES: 0
    1 row in set (0.00 sec)
    

    索引使用

    索引是数据库优化最常用也是最重要的手段之一, 通过索引通常可以帮助用户解决大多数的MySQL的性能优化问题。

    数据准备

    建表

    create table tb_item(
      id int auto_increment,
      title varchar(255),
      price double(10,2),
      num bigint,
      category_id int,
      status int,
      seller_id varchar(64),
      create_time date,
      update_time date,
    primary key (`id`)
    )engine=InnoDB default charset=utf8;
    

    存储过程

    delimiter $$
    create procedure gen_tb_item(in min int, in max int)
    begin
        declare i int default min;
    
        -- 异常处理
        declare exit handler for sqlexception,sqlwarning,not found
        begin
            rollback;
        end;
        
        -- 开启事务
        start transaction;
        while i <= max DO
            insert into tb_item(`title`,`price`,`num`, `category_id`, `status`, `seller_id`, `create_time`, `update_time`) 
              values(concat('苹果Apple 金色 移动联通5g手机',i), 5999.00, 1000, 2, 0, 'apple', '2021-05-08 00:00:00', '2021-05-08 10:00:00');
            set i = i + 1;
        end while;
        -- 事务提交
        commit;
    end$$
    delimiter ;
    

    调用存储过程,写入200w条

    mysql> call gen_tb_item(1,2000000);
    
    mysql> select count(id) from tb_item;
    +-----------+
    | count(id) |
    +-----------+
    |   2000000 |
    +-----------+
    1 row in set (7.65 sec)
    

    验证索引提升查询效率

    在我们准备的表结构tb_item 中, 一共存储了 200 万记录;

    1. 根据ID查询
    select * from tb_item where id = 1999G;
    
    mysql> select * from tb_item where id = 1999G;
    *************************** 1. row ***************************
             id: 1999
          title: 苹果Apple 金色 移动联通5g手机1999
          price: 5999.00
            num: 1000
    category_id: 2
         status: 0
      seller_id: apple
    create_time: 2021-05-08
    update_time: 2021-05-08
    1 row in set (0.03 sec)  -- 时间
    

    查询速度很快, 接近0s , 主要的原因是因为id为主键, 有索引

    mysql> explain select * from tb_item where id = 1999G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: tb_item
       partitions: NULL
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 4
              ref: const
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.01 sec)
    
    1. 根据title进行精确查询
    select * from tb_item where title='苹果Apple 金色 移动联通5g手机1999'G;
    
    mysql> select * from tb_item where title='苹果Apple 金色 移动联通5g手机1999'G;
    *************************** 1. row ***************************
             id: 1999
          title: 苹果Apple 金色 移动联通5g手机1999
          price: 5999.00
            num: 1000
    category_id: 2
         status: 0
      seller_id: apple
    create_time: 2021-05-08
    update_time: 2021-05-08
    1 row in set (1.65 sec)  -- 时间
    

    查看SQL语句的执行计划 :

    mysql> explain  select * from tb_item where title='苹果Apple 金色 移动联通5g手机1999'G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: tb_item
       partitions: NULL
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 1987370
         filtered: 10.00
            Extra: Using where
    1 row in set, 1 warning (0.00 sec)
    

    处理方案 , 针对title字段, 创建索引 :

    mysql> create index idx_item_title on tb_item(title);
    Query OK, 0 rows affected (7 min 39.49 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    

    索引创建完后,再次查询

    mysql> select * from tb_item where title='苹果Apple 金色 移动联通5g手机1999'G;
    *************************** 1. row ***************************
             id: 1999
          title: 苹果Apple 金色 移动联通5g手机1999
          price: 5999.00
            num: 1000
    category_id: 2
         status: 0
      seller_id: apple
    create_time: 2021-05-08
    update_time: 2021-05-08
    1 row in set (0.00 sec)   -- 时间
    

    通过explain查看执行计划,指定SQL时使用了刚才创建的索引

    mysql> explain select * from tb_item where title='苹果Apple 金色 移动联通5g手机1999'G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: tb_item
       partitions: NULL
             type: ref
    possible_keys: idx_item_title
              key: idx_item_title
          key_len: 768
              ref: const  -- 命中索引
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.00 sec)
    

    索引的使用

    准备环境

    create table `tb_seller` ( 
      `sellerid` varchar (100), 
      `name` varchar (100), 
      `nickname` varchar (50), 
      `password` varchar (60), 
      `status` varchar (1), 
      `address` varchar (100), 
      `createtime` datetime, 
      primary key(`sellerid`)
    )engine=innodb default charset=utf8mb4;
    
    -- 插入数据
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('alibaba','阿里巴巴','阿里小 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('baidu','百度科技有限公司','百度小 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('huawei','华为科技有限公司','华为小 店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itcast','传智播客教育科技有限公司','传智播 客','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itheima','黑马程序员','黑马程序 员','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('luoji','罗技科技有限公司','罗技小 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('oppo','OPPO科技有限公司','OPPO官方旗舰 店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('ourpalm','掌趣科技股份有限公司','掌趣小 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('qiandu','千度科技','千度小 店','e10adc3949ba59abbe56e057f20f883e','2','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('sina','新浪科技有限公司','新浪官方旗舰 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰 店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); 
    insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('yijia','宜家家居','宜家家居旗舰 店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
    
    -- 创建索引
    create index idx_seller_name_sta_addr on tb_seller(name,status,address);
    

    避免索引失败

    1. 全值匹配, 对索引中所有列都指定具体值

    该情况下,索引生效,执行效率高

    mysql> explain select * from tb_seller where name='小米科技' and status='1' and address='北京 市'G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: tb_seller
       partitions: NULL
             type: ref
    possible_keys: idx_seller_name_sta_addr
              key: idx_seller_name_sta_addr
          key_len: 813
              ref: const,const,const
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.00 sec)
    

    三个索引命中,效率高

    1. 最左前缀法则

    如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。

    匹配最左前缀法则,走索引:

    mysql> explain select * from tb_seller where name='小米科技';
    mysql> explain select * from tb_seller where name='小米科技' and status='1';
    mysql> explain select * from tb_seller where name='小米科技' and status='1' and address='北京市';
    

    image-20210508155824514

    违背最左前缀法则,索引失效:

    mysql> explain select * from tb_seller where status='1';
    mysql> explain select * from tb_seller where status='1' and address='北京市';
    

    image-20210508160030325

    如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效:

    mysql> explain select * from tb_seller where name='小米科技' and address='北京市';
    

    image-20210508160128720

    1. 查询范围右边的列,不能使用索引
    mysql> explain select * from tb_seller where name='小米科技' and status='1' and address='北京市';
    mysql> explain select * from tb_seller where name='小米科技' and status>'1' and address='北京市';
    

    image-20210508160342897

    根据前面的两个字段name , status 查询是走索引的, 但是最后一个条件address 没有用到索引。

    1. 不要在索引列上进行运算操作, 索引将失效。
    mysql> select * from tb_seller where substring(name, 3, 2) = "科技";
    mysql> explain select * from tb_seller where substring(name, 3, 2) = "科技";
    

    image-20210508160638347

    1. 字符串不加单引号,造成索引失效。
    mysql> explain select * from tb_seller where name="科技" and status="0";
    mysql> explain select * from tb_seller where name="科技" and status=0;
    

    image-20210508160832547

    由于在查询时,没有对字符串加单引号,MySQL的查询优化器,会自动的进行类型转换,造成索引失效。

    1. 尽量使用覆盖索引,避免select *

    尽量使用覆盖索引(只访问索引的查询(索引列完全包含查询列)),减少select * 。

    mysql> explain select * from tb_seller where name="科技" and status="0" and address="西安市";
    mysql> explain select name,status,address,password from tb_seller where name="科技" and status="0" and address="西安市";
    

    image-20210508161334197

    如果查询列,超出索引列,也会降低性能。

    Tips:

    using index :使用覆盖索引的时候就会出现

    using where:在查找使用索引的情况下,需要回表去查询所需的数据

    using index condition:查找使用了索引,但是需要回表查询数据

    using index ; using where:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表 查询数据

    1. 用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。

    示例,name字段是索引列 , 而createtime不是索引列,中间是or进行连接是不走索引的 :

    mysql> explain select * from tb_seller where name='黑马程序员' or createtime = '2088-01-01 12:00:00';
    mysql> explain select * from tb_seller where name='黑马程序员' and createtime = '2088-01-01 12:00:00';
    

    image-20210508161752623

    1. 以%开头的Like模糊查询,索引失效。

    如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

    mysql> explain select * from tb_seller where name like '黑马程序员%';
    mysql> explain select * from tb_seller where name like '%黑马程序员';
    mysql> explain select * from tb_seller where name like '%黑马程序员%';
    

    image-20210508162009486

    解决方案:通过覆盖索引解决,查询是带上主键id

    mysql> explain select sellerid from tb_seller where name like '%黑马程序员%';
    mysql> explain select sellerid,name from tb_seller where name like '%黑马程序员%';
    mysql> explain select sellerid,name,status,address from tb_seller where name like '%黑马程序员%';
    

    image-20210508162217562

    1. 如果MySQL评估使用索引比全表更慢,则不使用索引。
    mysql> explain select * from tb_seller where address='北京市';
    mysql> create index idx_address on tb_seller(address);	
    mysql> explain select * from tb_seller where address='北京市';
    mysql> explain select * from tb_seller where address='西安市';
    

    image-20210508163116193

    1. is NULL , is NOT NULL 有时索引失效
    mysql> explain select * from tb_seller where name is null;
    mysql> explain select * from tb_seller where name is not null;
    mysql> explain select * from t_user where name is null;
    mysql> explain select * from t_user where name is not null;
    

    image-20210508163411921

    1. in 走索引, not in 索引失效。

    image-20210508163635633

    这里都命中了,带考察

    1. 单列索引和复合索引。

    尽量使用复合索引,而少使用单列索引

    -- 创建复合索引
    create index idx_name_sta_address on tb_seller(name, status, address);
    -- 相当于创建3个索引:
    -- name, name + status, name + status + address
    

    创建单列索引

    create index idx_seller_name on tb_seller(name); 
    create index idx_seller_status on tb_seller(status); create index idx_seller_address on tb_seller(address);
    

    数据库会选择一个最优的索引(辨识度最高索引)来使用,并不会使用全部索引 。

    2.4 查看索引使用情况

    mysql> show status like 'Handler_read%';
    +-----------------------+---------+
    | Variable_name         | Value   |
    +-----------------------+---------+
    | Handler_read_first    | 38      |
    | Handler_read_key      | 2670    |
    | Handler_read_last     | 0       |
    | Handler_read_next     | 2392    |
    | Handler_read_prev     | 0       |
    | Handler_read_rnd      | 0       |
    | Handler_read_rnd_next | 2000683 |
    +-----------------------+---------+
    7 rows in set (0.04 sec)
    
    show global status like 'Handler_read%';
    

    说明:

    • Handler_read_first:索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低 越好)。
    • Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引得到的 性能改善不高,因为索引不经常使用(这个值越高越好)。
    • Handler_read_next :按照键顺序读下一行的请求数。如果你用范围约束或如果执行索引扫描来查询索引列, 该值增加。
    • Handler_read_prev:按照键顺序读前一行的请求数。该读方法主要用于优化ORDER BY ... DESC。
    • Handler_read_rnd :根据固定位置读一行的请求数。如果你正执行大量查询并需要对结果进行排序该值较高。 你可能使用了大量需要MySQL扫描整个表的查询或你的连接没有正确使用键。这个值较高,意味着运行效率低,应 该建立索引来补救。
    • Handler_read_rnd_next:在数据文件中读下一行的请求数。如果你正进行大量的表扫描,该值较高。通常说 明你的表索引不正确或写入的查询没有利用索引。

    SQL优化

    大批量插入数据

    环境准备

    CREATE TABLE `tb_user_2` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(45) NOT NULL,
      `password` varchar(96) NOT NULL,
      `name` varchar(45) NOT NULL,
      `birthday` datetime DEFAULT NULL,
      `sex` char(1) DEFAULT NULL,
      `email` varchar(45) DEFAULT NULL,
      `phone` varchar(45) DEFAULT NULL,
      `qq` varchar(32) DEFAULT NULL,
      `status` varchar(32) NOT NULL COMMENT '用户状态', `create_time` datetime NOT NULL,
      `update_time` datetime DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `unique_user_username` (`username`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
    

    当使用load 命令导入数据的时候,适当的设置可以提高导入的效率。

    对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率:

    主键顺序插入

    因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数 据的效率。如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个 主键,将可以利用这点,来提高导入数据的效率。

    插入ID顺序排列数据:20.58 sec

    插入ID无序排列数据:59.29 sec

    关闭唯一性校验

    在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性校验,在导入结束后执行SET UNIQUE_CHECKS=1,恢 复唯一性校验,可以提高导入的效率。

    mysql> SET UNIQUE_CHECKS=0;
    mysql> load data local infile '/root/sql1.log' into table `tb_user` fields terminated by ',' lines terminated by '
    ';
    mysql> SET UNIQUE_CHECKS=1;
    

    手动提交事务

    如果应用使用自动提交的方式,建议在导入前执行 SET AUTOCOMMIT=0,关闭自动提交,导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,也可以提高导入的效率。

    mysql> SET AUTOCOMMIT=0;
    mysql> load data local infile '/root/sql1.log' into table `tb_user` fields terminated by ',' lines terminated by '
    ';
    mysql> SET AUTOCOMMIT=1;
    

    优化Insert语句

    当进行数据的insert操作的时候,可以考虑采用以下几种优化方案

    • 如果需要同时对一张表插入很多行数据时,应该尽量使用多个值表的insert语句,这种方式将大大的缩减客户 端与数据库之间的连接、关闭等消耗。使得效率比分开执行的单个insert语句快。

    如,原始方式

    insert into tb_test values(1,'Tom'); 
    insert into tb_test values(2,'Cat'); 
    insert into tb_test values(3,'Jerry');
    

    优化后

    insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
    
    • 在事务中进行数据插入。
    start transaction;
    insert into tb_test values(1,'Tom'); 
    insert into tb_test values(2,'Cat'); 
    insert into tb_test values(3,'Jerry'); 
    commit;
    
    • 数据有序插入

    原始

    insert into tb_test values(4,'Tim'); 
    insert into tb_test values(1,'Tom'); 
    insert into tb_test values(3,'Jerry'); 
    insert into tb_test values(5,'Rose'); 
    insert into tb_test values(2,'Cat');
    

    优化后

    insert into tb_test values(1,'Tom'); 
    insert into tb_test values(2,'Cat'); 
    insert into tb_test values(3,'Jerry'); 
    insert into tb_test values(4,'Tim'); 
    insert into tb_test values(5,'Rose');
    

    优化order by语句

    环境准备

    -- 建表
    CREATE TABLE `emp` (
      `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL,
      `age` int(3) NOT NULL,
      `salary` int(11) DEFAULT NULL, PRIMARY KEY (`id`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;
    
    -- 插入数据
    insert into `emp` (`id`, `name`, `age`, `salary`) values('1','Tom','25','2300');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('2','Jerry','30','3500');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('3','Luci','25','2800');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('4','Jay','36','3500');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('5','Tom2','21','2200');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('6','Jerry2','31','3300');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('7','Luci2','26','2700');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('8','Jay2','33','3500');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('9','Tom3','23','2400');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('10','Jerry3','32','3100');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('11','Luci3','26','2900');
    insert into `emp` (`id`, `name`, `age`, `salary`) values('12','Jay3','37','4500');
    
    -- 创建索引
    create index idx_emp_age_salary on emp(age,salary);
    

    两种排序方式

    第一种是通过对返回数据进行排序,也就是通常说的 filesort 排序,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。

    mysql> explain select * from emp order by age desc;
    

    image-20210508165200416

    第二种通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。

    mysql> explain select id from emp order by age desc;
    mysql> explain select id,age from emp order by age desc;
    

    image-20210508165233524

    多字段排序

    mysql> explain select id,age,salary from emp order by age,salary;
    mysql> explain select id,age,salary from emp order by age desc, salary desc;
    mysql> explain select id,age,salary from emp order by salary desc, age desc;
    mysql> explain select id,age,salary from emp order by age desc, salary asc;
    

    image-20210508165458640

    了解了MySQL的排序方式,优化目标就清晰了:

    • 尽量减少额外的排序,通过索引直接返回有序数据。
    • where 条件 和Order by 使用相同的索引,并且Order By 的顺序和索引顺序相同, 并且Order by 的字段都是升序,或者都是 降序。否则肯定需要额外的操作,这样就会出现FileSort。

    Filesort 的优化

    通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况下,条件限制不能让Filesort消失,那就需要加快 Filesort的排序操作。

    对于Filesort , MySQL 有两种排序算法

    • 两次扫描算法 :MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果sort buffer不够,则在临时表 temporary table 中存储排序结果。完成排序之后,再根据 行指针回表读取记录,该操作可能会导致大量随机I/O操作。
    • 一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时 内存开销较大,但是排序效率比两次扫描算法要高。

    MySQL 通过比较系统变量 max_length_for_sort_data 的大小和Query语句取出的字段总大小, 来判定使用那种排序算法,如果max_length_for_sort_data 更大,那么使用第二种优化之后的算法;否则使用第一种。

    可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效 率。

    mysql> show variables like "max_length_for_sort_data";
    +--------------------------+-------+
    | Variable_name            | Value |
    +--------------------------+-------+
    | max_length_for_sort_data | 4096  |
    +--------------------------+-------+
    1 row in set (0.01 sec)
    
    mysql> show variables like "sort_buffer_size";
    +------------------+--------+
    | Variable_name    | Value  |
    +------------------+--------+
    | sort_buffer_size | 262144 |
    +------------------+--------+
    1 row in set (0.00 sec)
    

    优化 groupby 语句

    由于GROUP BY 实际上也同样会进行排序操作,而且与ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。

    如果查询包含 group by 但是用户想要避免排序结果的消耗, 则可以执行order by null 禁止排序。如下 :

    mysql> drop index idx_emp_age_salary on emp;
    mysql> explain select age,count(*) from emp group by age;
    
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
    |  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   12 |   100.00 | Using temporary |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
    1 row in set, 1 warning (0.00 sec)
    

    优化后

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

    从上面的例子可以看出,第一个SQL语句需要进行"filesort",而第二个SQL由于order by null 不需要进行 "filesort", 而上文提过Filesort往往非常耗费时间。

    创建索引

    mysql> drop index idx_emp_age_salary on emp;
    

    查询使用的index

    mysql> explain select age,count(*) from emp group by age order by null;
    +----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
    | id | select_type | table | partitions | type  | possible_keys      | key                | key_len | ref  | rows | filtered | Extra       |
    +----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
    |  1 | SIMPLE      | emp   | NULL       | index | idx_emp_age_salary | idx_emp_age_salary | 9       | NULL |   12 |   100.00 | Using index |
    +----+-------------+-------+------------+-------+--------------------+--------------------+---------+------+------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    

    优化嵌套查询

    Mysql4.1版本之后,开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把 这个结果作为过滤条件用在另一个查询中。

    使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询是可以被更高效的连接 (JOIN)替代。

    例如:查找有角色的所有的用户信息

    explain select * from t_user where id in (select user_id from user_role );
    

    image-20210508170731518

    优化后

    explain select * from t_user u , user_role ur where u.id = ur.user_id;
    

    image-20210508170809005

    连接(Join)查询之所以更有效率一些 ,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的 查询工作。

    优化OR条件

    对于包含OR的查询子句,如果要利用索引,则OR之间的每个条件列都必须用到索引 , 而且不能使用到复合索 引; 如果没有索引,则应该考虑增加索引。

    获取 emp 表中的所有的索引 :

    image-20210508170930367

    示例

    explain select * from emp where id = 1 or age = 30;
    

    image-20210508171249875


    image-20210508171216074

    建议使用 union 替换 or :

    explain select * from emp where id = 1 union select * from emp where id = 10;
    

    image-20210508171611035

    我们来比较下重要指标,发现主要差别是 type 和 ref 这两项

    type 显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:

    system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
    

    UNION 语句的 type 值为 ref,OR 语句的 type 值为 range,可以看到这是一个很明显的差距 UNION 语句的 ref 值为 const,OR 语句的 type 值为 null,const 表示是常量值引用,非常快 这两项的差距就说明了 UNION 要优于 OR .

    优化分页查询

    一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 2000000,10 , 此时需要MySQL排序前2000010 记录,仅仅返回2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非 常大 。

    mysql> explain select * from tb_item limit 1000000,10;
    +----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
    | id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |
    +----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
    |  1 | SIMPLE      | tb_item | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1987370 |   100.00 | NULL  |
    +----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    

    优化思路一:

    在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

    mysql> explain select * from tb_item t, (select id from tb_item order by id limit 1000000,10) a where t.id=a.id;
    +----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
    | id | select_type | table      | partitions | type   | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
    +----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
    |  1 | PRIMARY     | <derived2> | NULL       | ALL    | NULL          | NULL    | NULL    | NULL | 1000010 |   100.00 | NULL        |
    |  1 | PRIMARY     | t          | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | a.id |       1 |   100.00 | NULL        |
    |  2 | DERIVED     | tb_item    | NULL       | index  | NULL          | PRIMARY | 4       | NULL | 1000010 |   100.00 | Using index |
    +----+-------------+------------+------------+--------+---------------+---------+---------+------+---------+----------+-------------+
    3 rows in set, 1 warning (0.00 sec)
    

    优化思路二:

    该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

    mysql> explain select * from tb_item where id > 1000000 limit 10;
    +----+-------------+---------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
    | id | select_type | table   | partitions | type  | possible_keys | key     | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+---------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | tb_item | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL | 993685 |   100.00 | Using where |
    +----+-------------+---------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    

    使用SQL提示

    SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。

    USE INDEX

    在查询语句中表名的后面,添加 use index 来提供希望MySQL去参考的索引列表,就可以让MySQL不再考虑其他可用的索引。

    mysql> create index idx_seller_name on tb_seller(name);
    
    mysql> explain select * from tb_seller where name="小米科技";
    +----+-------------+-----------+------------+------+------------------------------------------+--------------------------+---------+-------+------+----------+-------+
    | id | select_type | table     | partitions | type | possible_keys                            | key                      | key_len | ref   | rows | filtered | Extra |
    +----+-------------+-----------+------------+------+------------------------------------------+--------------------------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_sta_addr,idx_seller_name | idx_seller_name_sta_addr | 403     | const |    1 |   100.00 | NULL  |
    +----+-------------+-----------+------------+------+------------------------------------------+--------------------------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> explain select * from tb_seller use index(idx_seller_name) where name="小米科技";
    +----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
    | id | select_type | table     | partitions | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra |
    +----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name | idx_seller_name | 403     | const |    1 |   100.00 | NULL  |
    +----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    

    IGNORE INDEX

    如果用户只是单纯的想让MySQL忽略一个或者多个索引,则可以使用 ignore index 作为 hint 。

    mysql> explain select * from tb_seller ignore index(idx_seller_name) where name = '小米科技';
    +----+-------------+-----------+------------+------+--------------------------+--------------------------+---------+-------+------+----------+-------+
    | id | select_type | table     | partitions | type | possible_keys            | key                      | key_len | ref   | rows | filtered | Extra |
    +----+-------------+-----------+------------+------+--------------------------+--------------------------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_name_sta_addr | idx_seller_name_sta_addr | 403     | const |    1 |   100.00 | NULL  |
    +----+-------------+-----------+------------+------+--------------------------+--------------------------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    

    为强制MySQL使用一个特定的索引,可在查询中使用 force index 作为hint

    create index idx_seller_address on tb_seller(address);
    

    强制指定索引

    mysql> explain select * from tb_seller where address="北京市";
    +----+-------------+-----------+------------+------+--------------------------------+------+---------+------+------+----------+-------------+
    | id | select_type | table     | partitions | type | possible_keys                  | key  | key_len | ref  | rows | filtered | Extra       |
    +----+-------------+-----------+------------+------+--------------------------------+------+---------+------+------+----------+-------------+
    |  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_address,idx_seller_address | NULL | NULL    | NULL |   12 |    91.67 | Using where |
    +----+-------------+-----------+------------+------+--------------------------------+------+---------+------+------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> explain select * from tb_seller use index(idx_seller_address) where address="北京市";
    +----+-------------+-----------+------------+------+--------------------+------+---------+------+------+----------+-------------+
    | id | select_type | table     | partitions | type | possible_keys      | key  | key_len | ref  | rows | filtered | Extra       |
    +----+-------------+-----------+------------+------+--------------------+------+---------+------+------+----------+-------------+
    |  1 | SIMPLE      | tb_seller | NULL       | ALL  | idx_seller_address | NULL | NULL    | NULL |   12 |    91.67 | Using where |
    +----+-------------+-----------+------------+------+--------------------+------+---------+------+------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> explain select * from tb_seller force index(idx_seller_address) where address="北京市";
    +----+-------------+-----------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
    | id | select_type | table     | partitions | type | possible_keys      | key                | key_len | ref   | rows | filtered | Extra |
    +----+-------------+-----------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | tb_seller | NULL       | ref  | idx_seller_address | idx_seller_address | 403     | const |   11 |   100.00 | NULL  |
    +----+-------------+-----------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    
    衣带渐宽终不悔,为伊消得人憔悴!
  • 相关阅读:
    PAT 解题报告 1009. Product of Polynomials (25)
    PAT 解题报告 1007. Maximum Subsequence Sum (25)
    PAT 解题报告 1003. Emergency (25)
    PAT 解题报告 1004. Counting Leaves (30)
    【转】DataSource高级应用
    tomcat下jndi配置
    java中DriverManager跟DataSource获取getConnection有什么不同?
    理解JDBC和JNDI
    JDBC
    Dive into python 实例学python (2) —— 自省,apihelper
  • 原文地址:https://www.cnblogs.com/ryxiong-blog/p/14746693.html
Copyright © 2011-2022 走看看