MySQL高级
如果你是一名开发者,数据库将伴随你的整个职业生涯。掌握SQL写出高效易用的SQL已成为开发者的必备技能。因为整个应用系统离不开数据库,应用系统是否流畅(数据量达到一定程度时),很大一部分也是由SQL语句决定的。
工作后才发现原来SQL可以动不动就可以几十行,书写起来很灵活,可以满足各种各样的业务场景。当数据量达到几千万,没有添加合适的索引、查询时总是在全表扫描。。。查询时会越来越慢,最终会导致数据库服务奔溃,对于应用来说这是一个很恐怖的存在。
所以学好SQL、学会分析SQL执行计划并以及优化。去提升自己的核心竞争力已成为重中之重。
1、MySQL简介与安装
1.1、简介
MySQL
是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下公司。MySQL 最流行的关系型数据库管理系统,在 WEB 应用方面MySQL是最好的 RDBMS
(Relational Database Management System,关系数据库管理系统) 应用软件之一。
需要注意的是:mysql
就是一个基于socket编写的C/S架构的软件。
1.2、Linux安装MySQL
下载地址:https://downloads.mysql.com/archives/community/
这里我们主要以rpm
(redhat package manager)包管理器进行管理
1、检查当前系统是否安装过MySQL
# 查询命令
[root@laizhenghua /]# rpm -qa|grep -i mysql
[root@laizhenghua /]# rpm -qa|grep mariadb
mariadb-libs-5.5.68-1.el7.x86_64
# 我们发现Linux自动帮我们安装了mariadb,需要把mariadb卸载,才能成功安装MySQL
# 强制卸载
[root@laizhenghua /]# rpm -e --nodeps mariadb-libs-5.5.68-1.el7.x86_64
下载软件包
2、安装MySQL
服务以及客户端
# 安装MySQL服务
[root@laizhenghua opt]# rpm -ivh MySQL-server-5.5.60-1.el7.x86_64.rpm
# 安装MySQL客户端
[root@laizhenghua opt]# rpm -ivh MySQL-client-5.5.60-1.el7.x86_64.rpm
注意看提示信息
MySQL安装好后,会像Linux一样默认初始化root
用户并且拥有最高权限,但是需要我们自己配置密码。
[root@laizhenghua opt]# /usr/bin/mysqladmin -u root password 123456
[root@laizhenghua opt]#
如果想要外网客户端软件连接MySQL,还需要配置外网访问权限:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
注意:123456
是root的密码。
3、查看MySQL
安装时创建的MySQL用户和mysql组
[root@laizhenghua opt]# cat /etc/passwd|grep mysql
mysql:x:995:991:MySQL server:/var/lib/mysql:/bin/bash
[root@laizhenghua opt]# ps -ef|grep mysql
root 30476 13546 0 23:09 pts/1 00:00:00 grep --color=auto mysql
4、MySQL
启动与停止
[root@laizhenghua opt]# systemctl start mysql
# 再次查询是否启动成功
[root@laizhenghua opt]# ps -ef|grep mysql
...
# 停止mysql服务
[root@laizhenghua opt]# systemctl stop mysql
MySQL服务启动成功后,我们就可以使用mysql
命令进入MySQL monitor(监控器)
:
[root@laizhenghua opt]# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.5.60 MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
设置开机自启动:
[root@laizhenghua /]# chkconfig mysql on
[root@laizhenghua /]#
1.3、MySQL的Linux安装位置
我们都知道windows
下安装好的MySQL,会有如下的目录结构,其中data
目录是用于存放数据,我们所建的数据表和添加的数据都会以.opt
文件的形式保存在硬盘中,那么在Linux
系统中又是怎样保存数据的呢?MySQL的安装位置又是在哪里?
在Linux
系统中,我们常常以ps -ef|grep mysql
命令查看MySQL
的路径信息,如:
[root@laizhenghua mysql]# ps -ef|grep mysql
root 22050 1 0 9月09 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/laizhenghua.pid
mysql 22137 22050 0 9月09 ? 00:00:59 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --user=mysql --log-error=laizhenghua.err --pid-file=/var/lib/mysql/laizhenghua.pid
root 28563 15579 0 23:23 pts/0 00:00:00 grep --color=auto mysql
相关说明:
路径 | 解释 | 备注 |
---|---|---|
/var/lib/mysql | MySQL数据库文件的存放路径 | /var/lib/mysql/laizhenghua.pid |
/usr/share/mysql | 配置文件目录 | mysql.server 命令及配置文件 |
/usr/bin | 相关命令目录 | mysqladmin mysqldump等命令 |
/etc/init.d/mysql | 启停相关脚本 |
修改配置文件位置:
[root@laizhenghua mysql]# pwd
/usr/share/mysql
# 停止MySQL
[root@laizhenghua mysql]# systemctl stop mysql
# 拷贝配置文件
[root@laizhenghua mysql]# cp my-huge.cnf /etc/my.cnf
注意:我们一般以/etc/my.cnf
作为MySQL的配置文件,my-huge.cnf
文件是MySQL5.5自带的。如果您安装的MySQL没有这个配置文件,则需要找到对应版本的默认配置文件。如5.6版本默认配置文件是/usr/share/mysql/my-default.cnf
。
修改字符集和数据存储路径:
为避免出现中文乱码,我们还需要对MySQL的默认字符集进行修改
# 查看字符集
mysql> show variables like '%char%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | latin1 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | latin1 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
# 默认的是客户端和服务器都用了latin1,所以我们要修改为utf-8
修改默认字符集(vi /etc/my.cnf
):
[client]
#password = your_password
port = 3306
socket = /var/lib/mysql/mysql.sock
default-character-set=utf8
# The MySQL server
[mysqld]
port = 3306
socket = /var/lib/mysql/mysql.sock
skip-external-locking
character_set_server=utf8
character_set_client=utf8
collation-server=utf8_general_ci
[mysql]
no-auto-rehash
default-character-set=utf8
# Remove the next comment character if you are not familiar with SQL
#safe-updates
1.4、mysql主要配置文件
二进制日志log-bin:主要用于主从复制
错误日志log-error:默认是关闭的,记录严重的警告和错误信息,每次启动和关闭的详细信息等。
查询日志log:默认关闭,记录查询的sql语句,如果开启会降低mysql的整体性能,因为记录日志也是需要消耗系统资源的。
数据文件(两系统):
- windows:
E:\mysql\mysql-5.7.30-winx64\data
目录下可以挑选很多库 - linux:默认路径是
/var/lib/mysql
其他配置文件:
- frm文件:存放表结构
- myd文件:存放表数据
- myi文件:存放表索引
如何配置:
- windows:my.ini文件
- Linux:/etc/my.cnf文件
2、MySQL逻辑架构
2.1、4层分类
和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离
。这种架构可以根据业务的需求和时机需要选择合适的存储引擎。
逻辑架构图:
从上到下划分为:
1、连接层⭐️
最上层是一些客户端和连接服务,包含本地sock
通信和大多数基于客户端/服务端工具实现的类似于tcp/ip
的通信。主要完成一些类似于连接处理、授权认证、以及相关的安全方案。在该层上引入线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
2、服务层⭐️
第二层架构主要完成核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序,是否利用索引等,最后生成相应的执行操作。如果是select
语句,服务器还会查询内部的缓存,如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。
3、引擎层⭐️
存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。后面会涉及到MylSAM
和InnoDB
4、存储层⭐️
数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎交互。
2.2、存储引擎简介
前面我们已经知道引擎层位于第三层,并且不同的存储引擎有着不同的功能。那么它们的查看方式和特点又是什么呢?
1、使用命令查看存储引擎信息
# 查看MySQL现在可以提供的所有存储引擎
SHOW ENGINES;
结果如下
我们重点关注常用的MyISAM
和InnoDB
:
查看MySQL当前默认的存储引擎:
mysql> show variables like '%storage_engine%';
+------------------------+--------+
| Variable_name | Value |
+------------------------+--------+
| default_storage_engine | InnoDB |
| storage_engine | InnoDB |
+------------------------+--------+
2 rows in set (0.00 sec)
3、索引优化分析
3.1、SQL机读顺序
导致慢SQL(执行时间长、等待时间长)
的原因可能是:
- 查询语句写的烂
- 索引失效(单值索引和复合索引)
- 关联查询太多join(设计缺陷或不得已的需求)
- 服务器调优各个参数设置(缓冲、线程数等)
以上几点都有可能导致SQL性能下降,我们优化SQL时也是从这几个方向出发。在做优化之前我们先要了解SQL的执行顺序。例如有这样一段SQL:
SELECT DISTINCT
<select_list>
FROM
<left_table>
JOIN <right_table> ON <join_condition>
WHERE
<where_condition>
GROUP BY
<group_by_list>
HAVING
<having_condition>
ORDER BY
<order_by_condition>
LIMIT <limit number>
然而MySQL、Oracle等传统的关系型数据库执行时,加载的顺序可不是这样的,也就是机读顺序
:
FROM
<left_table>
ON <join_condition>
<join_type> JOIN <right_table>
WHERE
<where_condition>
GROUP BY
<group_by_list>
SELECT
DISTINCT
<select_list>
ORDER BY
<order_by_condition>
LIMIT <limit number>
总结下来,SQL的解析流程如下图所示
3.2、7种JOIN连接理论
第1,2种:LEFT JOIN
(左)连接
SELECT <select_list>
FROM table_a a
LEFT JOIN table_b b ON a.key = b.key
SELECT <select_list>
FROM table_a a
LEFT JOIN table_b b ON a.key = b.key WHERE b.key IS NULL
第3,4种:RIGHT JOIN
(右)连接
SELECT <select_list>
FROM table_a a
LEFT JOIN table_b b ON a.key = b.key
SELECT <select_list>
FROM table_a a
LEFT JOIN table_b b ON a.key = b.key WHERE a.key IS NULL
第5种:INNER JOIN
(内)连接
SELECT <select_list>
FROM table_a a
INNER JOIN table_b b ON a.key = b.key
第6,7种:OUTER JOIN
(外)连接
SELECT <select_list>
FROM table_a a
FULL OUTER JOIN table_b b ON a.key = b.key
# 注意MySQL不支持FULL关键字,所以MySQL中的写法是
SELECT <select_list> FROM table_a a LEFT JOIN table_b b on a.key = b.key;
UNION
SELECT <select_list> FROM table_a a RIGHT JOIN table_b b on a.key = b.key;
SELECT <select_list>
FROM table_a a
FULL OUTER JOIN table_b b ON a.key = b.key
WHERE a.key IS NULL OR b.key IS NULL
# 注意MySQL不支持FULL关键字,所以MySQL中的写法是
SELECT <select_list> FROM table_a a LEFT JOIN table_b b on a.key = b.key WHERE b.key IS NULL;
UNION
SELECT <select_list> FROM table_a a RIGHT JOIN table_b b on a.key = b.key WHERE a.key IS NULL;
3.3、索引简介
⭐️1、索引是什么 ?
索引是一种数据结构!当我们给某个字段添加索引后,这个字段在硬盘中的排列方式也会随之改变。具体的排列方式由索引类型决定!
然而官方对索引的定义是:索引(Index)
是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构。
首先需要明确知道索引的目的在于提高查询条件。我们可以简单的理解为:排好序的快速查找数据结构
以B树
为例:
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据。这样就可以在这些数据结构上实现高级查找算法。这种数据结构就是索引
。如B+数索引:
最左边是数据记录的物理地址,为了加快Col2的查找,可以维护一个右边所示的二叉树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉树找在一定复杂度内获取到相应数据,从而快速检索出符合条件的记录。
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。我们平常所说的索引,如果没有特别指明,都是B树(多路搜索树,并不一定是二叉树)结构组织的索引。其中聚集索引、次要索引、复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种类型的索引之外还有哈希(hash index)索引等。
⭐️2、索引的优势与劣势
优势:
- 类似大学图书馆建书目录索引,
提高数据检索的效率,降低数据库的IO成本
。 - 通过索引列对数据进行排序,
降低数据排序成本,降低了CPU的消耗
。
劣势:
- 实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的。
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行
INSERT、UPDATE和DELETE
。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。 - 索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或者优化查询。
⭐️3、MySQL索引的分类
单值索引
:即一个索引只包含单个列,一个表可以有多个单列索引。唯一索引
:索引列的值必须唯一,但允许有空值。复合索引
:即一个索引包含多个列。
⭐️4、MySQL索引结构
- BTree索引(最常见、最常用)
- Hash索引
- full-text全文索引
- R-Tree索引
3.4、索引的基本语法
⭐️1、索引的创建
CREATE [UNIQUE] INDEX indexName ON tableName(columnName(length));
# 或者是
ALTER tableName ADD [UNIQUE] INDEX [indexName] ON (columnName(length));
温馨提示:如果表中有大量的数据,我们不能直接去创建索引,直接建立索引可能会引起各种各样的问题,特别是以上线的应用。正确的建法是先创建一张临时表(使用建表脚本获like语法),然后给数据为空的临时表添加合适的索引,建好索引后,在把数据全部插入临时表,最后把临时表重名即可。
⭐️2、查看索引
SHOW INDEX FROM tableName;
⭐️3、删除索引
DROP INDEX [indexName] ON tableName;
3.5、索引检索原理
以BTree
索引为例,
⭐️初始化说明:
一颗b+树
浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示)。如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。
真实的数据存在于叶子节点
,即3、5、9、10、13、15、28、29、36、60、75、79、90、99。
非叶子节点不存储真实的数据,只存储指引搜索方向的数据项
,如17、35并不是真实存在于数据表中。
⭐️查找过程:
基于图数据结构假设查找的数据项是29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO
,在内存中用二分法查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3有磁盘加载到内存中,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分法查找找到29,结束查询,总计三次IO。
真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本是非常非常的高。
3.6、索引建立场景分析
言外之意我们需要明白,那些情况下可以建立索引,那些情况下是不能够建立索引。只有添加了合适的索引才能满足我们的需求!否则从占用空间和影响更新角度带来更多的问题。
⭐️那些情况需要建立索引:
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引,如银行账号、手机号等
- 查询中与其它表关联的字段,外键关系建立索引
- 频繁更新的字段不适合创建索引
- where条件里用不到的字段不创建索引
- 单键/组合索引的选择问题(在高并发下倾向创建组合索引)
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组字段
⭐️那些情况不合适建立索引:
- 表记录太少
- 经常增删改的表
- 数据重复且分布平均的表字段,不适合建立索引在MySQL中,因此应该只为最经常查询和最经常排序的数据列建立索引。注意:如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。
⭐️扩展(索引的选择性):
索引的选择性是指索引列中不同的数目与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99
。一个索引的选择性越接近与1,这个索引的效率就越高。
3.7、SQL性能分析
⭐️1、MySQL Query Optimizer
MySQL
中有专门负责优化SELECT
语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query
提供他认为最优的执行计划(它认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分也是最耗费时间)
当客户端向MySQL
请求一条Query
,命令解析器模块完成请求分类,区别出是select
并转发给MySQL Query Optimizer
时,MySQL Query Optimizer
首先会对整条Query
进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query
中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等,然后分析Query
中的Hint信息
(如果有),看显示Hint信息
是否可以完成确定该Query
的执行计划、如果没有Hint或Hint信息
还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query
进行相应的计算分析,然后再得出最后的执行计划。
⭐️2、MySQL常见瓶颈
CPU
:cpu在饱和的时候一般发生在数据转入内存或从磁盘上读取数据时候IO
:磁盘I/O瓶颈发生在装入数据远大于内存容量的时候- 服务器硬件的性能瓶颈:
top、free、iostat和vmstat
来查看系统的性能状态
⭐️3、Explain语句
上面我们已知道,MySQL客观存在的性能瓶颈,这些瓶颈都有可能导致SQL性能下降。但是只知道瓶颈并不能去解决实际问题,我们需要找出具体导致SQL缓慢的原因。MySQL中Explain
语句可以模拟优化器执行SQL的查询语句,从而知道MySQL是如何处理SQL语句的。这样就能向检测报告一样分析出我们书写的查询语句或是表结构的性能瓶颈。
Explain
语句:查看SQL执行计划的语句。
具体能干什么呢?
- 表的读取顺序
- 数据读取操作的操作类型
- 那些索引可以使用
- 那些索引被实际使用
- 表之间的引用
- 每张表有多少行被优化器查询
如何使用?
# explain + sql语句,如
EXPLAIN SELECT * FROM hp_hotel.tb_house
输出结果(执行计划信息):
字段解释:
⭐️id
(得出表的读取和加载顺序)
- id相同:执行顺序由上至上
- id不相同:如果是子查询,id的序号会递增,
id值越大优先级越高,越先被执行
- id相同与不相同,同时存在:id入股相同,可以认为是一组,从上往下顺序执行,在所有组中,id值越大,优先级越高,越先执行。
如(derived 是衍生的意思):
⭐️select_type
(数据读取操作的操作类型)总共有6种
SIMPLE
:简单的select查询,查询中不包含子查询或者UNIONPRIMARY
:查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY(最后加载)SUBQUERY
:在select或where列表中包含了子查询DERIVED
:在from列表中包含的子查询被标记为derived(衍生)
MySQL会递归执行这些子查询,把结果放在临时表里。UNION
:若第二个select出现在UNION之后,则被标记为UNION,若UNION包含在from子句的子查询中,外层select将被标记为DERIVED。UNION RESULT
:从UNION表获取结果的SELECT
⭐️type
(显示MySQL访问类型)
- type显示的是访问类型,是较为重要的一个指标
- 访问类型排列,常见的类型有
system / const / eq_ref / ref / range / index / ALL
- 显示查询使用了何种类型,从最好到最差依次是:
system > const > eq_ref > ref > range > index > ALL
type(类型) | 含义 |
---|---|
system | 表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,可以忽略不记 |
const | 表示通过索引一次就找到了,const 用于比较primary key 或者unique 索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量 |
eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描 |
ref | 非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。 |
range | 只检索给定范围的行,使用一个索引来选择行。key列显示使用了那个索引,一般就是在你的where语句中出现了between、<、>、in 等查询。这种范围索引索引扫描比全表扫描要好,因为它只需要开始与索引的某一点,而结束语另一点,不用全部索引。 |
index | Full Index Scan ,index与ALL区别为index类型只遍历索引树,这通常比ALL块,因为索引文件通常比数据文件小。也就是说虽然ALL和Index都是读全表,但是index是从索引中读取的,而ALL是从硬盘中读取。如select id from tb |
ALL | Full Table Scan,全表扫描 |
备注:一般来说,得保证查询至少达到range
级别,最好能达到ref
⭐️possible_keys
(那些索引可以使用)
- 显示可能应用在这张表中的索引,一个或多个
- 查询涉及到的字段上,若存在索引,则该索引将被列出,但不一定被查询实际使用
⭐️key
(那些索引被实际使用)
- 实际使用的索引。如果为null,则没有使用索引
- 查询中若使用了覆盖索引,则该索引仅出现在key列表中
⭐️key_len
- 表示索引中使用的字节数,可通过该列计算查询中使用的索引长度。在不损失精确性的情况下,长度越短越好
- key_len显示的值为索引字段的最大可能长度,
并非实际使用长度
,即key_len是根据表定义计算而得,不是通过表内检索出的
⭐️ref
- 显示索引的那一列被使用了,如果可能的话,是一个常数,哪些列或常量被用于查找索引列上的值
⭐️rows
- 根据表统计信息即索引选用情况,大致估算出找到所需对的记录要读取的行数
⭐️Extra
- 包含不合适在其他列中显示但是十分重要的额外信息,可能会出现的信息如下
Using filesort
:说明MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为文件排序
Using temporary
:使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序order by
和分组查询group by
。(实际SQL中我们应该避免出现Using temporary)Using Index
:表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!如果同时出现了using where
,表明索引被用来执行索引键值的的查找。如果没有同时出现using where
,表明索引用来读取数据而非执行查找动作。- 等等还有其他的信息,可以自己查下。
扩展:索引覆盖(Covering Index)
就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖
。
3.8、索引优化实战
前面呢,我们学习了索引优化的理论知识包括explain
语法以及输出结果字段含义等等。空有理论而没有实战,是学不会的,在今后我们要勤写SQL多实践,只有不断的打磨才能真正理解与掌握索引,写出高效易用的SQL。
实验建表SQL(单表):
CREATE TABLE IF NOT EXISTS `article`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT (10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL ,
`views` INT(10) UNSIGNED NOT NULL ,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL
);
INSERT INTO `article`(`author_id`,`category_id` ,`views` ,`comments` ,`title` ,`content` )VALUES
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(3,1,3,3,'3','3');
SELECT * FROM ARTICLE;
⭐️查询 category_id 为 1 且 comments 大于 1 的情况下,views 最多的 article_id
# 初始版本
select a.id from db_test.article a where a.category_id = 1 and a.comments > 0 order by a.views desc limit 1;
explain select a.id from db_test.article a where a.category_id = 1 and a.comments > 1 order by a.views desc limit 1;
查看执行计划:
我们发现,type是ALL
是最坏的情况。Extra里还出现了Using filesort
也是最坏的情况,优化是必须的。
# 查看此表的索引
show index from db_test.article;
# 目前是没有任何索引
优化1:建立复合索引(三个字段组合,comments字段在中间)
create index category_id_comments_views on db_test.article(category_id, comments, views);
show index from db_test.article;
此种建立索引虽然可以解决掉全表扫描,但任然存在Using filesort
。
BTree
索引工作原理:
先排序category_id
,如果遇到相同的category_id
,则再排序comments
,如果遇到相同的comments
则再排序views。当comments
字段在联合索引里处于中间位置时,因comments > 1是一个范围值,MySQL无法利用索引再对后面的views部分进行检索,即range类型查询字段后面的索引无效。
优化2:建立复合索引(去掉comments)
drop index category_id_comments_views on db_test.article;
create index category_id_views on db_test.article(category_id, views);
可以看到type变为ref,Extra中的Using filesort也消失了,结果非常理想。
实验建表SQL(双表):
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
CREATE TABLE IF NOT EXISTS `book`(
`bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
⭐️左连接:
explain select * from book b left join class c on c.card = b.card;
-- 结论就是type = ALL。此时book表和class表都有card字段,那么我们应该给那边添加索引呢?
直接说答案,索引加在右最合适!可以自行验证一下(分别加在左表和右表)
alter table class add index card_index (card);
show index from class;
再次查看执行计划:
看一看到第二行 type 变为了ref,rows优化比较明显(比较加在左表)。这是由左连接特性决定的。left join 条件确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定要建立索引。
⭐️右连接:
explain select * from book b right join class c on c.card = b.card;
与左连接同理,此时把索引建在左最合适!
drop index card_index on class;
alter table book add index card_index (card);
关于左右连接记住口诀:索引相反建!
实验建表SQL:
CREATE TABLE IF NOT EXISTS `phone`(
`phoneid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
)ENGINE = INNODB;
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
此时我们有三个表分别是class、book、phone,并且都以card字段进行关联,那么使用索引优化SQL是又该如何选择呢??
explain select * from book b
left join class c on c.card = b.card
left join phone p on p.card = c.card
已左连接为例,索引相反建!建在与之关联的右表即可。
alter table class add index c_index (card);
alter table phone add index p_index (card);
JOIN
语句的优化结论:
- 尽可能减少
JOIN
语句中的NestedLoop
(嵌套循环连接)的循环总次数,永远用小结果集驱动大的结果集。 - 优先优化
NestedLoop
的内层循环。 - 当无法保证被驱动表的
JOIN
条件字段被索引且内存资源充足的前提下,不要太吝惜JoinBuffer
的设置(my.cnf)。
关于嵌套循环连接的理解:
select * from t1 inner join t2 on t1.id = t2.tid;
- t1 称为外层表,也可称为驱动表。
- t2 称为内层表,也可称为被驱动表。
3.9、索引失效场景分析
实验建表SQL:
CREATE TABLE staffs(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(24)NOT NULL DEFAULT'' COMMENT'姓名',
`age` INT NOT NULL DEFAULT 0 COMMENT'年龄',
`pos` VARCHAR(20) NOT NULL DEFAULT'' COMMENT'职位',
`add_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT'入职时间'
)CHARSET utf8 COMMENT'员工记录表';
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('z3',22,'manager',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('July',23,'dev',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('2000',23,'dev',NOW());
ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(`name`,`age`,`pos`)
⭐️1、最佳左前缀法则
先来看案例,注意复合索引中name是靠前的。
-- 所建索引为:ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(`name`,`age`,`pos`)
-- 命中索引情况
explain select * from staffs s where s.name = 'July';
explain select * from staffs s where s.name = 'July' and s.age = 23 and s.pos = 'dev';
-- 不会命中索引情况
explain select * from staffs s where s.age = 23;
explain select * from staffs s where s.pos = 'dev';
explain select * from staffs s where s.age = 23 and s.pos = 'dev';
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。
简单理解(带头大哥不能死,中间兄弟不能断
):靠前索引犹如火车头,靠后索引犹如火车厢。单独有火车头可以跑,并且火车头可以带着车厢跑,而车厢没有火车头则不能跑!最左前缀法则就是这个原理。
⭐️2、不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
-- 命中索引
explain select * from staffs s where s.name = 'July'
-- 索引失效
explain select * from staffs s where left(s.name, 4) = 'July'
口诀:索引列上少计算。
⭐️3、存储引擎不能使用索引中范围条件右边的列
-- 命中索引
explain select * from staffs s where s.name = 'z3' and s.age = '22' and s.pos = 'manager';
-- 索引局部失效
explain select * from staffs s where s.name = 'z3' and s.age > 20 and s.pos = 'manager';
⭐️4、尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少 select *
-- 使用 select *
explain select * from staffs s where s.name = 'z3' and s.age = '22' and s.pos = 'manager';
-- 未使用 select * ,索引列和查询列一致
explain select s.name, s.age, s.pos from staffs s where s.name = 'z3' and s.age = '22' and s.pos = 'manager';
⭐️5、MySQL在使用不等于(!= 或者 <>)的时候无法使用索引会导致全表扫描
-- 命中索引
explain select * from staffs s where s.name = 'July'
-- 索引失效(mysql 8.0做了优化,不再全表扫描)
explain select * from staffs s where s.name != 'July'
explain select * from staffs s where s.name <> 'July'
我们发现还是命中了索引!只是type
由ref变为了range。
⭐️6、is null、is not null 也无法使用索引
-- 索引失效
explain select * from staffs s where s.name is null; -- 极端
explain select * from staffs s where s.name is not null;
⭐️7、like以通配符开头 ('%abc')
mysql索引失效会变成全表扫描的操作
-- 命中索引
explain select * from staffs s where s.name like 'Ju%'
-- 索引失效
explain select * from staffs s where s.name like '%Ju'
口诀:%like
加右边!
然而我们在实际开发中like
的用法是左右两边都加百分号,例如:'%name%'
。这样就会导致索引失效!那么如何正确使用like语句呢?答:覆盖索引
CREATE TABLE `tbl_user`(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) DEFAULT NULL,
`age`INT(11) DEFAULT NULL,
`email` VARCHAR(20) DEFAULT NULL,
PRIMARY KEY(`id`)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('1aa1',21,'a@163.com');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('2bb2',23,'b@163.com');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('3cc3',24,'c@163.com');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('4dd4',26,'d@163.com');
深刻理解覆盖索引
-- 没加任何索引前
explain select * from tbl_user u where u.name like '%aa%';
explain select u.name, u.age from tbl_user u where u.name like '%aa%';
-- 结论就是通通全部全表扫描
-- 给namen,age字段建立复合索引
create index name_age_index on tbl_user(name,age);
-- 在此查看执行计划
explain select * from tbl_user u where u.name like '%aa%'; -- 全表扫描
explain select u.name, u.age from tbl_user u where u.name like '%aa%'; -- 命中索引
注意查看type
和key
字段:
-- 命中索引情况
explain select u.name from tbl_user u where u.name like '%aa%';
explain select u.age from tbl_user u where u.name like '%aa%';
explain select u.id, u.name, u.age from tbl_user u where u.name like '%aa%';
-- 索引失效情况
explain select * from tbl_user u where u.name like '%aa%';
explain select u.id, u.name, u.age, u.email from tbl_user u where u.name like '%aa%';
总结:如果模糊查询两边都有%
,如%aa%
适合使用覆盖索引避免索引失效。所谓覆盖索引就是:所建的索引和查询字段数量保持一致。口诀:覆盖索引不写*
⭐️8、字符串不加单引号索引失效
-- 在staffs表中,有 name=2000 的记录
-- 命中索引
explain select * from staffs s where s.name = '2000'
-- 索引失效(失效原因:MySQL自动做了隐式转换)
explain select * from staffs s where s.name = 2000
⭐️9、少用or,用它来连接时会导致索引失效
explain select * from staffs s where s.name = 'July' or s.name = 'za';
索引并没有失效,可能是MySQL8.0做了优化!但是type变为了range。
4、索引面试题
4.1、简单案例分析
实验建表SQL:
create table test03(
id int primary key not null auto_increment,
c1 char(10),
c2 char(10),
c3 char(10),
c4 char(10),
c5 char(10));
insert into test03(c1,c2,c3,c4,c5) values ('a1','a2','a3','a4','a5');
insert into test03(c1,c2,c3,c4,c5) values ('b1','b2','b3','b4','b5');
insert into test03(c1,c2,c3,c4,c5) values ('c1','c2','c3','c4','c5');
insert into test03(c1,c2,c3,c4,c5) values ('d1','d2','d3','d4','d5');
insert into test03(c1,c2,c3,c4,c5) values ('e1','e2','e3','e4','e5');
-- 建立复合索引
create index idx_test03_c1234 on test03(c1,c2,c3,c4);
⭐️问题:我们建了复合索引idx_test03_c1234
,根据以下SQL分析索引使用情况?
explain select * from test03 t where t.c1 = 'a1';
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2';
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' and t.c3 = 'a3';
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' and t.c3 = 'a3' and t.c4 = 'a4';
-- 我们发现带头大哥跑到最后一个去了
explain select * from test03 t where t.c4 = 'a4' and t.c3 = 'a3' and t.c2 = 'a2' and t.c1 = 'a1';
-- 这种情况:MySQL服务层Optimizer会自动进行优化,也会命中索引
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' and t.c3 > 'a3' and t.c4 = 'a4'; -- 范围之后全失效
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' and t.c4 > 'a4' and t.c3 = 'a3';
-- 索引用于排序情况
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' and t.c4 = 'a4' order by c3;
-- c3 的作用在于排序,不在于查找
explain select * from test03 t where t.c1 = 'a1' and t.c2 = 'a2' order by t.c4;
-- 会出现 Using filesort
-- 解释:我们建的索引是c1 c2 c3 c4,然而这条SQL以常量的形式命中了c1和c2字段,出现c3断层,MySQL为了返回c4排序结果,自己内部做了排序
explain select * from test03 t where t.c1 = 'a1' and t.c5 = 'a5' order by c2,c3;
-- 只有c1一个字段索引,但是c2、c3用于排序 ,无Using filesort
explain select * from test03 t where t.c1 = 'a1' and t.c5 = 'a5' order by c3,c2;
-- 出现了Using filesort,我们建的索引是1234,它没有按照顺序来,3 2颠倒了
explain select max(t.id) from test03 t where t.c1 = 'a1' and t.c4 = 'a4' group by t.c3, t.c2;
-- 产生临时表(Using temporary)
总结:
- 定值、范围还是排序,一般order by是给个范围
group by
基本上都是需要进行排序,会有临时表产生
⭐️一般性建议:
- 对于单键索引,尽量选择针对当前
Query
过滤性更好的索引。 - 在选择组合索引的时候,当前
Query
中过滤性最好的字段在索引字段顺序中,位置越靠前(左)越好。 - 在选择组合索引的时候,尽量选择可以能够包含当前
Query
中的where
语句中更多字段的索引。 - 尽可能通过分析统计信息和调整
Query
的写法来达到选择合适索引的目的。
4.2、优化总结口诀
全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
Like百分加右边,覆盖索引不写*
;
不等空值还有or,索引失效要少用;
优化SQL流程总结:
/*
1.观察,至少跑一天,看看生产环境的慢SQL情况
2.开启慢查询日志,设置阈值,比如超过5秒钟的就是慢SQL,并将它定位出来
3.explain + 慢SQL分析
4.show profile(查询SQL在MySQL服务器里面的执行细节和生命周期函数)
5.SQL服务器参数调优
*/
5、查询截取分析
5.1、小表驱动大表
我们都知道程序访问数据库时,都要做Connection
连接,Connection
连接资源是非常有限的,用完后必须马上释放,如果Connection
不能及时正确的关闭将导致系统宕机。对于Connection
连接耗时耗资源的一个过程,也是会导致SQL性能下降的一个原因。在特定的场景我们可以减少Connection
的连接次数来达到提高MySQL性能的效果。如:
// 假设每次循环都要进行一次 Connection 连接
for (int i = 0; i < 10000; i++) {
// 进行 10000 次 Connection 连接
for (int j = 0; j < 5; j++) {
...
}
}
for (int i = 0; i < 5; i++) {
// 进行 5 次 Connection 连接
for (int j = 0; j < 10000; j++) {
...
}
}
// 第二种访问数据库的方式性能要远大于第一种
因此我们得出一个结论:小表驱动大表,即小的数据集驱动大的数据集,可以有效提高数据库性能。
⭐️探讨in
与exists
:
select * from A a where a.id in (select b.id from B b);
-- 等价于
for select b.id from B b
for select * from A a where a.id = b.id
-- 使用 in 还是 exists 是 语句后面的数据集决定的
-- 如以上例子当B表的数据小于A表的数据集时,用 in 优于 exists,反之当B表的数据大于A表的数据集时,用 exists 优于 in
select * from A a where exists (select 1 from B b where b.id = a.id)
-- 等价于
for select * from A a
for select * from B b where B.id = A.id
⭐️关于exists
语法:
select ... from table where exists (subquery);
-- EXISTS(subquery)只返回TRUE或FALSE,因此子查询中的SELECT * 也可以是SELECT 1 或 SELECT 'X',官方说法是实际执行时会忽略SELECT清单,因此没有区别
-- 将主查询的数据,放到子查询中做条件验证,根据验证结果(true 或 false)来决定主查询的数据结果是否得以保留。
5.2、order by 关键字优化
实验建表SQL:
create table tblA(
#id int primary key not null auto_increment,
age int,
birth timestamp not null
);
insert into tblA(age, birth) values(22, now());
insert into tblA(age, birth) values(23, now());
insert into tblA(age, birth) values(24, now());
create index idx_A_ageBirth on tblA(age, birth);
select * from tblA;
查看索引:
show index from tblA;
MySQL支持两种排序方式,FileSort
和index
,index
效率更高,它指MySQL扫描索引本身完成排序。FileSort
方式效率较低。
记住:MySQL能为排序与查询使用相同的索引。
⭐️order by
子句,尽量使用index(索引)
方式排序,避免使用Filesort
方式排序。
-- 以下语句只关心会不会产生 Using filesort
explain select * from tblA t where t.age > 20 order by t.age; -- 索引带头大哥是age,因此不会产生Using filesort
explain select * from tblA t where t.age > 20 order by t.age,t.birth; -- 索引的顺序是 age birth 不会产生Using filesort
explain select * from tblA t where t.age > 20 order by t.birth; -- 产生Using filesort
explain select * from tblA t where t.age > 20 order by t.birth, t.age; -- 产生Using filesort
结论:order by
满足两种情况,会使用index
方式排序
order by
语句使用索引最左前缀列- 使用
where
子句与order by
子句条件列满足索引最左前列
⭐️因此:尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀。
然而,我们实际需要排序的字段,不可能都建立了索引,也就是排序字段不再索引列上,我们又想提高SQL性能,这种情况又应该怎么做呢?
如果不再索引列上,FileSort
有两种算法:
- 双路排序
- 单路排序
⭐️双路排序(两次 I/O ):
/*
1.MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据
2.读取行指针和 order by 列对它们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应数据输出
3.从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段
*/
要对磁盘进行两次扫描,众所周知,I/O
是很耗时的,所以在MySQL4.1之后,出现了第二种改进的算法,就是单路排序。
⭐️单路排序(一次 I/O ):
/*
1.从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的 列表进行输出。
2.效率比双路排序更高,避免了第二次读取数据,并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为把每一行都保存在内存中
*/
由于单路是后出的,总体而言好过双路,但是使用单路也存在一些问题!
在sort_buffer
中,方法B比方法A要多占用很多空间,因为方法B是把所有字段都取出,所以有可能取出的数据总大小超出了sort_buffer
这种的容量,导致每次只能读取sort_buffer
容量大小的数据,进行排序(创建tmp文件,多路合并),排完在取sort_buffer
容量大小的数据,再排序…从而导致多次I/O
。
本来想省一次I/O
操作,反而导致了大量的I/O
操作,反而得不偿失。
那么我们的优化策略又是什么呢?
MySQL服务器配置文件参数调优(前提是order by
排序字段不在索引列,并且是order by
导致的慢SQL):
⭐️1、order by
时select *
是一个大忌Query需要的字段,这点非常重要,在这里的影响是:
/*
1.当Query的字段大小总和小于max_length_for_sort_data而且排序字段不是TEXT|BLOB类型时,会用改进后的算法--单路排序,否则使用老算法--多路排序
2.两种算法的数据都有可能超出sort_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一下,所以提高sort_buffer_size。
*/
⭐️2、尝试增大sort_buffer_size
参数的设置
/*
1.不管用那种算法,提高这个参数都会提高效率,当然要根据系统的能力去提高,因为这个参是针对每个进程的
*/
⭐️3、尝试增大max_length_for_sort_data
参数的设置
/*
1.提高这个参数,会增加用改进算法的概率,但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是提高磁盘I/O活动和低的处理器使用率。
*/
5.3、gruop by关键字优化
gruop by
关键字的优化策略,几乎与order by
一致:
group by
实质是先排序后进行分组,遵照索引建的最佳左前缀- 当无法使用索引列,尝试增大
max_length_for_sort_data
参数的 设置 + 增大sort_buffer_size
参数的设置 where
高于having
,能写在where
限定的条件就不要写在having
限定了
5.4、慢查询日志
⭐️1、什么是慢查询日志
- MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过
long_query_time
值的SQL,则会被记录到慢查询日志中。 long_query_time
的默认值是10,意思是运行10秒以上的语句。- 由它来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前的explain进行全面分析。
默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。
查看是否开启慢查询日志语句:
show variables like '%slow_query_log%'; -- 输出参数配置和日志文件位置
MySQL8.0默认开启了
如果没有开启,我们可以通过slow_query_log
的值来开启:
set global slow_query_log = 1;
-- 注意以上配置只对当前数据库生效,如果MySQL重启则会失效
如果要永久生效,就必须修改配置文件my.cnf
(其他系统变量也是如此),然后重启MySQL服务器
[mysqld]
slow_query_log = 1
slow_query_log_file = /home/mysql/log/mysql/slow_query.log
⭐️2、那么开启了慢查询日志后,什么样的SQL才会记录到慢查询日志里面呢?
这个是由参数long_query_time
控制也称阈值,默认情况下long_query_time
的值为10秒,查看命令如下:
show variables like 'long_query_time%';
-- 设置阈值
set global long_query_time = 10;
-- 设置好以后,需要重新连接或新开一个会话才能看到修改值
以下结果是阿里MySQL服务:
假如运行时间正好等于long_query_time
的情况,并不会被记录下来。也就是说,在MySQL源码里是判断大于long_query_time
,而非大于等于。
例如我们执行一条SQL:
select sleep(4);
查看日志文件(/home/mysql/log/mysql/slow_query.log
),当然以下阿里云MySQL服务自带可视化
此日志文件会记录超过我们设定的阈值的SQL,拿到SQL后我们需要结合之前的explain进行全面分析,然后给予优化!
6、批量插入数据脚本
往表里插入1000W数据,你会怎么做呢?使用函数?使用存储过程?
由于数据量比较大,不推荐一次性全部插入。推荐分批次插入!
建表SQL(dept,emp):
drop table if exists dept;
create table dept (
id int unsigned primary key auto_increment,
deptno mediumint unsigned not null default 0,
dname varchar(20) not null default "",
loc varchar(13) not null default ""
) engine = innodb default charset=utf8;
select * from dept;
drop table if exists emp;
create table emp (
id int unsigned primary key auto_increment,
empno mediumint unsigned not null default 0,
ename varchar(20) not null default "",
job varchar(9) not null default "",
mgr mediumint unsigned not null default 0,
hiredate date not null,
sal decimal(7, 2) not null,
comm decimal(7, 2) not null,
deptno mediumint not null default 0
) engine = innodb default charset = utf8;
select * from emp;
由于开启过慢查询日志(因为我们开启了bin-log
),为了防止创建函数MySQL报错,我们需要为function
指定一个参数。
-- 查看 log_bin_trust_function_creators 参数是否被开启
show variables like 'log_bin_trust_function_creators';
set global log_bin_trust_function_creators = 1;
-- MySQL重启后,以上配置也会失效,想永久开启,需要修改配置文件
⭐️1、创建函数,保证每条数据都不同(随机产生字符串,随机产生部门编号)
-- 随机产生字符串
DELIMITER $$
CREATE FUNCTION RAND_STRING(N INT) RETURNS VARCHAR(255)
BEGIN
DECLARE CHARS_STR VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
DECLARE RETURN_STR VARCHAR(255) DEFAULT '';
DECLARE I INT DEFAULT 0;
WHILE I < N DO
SET RETURN_STR = CONCAT(RETURN_STR, SUBSTRING(CHARS_STR, FLOOR(1 + RAND() * 52), 1));
SET I = I + 1;
END WHILE;
RETURN RETURN_STR;
END $$
-- 测试函数
SELECT RAND_STRING(2) FROM DUAL;
-- 假如要删除函数
DROP FUNCTION RAND_STRING;
-- 随机产生部门编号
DELIMITER $$
CREATE FUNCTION RAND_NUM() RETURNS INT(5)
BEGIN
DECLARE I INT DEFAULT 0;
SET I = FLOOR(100 + RAND() * 10);
RETURN I;
END $$
⭐️2、创建存储过程,批量插入数的SQL脚本
-- 编写插入 emp 表数据的存储过程
DELIMITER $$
CREATE PROCEDURE INSERT_EMP(IN START INT(10), IN MAX_NUM INT(10))
BEGIN
DECLARE I INT DEFAULT 0;
SET AUTOCOMMIT = 0;
REPEAT
SET I = I + 1;
INSERT INTO EMP(`empno`,`ename`,`job`,`mgr`,`hiredate`,`sal`,`comm`,`deptno`) VALUES((START + I), RAND_STRING(6), 'TEST', 001, CURDATE(),20,400,RAND_NUM());
UNTIL I = MAX_NUM
END REPEAT;
COMMIT;
END $$
-- 编写插入 dept 表数据的存储过程
DELIMITER $$
CREATE PROCEDURE INSERT_DEPT(IN START INT(10), IN MAX_NUM INT(10))
BEGIN
DECLARE I INT DEFAULT 0;
SET AUTOCOMMIT = 0;
REPEAT
SET I = I + 1;
INSERT INTO DEPT(DEPTNO, DNAME, LOC) VALUES((START + I), RAND_STRING(10), RAND_STRING(8));
UNTIL I = MAX_NUM
END REPEAT;
COMMIT;
END $$
⭐️3、调用存储过程
-- 向 dept 表批量插入 10 条数据
DELIMITER ;
SELECT * FROM DEPT; -- 检查数据
CALL INSERT_DEPT(100, 10);
SELECT * FROM DEPT; -- 再次检查数据
-- 向 emp 表批量插入 10 条数据
SELECT * FROM EMP;
CALL INSERT_EMP(100, 10);
SELECT * FROM EMP;
以上就是批量插入数据的SQL脚本,当然如果想插入1000w条数据,只需调用存储过程时,第二个参数改为1000w即可!以后我们批量插入数据时,都可以根据此模板编写插入数据SQL脚本。
7、show profile
除了explain
语句,MySQL中也可以使用show profile
分析当前会话中语句执行的资源消耗情况。可以用于SQL的调优的测量。
但是默认情况下,show profile
参数处于关闭状态,如果是开启状态每次会保存最近15
次的运行结果。
show profile
使用步骤:
⭐️1、查看当前MySQL版本是否支持
show variables like 'profiling';
-- 我的MySQL是开启的,如果 value = OFF,只需执行
set profiling = on;
⭐️2、运行慢SQL进行测试
select 'test', sleep(10) from dual;
show profiles;
运行结果:
⭐️3、诊断指定的SQL
-- 查看SQL执行详细信息(完整生命周期)
show profile cpu, block io for query 3;
执行上面SQL后,我们会得到这个SQL的完整执行过程,如
我们发现,执行结果列出很多参数,我们怎么知道那个参数有问题或导致SQL执行慢?在这里我们主要关注4个参数
:
converting HEAP to MyISAM
:查询结果太大,内存不够用了往磁盘上搬了Creating tmp table
:创建临时表了(拷贝数据到临时表,用完再删除)Copying to tmp table on disk
:把内存中临时表复制到磁盘,危险!!!locked
:锁表
以上4个是导致SQL慢的根本原因,如果出现了都会显示花费了多长时间(这也是诊断SQL的依据),只要出现一个必须得优化!
8、MySQL锁机制
8.1、锁的定义
锁
是计算机协调多个进程或线程并发访问某一项资源的机制!
然而在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤为重要,也更加复杂。
举个生活中的例子:厕所里蹲坑数量是有限的,当你抢到一个蹲坑后,必须把门锁上防止其他人与你共用一个蹲坑!
8.2、锁的分类
从对数据操作类型(读\写)分
- 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
- 写锁(排他锁):当前写操作没有完成前,它不会阻断其他写锁和读锁
从对数据操作的粒度分
- 表锁
- 行锁
⭐️1、读锁
特点:偏向MyISAM
存储引擎,开销小、加锁块、无死锁、锁定粒度大。发生锁冲突的概率最高,并发度最低。
实验建表SQL:
create table mylock (
id int not null primary key auto_increment,
name varchar(20) default ''
) engine myisam;
insert into mylock(name) values('a');
insert into mylock(name) values('b');
insert into mylock(name) values('c');
insert into mylock(name) values('d');
insert into mylock(name) values('e');
select * from mylock;
首先,查看数据库中是否有锁表存在
show open tables;
输出结果里是没有锁表记录存在!
然后我们手动增加一个表锁,用于测试,添加语句如下:
-- 加锁
lock table tableName1 read/write, tableName2 read/write, ...;
-- 释放锁
unlock tables;
-- 如:给mylock表上读锁,给book表上写锁
lock table mylock read, book write;
-- 再次查看是否有锁表
show open tables;
我们发现,已经有锁表存在了,那么此时我们操作这些表又会发送什么呢?(session1中执行的锁表语句)
查询(select * from mylock;
):
- 读锁是共享锁,我们不仅能在当前会话(session1)中执行查询语句,还能在其他会话(session2)中执行查询语句,对于查询语句并没有产生什么影响!可以自行测试一下。
- session1中,不能查询其他没有锁定的表。
更新(update mylock l set l.name = '99' where l.id = 1;
):
- session1中执行失败,报错内容:Table ‘l’ was not locked with LOCK TABLES
- session2中执行会阻塞住,等待着mylock表释放锁!
⭐️2、写锁
-- 给mylock表添加写锁
lock table mylock write;
session1开启写锁后,seesion2再连接终端,然后进行以下测试
查询(select * from mylock;
):
- session1可以执行
- session2查询被锁定的表时被阻塞,等待锁释放
更新(update mylock set name = '999' where id = 1;
):
- session1更新、插入都可以执行
- session2被阻塞,等待锁释放
综合以上实验,对MyISAM表进行操作,会有以下情况(总结):
- 对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其他进程的写操作。
- 对MyISAM表的写操作(加写锁),会阻塞其他进程对同一个表的读和写操作,只有当写锁释放后,才会执行其他进程的读写。
- 总而言之,就会说读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。
8.3、表锁分析
表锁分析我们重点记住一个查询语句:
show status like 'table%';
可以通过检查table_locks_waited
和table_locks_immediate
状态变量来分析系统上的表锁定。
这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量的说明如下:
Table_locks_immediate
:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1;Table_locks_waited
:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况。- 此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。