一文道尽数据库底层原理,探讨Mysql调优之道

前段时间看过一部电影,叫做《英雄时代》,里面有句话是这样说的:“生活这条狗啊,追的我连从容撒泡尿的时间都没有。”

在这个聪明人满街乱窜的年代,稀缺的恰恰不是聪明,而是一心一意,孤注一掷,一条心,一根筋。

人生最可怕的是,一生碌碌无为,还安慰自己平凡可贵。

数据库调优

慢查询日志

慢查询日志是Mysql内置的一项功能,可以记录执行超过指定时间的Sql语句

考虑到慢查询日志细节比较多,在这里我专门记录了一部分手记,一起来看一下:

相关参数与默认值

参数 作用 默认值
log_output 日志输出到哪,默认FILE,表示文件;设置成TABLE,则将日志记录到mysql.slow_log中。也可能设置多种格式,比如:FILE,TABLE FILE
long_query_time 执行时间超过这么久才记录到慢查询日志,单位秒,可使用小数表示小于秒的时间 10
log_queries_not_using_indexes 是否要将未使用索引的SQL记录到慢查询日志中,此配置会无视log_query_time的配置。生产环境建议关闭;开发环境建议开启。 OFF
log_throttle_queries_not_using_indexes 和log_queries_not_using_indexes配合使用,如果log_queries_not_using_indexes打开,则该参数将限制每分钟写入的、未使用索引的SQL数量。 0
min_examined_row_limit 扫描行数至少达到这么多才记录的慢查询日志 0
log_slow_admin_statements 是否要记录管理语句,默认关闭。管理语句包括ALERT TABLE,ANALYZE TABLE,CHECK TABLE,CREATE INDEX,DROP INDEX,OPTIMIZE TABLE,and REPAIR TABLE OFF
slow_query_log_file 指定慢查询日志文件路径 /var路径
log_slow_slave_statements 该参数在从库上设置,决定是否记录在复制过程中超过long_query_time的SQL。如果binlog格式是row,则该参数无效 OFF
log_slow_extra 当log_output=FILE时,是否要记录额外信息(MySql 8.0.14开始提供),对log_output=TABLE的结果无影响。 OFF

使用方式

方式一、修改配置文件my.cnf,在[mysqld]段落中加上如下参数即可
例如:

[mysqld]
#...
log_output = 'FILE,TABLE'
slow_query_log = ON
long_query_time = 0.001

然后重启mysql

service mysqld restart

方式二、通过全局变量设置

这种方式无需重启即可生效,但一旦重启,配置又会失效。

例如:

set global log_output = 'FILE,TABLE';
set global slow_query_log = 'NO';
set global long_query_time = 0.001;

这样设置之后,就会将慢查询日志同时记录到文件以及mysql.slow_log表中。

分析慢查询日志

分析慢查询日志表

当log_output = TABLE时,可直接用如下语句分析:

select * from `mysql`.slow_log

然后按照条件做各种查询、统计、分析。

分析慢查询日志文件

mysqldumpslow
当log_output = FILE时,可使用mysqldumpslow分析

[server@server-test ~]$ mysqldumpslow --help
Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]

Parse and summarize the MySQL slow query log. Options are

  --verbose    verbose
  --debug      debug
  --help       write this text to standard output

  -v           展示更详细的信息
  -d           debug
  -s ORDER     以哪种方式排序 (al, at, ar, c, l, r, t), 默认 'at' 
                al: 平均锁定时间
                ar: 平均返回记录数
                at: 平均查询时间
                 c: 访问计数
                 l: 锁定时间
                 r: 返回记录
                 t: 查询时间
  -r           将-s的排序倒序
  -t NUM       top n的意思,展示最前面的几条
  -a           不去将数字展示成N,将字符串展示成'S'
  -n NUM       abstract numbers with at least n digits within names
  -g PATTERN   后边可以写一个正则,只有符合正则的行会展示
  -h HOSTNAME  慢查询日志以 主机名 =slow.log的格式命名,-h可指定读取指定主机名的慢查询日志,默认情况下是*,读取所有的慢查询日志
  -i NAME      Mysql Server的实例名称(如果使用了mysql.server startup脚本的话)
  -l           不将锁定时间从总时间减去

pt-query-digest
我们也可以用pt-query-digest分析慢查询日志文件。pt-query-digest是Percona公司开发的工具,是Percona Toolkit工具套件的使用工具之一。这里就不详细探究了,感兴趣的同学自行研究吧。

EXPLAIN详解

explain可以帮助我们分析mysql的执行计划,

EXPLAIN使用

explain可用来分析SQL的执行计划。

我们来演示一下:
在这里插入图片描述
这样我们就可以知道

SELECT * from user_info where nickname = 'Ant'

这条Sql语句是怎样执行的了。

结果输出展示:

字段 format=json时的名称 含义
id select_id 该语句的唯一标识
select_type 查询类型
table table_name 表名
partitions partitions 匹配的分区
type access_type 连接类型
possible_keys possible_keys 可能的索引选择
key key 实际选择的索引
key_len key_length 索引的长度
ref ref 索引的哪一列被引用了
rows rows 估计要扫描的行
filtered filtered 表示符合查询条件的数据百分比
Extra 没有 附加信息

接下来我们分析一下执行的结果:
在这里插入图片描述
首先,这里的select_type是SIMPLE 表示这是一个简单查询,table表示查询的表是user_info ,type是ALL表示发生了全表扫描,possible_keys、key、key_len都是空,表示没有使用任何索引,rows表示执行这个sql需要扫描25万多数据才能返回,filtered是10%,最后Extra是Using where表示使用了where条件。

根据这个分析来看,这条sql的性能是比较差的。
我们执行一下看看:
在这里插入图片描述
看到花费了800多毫秒,性能果然不太OK。

我们再来看下面一段演示:

在这里插入图片描述
从这里我们可以发现,explain执行后展示了两行结果,当有多条结果的时候这个id字段是有用的,它可以描述sql的执行过程。如果一个explain的执行结果包含多个id值,比如id=1以及id=2那么数字越大越先执行;而对于相同id的行,比如上图中id都是1,那么会从上到下依次执行。

SQL性能分析

一般来说,使用Explain已经满足大多数场景下分析SQL的需求,但是如果想更加细致的分析SQL的话,Explain可能还是不够的,那我们就来探讨一下,如何深入到Sql内部,去了解一条Sql到底执行了哪些步骤,每个步骤花费了多久,性能瓶颈出现在了哪个步骤。

如何深入SQL内部,去分析其性能,包括了三种方式:

  • SHOW PROFILE
  • INFORMATION_SCHEMA.PROFILING
  • PERFORMANCE_SCHEMA
SHOW PROFILE

SHOW PROFILE是MySQL的一个性能分析指令,可以跟踪SQL各种资源消耗。使用格式如下:

SHOW PROFILE [type [, type] ... ]
    [FOR QUERY n]
    [LIMIT row_count [OFFSET offset]]

