MySQL数据库调优
- 一、MySQL架构设计
- 1.1、引言
- 1.2、MySQL Server层
- 1.2.1、连接器(Connector)
- 1.2.2、查询缓存 (Query Cache)
- 1.2.3、分析器(Analyzer)
- 1.2.4、优化器(optimizer)
- 1.2.5、执行器(Actuator)
- 1.3、MySQ Engine层
- 1.3.1、什么是存储引擎?
- 1.3.2、mysql支持哪些存储引擎?
- 1.3.3、常见的存储引擎对比
- 1.3.3.1、INnoDB存储引擎
- 1.3.3.2、MyISAM存储引擎
- 1.3.3.3、MEMORY存储引擎
- 1.3.4、存储引擎的选择
- 二、MySQL索引原理&优化
- 2.1、什么是索引?
- 2.1.1、引言
- 2.1.2、索引原理
- 2.1.3、索引分类
- 2.1.3.1、主键索引
- 2.1.3.2、普通索引(单列索引)
- 2.1.3.3、复合索引(组合索引)
- 2.1.3.4、唯一索引
- 2.1.3.5、全文索引
- 2.1.3.6、索引的查询和删除
- 2.1.4、索引的优缺点
- 2.2、索引数据结构
- 2.2.1、Hash表
- 2.2.2、二叉树
- 2.2.3、平衡二叉树
- 2.2.4、B树(改造二叉树)
- 2.2.5、B+树(改造B树)
- 2.3、MySQL的索引实现
- 2.3.1、InnoDB索引
- 2.3.1.1、主键索引(聚簇索引)
- 2.3.1.2、辅助索引
- 2.3.1.3、组合索引
- 2.3.1.4、最左匹配原则
- 2.3.1.5、覆盖索引
- 2.3.2、MyISAM索引
- 2.3.2.1、根据主键等值查询数据
- 2.3.2.2、辅助索引
- 2.4、回表和联合索引的应用
- 2.4.1、回表查询
- 2.4.2、如何避免回表
- 2.4.3、联合索引的使用
- 三、性能瓶颈定位MySQL慢查询
- 3.1、性能优化的思路
- 3.2、引言
- 3.3、MySQL慢查询日志
- 3.3.1、慢查询参数
- 3.3.2、开启慢查询日志(临时)
- 3.3.3、开启慢查询日志(永久)
- 3.3.4、慢查询测试
- 3.4、MySQL性能分析 EXPLAIN
- 3.4.1、概述
- 3.4.2、EXPLAIN字段介绍
- 3.4.2.1、id字段
- 3.4.2.2、select_type 与 table字段
- 3.4.2.3、type字段
- 3.4.2.4、possible_keys 与 key字段
- 3.4.2.5、 key_len字段
- 3.4.2.6、ref 字段
- 3.4.2.7、rows 字段
- 3.4.2.8、filtered 字段
- 3.4.2.9、extra 字段
- 四、索引优化整合案例实现
- 4.1、JOIN优化
- 4.1.1、JOIN算法原理
- 4.1.2、in和exists函数
- 4.2、order by优化
- 4.2.1、索引排序
- 4.2.2、额外排序
- 4.2.3、排序优化
- 4.3、索引单表优化案例
- 4.3.1、建表
- 4.3.2、单表索引分析
- 4.3.2.1、需求一
- 4.3.2.2、优化
- 4.3.2.3、需求二
- 4.3.2.4、优化
- 4.3.2.5、需求三
- 4.3.2.6、优化
- 4.3.2.7、需求四
- 4.4、索引多表优化案例
- 4.4.1、准备数据
- 4.4.2、需求一
- 4.4.3、优化
- 4.4.4、需求二
- 4.4.5、优化分析
- 五、索引优化原则&失效情况
- 5.1、全值匹配
- 5.2、最佳左前缀法则
- 5.3、不要在索引列上做任何计算
- 5.4、范围之后全失效
- 5.5、尽量使用覆盖索引
- 5.6、使用不等于(!=或<>)会使索引失效
- 5.7、is null 或 is not null也无法使用索引
- 5.8、like通配符以%开头会使索引失效
- 5.9、字符串不加单引号导致索引失效
- 5.10、少用or,用or连接会使索引失效
- 六、工程优化如何实现
- 6.1、基础规范
- 6.2、命名规范
- 6.3、表设计规范
- 6.4、字段设计规范
- 6.5、索引设计规范
- 6.6、SQL使用规范
- 七、常见面试
- 7.1、各引擎索引实现
- 7.2、B树分析
- 7.3、B+树分析
- 7.4、联合索引
一、MySQL架构设计
1.1、引言
-- 查询语句
mysql> select * from sys_user;
+----+-----------+-----------+----------+--------+-------+-------------+------+--------+-----------+----------+
| id | user_name | nick_name | password | status | email | phonenumber | sex | avatar | user_type | del_flag |
+----+-----------+-----------+----------+--------+-------+-------------+------+--------+-----------+----------+
| 1 | admin | NULL | 123456 | 0 | NULL | NULL | NULL | NULL | 1 | 0 |
+----+-----------+-----------+----------+--------+-------+-------------+------+--------+-----------+----------+
1 row in set (0.00 sec)
思考:一条SQL查询语句是如何执行的?
1.2、MySQL Server层
MySQL
架构可以分为 Server层 和 Engine层两部分:
1.2.1、连接器(Connector)
Mysql作为服务器,一个客户端的Sql连接过来就需要分配一个线程进行处理,这个线程会专门负责监听请求并读取数据。这部分的线程和连接管理都是有一个连接器,专门负责跟客户端建立连接、权限认证、维持和管理连接。
思考:
- 一个
客户端
只会和MySQL
服务器建立一个连接吗? - 只能有一个
客户端
和MySQL
服务器建立连接吗?
答:
- 多个系统都可以和
MySQL
服务器建立连接,每个系统建立的连接肯定不止一个。 - 所以,为了解决
TCP
无限创建与TCP
频繁创建销毁带来的资源耗尽、性能下降问题。 MySQL
服务器里有专门的TCP
连接池限制接数,采用长连接模式复用TCP
连接,来解决上述问题。TCP
连接收到请求后,必须要分配给一个线程去执行,所以还会有个线程池,去走后面的流程。
连接器负责跟客户端建立连接、获取权限、维持和管理连接。
连接命令一般是这么写的:
mysql -hlocalhost -P3306 -uroot -proot
在完成 经典TCP 握手后,连接器会基于用户名和密码来验证身份。
-
验证不通过:"Access denied for user"错误
-
验证通过:连接器会到权限表里面查出拥有的权限,之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限
-- 查看连接状态
mysql> show processlist;
+----+------+-----------------+------+---------+------+----------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------------+------+---------+------+----------+------------------+
| 8 | root | localhost:55050 | test | Query | 0 | starting | show processlist |
| 9 | root | localhost:55874 | test | Sleep | 414 | | NULL |
| 10 | root | localhost:55889 | test | Sleep | 470 | | NULL |
+----+------+-----------------+------+---------+------+----------+------------------+
3 rows in set (0.00 sec)
1.2.2、查询缓存 (Query Cache)
经过了连接管理,现在MySQL
服务器已经获取到SQL
字符串。
执行逻辑就会来到第二步:查询缓存
查询语句,MySQL
服务器会使用select SQL
字符串作为key
,去缓存中获取:
-
缓存命中,直接返回结果
-
缓存未命中:执行后面的阶段,执行完成后,执行结果会被存入查询缓存中
缓存中数据:key:(查询的语句) value:(查询的结果)
注意:但是大多数情况下建议不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利
-
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空
-
5.x版本可以按需使用”的方式。可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定例:
select SQL_CACHE * from T where ID=10;
-
MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。
1.2.3、分析器(Analyzer)
缓存如果未命中,就要开始真正执行语句了
首先,MySQL 需要知道要做什么,因此需要对 SQL 语句做解析
-
词法分析
首先,会进行词法分析。 将一个完整的SQL语句,拆分成语句类型(select? insert? update? …)、表名、列名等等。
-
语法分析
其次,会进行语法分析。 根据语法规则,判断输入的这个 SQL 语句是否满足 MySQL 语法。 如果错误,会报出下面的错误:
mysql> elect * from t where ID=1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
这时,我们只要修正 use near 后面的语句即可。
1.2.4、优化器(optimizer)
通过了分析器,说明SQL
字符串符合语法规范,现在MySQL
服务器要执行SQL
语句了。
MySQL
服务器要怎么执行呢?
那么就需要产出执行计划,交给MySQL
服务器执行,所以来到了优化器阶段。
优化器不仅仅只是生成执行计划这么简单,这个过程它会帮你优化SQL
语句。
如外连接转换为内连接、表达式简化、子查询转为连接、连接顺序、索引选择等一堆东西,优化的结果就是执行计划。
例:执行下面这样的语句,这个语句是执行两个表的 join:
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
- 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
- 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。
1.2.5、执行器(Actuator)
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'xu'@'localhost' for table 'T'
如果有权限,就会根据表的 Engine 选择来调用对应的引擎接口。
例:
user_info 表的存储引擎是 InnoDB
。
select * from user_info where name = "xu";
如果 name 列没有声明任何索引,执行步骤如下:
- 调用 innoDB 引擎接口获取表的第一行,判断
name
是否等于xu
。如果不是,跳过。如果是,将结果保存。 - 调用 innoDB 引擎接口获取表的下一行,重复相同逻辑,一直到表的最后一行。
- 将所有满足条件的结果集返回给客户端。
如果 name 列有索引,执行步骤如下:
- 调用 innoDB 引擎接口获取索引树(B+树),基于索引树快速查到
name
等于xu
的所有主键id。 - 将所有满足条件的组件 id,回主表查详细信息。(这个操作称为**“回表”**)
- 将所有满足条件的结果集返回给客户端。
1.3、MySQ Engine层
1.3.1、什么是存储引擎?
引擎(Engine),我们都知道是机器发动机的核心所在,数据库存储引擎便是数据库的底层软件组织。
数据库使用数据存储引擎实现存储、处理和保护数据的核心服务
不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySql的核心就是插件式存储引擎。
1.3.2、mysql支持哪些存储引擎?
-- 使用MySQL命令行查看
mysql> SHOW ENGINES;
Engine | Support | Comment | Transactions | XA | Savepoints |
---|---|---|---|---|---|
InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
MyISAM | YES | MyISAM storage engine | NO | NO | NO |
CSV | YES | CSV storage engine | NO | NO | NO |
ARCHIVE | YES | Archive storage engine | NO | NO | NO |
PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
FEDERATED | NO | Federated MySQL storage engine |
可以发现,MySQL目前支持多种数据库存储引擎,默认引擎为InnoDB,且是唯一支持事务的存储引擎。
1.3.3、常见的存储引擎对比
1.3.3.1、INnoDB存储引擎
概述:InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB是默认的MySQL引擎。
主要特性:
- 为MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在 SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合
- InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上
- InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键
使用 InnoDB存储引擎 MySQL将在数据目录下创建一个名为 ibdata1的10MB大小的自动扩展数据文件,以及两个名为 ib_logfile0和 ib_logfile1的5MB大小的日志文件。
1.3.3.2、MyISAM存储引擎
概述:MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事务。
主要特性:
- 被大文件系统和操作系统支持
- 当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,若下一个块被删除,就扩展到下一块自动完成
- 每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16
- 最大的键长度是1000字节,这也可以通过编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上
- BLOB和TEXT列可以被索引
- NULL被允许在索引的列中,这个值占每个键的0~1个字节
- 所有数字键值以高字节优先被存储以允许一个更高的索引压缩
- 每个MyISAM类型的表都有一个AUTOINCREMENT的内部列,当INSERT和UPDATE操作的时候该列被更新,同时AUTOINCREMENT列将被刷新。所以说,MyISAM类型表的AUTOINCREMENT列更新比InnoDB类型的AUTOINCREMENT更快
- 可以把数据文件和索引文件放在不同目录
- 每个字符列可以有不同的字符集
- 有VARCHAR的表可以固定或动态记录长度
- VARCHAR和CHAR列可以多达64KB
使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex)。
1.3.3.3、MEMORY存储引擎
概述:MEMORY存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。
主要特性:
- MEMORY表的每个表可以有多达32个索引,每个索引16列,以及500字节的最大键长度
- MEMORY存储引擎执行HASH和BTREE缩影
- 可以在一个MEMORY表中有非唯一键值
- MEMORY表使用一个固定的记录长度格式
- MEMORY不支持BLOB或TEXT列
- MEMORY支持AUTO_INCREMENT列和对可包含NULL值的列的索引
- MEMORY表在所由客户端之间共享(就像其他任何非TEMPORARY表)
- MEMORY表内存被存储在内存中,内存是MEMORY表和服务器在查询处理时的空闲中,创建的内部表共享
- 当不再需要MEMORY表的内容时,要释放被MEMORY表使用的内存,应该执行 DELETE FROM或 TRUNCATE TABLE,或者删除整个表(使用DROP TABLE)
1.3.4、存储引擎的选择
不同的数据处理选择适合的存储引擎是使用MySQL的一大优势。
特点 | MyISAM | BDB | MEMORY | InnoDB | ARCHIVE |
---|---|---|---|---|---|
存储限制 | 没有 | 没有 | 有 | 64TB | 没有 |
事务安全 | 支持 | 支持 | |||
锁机制 | 表锁 | 页锁 | 表锁 | 行锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | |
哈希索引 | 支持 | 支持 | |||
全文索引 | 支持 | ||||
集群索引 | 支持 | ||||
数据缓存 | 支持 | 支持 | |||
索引缓存 | 支持 | 支持 | 支持 | ||
数据可压缩 | 支持 | 支持 | |||
空间使用 | 低 | 低 | N/A | 高 | 非常低 |
内存使用 | 低 | 低 | 中等 | 高 | 低 |
批量插入的速度 | 高 | 高 | 高 | 低 | 非常高 |
支持外键 | 支持 |
- InnoDB: **支持事务处理,支持外键,支持崩溃修复能力和并发控制。**如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
- MyISAM: **插入数据快,空间和内存使用比较低。**如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。
- MEMORY: **所有的数据都在内存中,数据的处理速度快,但是安全性不高。**如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。
注意:同一个数据库也可以使用多种存储引擎的表。如果一个表要求比较高的事务处理,可以选择InnoDB。这个数据库中可以将查询要求比较高的表选择MyISAM存储。如果该数据库需要一个用于查询的临时表,可以选择MEMORY存储引擎。
二、MySQL索引原理&优化
2.1、什么是索引?
2.1.1、引言
-
官方上面说索引是帮助MySQL
高效获取数据
的数据结构
,通俗点的说,数据库索引好比是一本书的目录,可以直接根据页码找到对应的内容,目的就是为了加快数据库的查询速度
。 -
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
-
一种能帮助mysql提高了查询效率的数据结构:索引数据结构。
2.1.2、索引原理
索引的存储原理可以概括为一句话:以空间换时间
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。
数据库在未添加索引进行查询的时候默认是进行全文搜索,也就是说有多少数据就进行多少次查询,然后找到相应的数据就把它们放到结果集中,直到全文扫描完毕。
2.1.3、索引分类
2.1.3.1、主键索引
设定为主键后,数据库自动建立索引,InnoDB为聚簇索引,主键索引列值不能为空(Null)。
-- (1) 创建表添加主键索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`col_name`),
)
-- (2) 添加主键索引
ALTER TABLE `table_name` ADD PRIMARY KEY (`col_name`);
2.1.3.2、普通索引(单列索引)
普通索引(单列索引):单列索引是最基本的索引,它没有任何限制。
-- (1) 直接创建索引
CREATE INDEX index_name ON table_name(`col_name`);
-- (2) 修改表结构的方式添加索引
ALTER TABLE `table_name` ADD INDEX index_name(`col_name`);
-- (3) 创建表的时候同时创建索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
INDEX index_name (`col_name`)
)
-- (4) 删除索引
DROP INDEX index_name ON table_name;
alter table `表名` drop index 索引名;
2.1.3.3、复合索引(组合索引)
复合索引:复合索引是在多个字段上创建的索引。复合索引遵守“最左前缀”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。
-- 创建一个复合索引
create index index_name on table_name(`col_name1`,`col_name2`,...);
-- 修改表结构的方式添加索引
alter table table_name add index index_name(`col_name1`,`col_name2`,...);
2.1.3.4、唯一索引
唯一索引:唯一索引和普通索引类似,主要的区别在于,唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)。
如果在已经有数据的表上添加唯一性索引的话:
-
如果添加索引的列的值存在两个或者两个以上的空值,则不能创建唯一性索引会失败。(一般在创建表的时候,要对自动设置唯一性索引,需要在字段上加上 not null)
-
如果添加索引的列的值存在两个或者两个以上的null值,还是可以创建唯一性索引,只是后面创建的数据不能再插入null值 ,并且严格意义上此列并不是唯一的,因为存在多个null值。
-
对于多个字段创建唯一索引规定列值的组合必须唯一
“空值” 和”NULL”的概念:
- 空值是不占用空间的 .
- MySQL中的NULL其实是占用空间的.
长度验证:注意空值的之间是没有空格的。
mysql> select length(''),length(null),length(' ');
+------------+--------------+-------------+
| length('') | length(null) | length(' ') |
+------------+--------------+-------------+
| 0 | NULL | 1 |
+------------+--------------+-------------+
-- (1)创建唯一索引
-- 创建单个索引
CREATE UNIQUE INDEX index_name ON table_name(`col_name`);
-- 创建多个索引
CREATE UNIQUE INDEX index_name on table_name(`col_name`,...);
-- (2)修改表结构
-- 单个
ALTER TABLE table_name ADD UNIQUE index index_name(`col_name`);
-- 多个
ALTER TABLE table_name ADD UNIQUE index index_name(`col_name`,...);
-- (3)创建表的时候直接指定索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
UNIQUE index_name_unique(`col_name`)
)
2.1.3.5、全文索引
-
Full Text类型索引(FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB,而之前的版本只支持 MyISAM 表)。
-
全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较,目前只有char、varchar,text 列上可以创建全文索引。
-- (1)创建表的适合添加全文索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
FULLTEXT (`col_name`)
)
-- (2)修改表结构添加全文索引
ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(`col_name`)
-- (3)直接创建索引
CREATE FULLTEXT INDEX index_fulltext_content ON table_name(`col_name`)
注意:
-
默认 MySQL 不支持中文全文检索!
-
MySQL 全文搜索只是一个临时方案,对于全文搜索场景,更专业的做法是使用全文搜索引擎,例如 ElasticSearch 或 Solr。
2.1.3.6、索引的查询和删除
-- 查看:
show indexes from `表名`;
-- 或
show keys from `表名`;
-- 删除
alter table `表名` drop index 索引名;
2.1.4、索引的优缺点
优点:
-
大大提高数据查询速度。
-
可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
-
通过索引列对数据进行排序,降低数据的排序成本降低了CPU的消耗。
-
被索引的列会自动进行排序,包括【单例索引】和【组合索引】,只是组合索引的排序需要复杂一些。
-
如果按照索引列的顺序进行排序,对order 不用语句来说,效率就会提高很多。
缺点:
-
索引会占据磁盘空间。
-
索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改查操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。
-
维护索引需要消耗数据库资源。
综合索引的优缺点:
- 数据库表中不是索引越多越好,而是仅为那些常用的搜索字段建立索引效果最佳!
2.2、索引数据结构
MySQL索引使用的数据结构主要有BTree索引
和hash索引
。
对于hash索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景建议选择BTree索引。
2.2.1、Hash表
Hash表,在Java中的HashMap,TreeMap就是Hash表结构,以键值对的形式存储数据。我们使用hash表存储表数据结构,Key可以存储索引列,Value可以存储行记录或者行磁盘地址。Hash表在等值查询时效率很高,时间复杂度为O(1);但是不支持范围快速查找,范围查找时只能通过扫描全表的方式,筛选出符合条件的数据。
显然这种方式,不适合我们经常需要查找和范围查找的数据库索引使用。
2.2.2、二叉树
上面这个图就是我们常说的二叉树:每个节点最多有两个分叉节点,左子树和右子树数据按顺序左小右大。
二叉树的特点就是为了保证每次查找都可以进行折半查找,从而减少IO次数。
但是二叉树不是一直保持二叉平衡,因为二叉树很考验根节点的取值,因为很容易在某个节点下不分叉了,这样的话二叉树就不平衡了,也就没有了所谓的能进行折半查找了,如下图:
显然这种不稳定的情况,我们在选择存储数据结构的时候就会尽量避免这种的情况发生。
2.2.3、平衡二叉树
平衡二叉树采用的是二分法思维,平衡二叉查找树除了具备二叉树的特点,最主要的特征是树的左右两个子树的层级最多差1。在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,不会出现左子树很高、右子树很矮的情况。
使用平衡二叉查找树查询的性能接近与二分查找,时间复杂度为O(log2n),查询id=6,只需要两次IO。
就上述平衡二叉树的特点来看,其实是我们理想的状态下,然而其实内部还是存在一些问题:
-
时间复杂度和树的高度有关。树有多高就需要检索多少次,每个节点的读取,都对应一次磁盘的IO操作。树的高度就等于每次查询数据时磁盘IO操作的次数。磁盘每次寻道的时间为10ms,在数据量大时,查询性能会很差。(1百万的数据量,log2n约等于20次磁盘IO读写,时间消耗约等于:20*10=0.2S)。
-
平衡二叉树不支持范围查询快速查找,范围查询需要从根节点多次遍历,查询效率不高。
2.2.4、B树(改造二叉树)
MySQL的数据是存储在磁盘文件中的,查询处理数据时,需要先把磁盘中的数据加载到内存中,磁盘IO操作非常耗时,所以我们优化的重点就是尽量减少磁盘的IO操作。访问二叉树的每个节点都会发生一次IO,如果想要减少磁盘IO操作,就需要尽量降低树的高度。
那如何降低树的高度呢?
假如key为bigint=8字节,每个节点有两个指针,每个指针为4个字节,一个节点占用的空间为(8+4*2=16)。
因为在MySQL的InnoDB引擎的一次IO操作会读取一页的数据量(默认一页大小为16K),而二叉树一次IO操作的有效数据量只有16字节,空间利用率极低。为了最大化的利用一次IO操作空间,一个解决方法就是在一个节点处存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引(16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的分叉树,将树的体型从高瘦变成了矮胖。构建1百万条数据,树的高度需要2层就可以(1000*1000=1百万),也就是说只需要两次磁盘IO操作就可以查询到数据,磁盘IO操作次数变少了,查询数据的效率整体也就提高了。
这种数据结构我们称之为B树,B树是一种多叉平衡查找树,如下图主要特点:
-
B树的节点中存储这多个元素,每个内节点有多个分叉。
-
节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点中都存储数据。
-
父节点当中的元素不会出现在子节点中。
-
所有的叶子节点都位于同一层,叶子节点具有相同的深度,叶子节点之间没有指针连接。
举个简单的例子,在B树中查询数据的情况:
假如我们要查询key等于10对应的数据data,根据上图我们可知在磁盘中的查询路径是:磁盘块1->磁盘块2->磁盘块6
-
第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,10<15,走左子树,到磁盘中寻址到磁盘块2。
-
第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,10>7,走右子树,到磁盘中寻址到磁盘块6。
-
第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,10=10,找到key=10的位置,取出对应的数据data,如果data存储的是行记录,直接取出数据,查询结束;如果data存储的是行磁盘地址,还需要根据磁盘地址到对应的磁盘中取出数据,查询结束。
相比较二叉平衡查找树,在整个查找过程中,虽然数据的比较次数并没有明显减少,但是对于磁盘IO的次数会大大减少,同时,由于我们是在内存中进行的数据比较,所以比较数据所消耗的时间可以忽略不计。B树的高度一般2至3层就能满足大部分的应用场景,所以使用B树构建索引可以很好的提升查询的效率。
过程如图:
看到上面的情况,觉得B树已经很理想了,但是其中还是存在可以优化的地方:
-
B树不支持范围查询的快速查找,例如:仍然根据上图,我们想要查询10到35之间的数据,查找到10之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
-
如果data存储的是行记录,行的大小随着列数的增加,所占空间会变大,这时一页中可存储的数据量就会减少,树相应就会变高,磁盘IO次数就会随之增加,有待优化。
2.2.5、B+树(改造B树)
B+树,作为B树的升级版,MySQL在B树的基础上继续进行改造,使用B+树构建索引。B+树和B树最主要的区别在于非叶子节点是否存储数据的问题。
-
B树:叶子节点和非叶子节点都会存储数据。
-
B+树:只有叶子节点才会存储数据,非叶子节点只存储键值key;叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。
B+树的大致数据结构:
B+树的最底层叶子节点包含了所有的索引项。从图上可以看到,B+树在查找数据的时候,由于数据都存放在最底层的叶子节点上,所以每次查找都需要检索到叶子节点才能查询到数据。所以在需要查询数据的情况下每次的磁盘的IO跟树高有直接的关系,但是从另一方面来说,由于数据都被放到了叶子节点,所以放索引的磁盘块锁存放的索引数量是会跟这增加的,所以相对于B树来说,B+树的树高理论上情况下是比B树要矮的。也存在索引覆盖查询的情况,在索引中数据满足了当前查询语句所需要的全部数据,此时只需要找到索引即可立刻返回,不需要检索到最底层的叶子节点。
举例:等值查询
假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。
-
第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2。
-
第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6。
-
第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。)
过程如图:
举例:范围查询
假如我们想要查找9和26之间的数据,查找路径为:磁盘块1->磁盘块2->磁盘块6->磁盘块7
-
前三次磁盘IO:首先查找到键值为9对应的数据(定位到磁盘块6),然后缓存大结果集中。这一步和前面等值查询流程一样,发生了三次磁盘IO。
-
继续查询,查找到节点15之后,底层的所有叶子节点是一个有序列表,我们从磁盘块6中的键值9开始向后遍历筛选出所有符合条件的数据。
-
第四次磁盘IO:根据磁盘块6的后继指针到磁盘中寻址定位到磁盘块7,将磁盘块7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将数据data缓存到结果集中。
-
逐渐具备唯一性(后面不会再有<=26的数据),不需要再向后查找,查询结束,将结果集返回给用户。
可以看到B+树可以保证等值和范围查询的快速查找,MySQL的索引就采用了B+树的数据结构。
2.3、MySQL的索引实现
介绍完了索引数据结构,那肯定是要带入到Mysql里面看看真实的使用场景的,所以这里分析Mysql的两种存储引擎的索引实现:MyISAM索引和InnoDB索引
2.3.1、InnoDB索引
2.3.1.1、主键索引(聚簇索引)
每个InnoDB表都有一个聚簇索引 ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。
InnoDB创建索引的具体规则如下:
-
在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引。
-
如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。
-
如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。
除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值都。 在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。
这里以user_innodb为例,user_innodb的id列为主键,age列为普通索引。
CREATE TABLE `user_innodb` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 20 ) DEFAULT NULL,
`age` INT ( 11 ) DEFAULT NULL,
PRIMARY KEY ( `id` ) USING BTREE,
KEY `idx_age` ( `age` ) USING BTREE
) ENGINE = INNODB;
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (12, '张1', 16);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (16, '张2', 17);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (18, '张3', 18);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (28, '张4', 18);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (47, '张5', 19);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (48, '张6', 52);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (54, '张7', 25);
INSERT INTO `test`.`user_innodb`(`id`, `username`, `age`) VALUES (75, '张8', 34);
-
InnoDB的数据和索引存储在
t_user_innodb.ibd
文件中,InnoDB的数据组织方式,是聚簇索引。 -
主键索引的叶子节点会存储数据行,辅助索引的叶子节点只会存储主键值。
等值查询数据:
select * from user_innodb where id = 28;
-
先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
-
将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
-
检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO)
磁盘IO数量:3次。
2.3.1.2、辅助索引
除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。
以表user_innodb的age列为例,age索引的索引结果如下图。
-
辅助索引的底层叶子节点是按照(age,id)的顺序排序,先按照age列从小到大排序,age相同时按照id列从小到大排序。
-
使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后根据主键到主键索引中检索获得数据记录。
辅助索引等值查询的情况:
select * from t_user_innodb where age=19;
根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询。
磁盘IO数:辅助索引3次+获取记录回表3次
2.3.1.3、组合索引
以表abc_innodb为例,id列为主键索引,创建一个联合索引idx_abc(a,b,c)
。
CREATE TABLE `abc_innodb` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`a` INT ( 11 ) DEFAULT NULL,
`b` INT ( 11 ) DEFAULT NULL,
`c` VARCHAR ( 10 ) DEFAULT NULL,
`d` VARCHAR ( 10 ) DEFAULT NULL,
PRIMARY KEY ( `id` ) USING BTREE,
KEY `idx_abc` ( `a`, `b`, `c` )
) ENGINE = INNODB;
组合索引的数据结构:
组合索引的查询过程:
select * from abc_innodb where a = 13 and b = 16 and c = 4;
2.3.1.4、最左匹配原则
最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。
在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排序,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内有序递增;而c列只能在a和b两列值相等的情况下小范围内有序递增。
就像上面的查询,B+ 树会先比较a列来确定下一步应该检索的方向,往左还是往右。如果a列相同再比较b列,但是如果查询条件中没有a列,B+树就不知道第一步应该从那个节点开始查起。
可以说创建的idx_(a,b,c)索引,相当于创建了(a)、(a,b)、(a,b,c)三个索引。
组合索引的最左前缀匹配原则:
使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)等就会停止匹配。
2.3.1.5、覆盖索引
覆盖索引并不是一种索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到相应的主键值,想要获取最终的数据记录,还需要根据主键通过主键索引再去检索,最终获取到符合条件的数据记录。
在上面的abc_innodb表中的组合索引查询时,如果我们查询的结果只需要a、b、c这三个字段,那我们使用这个idx_index(a,b,c)组合索引查询到叶子节点时就可以直接返回了,而不需要再次回表查询,这种情况就是覆盖索引。
未使用索引覆盖的情况:
EXPLAIN SELECT a,b,c FROM abc_innodb WHERE a=13 AND b=16 AND c=4;
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | abc_innodb | ref | idx_abc | idx_abc | 10 | const,const | 1 | 100 | Using where; Using index |
索引覆盖的情况
EXPLAIN SELECT * FROM abc_innodb WHERE a=13 AND b=16 AND c=4;
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | abc_innodb | ref | idx_abc | idx_abc | 10 | const,const | 1 | 100 | Using index condition |
2.3.2、MyISAM索引
以一个简单的user表为例。user表存在两个索引,id列为主键索引,age列为普通索引
-- 创建表
CREATE TABLE `user` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 20 ) DEFAULT NULL,
`age` INT ( 11 ) DEFAULT NULL,
PRIMARY KEY ( `id` ) USING BTREE,
KEY `idx_age` ( `age` ) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
-- 插入数据
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (12, 'zhangyi', 16);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (16, 'zhanger', 17);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (18, 'zhangsan', 18);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (28, 'zhangsi', 45);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (47, 'zhangwu', 19);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (48, 'zhangliu', 52);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (54, 'zhangqi', 25);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (75, 'zhangba', 35);
MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。
主键ID列索引:
表user的索引存储在索引文件user.MYI
中,数据文件存储在数据文件 user.MYD
中。
简单分析下查询时的磁盘IO情况:
2.3.2.1、根据主键等值查询数据
select * from user where id = 28
-
第一次磁盘IO:先在主键索引树中从根节点开始检索,将根节点加载到内存中,比较28<75,所以走左子树。
-
第二次磁盘IO:将左子树节点加载到内存中,比较16<28<47,向下检索。
-
第三次磁盘IO:检索到叶子节点,将节点加载到内存中遍历,从16<28,18<28,28=28,查找到键值等于28的索引项。
-
第四次磁盘IO:从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。
-
将记录返回给客户端。
磁盘IO次数:3次索引检索+记录数据检索。
根据主键范围查询数据:
select * from user where id between 28 and 47;
-
先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
-
将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
-
检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。
-
根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
-
我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。
-
向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
-
最后得到两条符合筛选条件,将查询结果集返给客户端。
磁盘IO次数:4次索引检索+记录数据检索。
2.3.2.2、辅助索引
在MyISAM存储引擎中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点中data阈存储的都是行记录的磁盘地址。 主键列索引的键值是唯一的,而辅助索引的键值是可以重复的。
查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树种检索数据。
2.4、回表和联合索引的应用
2.4.1、回表查询
在InnoDB的存储引擎中,使用辅助索引查询的时候,因为辅助索引叶子节点保存的数据不是当前数据记录,而是当前数据记录的主键索引。如果需要获取当前记录完整的数据,就必须要再次根据主键从主键索引中继续检索查询,这个过程我们称之为回表查询。
由此可见,在数据量比较大的时候,回表必然会消耗很多的时间影响性能,所以我们要尽量避免回表的发生。
2.4.2、如何避免回表
使用索引覆盖
-- 删除表
drop table user;
-- 创建表
CREATE TABLE `user` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`name` INT ( 11 ) DEFAULT NULL,
`sex` CHAR ( 3 ) DEFAULT NULL,
`address` VARCHAR ( 10 ) DEFAULT NULL,
`hobby` VARCHAR ( 10 ) DEFAULT NULL,
PRIMARY KEY ( `id` ) USING BTREE,
KEY `i_name` ( `name` )
) ENGINE = INNODB;
如果有一个场景:
select id,name,sex from user where name = 'zhangsan';
这个语句在业务上频繁使用到,而user表中的其他字段使用频率远低于这几个字段,在这个情况下,如果我们在建立name字段的索引时,不是使用单一索引,而是使用联合索引(name,sex),这样的话再执行这个查询语句,根据这个辅助索引(name,sex)查询到的结果就包括了我们所需要的查询结果的所有字段的完整数据,这样就不需要再次回表查询去检索sex字段的数据了。
以上就是一个典型的使用覆盖索引的优化策略减少了回表查询的情况。
2.4.3、联合索引的使用
联合索引:
在建立索引的时候,尽量在多个单列索引上判断下是否可以使用联合索引。联合索引的使用不仅可以节省空间,还可以更容易的使用到索引覆盖。
节省空间:
试想一下,索引的字段越多,是不是更容易满足查询需要返回的数据呢。比如联合索引(a_b_c),是不是等于有了索引:a,a_b,a_b_c三个索引,这样是不是节省了空间,当然节省的空间并不是三倍于(a,a_b,a_b_c)三个索引,因为索引树的数据没变,但是索引data字段的数据确实真实的节省了。
联合索引的创建原则:
在创建联合索引的时候因该把频繁使用的列、区分度高的列放在前面,频繁使用代表索引利用率高,区分度高代表筛选粒度大,这些都是在索引创建的需要考虑到的优化场景,也可以在常需要作为查询返回的字段上增加到联合索引中。
如果在联合索引上增加一个字段而使用到了覆盖索引,那建议这种情况下使用联合索引。
联合索引的使用:
- 考虑当前是否已经存在多个可以合并的单列索引,如果有,那么将当前多个单列索引创建为一个联合索引。
- 当前索引存在频繁使用作为返回字段的列,这个时候就可以考虑当前列是否可以加入到当前已经存在索引上,使其查询语句可以使用到覆盖索引。
三、性能瓶颈定位MySQL慢查询
3.1、性能优化的思路
-
首先需要使用慢查询功能,去获取所有查询时间比较长的SQL语句。
-
其次使用explain命令去查询由问题的SQL的执行计划。
-
最后可以使用show profile[s] 查看由问题的SQL的性能使用情况。
-
优化SQL语句。
3.2、引言
数据库查询快慢是影响项目性能的一大因素,对于数据库,我们除了要优化SQL,更重要的是得先找到需要优化的SQL语句。
MySQL数据库有一个“慢查询日志”功能,用来记录查询时间超过某个设定值的SQL,这将极大程度帮助我们快速定位到问题所在,以便对症下药。
3.3、MySQL慢查询日志
慢查询日志用来记录在 MySQL 中执行时间超过指定时间的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率低,以便进行优化。
3.3.1、慢查询参数
mysql> SHOW VARIABLES LIKE "slow_query%";
+---------------------+-------------+
| Variable_name | Value |
+---------------------+-------------+
| slow_query_log | ON |
| slow_query_log_file | XU-slow.log |
+---------------------+-------------+
-
slow_query_log:是否开启慢查询,on为开启,off为关闭。
-
log-slow-queries:慢查询日志文件路径。
mysql> SHOW VARIABLES LIKE "%long_query_time%";
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
- long_query_time : 阈值,超过多少秒的查询就写入日志 。
mysql> SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | OFF |
+-------------------------------+-------+
- 系统变量
log-queries-not-using-indexes
:未使用索引的查询也被记录到慢查询日志中(可选项)。如果调优的话,建议开启这个选项。
3.3.2、开启慢查询日志(临时)
在MySQL执行SQL语句设置,但是如果重启MySQL的话会失效。
set global slow_query_log=on;
set global long_query_time=1;
3.3.3、开启慢查询日志(永久)
修改:/etc/my.cnf
,添加以下内容,然后重启MySQL服务
[mysqld]
lower_case_table_names=1
slow_query_log=ON
slow_query_log_file=D:\Java\MySQL Server 5.7\data\Data\XU-slow.log
long_query_time=1
(数据库操作超过100毫秒认为是慢查询,可根据需要进行设定,如果过多,可逐步设定,比如先行设定为2秒,逐渐降低来确认瓶颈所在)
3.3.4、慢查询测试
select SLEEP(11);
-- 查看慢SQL文件
TCP Port: 3306, Named Pipe: (null)
Time Id Command Argument
# Time: 2022-09-27T02:52:45.225977Z
# User@Host: root[root] @ localhost [::1] Id: 8
# Query_time: 11.013691 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
use test;
SET timestamp=1664247165;
select SLEEP(11);
格式说明:
-
第一行,SQL查询执行的具体时间
-
第二行,执行SQL查询的连接信息,用户和连接IP
-
第三行,记录了一些我们比较有用的信息,
-
Query_timme,这条SQL执行的时间,越长则越慢
-
Lock_time,在MySQL服务器阶段(不是在存储引擎阶段)等待表锁时间
-
Rows_sent,查询返回的行数
-
Rows_examined,查询检查的行数,越长就越浪费时间
-
-
第四行,设置时间戳,没有实际意义,只是和第一行对应执行时间。
-
第五行,执行的SQL语句记录信息
3.4、MySQL性能分析 EXPLAIN
3.4.1、概述
explain(执行计划),使用explain关键字可以模拟优化器执行sql查询语句,从而知道MySQL是如何处理sql语句。
explain主要用于分析查询语句或表结构的性能瓶颈。
通过explain命令可以得到:
-
– 表的读取顺序
-
– 数据读取操作的操作类型
-
– 哪些索引可以使用
-
– 哪些索引被实际使用
-
– 表之间的引用
-
– 每张表有多少行被优化器查询
3.4.2、EXPLAIN字段介绍
explain使用:explain+sql语句,通过执行explain可以获得sql语句执行的相关信息。
explain select * from user;
expain出来的信息有10列,分别是id
、select_type
、table
、type
、possible_keys
、key
、key_len
、ref
、rows
、Extra
。
数据准备
-- 创建数据库
CREATE DATABASE test_explain CHARACTER SET 'utf8';
-- 选择库
USE test_explain;
-- 创建表
CREATE TABLE L1 (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR (100));
CREATE TABLE L2 (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR (100));
CREATE TABLE L3 (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR (100));
CREATE TABLE L4 (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR (100));
-- 每张表插入3条数据
INSERT INTO L1 (title) VALUES ('xuxu001'),('xuxu002'),('xuxu003');
INSERT INTO L2 (title) VALUES ('xuxu004'),('xuxu005'),('xuxu006');
INSERT INTO L3 (title) VALUES ('xuxu007'),('xuxu008'),('xuxu009');
INSERT INTO L4 (title) VALUES ('xuxu010'),('xuxu011'),('xuxu012');
3.4.2.1、id字段
select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
-
id相同,执行顺序由上至下
EXPLAIN SELECT * FROM L1,L2,L3 WHERE L1.id=L2.id AND L2.id = L3.id;
-
id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
EXPLAIN SELECT*FROM L2 WHERE id=( SELECT id FROM L1 WHERE id=(SELECT L3.id FROM L3 WHERE L3.title='xuxu007'));
3.4.2.2、select_type 与 table字段
查询类型,主要用于区别普通查询,联合查询,子查询等的复杂查询
-
simple
: 简单的select查询,查询中不包含子查询或者UNION
-
primary
: 查询中若包含任何复杂的子部分,最外层查询被标记EXPLAIN SELECT * FROM L2 WHERE id = ( SELECT id FROM L1 WHERE id = (SELECT L3.id FROM L3 WHERE L3.title = 'xuxu007'));
-
subquery
: 在select或where列表中包含了子查询EXPLAIN SELECT * FROM L2 WHERE L2.id = (SELECT id FROM L3 WHERE L3.title = 'xuxu007' );
-
derived
: 在from列表中包含的子查询被标记为derived(衍生),MySQL会递归执行这些子查询, 把结果放到临时表中 -
union
: 如果第二个select出现在UNION之后,则被标记为UNION,如果union包含在from子句的子查询中,外层select被标记为derived -
union result
: UNION 的结果EXPLAIN SELECT * FROM L2 UNION SELECT * FROM L3
3.4.2.3、type字段
type显示的是连接类型,是较为重要的一个指标。下面给出各种连接类型,按照从最佳类型到最坏类型进行排序:
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
-
system : 表仅有一行 (等于系统表)。这是const连接类型的一个特例,很少出现。
-
const : 表示通过索引 一次就找到了, const用于比较 primary key 或者 unique 索引。因为只匹配一行数据,所以如果将主键 放在 where条件中,MySQL就能将该查询转换为一个常量。
EXPLAIN SELECT * FROM L1 WHERE L1.id = 1;
-
eq_ref : 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见与主键或唯一索引扫描。
DELETE FROM L1 WHERE id IN (2,3); EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.id = L2.id ;
-
ref : 非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,这是比较常见连接类型。
- 未加索引之前
EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.title = L2.title ;
- 加索引之后
CREATE INDEX idx_title ON L2(title); EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.title = L2.title ;
-
range : 只检索给定范围的行,使用一个索引来选择行。
EXPLAIN SELECT * FROM L1 WHERE L1.id > 10; EXPLAIN SELECT * FROM L1 WHERE L1.id IN (1,2);
key显示使用了哪个索引,where 子句后面 使用 between 、< 、> 、in 等查询,这种范围查询要比全表扫描好。
-
index : 出现index 是 SQL 使用了索引,但是没有通过索引进行过滤,一般是使用了索引进行排序分组。
EXPLAIN SELECT * FROM L1 ORDER BY id;
-
ALL : 对于每个来自于先前的表的行组合,进行完整的表扫描。
EXPLAIN SELECT * FROM L1;
一般来说,需要保证查询至少达到
range
级别,最好能到ref
。
3.4.2.4、possible_keys 与 key字段
-
possible_keys
显示可能应用到这张表上的索引,一个或者多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。
-
key
-
实际使用的索引,若为null,则没有使用到索引。(两种可能:1.没建立索引,2.建立索引,但索引失效)。查询中若使用了覆盖索引,则该索引仅出现在key列表中。
-
覆盖索引:一个索引包含(或覆盖)所有需要查询的字段的值,通过查询索引就可以获取到字段值。
-
1、理论上没有使用索引,但实际上使用了索引。
EXPLAIN SELECT L1.id FROM L1;
2、理论和实际上都没有使用索引
EXPLAIN SELECT * FROM L1 WHERE title = 'xuxu001';
3、理论和实际上都使用了索引
EXPLAIN SELECT * FROM L2 WHERE title = 'xuxu001';
3.4.2.5、 key_len字段
表示索引中使用的字节数,可以通过该列计算查询中使用索引的长度。
key_len 字段能够帮你检查是否充分利用了索引 ken_len 越长, 说明索引使用的越充分
-
创建表
CREATE TABLE L5( a INT PRIMARY KEY, b INT NOT NULL, c INT DEFAULT NULL, d CHAR(10) NOT NULL );
-
使用explain 进行测试
EXPLAIN SELECT * FROM L5 WHERE a > 1 AND b = 1;
索引中只包含了1列,所以,key_len是4。
-
为b字段添加索引
ALTER TABLE L5 ADD INDEX idx_b(b); -- 执行SQL,这次将b字段也作为条件。 EXPLAIN SELECT * FROM L5 WHERE a > 1 AND b = 1;
-
再次测试,为c、d字段添加联合索引,然后进行测试。
ALTER TABLE L5 ADD INDEX idx_c_b(c,d); explain select * from L5 where c = 1 and d = '';
c字段是int类型 4个字节, d字段是 char(10)代表的是10个字符相当30个字节数据库的字符集是utf8 一个字符3个字节,d字段是 char(10)代表的是10个字符相当30个字节,多出的一个字节用来表示是联合索引。
下面这个例子中,虽然使用了联合索引,但是可以根据ken_len的长度推测出该联合索引只使用了一部分,没有充分利用索引,还有优化空间。
explain select * from L5 where c = 1;
3.4.2.6、ref 字段
显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。
-
L1.id=‘1’; 1是常量 , ref = const
EXPLAIN SELECT * FROM L1 WHERE L1.id='1';
-
L2表被关联查询的时候,使用了主键索引, 而值使用的是驱动表(执行计划中靠前的表是驱动表)L1表的ID, 所以 ref = test_explain.L1.id
EXPLAIN SELECT * FROM L1 LEFT JOIN L2 ON L1.id = L2.id WHERE L1.title = 'xuxu001';
3.4.2.7、rows 字段
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数;越少越好。
-
使用like 查询,会产生全表扫描, L2中有3条记录,就需要读取3条记录进行查找
EXPLAIN SELECT * FROM L1,L2 WHERE L1.id = L2.id AND L2.title LIKE '%xu%';
-
如果使用等值查询,则可以直接找到要查询的记录,返回即可,所以只需要读取一条。
EXPLAIN SELECT * FROM L1,L2 WHERE L1.id = L2.id AND L2.title = 'xuxu001';
总结: 当我们需要优化一个SQL语句的时候,我们需要知道该SQL的执行计划,比如是全表扫描,还是索引扫描; 使用explain
关键字可以模拟优化器执行sql
语句,从而知道mysql
是如何处理sql
语句的,方便我们开发人员有针对性的对SQL进行优化。
- 表的读取顺序。(对应id)
- 数据读取操作的操作类型。(对应select_type)
- 哪些索引可以使用。(对应possible_keys)
- 哪些索引被实际使用。(对应key)
- 每张表有多少行被优化器查询。(对应rows)
- 评估sql的质量与效率 (对应type)
3.4.2.8、filtered 字段
它指返回结果的行占需要读到的行(rows列的值)的百分比。
3.4.2.9、extra 字段
Extra 是 EXPLAIN 输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信息。
准备数据
CREATE TABLE users (
uid INT PRIMARY KEY AUTO_INCREMENT,
uname VARCHAR(20),
age INT(11)
);
INSERT INTO users VALUES(NULL, 'lisa',10);
INSERT INTO users VALUES(NULL, 'lisa',10);
INSERT INTO users VALUES(NULL, 'rose',11);
INSERT INTO users VALUES(NULL, 'jack', 12);
INSERT INTO users VALUES(NULL, 'sam', 13);
-
Using filesort
EXPLAIN SELECT * FROM users ORDER BY age;
执行结果Extra为Using filesort
,这说明,得到所需结果集,需要对所有记录进行文件排序。这类SQL语句性能极差,需要进行优化。典型的,在一个没有建立索引的列上进行了order by,就会触发filesort,常见的优化方案是,在order by的列上添加索引,避免每次查询都全量排序。
filtered 它指返回结果的行占需要读到的行(rows列的值)的百分比
-
Using temporary
EXPLAIN SELECT COUNT(*),uname FROM users WHERE uid > 2 GROUP BY uname;
执行结果Extra为
Using temporary
,这说明需要建立临时表 (temporary table) 来暂存中间结果。 常见与 group by 和 order by,这类SQL语句性能较低,往往也需要进行优化。 -
Using where
意味着全表扫描或者在查找使用索引的情况下,但是还有查询条件不在索引字段当中.
EXPLAIN SELECT * FROM users WHERE age=10;
此语句的执行结果Extra为Using where,表示使用了where条件过滤数据。需要注意的是:
-
返回所有记录的SQL,不使用where条件过滤数据,大概率不符合预期,对于这类SQL往往需要进行优化;
-
使用了where条件的SQL,并不代表不需要优化,往往需要配合explain结果中的type(连接类型)来综合判断。例如本例查询的 age 未设置索引,所以返回的type为ALL,仍有优化空间,可以建立索引优化查询。
-
-
Using index
表示直接访问索引就能够获取到所需要的数据(覆盖索引),不需要通过索引回表。
-- 为uname创建索引 alter table users add index idx_uname(uname); EXPLAIN SELECT uid,uname FROM users WHERE uname='lisa';
此句执行结果为Extra为Using index,说明sql所需要返回的所有列数据均在一棵索引树上,而无需访问实际的行记录。 -
Using join buffer
使用了连接缓存, 会显示join连接查询时,MySQL选择的查询算法 .
-- 删除索引 alter table users drop index idx_uname; EXPLAIN SELECT * FROM users u1 LEFT JOIN (SELECT * FROM users) u2 ON u1.uname = u2.uname;
执行结果Extra为Using join buffer (Block Nested Loop)
说明,需要进行嵌套循环计算,这里每个表都有五条记录,内外表查询的type都为ALL。问题在于 两个关联表join 使用 uname,关联字段均未建立索引,就会出现这种情况。
常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。
-
Using index condition
查找使用了索引 (但是只使用了一部分,一般是指联合索引),但是需要回表查询数。
explain select * from L5 where c > 10 and d = '';
Extra主要指标的含义(有时会同时出现)
-
using index
:使用覆盖索引的时候就会出现。 -
using where
:在查找使用索引的情况下,需要回表去查询所需的数据。 -
using index condition
:查找使用了索引,但是需要回表查询数据。 -
using index & using where
:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据。
四、索引优化整合案例实现
4.1、JOIN优化
4.1.1、JOIN算法原理
1、JOIN回顾
JOIN 是 MySQL 用来进行联表操作的,用来匹配两个表的数据,筛选并合并出符合我们要求的结果集。
JOIN 操作有多种方式,取决于最终数据的合并效果。常用连接方式的有以下几种:
2、驱动表的定义
什么是驱动表 ?
-
多表关联查询时,第一个被处理的表就是驱动表,使用驱动表去关联其他表。
-
驱动表的确定非常的关键,会直接影响多表关联的顺序,也决定后续关联查询的性能。
驱动表的选择要遵循一个规则:
- 在对最终的结果集没有影响的前提下,优先选择结果集最小的那张表作为驱动表。
3、三种JOIN算法
1)、Simple Nested-Loop Join( 简单的嵌套循环连接 )
-
简单来说嵌套循环连接算法就是一个双层for 循环 ,通过循环外层表的行数据,逐个与内层表的所有行数据进行比较来获取结果.
-
这种算法是最简单的方案,性能也一般。对内循环没优化。
-
例如有这样一条SQL:
-- 连接用户表与订单表 连接条件是 u.id = o.user_id select * from user t1 left join order t2 on t1.id = t2.user_id; -- user表为驱动表,order表为被驱动表
-
转换成代码执行时的思路是这样的:
for(user表行 uRow : user表){ for(Order表的行 oRow : order表){ if(uRow.id = oRow.user_id){ return uRow; } } }
-
匹配过程如下图
-
SNL 的特点
-
简单粗暴容易理解,就是通过双层循环比较数据来获得结果
-
查询效率会非常慢,假设 A 表有 N 行,B 表有 M 行。SNL 的开销如下:
-
A 表扫描 1 次。
-
B 表扫描 M 次。
-
一共有 N 个内循环,每个内循环要 M 次,一共有内循环 N * M 次
-
-
2)、Index Nested-Loop Join( 索引嵌套循环连接 )
-
Index Nested-Loop Join 其优化的思路: 主要是为了减少内层表数据的匹配次数 , 最大的区别在于,用来进行 join 的字段已经在被驱动表中建立了索引。
-
从原来的
匹配次数 = 外层表行数 * 内层表行数
, 变成了匹配次数 = 外层表的行数 * 内层表索引的高度
,极大的提升了 join的性能。 -
当
order
表的user_id
为索引的时候执行过程会如下图:
注意:使用Index Nested-Loop Join 算法的前提是匹配的字段必须建立了索引。
3)、Block Nested-Loop Join( 块嵌套循环连接 )
-
如果 join 的字段有索引,MySQL 会使用 INL 算法。如果没有的话,MySQL 会如何处理?
-
因为不存在索引了,所以被驱动表需要进行扫描。这里 MySQL 并不会简单粗暴的应用 SNL 算法,而是加入了 buffer 缓冲区,降低了内循环的个数,也就是被驱动表的扫描次数。
-
在外层循环扫描 user表中的所有记录。扫描的时候,会把需要进行 join 用到的列都缓存到 buffer 中。buffer 中的数据有一个特点,里面的记录不需要一条一条地取出来和 order 表进行比较,而是整个 buffer 和 order表进行批量比较。
-
如果我们把 buffer 的空间开得很大,可以容纳下 user 表的所有记录,那么 order 表也只需要访问一次。
-
MySQL 默认 buffer 大小 256K,如果有 n 个 join 操作,会生成 n-1 个 join buffer。
mysql> show variables like '%join_buffer%'; +------------------+--------+ | Variable_name | Value | +------------------+--------+ | join_buffer_size | 262144 | +------------------+--------+ mysql> set session join_buffer_size=262144; Query OK, 0 rows affected (0.00 sec)
-
4、总结
-
永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量)
-
为匹配的条件增加索引(减少内层表的循环匹配次数)
-
增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)
-
减少不必要的字段查询(字段越少,join buffer 所缓存的数据就越多
4.1.2、in和exists函数
上面我们说了 小表驱动大表,就是小的数据集驱动大的数据集, 主要是为了减少数据库的连接次数,根据具体情况的不同,又出现了两个函数 exists
和 in
函数
创建部门表与员工表,并插入数据
-- 部门表
CREATE TABLE department (
id INT(11) PRIMARY KEY,
deptName VARCHAR(30) ,
address VARCHAR(40)
) ;
-- 部门表测试数据
INSERT INTO `department` VALUES (1, '研发部', '1层');
INSERT INTO `department` VALUES (2, '人事部', '3层');
INSERT INTO `department` VALUES (3, '市场部', '4层');
INSERT INTO `department` VALUES (5, '财务部', '2层');
-- 员工表
CREATE TABLE employee (
id INT(11) PRIMARY KEY,
NAME VARCHAR(20) ,
dep_id INT(11) ,
age INT(11) ,
salary DECIMAL(10, 2)
);
-- 员工表测试数据
INSERT INTO `employee` VALUES (1, '鲁班', 1, 15, 1000.00);
INSERT INTO `employee` VALUES (2, '后裔', 1, 22, 2000.00);
INSERT INTO `employee` VALUES (4, '阿凯', 2, 20, 3000.00);
INSERT INTO `employee` VALUES (5, '露娜', 2, 30, 3500.00);
INSERT INTO `employee` VALUES (6, '李白', 3, 25, 5000.00);
INSERT INTO `employee` VALUES (7, '韩信', 3, 50, 5000.00);
INSERT INTO `employee` VALUES (8, '蔡文姬', 3, 35, 4000.00);
INSERT INTO `employee` VALUES (3, '孙尚香', 4, 20, 2500.00);
1、in 函数
-
假设:department表的数据小于 employee表数据,将所有部门下的员工都查出来,应该使用 in 函数。
-- 编写SQL,使in函数 SELECT * FROM employee e WHERE e.dep_id IN (SELECT id FROM department);
-
in函数的执行原理
-
in
语句,只执行一次,将department
表中的所有id字段查询出来并且缓存。 -
检查
department
表中的id与employee
表中的dep_id
是否相等,如果相等 添加到结果集,直到遍历完department
所有的记录。
-- 先循环: select id from department; 相当于得到了小表的数据 for(i = 0; i < $dept.length; i++){ -- 小表 -- 后循环: select * from employee where e.dep_id = d.id; for(j = 0 ; j < $emp.legth; j++){ -- 大表 if($dept[i].id == $emp[j].dep_id){ $result[i] = $emp[j] break; } } }
-
结论: 如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用
in
。
2、exists 函数
-
假设: department表的数据大于 employee表数据,将所有部门下的的员工都查出来,应该使用 exists 函数。
explain SELECT * FROM employee e WHERE EXISTS (SELECT id FROM department d WHERE d.id = e.dep_id);
-
exists
特点exists
子句返回的是一个 布尔值,如果有返回数据,则返回值是true
,反之是false
。如果结果为
true
,外层的查询语句会进行匹配,否则外层查询语句将不进行查询或者查不出任何记录。
-
exists 函数的执行原理
-- 先循环: SELECT * FROM employee e; -- 再判断: SELECT id FROM department d WHERE d.id = e.dep_id for(j = 0; j < $emp.length; j++){ -- 小表 -- 遍历循环外表,检查外表中的记录有没有和内表的的数据一致的, 匹配得上就放入结果集。 if(exists(emp[i].dep_id)){ -- 大表 $result[i] = $emp[i]; } }
3、in 和 exists 的区别
-
如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用
in
。 -
如果主查询得出的结果集记录较少,子查询中的表较大且又有索引时应该用
exists
。 -
一句话: in后面跟的是小表,exists后面跟的是大表。
4.2、order by优化
MySQL中的两种排序方式
-
索引排序: 通过有序索引顺序扫描直接返回有序数据
-
额外排序: 对返回的数据进行文件排序
-
ORDER BY优化的核心原则: 尽量减少额外的排序,通过索引直接返回有序数据。
4.2.1、索引排序
因为索引的结构是B+树,索引中的数据是按照一定顺序进行排列的,所以在排序查询中如果能利用索引,就能避免额外的排序操作。EXPLAIN分析查询时,Extra显示为Using index。
比如查询条件是 where age = 21 order by name
,那么查询过程就是会找到满足 age = 21
的记录,而符合这条的所有记录一定是按照 name 排序的,所以也不需要额外进行排序.
4.2.2、额外排序
所有不是通过索引直接返回排序结果的操作都是Filesort排序,也就是说进行了额外的排序操作。EXPLAIN分析查询时,Extra显示为Using filesort。
1) 按执行位置划分
-
Sort_Buffer MySQL 为每个线程各维护了一块内存区域 sort_buffer ,用于进行排序。sort_buffer 的大小可以通过 sort_buffer_size 来设置。
mysql> show variables like '%sort_buffer_size%'; +-------------------------+---------+ | Variable_name | Value | +-------------------------+---------+ | sort_buffer_size | 262144 | +-------------------------+---------+ mysql> select 262144 / 1024; +---------------+ | 262144 / 1024 | +---------------+ | 256.0000 | +---------------+
注: sort_Buffer_Size 并不是越大越好,由于是connection级的参数,过大的设置+高并发可能会耗尽系统内存资源。
-
Sort_Buffer + 临时文件
如果加载的记录字段总长度(可能是全字段也可能是 rowid排序的字段)小于 sort_buffer_size 便使用 sort_buffer 排序;如果超过则使用 sort_buffer + 临时文件进行排序。
临时文件种类:
临时表种类由参数 tmp_table_size 与临时表大小决定,如果内存临时表大小超过 tmp_table_size ,那么就会转成磁盘临时表。因为磁盘临时表在磁盘上,所以使用内存临时表的效率是大于磁盘临时表的。
2) 按执行方式划分
执行方式是由 max_length_for_sort_data
参数与用于排序的单条记录字段长度决定的,如果用于排序的单条记录字段长度 <= max_length_for_sort_data
,就使用全字段排序;反之则使用 rowid 排序。
mysql> show variables like 'max_length_for_sort_data';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| max_length_for_sort_data | 1024 |
+--------------------------+-------+
2.1) 全字段排序
全字段排序就是将查询的所有字段全部加载进来进行排序。
-
优点:查询快,执行过程简单。
-
缺点:需要的空间大。
select name,age,add from user where addr = '北京' order by name limit 1000; -- addr有索引
上面查询语句的执行流程:
-
初始化 sort_buffer,确定放入 name、age、addr 这3个字段。
-
从索引 addr 中找到第一个满足 addr=’北京’ 的主键ID(ID_x)。
-
到主键索引中找到 ID_x,取出整行,取 name、addr、age 3个字段的值,存入 sort_buffer。
-
从索引 addr 取下一个记录的主键ID。
-
重复3、4,直到 addr 值不满足条件。
-
对 sort_buffer 中的数据按照 name 做快速排序。
-
把排序结果中的前1000行返回给客户端。
2.2) rowid排序
rowid 排序相对于全字段排序,不会把所有字段都放入sort_buffer。所以在sort buffer中进行排序之后还得回表查询。
-
缺点:会产生更多次数的回表查询,查询可能会慢一些。
-
优点:所需的空间更小
select name,age,add from user where addr = '北京' order by name limit 1000; -- addr有索引
假设 name、age、addr3个字段定义的总长度为36,而 max_length_for_sort_data = 16,就是单行的长度超了,MySQL认为单行太大,需要换一个算法。 放入 sort_buffer 的字段就会只有要排序的字段 name,和主键 id,那么排序的结果中就少了 addr 和 age,就需要回表了。
上面查询语句的执行流程:
-
初始化 sort_buffer,确定放入2个字段,name 和 id。
-
从索引 addr 中找到第一个满足addr=’北京’的主键ID(ID_x)。
-
到主键索引中取出整行,把 name、id 这2个字段放入 sort_buffer。
-
从索引 addr 取下一个记录的主键ID。
-
重复3、4,直到addr值不满足条件。
-
对 sort_buffer 中的数据按照 name 做快速排序。
-
取排序结果中的前1000行,并按照 id 的值到原表中取出 name、age、addr 3个字段的值返回给客户端。
总结
-
如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer中, 这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
-
MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。 对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
4.2.3、排序优化
添加索引
-
为
employee
表 创建索引-- 联合索引 ALTER TABLE employee ADD INDEX idx_name_age(NAME,age); -- 为薪资字段添加索引 ALTER TABLE employee ADD INDEX idx_salary(salary);
-
查看
employee
表的索引情况SHOW INDEX FROM employee;
场景1: 只查询用于排序的 索引字段,可以利用索引进行排序,最左原则。
-
查询
name, age
两个字段,并使用name
与age
行排序。EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name,e.age;
场景2: 排序字段在多个索引中,无法使用索引排序
-
查询
name , salary
字段,并使用name
与salary
排序。EXPLAIN SELECT e.name, e.salary FROM employee e ORDER BY e.name,e.salary;
场景3: 只查询用于排序的索引字段和主键, 可以利用索引进行排序
-
查询
id , name
,使用name
排序。EXPLAIN SELECT e.id, e.name FROM employee e ORDER BY e.name;
场景4: 查询主键之外的没有添加索引的字段,不会利用索引排序
-
查询
dep_id
,使用name
进行排序。EXPLAIN SELECT e.dep_id FROM employee e ORDER BY e.name;
场景5: 排序字段顺序与索引列顺序不一致,无法利用索引排序
-
使用联合索引时,ORDER BY子句也要求,排序字段顺序和联合索引列顺序匹配。
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.age,e.name;
场景6: where 条件是 范围查询时, 会使order by 索引 失效
-
比如 添加一个条件 :
age > 18
,然后再根据age
排序.EXPLAIN SELECT e.name, e.age FROM employee e WHERE e.age > 10 ORDER BY e.age;
-
注意: ORDERBY子句不要求必须索引中第一列,没有仍然可以利用索引排序。但是有个前提条件,只有在等值过滤时才可以,范围查询时不
EXPLAIN SELECT e.name, e.age FROM employee e WHERE e.age = 18 ORDER BY e.age;
场景7: 升降序不一致,无法利用索引排序
-
ORDER BY排序字段要么全部正序排序,要么全部倒序排序,否则无法利用索引排序。
-- 升序 EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name , e.age ; -- 降序 EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name DESC, e.age DESC;
-
name字段升序,age字段降序,索引失效
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name, e.age DESC;
4.3、索引单表优化案例
4.3.1、建表
创建表 插入数据
下面是一张用户通讯表
的表结构信息,这张表来源于真实企业的实际项目中,有接近500万条数据。
CREATE TABLE user_contacts (
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT(11) DEFAULT NULL COMMENT '用户标识',
mobile VARCHAR(50) DEFAULT NULL COMMENT '手机号',
NAME VARCHAR(20) DEFAULT NULL COMMENT '姓名',
verson INT(11) NOT NULL DEFAULT '0' COMMENT '版本',
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建者',
create_date DATETIME NOT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新者',
update_date DATETIME NOT NULL COMMENT '更新时间',
remarks VARCHAR(255) DEFAULT NULL COMMENT '备注信息',
del_flag CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (id)
);
-- 数据:资料 sql脚本中(测试前需删除表全部索引)
4.3.2、单表索引分析
4.3.2.1、需求一
查询所有名字中包含李的用户姓名和手机号,并根据user_id字段排序
SELECT NAME, mobile FROM user_contacts WHERE NAME LIKE '李%' ORDER BY user_id;
通过explain命令 查看SQL查询优化信息
EXPLAIN SELECT NAME, mobile FROM user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;
结论:很显然type是ALL,即最坏情况。Extra里还出现Using filesort(文件内排序,未使用到索引),也是最坏情况,所以优化是必须的。
4.3.2.2、优化
1、首先添加联合索引,该联合索引包含所有要查询的字段,使其成为覆盖索引,一并解决like模糊查询时索引失效问题。
-- 添加联合索引
ALTER TABLE user_contacts ADD INDEX idx_nmu(NAME,mobile,user_id);
2、进行分析
EXPLAIN SELECT NAME, mobile FROM user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;
3、结果: type的类型提升到了index,但是 Using filesort 还有。
分析结果显示: type连接类型提升到了index级别,通过索引就获取到了全部数据,但是Extra字段中还是存在 Using filesort.
4、继续优化: 根根据最佳左前缀法则,之后最左侧列是有序的,在创建联合索引时,正确的顺序应该是:user_id,NAME,mobile
-- 删除索引
DROP INDEX idx_nmu ON user_contacts
-- 添加重新排序后的索引
ALTER TABLE user_contacts ADD INDEX idx_unm(user_id,NAME,mobile);
5、执行查询,发现type=index,Using filesort没有了。
EXPLAIN SELECT NAME, mobile FROM user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;
4.3.2.3、需求二
统计手机号是135、136、186、187开头的用户数量.
EXPLAIN SELECT COUNT(*) FROM user_contacts WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
通过explain命令 查看SQL查询优化信息
type=index
:用到了索引,但是进行了索引全表扫描。
key=idx_unm
:使用到了联合索引,但是效果并不是很好。
Extra=Using where; Using index
:查询的列被索引覆盖了,但是无法通过该索引直接获取数据。
综合上面的执行计划给出的信息,需要进行优化。
4.3.2.4、优化
1、经过上面的分析,发现联合索引没有发挥作用,所以尝试对 mobile字段单独建立索引。
ALTER TABLE user_contacts ADD INDEX idx_m(mobile);
2、再次执行,得到下面的分析结果
EXPLAIN SELECT COUNT(*) FROM user_contacts WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
type=range
: 使用了索引进行范围查询,常见于使用>,>=,<,<=,BETWEEN,IN() 或者 like 等运算符的查询中。
key=idx_m
: mysql选择了我们为mobile字段创建的索引,进行数据检索。
rows=1575026
: 为获取所需数据而进行扫描的行数,比之前减少了近三分之一。
count(*) 和 count(1)和count(列名)区别
进行统计操作时,count中的统计条件可以三种选择:
EXPLAIN SELECT COUNT(*) FROM user_contacts
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
EXPLAIN SELECT COUNT(id) FROM user_contacts
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
EXPLAIN SELECT COUNT(1) FROM user_contacts
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
执行效果:
count(*) 包括了所有的列,在统计时 不会忽略列值为null的数据.
count(1) 用1表示代码行,在统计时,不会忽略列值为null的数据.
count(列名)在统计时,会忽略列值为空的数据,就是说某个字段的值为null时不统计.
执行效率:
列名为主键, count(列名)会比count(1)快
列名为不是主键, count(1)会比count(列名)快
如果表没有主键,count(1)会比count(*)快
如果表只有一个字段,则count(*) 最优.
4.3.2.5、需求三
查询2017-2-16日,新增的用户联系人信息,查询字段:name、mobile。
EXPLAIN SELECT NAME,mobile FROM user_contacts WHERE DATE_FORMAT(create_date,'%Y-%m-%d')='2017-02-16';
4.3.2.6、优化
-
explain分析的结果显示
type=ALL
:进行了全表扫描,需要进行优化,为create_date字段添加索引。ALTER TABLE user_contacts ADD INDEX idx_cd(create_date); EXPLAIN SELECT NAME,mobile FROM user_contacts WHERE DATE_FORMAT(create_date,'%Y-%m-%d')='2017-02-16';
添加索引后,发现并没有使用到索引 key=null
。
- 分析原因: create_date字段是datetime类型,转换为日期再匹配,需要查询出所有行进行过滤,所以导致索引失效。
继续优化:
-
改为使用 between … and …,使索引生效。
EXPLAIN SELECT NAME,mobile FROM user_contacts WHERE create_date BETWEEN '2017-02-16 00:00:00' AND '2017-02-16 23:59:59';
type=range
:使用了索引进行范围查询Extra=Using index condition; Using MRR
:Using index condition 表示使用了部分索引,MRR表示InnoDB存储引擎 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。
4.3.2.7、需求四
获取用户通讯录表第10万条数据开始后的100条数据.
EXPLAIN SELECT * FROM user_contacts uc LIMIT 100000,100;
-- 查询记录量越来越大,所花费的时间也会越来越多
EXPLAIN SELECT * FROM user_contacts uc LIMIT 1000000,1000;
EXPLAIN SELECT * FROM user_contacts uc LIMIT 2000000,10000;
EXPLAIN SELECT * FROM user_contacts uc LIMIT 3000000,100000;
LIMIT 子句可以被用于指定 SELECT 语句返回的记录数。需注意以下几点:
-
第一个参数指定第一个返回记录行的偏移量,注意从0开始()
-
第二个参数指定返回记录行的最大数目
-
如果只给定一个参数:它表示返回最大的记录行数目
-
初始记录行的偏移量是 0(而不是 1)
优化1:通过索引进行分页
直接进行limit操作会产生全表扫描,速度很慢。Limit限制的是从结果集的M位置处取出N条输出,其余抛弃。
假设ID是连续递增的,我们根据查询的页数和查询的记录数可以算出查询的id的范围,然后配合 limit使用。
EXPLAIN SELECT * FROM user_contacts WHERE id >= 100001 LIMIT 100;
type类型提升到了 range级别
优化2:使用子查询优化
-- 首先定位偏移位置的id
SELECT id FROM user_contacts LIMIT 100000,1;
-- 根据获取到的id值向后查询.
EXPLAIN SELECT * FROM user_contacts WHERE id >=
(SELECT id FROM user_contacts LIMIT 100000,1) LIMIT 100;
4.4、索引多表优化案例
4.4.1、准备数据
用户手机认证表
-
该表约有11万数据,保存的是通过手机认证后的用户数据
-
关联字段:
user_id
CREATE TABLE `mob_autht` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标识',
`user_id` int(11) NOT NULL COMMENT '用户标识',
`mobile` varchar(11) NOT NULL COMMENT '手机号码',
`seevc_pwd` varchar(12) NOT NULL COMMENT '服务密码',
`autht_indc` varchar(1) NOT NULL DEFAULT '0' COMMENT '认证标志',
`verson` int(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_date` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_date` datetime NOT NULL COMMENT '更新时间',
`remarks` varchar(255) DEFAULT NULL COMMENT '备注信息',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`)
) ;
紧急联系人表
-
该表约有22万数据,注册成功后,用户添加的紧急联系人信息.
-
关联字段:
user_id
CREATE TABLE `ugncy_cntct_psn` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标识',
`psn_info_id` int(11) DEFAULT NULL COMMENT '个人信息标识',
`user_id` int(11) NOT NULL COMMENT '向钱用户标识',
`cntct_psn_name` varchar(10) NOT NULL COMMENT '联系人姓名',
`cntct_psn_mob` varchar(11) NOT NULL COMMENT '联系手机号',
`and_self_rltn_cde` char(2) NOT NULL COMMENT '与本人关系代码 字典表关联',
`verson` int(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_date` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_date` datetime NOT NULL COMMENT '更新时间',
`remarks` varchar(255) DEFAULT NULL COMMENT '备注信息',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`)
) ;
借款申请表
-
该表约有11万数据,保存的是每次用户申请借款时 填写的信息.
-
关联字段:
user_id
CREATE TABLE `loan_apply` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '借款申请标识',
`loan_nbr` VARCHAR(50) NOT NULL COMMENT '借款编号',
`user_id` INT(11) NOT NULL COMMENT '用户标识',
`idnt_info_id` INT(11) DEFAULT NULL COMMENT '身份信息标识',
`psn_info_id` INT(11) DEFAULT NULL COMMENT '个人信息标识',
`mob_autht_id` INT(11) DEFAULT NULL COMMENT '手机认证标识',
`bnk_card_id` INT(11) DEFAULT NULL COMMENT '银行卡标识',
`apply_limit` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '申请额度',
`apply_tlmt` INT(3) NOT NULL COMMENT '申请期限',
`apply_time` DATETIME NOT NULL COMMENT '申请时间',
`audit_limit` DECIMAL(16,2) NOT NULL COMMENT '审核额度',
`audit_tlmt` INT(3) NOT NULL COMMENT '审核期限',
`audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
`cfrm_limit` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '确认额度',
`cfrm_tlmt` INT(3) NOT NULL COMMENT '确认期限',
`cfrm_time` DATETIME DEFAULT NULL COMMENT '确认时间',
`loan_sts_cde` CHAR(1) NOT NULL COMMENT '借款状态:0 未提交 1 提交申请(初始) 2 已校验 3 通过审核4 未通过审核 5开始放款 6放弃借款 7 放款成功 ',
`audit_mod_cde` CHAR(1) NOT NULL COMMENT '审核模式:1 人工 2 智能',
`day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '日利率',
`seevc_fee_day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '服务费日利率',
`normal_paybk_tot_day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '正常还款总日利率',
`ovrdu_fee_day_rate` DECIMAL(16,8) DEFAULT NULL COMMENT '逾期违约金日利率',
`day_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '日利率金额',
`seevc_fee_day_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '服务日利率金额',
`normal_paybk_tot_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '综合日利率金额',
`cnl_resn_time` DATETIME DEFAULT NULL COMMENT '放弃时间',
`cnl_resn_cde` CHAR(8) DEFAULT NULL COMMENT '放弃原因:关联字典代码',
`cnl_resn_othr` VARCHAR(255) DEFAULT NULL COMMENT '放弃的其他原因',
`verson` INT(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`create_date` DATETIME NOT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`update_date` DATETIME NOT NULL COMMENT '更新时间',
`remarks` VARCHAR(255) DEFAULT NULL COMMENT '备注信息',
`loan_dst_cde` CHAR(1) NOT NULL DEFAULT '0' COMMENT '0,未分配; 1,已分配',
`del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
`last_loan_apply_id` INT(11) DEFAULT NULL COMMENT '上次借款申请标识',
PRIMARY KEY (`id`),
UNIQUE KEY `ind_loan_nbr` (`loan_nbr`) USING BTREE
) ;
4.4.2、需求一
查询所有认证用户的手机号以及认证用户的紧急联系人的姓名与手机号信息
explain select
ma.mobile '认证用户手机号',
ucp.cntct_psn_name '紧急联系人姓名',
ucp.cntct_psn_mob '紧急联系人手机号'
from mob_autht ma left join ugncy_cntct_psn ucp
on ma.user_id = ucp.user_id;
type 类型都是ALL,使用了全表扫描。
4.4.3、优化
为mob_autht
表的 user_id字段添加索引
ALTER TABLE mob_autht ADD INDEX idx_user_id (user_id);
-
根据小结果及驱动大结果集的原则,
mob_autht
是驱动表,驱动表即使建立索引也不会生效。 -
一般情况下: 左外连接左表是驱动表,右外连接右表就是驱动表。
-
explain分析结果的第一行的表,就是驱动表。
继续优化:为ugncy_cntct_psn
表的 user_id字段添加索引
ALTER TABLE ugncy_cntct_psn ADD INDEX idx_userid(user_id);
mob_autht
的type类型为ALL,
ugncy_cntct_psn`的type类型是ref
4.4.4、需求二
获取所有智能审核的用户手机号和申请额度、申请时间、审核额度。
EXPLAIN SELECT
ma.mobile '用户认证手机号',
la.apply_limit '申请额度',
la.apply_time '申请时间',
la.audit_limit '审核额度'
FROM mob_autht ma inner JOIN loan_apply la ON ma.id = la.mob_autht_id
WHERE la.audit_mod_cde = '2';
4.4.5、优化分析
-
查询
loan_apply
表,使用的条件字段为audit_mod_cde
,因为该字段没有添加索引,导致type=ALL
发生全表扫描。 -
为
audit_mod_cde
字段添加索引,来提高查询效率。ALTER TABLE loan_apply ADD INDEX idx_amc(audit_mod_cde);
添加索引后type的类型确实提升了,但是需要注意的扫描的行还是很高,并且 Extra字段的值为
Using where
表示: 通过索引访问时,需要再回表访问所需的数据.注意: 如果执行计划中显示走了索引,但是rows值很高,extra显示为using where,那么执行效果就不会很好。因为索引访问的成本主要在回表上.
继续优化
-
audit_mod_cde
字段的含义是审核模式,只有两个值:1 人工 2 智能,所以在根据该字段进行查询时,会有大量的相同数据。 -
比如:统计一下
audit_mod_cde = '2'
的数据总条数,查询结果是9万多条,该表的总数接近11万条,查询出的数据行超过了表的总记录数的30%,这时就不建议添加索引 ( 比如有1000万的数据,就算平均分后结果集也有500万条,结果集还是太大,查询效率依然不高 )。SELECT COUNT(*) FROM loan_apply; -- 109181条 SELECT COUNT(*) FROM loan_apply la WHERE la.audit_mod_cde = '2' ; -- 91630条
-
总结:唯一性太差的字段不需要创建索引,即便用于where条件。
继续优化
如果一定要根据状态字段进行查询,我们可以根据业务需求 添加一个日期条件,比如获取某一时间段的数据,然后再区分状态字段。
-- 获取2017年 1月1号~1月5号的数据
EXPLAIN SELECT
ma.mobile '用户认证手机号',
la.apply_time '申请时间',
la.apply_limit '申请额度',
la.audit_limit '审核额度'
FROM loan_apply la INNER JOIN mob_autht ma ON la.mob_autht_id = ma.id
WHERE apply_time BETWEEN '2017-01-01 00:00:00'
AND '2017-01-05 23:59:59' AND la.audit_mod_cde = '2';
extra = Using index condition;
: 只有一部分索引生效
MRR
算法: 通过范围扫描将数据存入 read_rnd_buffer_size
,然后对其按照 Primary Key(RowID)排序,最后使用排序好的数据进行顺序回表,因为 InnoDB 中叶子节点数据是按照 Primary Key(RowID)进行排列的,这样就转换随机IO为顺序IO了,从而减小磁盘的随机访问。
五、索引优化原则&失效情况
创建表 插入数据
drop table users;
CREATE TABLE users(
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(20) NOT NULL COMMENT '姓名',
user_age INT NOT NULL DEFAULT 0 COMMENT '年龄',
user_level VARCHAR(20) NOT NULL COMMENT '用户等级',
reg_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
);
INSERT INTO users(user_name,user_age,user_level,reg_time)
VALUES('tom',17,'A',NOW()),('jack',18,'B',NOW()),('lucy',18,'C',NOW());
创建联合索引
ALTER TABLE users ADD INDEX idx_nal (user_name,user_age,user_level) USING BTREE;
5.1、全值匹配
按索引字段顺序匹配使用。
EXPLAIN SELECT * FROM users WHERE user_name = 'tom';
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17
AND user_level = 'A';
按顺序使用联合索引时, type类型都是 ref ,使用到了索引 效率比较高
5.2、最佳左前缀法则
如果创建的是联合索引,就要遵循 最佳左前缀法则: 使用索引时,where后面的条件需要从索引的最左前列开始并且不跳过索引中的列使用。
场景1:按照索引字段顺序使用,三个字段都使用了索引,没有问题。
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';
场景2:直接跳过user_name使用索引字段,索引无效,未使用到索引。
EXPLAIN SELECT * FROM users WHERE user_age = 17 AND user_level = 'A';
场景3:不按照创建联合索引的顺序,使用索引。
EXPLAIN SELECT * FROM users WHERE user_age = 17 AND user_name = 'tom' AND user_level = 'A';
where后面查询条件顺序是 user_age、user_level、user_name与我们建的索引顺序user_name、user_age、user_level不一致,为什么还是使用了索引,这是因为MySql底层优化器给咱们做了优化。
但是,最好还是要按照顺序使用索引。
最佳左前缀底层原理
MySQL创建联合索引的规则是:首先会对联合索引最左边的字段进行排序 ( 例子中是 user_name
),在第一个字段的基础之上 再对第二个字段进行排序 ( 例子中是 user_age
) 。
所以:最佳左前缀原则其实是个B+树的结构有关系,最左字段肯定是有序的,第二个字段则是无序的(联合索引的排序方式是:先按照第一个字段进行排序,如果第一个字段相等再根据第二个字段排序)。所以如果直接使用第二个字段 user_age
通常是使用不到索引的。
5.3、不要在索引列上做任何计算
不要在索引列上做任何操作,比如计算、使用函数、自动或手动进行类型转换,会导致索引失效,从而使查询转向全表扫描。
-- 插入数据
INSERT INTO users(user_name,user_age,user_level,reg_time) VALUES('11223344',22,'D',NOW());
场景1:使用系统函数 left()函数
EXPLAIN SELECT * FROM users WHERE LEFT(user_name, 6) = '112233';
where条件使用计算后的索引字段 user_name,没有使用索引,索引失效。
场景2:字符串不加单引号 (隐式类型转换)
EXPLAIN SELECT * FROM users WHERE user_name = 11223344;
注:
Extra = Using where
表示Mysql将对storage engine提取的结果进行过滤,过滤条件字段无索引;
( 需要回表去查询所需的数据 )
5.4、范围之后全失效
存储引擎不能使用索引中范围条件右边的列
场景1:条件单独使用user_name时,type=ref
, key_len=62
-- 条件只有一个 user_name
EXPLAIN SELECT * FROM users WHERE user_name = 'tom';
场景2:条件增加一个 user_age ( 使用常量等值),type= ref
,key_len = 66
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17;
场景3:使用全值匹配,type = ref
, key_len = 168
,索引都利用上了。
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';
场景4:使用范围条件时,avg > 17,type = range
, key_len = 66
,与场景3 比较,可以发现 user_level
索引没有用上。
EXPLAIN SELECT * FROM users WHERE user_name = 'tom'
AND user_age > 17 AND user_level = 'A';
5.5、尽量使用覆盖索引
尽量使用覆盖索引(查询列和索引列尽量一致,通俗说就是对A、B列创建了索引,然后查询中也使用A、B列),减少select *的使用。
场景1:全值匹配查询, 使用 select *
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';
场景2:全值匹配查询, 使用 select 字段名1 ,字段名2
EXPLAIN SELECT user_name , user_age , user_level FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';
使用覆盖索引(查询列与条件列对应),可看到Extra从Null变成了Using index,提高检索效率。
注:
Using index
表示 使用到了索引,并且所取的数据完全在索引中就能拿到。(使用覆盖索引的时候就会出现)
5.6、使用不等于(!=或<>)会使索引失效
使用 != 会使type=ALL,key=Null,导致全表扫描,并且索引失效。
使用 !=
EXPLAIN SELECT * FROM users WHERE user_name != 'tom';
5.7、is null 或 is not null也无法使用索引
在使用is null的时候,索引完全失效,使用is not null的时候,type=ALL全表扫描,key=Null索引失效。
场景1:使用 is null
EXPLAIN SELECT * FROM users WHERE user_name IS NULL;
场景2:使用 not null
EXPLAIN SELECT * FROM users WHERE user_name IS NOT NULL;
5.8、like通配符以%开头会使索引失效
like查询为范围查询,%出现在左边,则索引失效。%出现在右边索引未失效。口诀:like百分加右边。
场景1
EXPLAIN SELECT * FROM users WHERE user_name LIKE '%tom%';
场景2
EXPLAIN SELECT * FROM users WHERE user_name LIKE '%tom';
场景3
EXPLAIN SELECT * FROM users WHERE user_name LIKE 'tom%';
注:
Using index condition
表示查找使用了索引,但是需要查询数据。
解决%出现在左边索引失效的方法:使用覆盖索引。
Case1:
EXPLAIN SELECT user_name FROM users WHERE user_name LIKE '%jack%';
对比场景1可以知道,通过使用覆盖索引 type = index,并且使用了 Using index,从全表扫描变成了全索引扫描。
注:
Useing where; Using index;
查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据
Case2:
EXPLAIN SELECT id FROM users WHERE user_name LIKE '%jack%';
这里出现 type=index
,因为主键自动创建唯一索引。
Case3:
EXPLAIN SELECT user_name,user_age FROM users WHERE user_name LIKE '%jack%';
EXPLAIN SELECT user_name,user_age,user_level FROM users WHERE user_name LIKE '%jack%';
EXPLAIN SELECT id,user_name,user_age,user_level FROM users WHERE user_name LIKE '%jack%';
上面三组, explain执行的结果都相同,表明都使用了索引.
Case4:
EXPLAIN SELECT id,user_name,user_age,user_level,reg_time FROM users WHERE user_name
LIKE '%jack%';
分析:由于只在(user_name,user_age,user_level)上创建索引, 当包含reg_time时,导致结果集偏大(reg_time未建索引)【锅大,锅盖小,不能匹配】,所以type=ALL。
like 失效的原理
-
%号在右: 由于B+树的索引顺序,是按照首字母的大小进行排序,%号在右的匹配又是匹配首字母。所以可以在B+树上进行有序的查找,查找首字母符合要求的数据。所以有些时候可以用到索引。
-
%号在左: 是匹配字符串尾部的数据,我们上面说了排序规则,尾部的字母是没有顺序的,所以不能按照索引顺序查询,就用不到索引。
-
两个%%号: 这个是查询任意位置的字母满足条件即可,只有首字母是进行索引排序的,其他位置的字母都是相对无序的,所以查找任意位置的字母是用不上索引的。
5.9、字符串不加单引号导致索引失效
varchar类型的字段,在查询的时候不加单引号导致索引失效,转向全表扫描。
场景1
-- 两条sql语句都能查询出相同的数据
SELECT * FROM users WHERE user_name = '11223344';
SELECT * FROM users WHERE user_name = 11223344;
场景2:
通过explain执行结果可以看出,字符串(name)不加单引号在查询的时候,导致索引失效(type=ref变成了type=ALL,并且key=Null),并全表扫描。
5.10、少用or,用or连接会使索引失效
在使用or连接的时候 type=ALL
,key=Null,索引失效,并全表扫描。
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' OR user_name = 'jack';
六、工程优化如何实现
58到家数据库30条军规解读
军规适用场景:并发量大、数据量大的互联网业务
6.1、基础规范
1、必须使用InnoDB存储引擎
解读:支持事务、行级锁、并发性能更好、CPU及内存缓存页优化使得资源利用率更高
2、必须使用UTF8字符集
解读:万国码,无需转码,无乱码风险,节省空间
3、数据表、数据字段必须加入中文注释
解读:N年后谁tm知道这个r1,r2,r3字段是干嘛的
4、禁止使用存储过程、视图、触发器、Event
解读:高并发大数据的互联网业务,架构设计思路是“解放数据库CPU,将计算转移到服务层”,并发量大的情况下,这些功能很可能将数据库拖死,业务逻辑放到服务层具备更好的扩展性,能够轻易实现“增机器就加性能”。数据库擅长存储与索引,CPU计算还是上移吧。
5、禁止存储大文件或者大照片
解读:为何要让数据库做它不擅长的事情?大文件和照片存储在文件系统,数据库里存URI多好。
6.2、命名规范
6、只允许使用内网域名,而不是ip连接数据库。
7、线上环境、开发环境、测试环境数据库内网域名遵循命名规范。
业务名称:xxx
- 线上环境:dj.xxx.db
- 开发环境:dj.xxx.rdb
- 测试环境:dj.xxx.tdb
从库在名称后加-s
标识,备库在名称后加-ss
标识
- 线上从库:dj.xxx-s.db
- 线上备库:dj.xxx-sss.db
8、库名、表名、字段名:小写,下划线风格,不超过32个字符,必须见名知意,禁止拼音英文混用。
9、表名t_xxx,非唯一索引名idx_xxx,唯一索引名uniq_xxx。
6.3、表设计规范
10、单实例表数目必须小于500
11、单表列数目必须小于30
12、表必须有主键,例如自增主键。
解读:
-
主键递增,数据行写入可以提高插入性能,可以避免page分裂,减少表碎片提升空间和内存的使用。
-
主键要选择较短的数据类型, Innodb引擎普通索引都会保存主键的值,较短的数据类型可以有效的减少索引的磁盘空间,提高索引的缓存效率。
-
无主键的表删除,在row模式的主从架构,会导致备库夯住。
13、禁止使用外键,如果有外键完整性约束,需要应用程序控制
解读:外键会导致表与表之间耦合,update与delete操作都会涉及相关联的表,十分影响sql 的性能,甚至会造成死锁。高并发情况下容易造成数据库性能,大数据高并发业务场景数据库使用以性能优先
6.4、字段设计规范
14、必须把字段定义为NOT NULL并且提供默认值
解读:
-
null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化。
-
null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时候,数据库的处理性能会降低很多。
-
null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标识。
-
对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、not in这些操作符号。如:where name!=’shenjian’,如果存在name为null值的记录,查询结果就不会包含name为null值的记录。
15、禁止使用TEXT、BLOB类型
解读:会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数据库性能。
16、禁止使用小数存储货币
解读:使用整数吧,小数容易导致钱对不上
17、必须使用varchar(20)存储手机号
-
涉及到区号或者国家代号,可能出现±()
-
手机号会去做数学运算么?
-
varchar可以支持模糊查询,例如:like“138%”
18、禁止使用ENUM,可使用TINYINT代替
-
增加新的ENUM值要做DDL操作
-
ENUM的内部实际存储就是整数,你以为自己定义的是字符串?
6.5、索引设计规范
19、单表索引建议控制在5个以内
20、单索引字段数不允许超过5个
解读:字段超过5个时,实际已经起不到有效过滤数据的作用了。
21、禁止在更新十分频繁、区分度不高的属性上建立索引。
-
更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能。
-
“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似。
22、建立组合索引,必须把区分度高的字段放在前面。
解读:能够更加有效的过滤数据
6.6、SQL使用规范
23、禁止使用SELECT *,只获取必要的字段,需要显示说明列属性
-
读取不需要的列会增加CPU、IO、NET消耗
-
不能有效的利用覆盖索引
-
使用SELECT *容易在增加或者删除字段后出现程序BUG
24、禁止使用INSERT INTO t_xxx VALUES(xxx),必须显示指定插入的列属性。
解读:容易在增加或者删除字段后出现程序BUG
25、禁止使用属性隐式转换
解读:SELECT uid FROM t_user WHERE phone=13812345678 会导致全表扫描,而不能命中phone索引,猜猜为什么?(这个线上问题不止出现过一次)
26、)禁止在WHERE条件的属性上使用函数或者表达式
解读:SELECT uid FROM t_user WHERE from_unixtime(day)>=‘2017-02-15’ 会导致全表扫描
正确的写法是:SELECT uid FROM t_user WHERE day>= unix_timestamp(‘2017-02-15 00:00:00’)
27、禁止负向查询,以及%开头的模糊查询
-
负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
-
%开头的模糊查询,会导致全表扫描
28、禁止大表使用JOIN查询,禁止大表使用子查询。
解读:会产生临时表,消耗较多内存与CPU,极大影响数据库性能
29、禁止使用OR条件,必须改为IN查询。
解读:旧版本Mysql的OR查询是不能命中索引的,即使能命中索引,为何要让数据库耗费更多的CPU帮助实施查询优化呢?
30)应用程序必须捕获SQL异常,并有相应处理。
总结:大数据量高并发的互联网业务,极大影响数据库性能的都不让用,不让用哟。