type: {
    ALL                     显示所有信息
  | BLOCK IO                显示阻塞的输入输出次数
  | CONTEXT SWITCHES				显示自愿及非自愿的上下文切换次数
  | CPU											显示用户与系统CPU使用时间
  | IPC											显示消息发送与接收的次数
  | MEMORY									显示内存相关的开销,目前未实现此功能
  | PAGE FAULTS							显示页错误相关开销信息
  | SOURCE									列出相应操作对应的函数名及其在源码中的位置()
  | SWAPS										显示swap交换次数
}

默认情况下,SHOW PROFILE只展示Status和Duration两列,如果想展示更多信息,可指定type。

  • 使用如下命令,查看是否支持SHOW PROFILE功能,yes标志支持。从MySQL 5.0.37开始,MySQL支持SHOW PROFILE

    select @@have_profiling;
    
  • 查看当前是否启用了SHOW PROFILE,0表示未启用,1表示已启用

    select @@profiling;
    
  • 使用如下命令为当前会话开启或关闭性能分析,设成1表示开启,0表示关闭

    set profiling = 1;
    
  • 使用SHOW PROFILES命令,可为最近发送的SQL语句做一个概要的性能分析。展示的条目数目由profiling_history_size会话变量控制,该变量的默认值为15。最大值为100。将值设置为0具有禁用分析的实际效果。

  • – 默认展示15条

    show profiles
    
    -- 使用profiling_history_size调整展示的条目数
    set profiling_history_size = 100;
    
  • 使用show profile分析指定查询:

    mysql> SHOW PROFILES;
    +----------+----------+--------------------------+
    | Query_ID | Duration | Query                    |
    +----------+----------+--------------------------+
    |        0 | 0.000088 | SET PROFILING = 1        |
    |        1 | 0.000136 | DROP TABLE IF EXISTS t1  |
    |        2 | 0.011947 | CREATE TABLE t1 (id INT) |
    +----------+----------+--------------------------+
    3 rows in set (0.00 sec)
    
    mysql> SHOW PROFILE;
    +----------------------+----------+
    | Status               | Duration |
    +----------------------+----------+
    | checking permissions | 0.000040 |
    | creating table       | 0.000056 |
    | After create         | 0.011363 |
    | query end            | 0.000375 |
    | freeing items        | 0.000089 |
    | logging slow query   | 0.000019 |
    | cleaning up          | 0.000005 |
    +----------------------+----------+
    7 rows in set (0.00 sec)
    
    -- 默认情况下,只展示Status和Duration两列,如果想展示更多信息,可指定type。
    mysql> SHOW PROFILE FOR QUERY 1;
    +--------------------+----------+
    | Status             | Duration |
    +--------------------+----------+
    | query end          | 0.000107 |
    | freeing items      | 0.000008 |
    | logging slow query | 0.000015 |
    | cleaning up        | 0.000006 |
    +--------------------+----------+
    4 rows in set (0.00 sec)
    
    -- 展示CPU相关的开销
    mysql> SHOW PROFILE CPU FOR QUERY 2;
    +----------------------+----------+----------+------------+
    | Status               | Duration | CPU_user | CPU_system |
    +----------------------+----------+----------+------------+
    | checking permissions | 0.000040 | 0.000038 |   0.000002 |
    | creating table       | 0.000056 | 0.000028 |   0.000028 |
    | After create         | 0.011363 | 0.000217 |   0.001571 |
    | query end            | 0.000375 | 0.000013 |   0.000028 |
    | freeing items        | 0.000089 | 0.000010 |   0.000014 |
    | logging slow query   | 0.000019 | 0.000009 |   0.000010 |
    | cleaning up          | 0.000005 | 0.000003 |   0.000002 |
    +----------------------+----------+----------+------------+
    7 rows in set (0.00 sec) ```
    
    
  • 分析完成后,记得关闭掉SHOW PROFILE功能:

    set profiling = 0;
    

TIPS

  • MySQL官方文档声明SHOW PROFILE已被废弃,并建议使用Performance Schema作为替代品。
  • 在某些系统上,性能分析只有部分功能可用。比如,部分功能在Windows系统下无效(show profile使用了getrusage()这个API,而在Windows上将会返回false,因为Windows不支持这个API);此外,性能分析是进程级的,而不是线程级的,这意味着其他线程的活动可能会影响到你看到的计时信息。
INFORMATION_SCHEMA.PROFILING

INFORMATION_SCHEMA.PROFILING用来做性能分析。它的内容对应SHOW PROFILE和SHOW PROFILES 语句产生的信息。除非设置了 set profiling = 1; ,否则该表不会有任何数据。该表包括以下字段:

  • QUERY_ID:语句的唯一标识
  • SEQ:一个序号,展示具有相同QUERY_ID值的行的显示顺序
  • STATE:分析状态
  • DURATION:在这个状态下持续了多久(秒)
  • CPU_USER,CPU_SYSTEM:用户和系统CPU使用情况(秒)
  • CONTEXT_VOLUNTARY,CONTEXT_INVOLUNTARY:发生了多少自愿和非自愿的上下文转换
  • BLOCK_OPS_IN,BLOCK_OPS_OUT:块输入和输出操作的数量
  • MESSAGES_SENT,MESSAGES_RECEIVED:发送和接收的消息数
  • PAGE_FAULTS_MAJOR,PAGE_FAULTS_MINOR:主要和次要的页错误信息
  • SWAPS:发生了多少SWAP
  • SOURCE_FUNCTION,SOURCE_FILE,SOURCE_LINE:当前状态是在源码的哪里执行的

TIPS

  • SHOW PROFILE本质上使用的也是INFORMATION_SCHEMA.PROFILING
  • INFORMATION_SCHEMA.PROFILING表已被废弃,在未来可能会被删除。未来将可使用Performance Schema替代,详见 “Query Profiling Using Performance Schema”
  • 下面两个SQL是等价的:
    SHOW PROFILE FOR QUERY 2;
    
    SELECT STATE, FORMAT(DURATION, 6) AS DURATION
    FROM INFORMATION_SCHEMA.PROFILING
    WHERE QUERY_ID = 2 ORDER BY SEQ;
    
PERFORMANCE_SCHEMA

PERFORMANCE_SCHEMA是MySQL建议的性能分析方式,未来SHOW PROFILE、INFORMATION_SCHEMA.PROFILING都会废弃。据笔者研究,PERFORMANCE_SCHEMA在MySQL 5.6引入,因此,在MySQL 5.6及更高版本才能使用。可使用SHOW VARIABLES LIKE 'performance_schema';查看启用情况,MySQL 5.7开始默认启用。

下面来用PERFORMANCE_SCHEMA去实现SHOW PROFILE类似的效果:

  • 查看是否开启性能监控

    mysql> SELECT * FROM performance_schema.setup_actors;
    +------+------+------+---------+---------+
    | HOST | USER | ROLE | ENABLED | HISTORY |
    +------+------+------+---------+---------+
    | %    | %    | %    | YES     | YES     |
    +------+------+------+---------+---------+
    

    默认是开启的。

  • 你也可以执行类似如下的SQL语句,只监控指定用户执行的SQL:

    mysql> UPDATE performance_schema.setup_actors
      	 SET ENABLED = 'NO', HISTORY = 'NO'
       	WHERE HOST = '%' AND USER = '%';
    
    mysql> INSERT INTO performance_schema.setup_actors
           (HOST,USER,ROLE,ENABLED,HISTORY)
           VALUES('localhost','test_user','%','YES','YES');
    

    这样,就只会监控localhost机器上test_user用户发送过来的SQL。其他主机、其他用户发过来的SQL统统不监控。

  • 执行如下SQL语句,开启相关监控项:

    mysql> UPDATE performance_schema.setup_instruments
           SET ENABLED = 'YES', TIMED = 'YES'
           WHERE NAME LIKE '%statement/%';
           
    mysql> UPDATE performance_schema.setup_instruments
           SET ENABLED = 'YES', TIMED = 'YES'
           WHERE NAME LIKE '%stage/%';
           	       
    mysql> UPDATE performance_schema.setup_consumers
           SET ENABLED = 'YES'
           WHERE NAME LIKE '%events_statements_%';
           	
    mysql> UPDATE performance_schema.setup_consumers
           SET ENABLED = 'YES'
           WHERE NAME LIKE '%events_stages_%';  
           
    
  • 使用开启监控的用户,执行SQL语句,比如:

    mysql> SELECT * FROM employees.employees WHERE emp_no = 10001;
    +--------+------------+------------+-----------+--------+------------+
    | emp_no | birth_date | first_name | last_name | gender | hire_date |
    +--------+------------+------------+-----------+--------+------------+
    |  10001 | 1953-09-02 | Georgi     | Facello   | M      | 1986-06-26 |
    +--------+------------+------------+-----------+--------+------------+
    
  • 执行如下SQL,获得语句的EVENT_ID。

    mysql> SELECT EVENT_ID, TRUNCATE(TIMER_WAIT/1000000000000,6) as Duration, SQL_TEXT
           FROM performance_schema.events_statements_history_long WHERE SQL_TEXT like '%10001%';
    +----------+----------+--------------------------------------------------------+
    | event_id | duration | sql_text                                               |
    +----------+----------+--------------------------------------------------------+
    |       31 | 0.028310 | SELECT * FROM employees.employees WHERE emp_no = 10001 |
    +----------+----------+--------------------------------------------------------+
    

    这一步类似于 SHOW PROFILES。

  • 执行如下SQL语句做性能分析,这样就可以知道这条语句各种阶段的信息了。

    mysql> SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration
           FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=31;
    +--------------------------------+----------+
    | Stage                          | Duration |
    +--------------------------------+----------+
    | stage/sql/starting             | 0.000080 |
    | stage/sql/checking permissions | 0.000005 |
    | stage/sql/Opening tables       | 0.027759 |
    | stage/sql/init                 | 0.000052 |
    | stage/sql/System lock          | 0.000009 |
    | stage/sql/optimizing           | 0.000006 |
    | stage/sql/statistics           | 0.000082 |
    | stage/sql/preparing            | 0.000008 |
    | stage/sql/executing            | 0.000000 |
    | stage/sql/Sending data         | 0.000017 |
    | stage/sql/end                  | 0.000001 |
    | stage/sql/query end            | 0.000004 |
    | stage/sql/closing tables       | 0.000006 |
    | stage/sql/freeing items        | 0.000272 |
    | stage/sql/cleaning up          | 0.000001 |
    +--------------------------------+----------+
    

OPTIMIZER_TRACE

我们再来聊一下分析SQL的另一款神器:OPTIMIZER_TRACE
翻译成中文叫做“优化器跟踪”。

它可以跟踪优化器做出各种决策,了解优化器的执行细节,进而帮助我们理解SQL的执行过程,并且优化SQL。

OPTIMIZER_TRACE是MySQL5.6引入的一项功能,此功能默认是关闭的,开启后,可分析如下语句:

  • SELECT
  • INSERT
  • REPLACE
  • UPDATE
  • DELETE
  • EXPLAIN
  • SET
  • DECLARE
  • CASE
  • IF
  • RETURN
  • CALL

OPTIMIZER_TRACE相关参数

参考 https://dev.mysql.com/doc/internals/en/system-variables-controlling-trace.html

  • –optimizer-trace
    • –optimizer-trace:总开关,默认值:enabled=off,one_line=off
    • enabled:是否开启optimizer_trace;on表示开启,off表示关闭。
    • one_line:是否开启单行存储。on表示开启;off表示关闭,将会用标准的JSON格式化存储。设置成on将会有良好的格式,设置成off可节省一些空间。
  • optimizer_trace_features
    • 控制optimizer_trace跟踪的内容,默认值:greedy_search=on,range_optimizer=on,dynamic_range=on,repeated_subselect=on,表示开启所有跟踪项。
    • greedy_search:是否跟踪贪心搜索,有关贪心算法详见:https://blog.csdn.net/qq_37763204/article/details/79289532
    • range_optimizer:是否跟踪范围优化器
    • dynamic_range:是否跟踪动态范围优化
    • repeated_subselect:是否跟踪子查询,如果设置成off,只跟踪第一条Item_subselect的执行
  • optimizer_trace_limit:控制optimizer_trace展示多少条结果,默认1
  • optimizer_trace_max_mem_size:optimizer_trace堆栈信息允许的最大内存,默认1048586
  • optimizer_trace_offset:第一个要展示的optimizer trace的偏移量,默认-1。
  • end_markers_in_json:如果JSON结构很大,则很难将右括号和左括号配对。为了帮助读者阅读,可将其设置成on,这样会在右括号附近加上注释,默认off。

以上参数可以用作SET语句操作,例如,用如下命令即可打开OPTIMIZER TRACE

SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;

也可以用SET GLOBAL全局开启,但即使全局开启,每个Session也只能跟踪它自己执行的语句:

SET GLOBAL OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on

optimizer_trace_limit和optimizer_trace_offset这两个参数经常配合使用,例如:

SET optimizer_trace_offset=<OFFSET>,optimizer_trace_limit=<LIMIT>

这两个参数配合使用,有点类似MySQL里面的limit语句。
默认情况下,由于optimizer_trace_offset=-1,optimizer_trace_limit=1,记录最近的一条SQL语句,展示时,每次展示一条数据;

OPTIMIZER_TRACE使用

  1. 开启OPTIMIZER_TRACE功能,并设置要展示的数据条目数:
    SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;
    SET optimizer_trace_offset=-30,optimizer_trace_limit=30;
    
  2. 发送你想要分析的查询语句,例如:
    select *
    from user_info 
    where nickname = 'Ant'
    and ctime > '2021-02-01'
    
  3. 使用如下语句分析,即可获得类似如下的结果:
    mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE limit 30 ;
    

在这里插入图片描述
其中QUERY这一列会展示出执行的sql是什么,TRACE则是Object的结果,它是一段很长的JSON,我把结果拷贝出来:

{
    
    
  "steps": [
    {
    
    
      "join_preparation": {
    
    
        "select#": 1,
        "steps": [
          {
    
    
            "expanded_query": "/* select#1 */ select `user_info`.`id` AS `id`,`user_info`.`username` AS `username`,`user_info`.`password` AS `password`,`user_info`.`real_name` AS `real_name`,`user_info`.`sex` AS `sex`,`user_info`.`birthday` AS `birthday`,`user_info`.`card_id` AS `card_id`,`user_info`.`mark` AS `mark`,`user_info`.`partner_id` AS `partner_id`,`user_info`.`group_id` AS `group_id`,`user_info`.`nickname` AS `nickname`,`user_info`.`avatar` AS `avatar`,`user_info`.`phone` AS `phone`,`user_info`.`add_ip` AS `add_ip`,`user_info`.`last_time` AS `last_time`,`user_info`.`last_ip` AS `last_ip`,`user_info`.`now_money` AS `now_money`,`user_info`.`brokerage_price` AS `brokerage_price`,`user_info`.`integral` AS `integral`,`user_info`.`sign_num` AS `sign_num`,`user_info`.`status` AS `status`,`user_info`.`level` AS `level`,`user_info`.`spread_uid` AS `spread_uid`,`user_info`.`spread_time` AS `spread_time`,`user_info`.`user_type` AS `user_type`,`user_info`.`is_promoter` AS `is_promoter`,`user_info`.`pay_count` AS `pay_count`,`user_info`.`spread_count` AS `spread_count`,`user_info`.`clean_time` AS `clean_time`,`user_info`.`addres` AS `addres`,`user_info`.`adminid` AS `adminid`,`user_info`.`login_type` AS `login_type`,`user_info`.`union_id` AS `union_id`,`user_info`.`open_id` AS `open_id`,`user_info`.`superior_user_id` AS `superior_user_id`,`user_info`.`is_indentor` AS `is_indentor`,`user_info`.`indentor_level_name` AS `indentor_level_name`,`user_info`.`direct_superior_user_id` AS `direct_superior_user_id`,`user_info`.`member_level_name` AS `member_level_name`,`user_info`.`upgrade_time` AS `upgrade_time`,`user_info`.`password_app` AS `password_app`,`user_info`.`store_name` AS `store_name`,`user_info`.`rank_indentor_id` AS `rank_indentor_id`,`user_info`.`rank_member_id` AS `rank_member_id`,`user_info`.`manage_pending` AS `manage_pending`,`user_info`.`manage_done` AS `manage_done`,`user_info`.`develop_pending` AS `develop_pending`,`user_info`.`develop_done` AS `develop_done`,`user_info`.`range_pending` AS `range_pending`,`user_info`.`range_done` AS `range_done`,`user_info`.`corpus_pending` AS `corpus_pending`,`user_info`.`corpus_done` AS `corpus_done`,`user_info`.`yeji_pending` AS `yeji_pending`,`user_info`.`yeji_done` AS `yeji_done`,`user_info`.`commission_pending` AS `commission_pending`,`user_info`.`commission_done` AS `commission_done`,`user_info`.`can_edit_material` AS `can_edit_material`,`user_info`.`poster_url` AS `poster_url`,`user_info`.`ctime` AS `ctime`,`user_info`.`mtime` AS `mtime`,`user_info`.`health_vip_ctime` AS `health_vip_ctime`,`user_info`.`makeup_vip_ctime` AS `makeup_vip_ctime`,`user_info`.`purchase_balance` AS `purchase_balance`,`user_info`.`ay_card_money` AS `ay_card_money`,`user_info`.`other_rank_id` AS `other_rank_id`,`user_info`.`superior_other_id` AS `superior_other_id`,`user_info`.`star_health_vip_ctime` AS `star_health_vip_ctime`,`user_info`.`star_makeup_vip_ctime` AS `star_makeup_vip_ctime`,`user_info`.`lock_money` AS `lock_money` from `user_info` where ((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))"
          }
        ] /* steps */
      } /* join_preparation */
    },
    {
    
    
      "join_optimization": {
    
    
        "select#": 1,
        "steps": [
          {
    
    
            "condition_processing": {
    
    
              "condition": "WHERE",
              "original_condition": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))",
              "steps": [
                {
    
    
                  "transformation": "equality_propagation",
                  "resulting_condition": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))"
                },
                {
    
    
                  "transformation": "constant_propagation",
                  "resulting_condition": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))"
                },
                {
    
    
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))"
                }
              ] /* steps */
            } /* condition_processing */
          },
          {
    
    
            "substitute_generated_columns": {
    
    
            } /* substitute_generated_columns */
          },
          {
    
    
            "table_dependencies": [
              {
    
    
                "table": "`user_info`",
                "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": "`user_info`",
                "table_scan": {
    
    
                  "rows": 229694,
                  "cost": 8441
                } /* table_scan */
              }
            ] /* rows_estimation */
          },
          {
    
    
            "considered_execution_plans": [
              {
    
    
                "plan_prefix": [
                ] /* plan_prefix */,
                "table": "`user_info`",
                "best_access_path": {
    
    
                  "considered_access_paths": [
                    {
    
    
                      "rows_to_scan": 229694,
                      "access_type": "scan",
                      "resulting_rows": 229694,
                      "cost": 54380,
                      "chosen": true
                    }
                  ] /* considered_access_paths */
                } /* best_access_path */,
                "condition_filtering_pct": 100,
                "rows_for_plan": 229694,
                "cost_for_plan": 54380,
                "chosen": true
              }
            ] /* considered_execution_plans */
          },
          {
    
    
            "attaching_conditions_to_tables": {
    
    
              "original_condition": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))",
              "attached_conditions_computation": [
              ] /* attached_conditions_computation */,
              "attached_conditions_summary": [
                {
    
    
                  "table": "`user_info`",
                  "attached": "((`user_info`.`nickname` = 'Ant') and (`user_info`.`ctime` > '2021-02-01'))"
                }
              ] /* attached_conditions_summary */
            } /* attaching_conditions_to_tables */
          },
          {
    
    
            "refine_plan": [
              {
    
    
                "table": "`user_info`"
              }
            ] /* refine_plan */
          }
        ] /* steps */
      } /* join_optimization */
    },
    {
    
    
      "join_execution": {
    
    
        "select#": 1,
        "steps": [
        ] /* steps */
      } /* join_execution */
    }
  ] /* steps */
}

我们可以看出,整个JSON分为三大部分,join_preparation:准备阶段;join_optimization:优化阶段;join_execution:执行阶段

经过了解,不难发现OPTIMIZER_TRACE的强大,它可以剖析sql的执行细节并且告诉我们各种开销,如果在做sql调优的时候,如果想深入到sql的内部,就可以使用OPTIMIZER_TRACE。

MySQL数据库诊断命令

我们已经聊完了EXPLAIN、SHOW PROFILE、OPTIMIZER_TRACE我们已经可以很方便的分析某个sql的问题了,但是数据库本身也可能会出现问题,那如果数据库出了问题该怎么定位呢?
我们就一起探讨一下常用的数据库诊断命令。

MySql提供了非常多的诊断命令,其中,我们专门挑几个重要的命令一起探讨一下吧:

SHOW PROCESSLIST

作用:
SHOW [FULL] PROCESSLIST 用于查看当前正在运行的线程。如果执行此命令的用户拥有PROCESS权限,则可看到所有线程;否则只能看到自己的线程(即当前用户登录关联的线程)。如果不使用FULL关键字,只在Info字段中展示100个字符。

当遇到”too many connections"错误信息时,想要了解发生了什么,SHOW PROCESSLIST就非常有用。MySQL保留了一个额外的连接,用于让拥有CONNECTION_ADMIN(或已废弃的SUPER)权限的账户使用,从而确保管理员始终能够连接并检查系统。
可使用KILL语句杀死线程。
语法:

SHOW [FULL] PROCESSLIST

执行结果:
在这里插入图片描述
由结果可知,结果包含如下几列:

  • Id:连接的唯一标识,是CONNECTION_ID()函数的返回。

  • User:发出该语句的MySql用户。

    • system_user表示服务器产生的非客户端线程,用于处理内部任务。这可能是用来在从库复制或延迟行处理器IO/SQL线程。对于system_user,Host字段将会为空。
    • unauthenticated user是指与客户端连接,但尚未完成客户端身份认证的线程。
    • event_scheduler是指事件调度器

    User字段的值是system_user和SYSTEM_USER权限不是一回事,前者指内部线程,后者用来区分系统账户和普通账户的区别。

  • Host:发出该语句的客户端的主机名(当User时system_user时,Host为空)。TCP/IP链接的主机名以host_name:client_port格式上报,以便更轻松地了解哪个客户端在干什么。

  • db:当前执行的命令是在哪一个数据库上。如果没有指定数据库,则值为NULL

  • Command:当前线程正在执行的命令。

  • Time:线程处于当前状态的时间(单位秒)。对于从库的SQL线程,该字段的值表示上次复制事件的时间和从库机器的实际时间之间经过了多少秒。

  • State:指线程正在执行的操作、事件或状态。大多数State对应于非常快的操作。如果线程在给定状态下很久,则需要排查。

  • info当前线程正在执行的语句,如果未执行任何语句则为NULL。该语句是发送到服务器的那条语句,也可能是内部的语句(如果某个语句执行了其他语句)。例如一条CALL语句执行了一条正在执行SELECT语句的存储过程,则Info字段会展示SELECT语句。

其中Command取值:

  • Binlog Dump:主库上的线程,用于将binlog内容发送到从库
  • Change user:线程正在执行更改用户操作
  • Close stmt:线程正在关闭一个prepared statement
  • Connect:一个复制从库已连接到其主库
  • Connect Out:一个复制从库正在连接到其主库
  • Create DB:线程正在执行create-database操作
  • Deamon:服务器内部线程,而非为客户端连接提供服务的线程
  • Debug:该线程正在生成调试信息
  • Delayed insert:该线程是延迟插入处理程序
  • Drop DB:线程正在执行drop-database操作
  • Error:你懂得
  • Execute:线程正在执行一个prepared statement
  • Fetch:正在从Prepared Statement中获取执行结果
  • Field List:该线程正在获取表的字段信息
  • Init DB:线程正在选择默认数据库。
  • Kill:该线程正在杀死一个线程
  • Long Data:正在从prepared statement中检索long data
  • Ping:线程正在处理server-ping请求。
  • Prepare:该线程正在准备一个prepared statement
  • Processlist:该线程正在生成服务器线程相关信息
  • Query:线程正在执行一条语句
  • Quit:线程正在终止
  • Refresh:该线程是刷新表,日志或缓存;或者正在重置状态变量或在复制服务器信息。
  • Register Slave:该线程正在注册一个从库
  • Rest stmt:线程正在重置prepared statement
  • Set option:线程正在设置或重置client statement-execution选项
  • Shutdown:线程正在关闭服务器
  • Sleep:线程正在等待客户端向其发送statement
  • Statistics:该线程正在生成服务器状态信息
  • Table Dump:线程正在将表内容发送到从属服务器。
  • Time:Unused

注意
事实上,SHOW PROCESSLIST的结果就是从INFORMATION_SCHEMA.PROCESSLIST表中获取的
因此,执行

SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST

也可以获取同样的结果。

实用SQL

-- 按照客户端IP分组,看哪个客户端的连接数最多
select client_ip, count(client_ip) as client_num
from (select substring_index(host,':',1) as client_ip
		from `information_schema`.processlist) as connect_info
group by client_ip
order by client_num desc;

-- 查看正在执行的线程,并按Time 倒排序,看看有没有执行时间特别长的线程
select * 
from `information_schema`.processlist
where Command != 'Sleep'
order by Time desc;

-- 找出所有执行时间超过5分钟的线程,拼凑出 kill 语句,方便后面查杀
select concat('kill ',id, ';')
from `information_schema`.processlist
where Command = 'Sleep'
	and Time > 300
order by Time desc;

SHOW STATUS

作用:查看服务器相关信息。
语法:

SHOW [GLOBAL | SESSION] STATUS
	[LIKE 'pattern' | WHERE expr]

示例:

SHOW STATUS
SHOW GLOBAL STATUS like '%Slow%'

SHOW VARIABLES

作用:查看Mysql变量
语法:

SHOW [GLOBAL | SESSION] VARIABLES
	[LIKE 'pattern' | WHERE expr]

示例:

SHOW VARIABLES;

SHOW TABLE STATUS

作用:查看表以及视图的状态
语法:

SHOW TABLE STATUS
	[{
   
   FROM | IN} db_name]
	[LIKE 'pattern' | WHERE expr]

示例:

SHOW TABLE STATUS from employess;

SHOW INDEX

作用:查看索引相关信息
语法:

SHOW [EXTENDED] {
   
   INDEX | INDEXES | KEYS}
	{
   
   FROM | IN} tb1_name
	[{
   
   FROM | IN} db_name]
	[WHERE expr]

示例:

SHOW INDEX FROM mytable FROM mydb;
SHOW INDEX FROM mydb.mytable;

SHOW ENGINE

作用:展示有关存储引擎的相关信息
语法:

SHOW ENGINE engine_name {
   
   STATUS | MUTEX}

示例:

-- 有关innodb的内容解读详见:https://dev.mysql.com/doc/refman/8.0/en/innodb-standard-monitor.html
SHOW ENGINE INNODB STATUS
SHOW ENGINE INNODB MUTEX

数据库索引

在探讨这部分话题前,我们先来了解几个概念:

平衡二叉树

  • 每个节点的左子树和右子树的高度差不超过1
  • 对于n个节点,树的深度是log2n,查询的时间复杂度是O(log2n)

关于平衡二叉树想必大家都很了解,这里不做详细探讨了。
不了解的同学可看一下这篇文章:《什么是平衡二叉树(AVL)》

B-Tree(Balance Tree)

B-Tree(-不是减) 全称是 Balance Tree,意思是:平衡多路搜索树。

看下图:(图中磁盘块3也应有3个子节点,只是图放不下了,没有画)
在这里插入图片描述

图里面,灰色代表指针,它指向了子节点对应的磁盘块,关键字表示主键或者索引,数据表示关键字锁对应的数据。
比如:中17-Data 你可以理解为主键或索引为17对应的一条数据为Data

假设,我们想要搜索主键为5的那条数据,如果通过B-Tree去查找的话,大致是这样玩的:首先找到根节点的关键字17或35,5小于17,于是就通过P1指针定位到磁盘块2。而磁盘块2里面的关键字是8和12,5小于8,于是通过磁盘块2里面的P1指针找到磁盘块5,最后在磁盘块5里面就能找到这条数据了。

B-Tree特性
  • 根节点的子节点数2 <= x <= m,m是树的阶
    • 假设m=3,则根节点可以有2-3个孩子
  • 中间节点的子节点数 m/2 <= y <= m
    • 假设m=3,中间节点至少有2个孩子,最多3个孩子
  • 每个中间节点包含n个关键字,n=子节点个数-1,且按升序排序
    • 如果中间节点有3个子节点,则里面会有2个关键字,且按升序排序
  • 每个中间节点包含n个关键字,n=子节点个数-1,且升序排序
    • 如果中间节点有3个子节点,则里面会有2个关键字,且按升序排序
    • Pi(i=1,…n+1)为指向子树根节点的指针。其中P[1]指向关键字小于Key[1]的子树,P[i]指向关键字属于(Key[i-1],Key[i]的子树,P[n+1]指向关键字大于Key[n]的子树
    • P1、P2、P3为指向子树根节点的指针。P1指向关键字小于Key1的树;P2指向Key1—Key2之间的子树;P3指向大于Key2的树

通俗一点,就是说,每有几个关键字就对应有n+1个指针,比如图中磁盘块2,有两个关键字,那么就对应有3个指针P1、P2、P3,这三个指针分别指向比8小的对应的磁盘块,8—12对应的磁盘块,以及大于12的关键字对应的磁盘块。

通过B-Tree可以有效的降低树的高度,树的阶越大,高度就越低,查询的次数也会越少。

B+Tree

B+Tree是B-Tree基础上的优化,MySQL里面的InnoDB存储引擎使用的就是B+Tree实现其存储结构的。

在理解完B-Tree后,再来理解B+Tree还是比较轻松地。

如图:
在这里插入图片描述

假设我们要搜索关键字为8的数据。大致的过程是这样:首先在根节点用8与5、28、65对比,然后发现8大于5小于28,于是就会用P1指针找到磁盘块2,接着就会在磁盘2用8和5、10、20对比,发现8大于5小于10于是就会用磁盘块2里的P1指针找到下方的数据。

我们可以发现,B+Tree和B-Tree还是比较类似的,但是也存在一些差异。

B-Tree与B+Tree的差异
  • B+Tree有n个子节点的节点中含有n个关键字
    • B-Tree是n个子节点的节点有n-1个关键字
  • B+Tree中,所有的叶子节点中包含了父节点全部关键字的信息,且叶子节点按照关键字的大小自小到大顺序链接,构成一个有序链表
    • B-Tree的叶子节点不包括全部关键字
  • B+Tree中非叶子节点仅用于索引,不保存数据记录,记录存放在叶子节点中。
    • B-Tree中,非叶子节点既保存索引,也保存数据记录

在上图中可以看到,每个磁盘块都有3个关键字对应的也有3个指针,而磁盘块1中的5、28、65又会分别展示在了磁盘块2、3、4中。不管哪个父节点里的关键字,在叶子节点里都会记录一份。B-Tree中却没有这样的要求。

B-Tree vs B+Tree

我们来对比一下B-Tree 与B+Tree,假设,我们要查询一个关键字是5的数据。(where id = 5)

经过分析后,我们可以发现,在这种情况,他们的查询过程区别是不大的,但是由于B+Tree的中间节点,只用来做索引,所以对于相同的空间,B+Tree里面可以存放的关键字更多,于是B+Tree就相对矮胖一些。所以磁盘IO的次数也要少一些。
由于B-Tree的中间节点也要存储数据,他的查询效率就不是很稳定。最好的情况就是在根节点直接查到数据了,而最差的情况是在叶子节点才能找到数据,而B+Tree不管什么时候都必须到叶子节点才能找到。

再做一个对比,如果查询一个关键字在5到10之间的数据(where id between 5 and 10)

这种情况,如果使用的是B-Tree,需要先查5,再查6依次类推一直查到10,最终再把结果组装在一起返回。
而如果是使用的B+Tree只要先查5,查到5这条数据之后,就会从5这个节点的有序链表依次遍历,一直遍历到10就可以了。
所以B+Tree对于范围查询的性能要比B-Tree好一些。

InnoDB存储方式

对于InnoDB引擎,它使用的是B+Tree索引。而数据的存储对于不同的数据结构是不一样的。如果索引是主键的话,叶子节点会存储主键以及数据;而对于非主键索引(二级索引、辅助索引),叶子节点存储的是非主键索引以及主键。

也就是说,你发送的Sql语句使用的是非主键索引,那么需要先通过非主键索引查到主键,然后再根据主键查到数据。

MyISAM存储方式

对于MyISAM使用的也是B+Tree,但是不管这个索引是不是主键,叶子节点存储的都是指向数据块的指针。
也就是说,在MyISAM里面,索引和数据是分开存储的。

MyISAM VS InnoDB

我们把Inno DB这种存储方式i称为聚簇索引,而MyISAM这种存储方式称为非聚簇索引

Hash索引

下面我们再来聊一聊Hash索引
如图:

在这里插入图片描述
这里的Keys代表创建索引的字段,buckets是索引字段计算出来的Hash值和所对应数据的物理位置所组成的一个Hash表,entries代表具体的数据。
这样解释后我们发现,Hash索引的核心就是Hash表,每行数据都会基于基于索引字段计算出所对应的hashCode存放到buckets,在查询的时候为我们的关键字计算出hashCode,然后再从buckets里面去查,那么正常情况下,由于Hash索引是基于hash表的所以时间复杂度是O1,性能非常好。
至于为什么时间复杂度是O1,大家如果对HashMap或者HashSet的源码比较了解的话,就很清楚了,如果不了解可以自行百度一下。这部分内容比较简单,就不展开介绍了。

那么如果出现了Hash冲突,查询过程大致是这样的:比方说图里小飞龙和小油子的hashCode都是139,当你做查询的时候,如果查询条件是小油子,那么会用小油子计算出hashCode得出来139,然后到buckets里面去匹配,找到一个hashCode是139的指针数组,当然也可能是链表,看数据引擎是怎样实现的,之后再利用指针数组或链表找到对应的数据,可以发现,在hash冲突的情况下,性能会降低一些,所以说,使用hash索引的话要尽量防止hash冲突。

Hash索引支持情况

就目前来说,MySQL的Memory引擎支持显式的Hash索引。
我们可以这样玩:

create table test_hash_table(
	name varchar(45) not null,
	age tinyint(4) not null,
	key using hash(name)
)engine = memory

这样那么字段就有了一个hash索引。

当然除了Memory引擎外,InnoDB引擎也是可以使用的。InnoDB支持“自适应Hash索引”,当InnoDB发现某个索引使用非常频繁的时候,它会在内存里面基于B+Tree之上再创建一个Hash索引,从而提升查询效率。

这个自适应索引,我们没有办法直接介入,他只有一个开关,你可以使用show variables like 'innodb_adaptive_hash_index'查看开关情况。

在这里插入图片描述

可以看到,默认是打开的。
如果你想把这个功能关闭的话,可以使用set global innodb_adaptive_hash_index = 'OFF'
一般来说,这个不需要修改。

空间索引(R-Tree索引)

空间索引是用来存储GIS数据,基于R-Tree构建的,所以也叫R-Tree索引。
在早期,只有MyISAM支持空间索引。从MySQL5.7开始InnoDB也支持了空间索引。

由于目前Mysql对GIS的支持不是很完善,所以大部分人都不会用它相关的功能。由于这块知识用的很少,就不详细介绍了,大家只要知道有这么回事就足够了。如果大家想要了解R-Tree的底层结构,可以看下这篇文章:《经典查找算法 — R树》,写的非常不错。
如果想了解空间索引怎么使用。可以看下这篇文章:《MySQL空间索引简单使用》

全文索引

全文索引主要用于适应全文搜索的需求,在MySql5.7之前,全文索引不支持中文,经常搭配Sphinx。
而从MySql5.7起,已经内置了一个解析器ngram,是支持中文的。
官方文档地址:https://dev.mysql.com/doc/refman/8.0/en/fulltext-search-ngram.html

虽然如此,但是~ 大人,时代变了!
就目前来说,应对全文搜索的需求,我们更多的会上一些搜索引擎,比如ElasticSearch 或者 Solr,所以全文索引用的也不是很多,这里只做一个了解就可以了。如果感兴趣的话可以看下这篇文章:《MySQL 之全文索引》

B-Tree(B+Tree) & Hash索引特性与限制

上面,我们已经探讨了四种索引的底层结构,我们知道InnoDB主要使用的是B+Tree另外还有一个自适应的Hash索引。
这里,我们就探讨一下B-Tree(B+Tree)以及Hash索引的特性与限制。

这里我们不去区分B-Tree和B+Tree,统一称为B-Tree。
因为这部分对于B-Tree和B+Tree都是适用的。

B-Tree特性

B-Tree索引的查询是比较完善的。
查询条件与键值可以完全匹配,比如我们在name字段上创建索引,当使用where name = '小飞龙’的时候是可以用到索引的。
可以范围匹配,比方说在age字段创建索引,查询条件是where age > 20的时候也可以使用索引。
可以前缀匹配,比如在name 创建索引,使用where name like ‘小%’,这样也可以使用索引,不过当%用在前面就没办法使用索引了。即右模糊可以使用索引,左模糊不能使用索引。

B-Tree限制

比如我们创建一个组合索引,index(name,age,sex)作用在三个字段上面。

  • 当查询条件不包括最左列,无法使用索引。
    • where age = 5 and sex = 1无法使用索引。
  • 跳过了索引中的列,则无法完全使用索引
    • where name = ‘小飞龙’ and sex = 1 因为sex跳过了age字段,那么只能使用name这一列
  • 查询中有某个列的范围查询(模糊)查询,则其右边所有列都无法使用索引。
    • where name = ‘小飞龙’ and age > 20 and sex = 1 因为age使用范围查询,则sex没法使用索引,只能用name、age两列。

这三个限制就是著名的“最左匹配原则 ”,最左匹配原则指的是:索引按照最左优先的方式匹配索引,不满足上述三个条件的时候,都没办法完全使用索引。

Hash索引特性

一般来说,Hash索引要比B-Tree(B+Tree)的性能好一些,只要hash不冲突,那么他的时间复杂度就是O1

Hash索引限制

但是它的限制也比较明显,首先Hash索引不是按照索引的值排序,所以没办法使用排序,这就意味着如果你的查询条件里带有order by查询条件的时候,是没办法使用hash索引的。

其次就是它不支持部分索引列匹配查找
hash索引是用索引内容全部列去计算的,比如:我们在a,b两个字段上面创建了一个hash索引,如果你的查询条件是where a = 1这样只有一个的时候,它是没办法使用hash索引的。

hash索引只支持等值查询(例如 = 、IN),如果是范围查询、模糊查询那么都没办法使用hash索引。

最后,Hash索引的性能取决于Hash冲突,如果Hash冲突越严重,其性能下降越严重。

创建索引的原则

我们再来聊聊创建索引的原则:

  • 哪些场景建议创建索引?
  • 哪些场景不建议创建索引?
建议创建索引的场景

1)对于select 语句,频繁作为where条件的字段,可以考虑为这个字段创建索引。如果经常需要多个字段筛选数据的话,可以考虑组合索引,但是使用组合索引一定要考虑最左匹配原则。

比如:有一张员工表(employees),其中有两个字段,first_name,和last_name,由于经常用这两个字段筛选数据,就建立组合索引,
比如我们有一个动态查询,first_name是一个必选条件,last_name是一个可选条件,当使用index(first_name,last_name)的时候,是可以满足各种查询情况的。那么当使用index(last_name,first_name)由于last_name是一个可选条件,就会导致 where first_name = ‘魏’ 无法使用这个索引

2)对于update/delete语句 where条件使用的字段也需要使用索引
这是因为,update或delete语句会先根据where条件查询出对应的数据,然后再对这些数据进行更新或者删除。创建索引可以提升其中查询的效率。

3)对于需要分组或者排序的字段,也需要创建索引。

4)distinct所使用的字段也是需要考虑使用索引的。

5)如果字段的值有唯一性约束,也可以创建索引。比如唯一索引,主键索引都是有唯一性约束的。

6)对于多表查询,联接字段应创建索引,且类型务必保持一致,如果不一致,可能会导致隐式转换,进而可能导致索引无法使用。

不建议创建索引的场景

1)因为索引是为了快速定位到查询的数据,如果你的查询条件里根本用不到,那么就没有必要给这个字段创建索引。

2)如果表里的数据非常少,那么也没有必要创建索引。

3)如果一张表里某个字段有非常多重复数据,数据选择性非常低。那么创建索引的作用是不大的,也不建议创建索引,这是因为,索引的选择性越高,查询效率越好,因为可以在查找过程过滤更多行。

4)对于频繁更新的字段,如果创建索引要考虑其索引维护的开销,在修改或者删除数据的时候也是要更新索引,如果一个字段修改非常频繁,而查询很少的话,那么不建议创建索引。

不过需要注意,这里只是一般情况下的原则,实际项目中,一定要活学活用,千万不要死守教条。原则只是用来指导工作的,实际项目中应该结合实际情况合理变通。

索引失效与解决方案

我们再来探讨一下索引失效的场景与解决方案。
我这里总结了7种导致索引失效的场景:

  1. 索引列不独立。独立是指:列不能是表达式的一部分,也不能是函数的参数。
  2. 使用了左模糊
  3. 使用OR查询的部分字段没有索引
  4. 字符串条件未使用’ '引起来
  5. 不符合最左匹配原则的查询
  6. 索引字段建议添加NOT NULL约束
  7. 隐式转换导致索引失效

我们来看一下这几种场景。
1)索引列不独立
在这里插入图片描述
这里的is_indentor列作为了表达式的一部分,所以这种情况是用不了索引的。我们用explain分析一下:
在这里插入图片描述
可以看到type是ALL发生了全表扫描。

解决方案:事先计算好表达式的值,再传过来,避免在SQL where条件=左侧计算。

因此可以改写成这样:
在这里插入图片描述
可以看到type成了ref这样性能就提升了很多。

再看一下这种场景,索引字段作为了函数的参数:
比如这条sql
这里因为nickname是SUBSTRING函数的参数,所以也没法办使用索引。
在这里插入图片描述
解决方案:预先算好结果,再传过来,在where条件的左侧,不要使用函数;或者使用等价的sql查询。

我们上面的sql其实和使用like 'Ant%'实现同样的效果,因此可以改写成:
在这里插入图片描述

2)使用了左模糊查询

使用左模糊也是无法使用索引的
解决方案是:尽量避免使用左模糊,如果避免不了可以考虑使用搜索引擎。

3)使用OR查询的部分字段没有索引
比如如下SQL

在这里插入图片描述

在这里nickname是没有索引,is_indentor是有索引的,因此使用了OR这个时候也是没办法使用索引的。可以看到type为ALL使用了全表扫描。

解决办法:为另一个字段添加索引。

nickname添加索引后,再次执行:
在这里插入图片描述

可以看到,type成了index_merge,叫索引合并,在Extra字段里面它告诉我们Using sort_union(nickname,is_indentor);也就是说mysql使用了nickname和is_indentor这两个索引,并做了合并。索引合并是mysql内部的优化机制,它针对这两个索引分别进行了扫描,最终再把两个结果集进行了合并,带来的好处就是避免了全表扫描。

4)字符串条件未使用’'引起来
在这里插入图片描述
比如这里,我的is_indentor是一个varchar类型,但是查询的时候写成了数字,这个时候也是没办法走索引的。

解决方案:把字符串用’'引起来。
在这里插入图片描述
这时候type变成了ref。

5)不符合最左匹配原则
我们先调整一下索引,把rank_indentor_id放在nickname后面,
在这里插入图片描述
然后使用rank_indentor_id作为条件查询,可以看到用的是全表扫描。

在这里插入图片描述
解决方案:调整索引顺序。
在这里插入图片描述
我们这样改一下,再看看执行结果。
在这里插入图片描述
可以看到,这样是走了索引。

6)索引字段建议添加NOT NULL约束

这个是一个建议,建议索引添加NOT NULL约束,这是因为,对于一个单列索引,它没有办法存储NULL值,而对于复合索引不能存储全部为NULL的值,并且当使用ISNULL的时候,是没办法使用索引的。
其中官方文档也给了明确的描述,大致是这个意思:字段建议设置成NOT NULL,可以让sql执行的更快,并且减少额外bit的存储开销,以及判断的开销。
https://dev.mysql.com/doc/refman/8.0/en/data-size.html
在这里插入图片描述
因此,在你的业务不需要存储NULL值得时候,建议把所有字段设为NOT NULL 并设置默认值。

7)隐式转换导致索引失效

因为目前没有这样的表,没法演示,大家可以自行实验一下。

解决办法:创建索引时尽量规范一些,比如统一使用int或者bigint。

索引调优技巧

这部分内容主要分享一些比较实用的索引调优技巧。主要包括:

  • 长字段的索引调优
  • 使用组合索引的技巧
  • 覆盖索引
  • 排序优化
  • 冗余重复索引优化
长字段索引调优

实际项目中,我们可能需要很长的索引字符串字段。
如果索引长度很大,那么就会导致索引占用空间很大,作用在超字段上的索引,查询效率也是不高的,那么如何优化呢?

第一种做法就是额外创建一个字段,可以存储这个长字段所能代表的值。比如它的hashCode…
这个额外创建的字段应该具备一下要求:

  1. 该字段长度应该比较小,SHA1/MD5是不合适的。
  2. 应当尽量避免hash冲突,就目前来说,流行使用CRC32()或者FNV64()

比如user_info 表里nickname是一个很大的值,我们就可以创建nickname_hash作为索引。对应的sql就可以改造为:

select * from user_info 
where nickname_hash = CRC32('Ant')
	and nickname = 'Ant'

这样直接可以在nickname_hash字段上添加索引就可以了,之所以查询依然带上 nickname = ‘Ant’ 这样的条件,是为了让sql在Hash冲突的时候也能正确的返回结果。

但是这种优化的方案,对于模糊查询是无能为力的,因为我们是对nickname的完整值进行Hash的,所以like查询没办法用这种方式优化。
如果我们依然希望索引比较小,该怎么办呢?

mysql支持前缀索引
创建前缀索引的语法是这样的:

alter table user_info add key (nickname(5)) 

这样就表示取nickname的前5个值作为索引。但问题是这个数字写多少更合适呢?我们希望的是这个数字尽可能小,因为这样可以尽可能减少空间也能提升性能,同时我们也希望这个索引的选择性足够高。

在这里,给大家介绍一下索引的选择性公式:
索引选择性 = 不重复的索引值/数据表的总记录数
得到的结果数值越大,表示选择性越高,性能越好。

我们可以这样玩:
在这里插入图片描述
这样得出来的值就是完整列的选择性了,那么这个值就是这个字段的最大选择性了。

然后我们再这样:
选择一个测试的值为3,得出的结果是0.5737
在这里插入图片描述

再试试传5,计算出的是0.6669
在这里插入图片描述
再传6,计算出的是0.6787
可以看出到5后,是已经接近最大值了,且再次递增变化不大,因此经测试可以得出,选择5是比较合理的。

经过分析,我们可以发现,前缀索引可以让我们的表更加高效,而且对应用是透明的,我们的应用不需要做任何改造。使用的成本也是比较低的。那么这是一种比较容易落地的优化方案。但是他也有局限性,就是无法做order by、group by;无法使用覆盖索引。

单列索引 vs 组合索引

我们再来探讨一下单列索引与组合索引,对于查询的执行差异。

//todo 玩命更新中…

猜你喜欢

转载自blog.csdn.net/qq_45455361/article/details/121021997#comments_24617409