知识点:Mysql 索引原理完全手册(2)

时间:2022-06-25 05:25:59

知识点:Mysql 索引原理完全手册(1)

知识点:Mysql 索引原理完全手册(2)

知识点:Mysql 索引优化实战(3)

知识点:Mysql 数据库索引优化实战(4)

八、 联合索引与覆盖索引

一 、联合索引

联合索引时指对表上的多个列合起来做一个索引。联合索引的创建方法与单个索引的创建方法一样,不同之处在仅在于有多个索引列,如下

mysql> create table t(
-> a int,
-> b int,
-> primary key(a),
-> key idx_a_b(a,b)
-> );
Query OK, 0 rows affected (0.11 sec)

那么何时需要使用联合索引呢?

从本质上来说,联合索引就是一棵B+树,不同的是联合索引的键值得数量不是1,而是>=2。

接着来讨论两个整型列组成的联合索引,假定两个键值得名称分别为a、b如图 知识点:Mysql 索引原理完全手册(2)

可以看到这与我们之前看到的单个键的B+树并没有什么不同,键值都是排序的,通过叶子结点可以逻辑上顺序地读出所有数据,就上面的例子来说,即(1,1),(1,2),(2,1),(2,4),(3,1),(3,2),数据按(a,b)的顺序进行了存放。 因此,对于查询select * from table where a=xxx and b=xxx, 显然是可以使用(a,b) 这个联合索引的,对于单个列a的查询select * from table where a=xxx,也是可以使用(a,b)这个索引的。

但对于b列的查询select * from table where b=xxx,则不可以使用(a,b) 索引,其实你不难发现原因,叶子节点上b的值为1、2、1、4、1、2显然不是排序的,因此对于b列的查询使用不到(a,b) 索引

联合索引的第二个好处是在第一个键相同的情况下,已经对第二个键进行了排序处理

例如在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以帮我们避免多一次的排序操作,因为索引本身在叶子节点已经排序了,如下

#===========数据表==============
create table base_purchate_log(
userid int unsigned not null,
buy_date date
); insert into base_purchate_log values
(1,'2009-01-01'),
(2,'2009-01-01'),
(3,'2009-01-01'),
(1,'2009-02-01'),
(3,'2009-02-01'),
(1,'2009-03-01'),
(1,'2009-04-01'); alter table base_purchate_log add key(userid);
alter table base_purchate_log add key(userid,buy_date); #===========数据表验证==============
mysql> show create table base_purchate_log;
| base_purchate_log | CREATE TABLE `base_purchate_log` (
`userid` int(10) unsigned NOT NULL,
`buy_date` date DEFAULT NULL,
KEY `userid` (`userid`),
KEY `userid_2` (`userid`,`buy_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 | #可以看到possible_keys在这里有两个索引可以用,分别是单个索引userid与联合索引userid_2,
但是优化器最终选择了使用的key是userid因为该索引的叶子节点包含单个键值,所以理论上一个页能存放的记录应该更多
mysql> explain select * from base_purchate_log where userid=2;
+----+-------------+---------+------+-----------------+--------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+-----------------+--------+---------+-------+------+-------+
| 1 | SIMPLE | base_purchate_log | ref | userid,userid_2 | userid | 4 | const | 1 | |
+----+-------------+---------+------+-----------------+--------+---------+-------+------+-------+
1 row in set (0.00 sec) #接着假定要取出userid为1的最近3次的购买记录,用的就是联合索引userid_2了,因为在这个索引中,在userid=1的情况下,buy_date都已经排序好了
mysql> explain select * from base_purchate_log where userid=1 order by buy_date desc limit 3;
+----+-------------+---------+------+-----------------+----------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+-----------------+----------+---------+-------+------+--------------------------+
| 1 | SIMPLE | base_purchate_log | ref | userid,userid_2 | userid_2 | 4 | const | 4 | Using where; Using index |
+----+-------------+---------+------+-----------------+----------+---------+-------+------+--------------------------+
1 row in set (0.00 sec) #ps:如果extra的排序显示是Using filesort,则意味着在查出数据后需要二次排序 #对于联合索引(a,b),下述语句可以直接使用该索引,无需二次排序
select ... from table where a=xxx order by b; #然后对于联合索引(a,b,c)来首,下列语句同样可以直接通过索引得到结果
select ... from table where a=xxx order by b;
select ... from table where a=xxx and b=xxx order by c; #但是对于联合索引(a,b,c),下列语句不能通过索引直接得到结果,还需要自己执行一次filesort操作,因为索引(a,c)并未排序
select ... from table where a=xxx order by c;

二、 覆盖索引

InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录。

使用覆盖索引的一个好处是:辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作

注意:覆盖索引技术最早是在InnoDB Plugin中完成并实现,这意味着对于InnoDB版本小于1.0的,或者MySQL数据库版本为5.0以下的,InnoDB存储引擎不支持覆盖索引特性

对于InnoDB存储引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key1,priamey key2,...,key1,key2,...)。例如

select age from s1 where id=123 and name = 'duoduo'; #id字段有索引,但是name字段没有索引,该sql命中了索引,但未覆盖,需要去聚集索引中再查找详细信息。
最牛逼的情况是,索引字段覆盖了所有,那全程通过索引来加速查询以及获取结果就ok了
mysql> desc s1;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int(11) | NO | | NULL | |
| name | varchar(20) | YES | | NULL | |
| gender | char(6) | YES | | NULL | |
| email | varchar(50) | YES | | NULL | |
+--------+-------------+------+-----+---------+-------+
4 rows in set (0.21 sec) mysql> explain select name from s1 where id=1000; #没有任何索引
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 2688336 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec) mysql> create index idx_id on s1(id); #创建索引
Query OK, 0 rows affected (4.16 sec)
Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select name from s1 where id=1000; #命中辅助索引,但是未覆盖索引,还需要从聚集索引中查找name
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ref | idx_id | idx_id | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.08 sec) mysql> explain select id from s1 where id=1000; #在辅助索引中就找到了全部信息,Using index代表覆盖索引
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | idx_id | idx_id | 4 | const | 1 | 100.00 | Using index |
+----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.03 sec)

覆盖索引的另外一个好处是对某些统计问题而言的。base_purchate_log,查询计划如下

mysql> explain select count(*) from base_purchate_log;
+----+-------------+---------+-------+---------------+--------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+--------+---------+------+------+-------------+
| 1 | SIMPLE | base_purchate_log | index | NULL | userid | 4 | NULL | 7 | Using index |
+----+-------------+---------+-------+---------------+--------+---------+------+------+-------------+
1 row in set (0.00 sec)

innodb存储引擎并不会选择通过查询聚集索引来进行统计。base_purchate_log,而辅助索引远小于聚集索引,选择辅助索引可以减少IO操作,故优化器的选择如上key为userid辅助索引

对于(a,b)形式的联合索引,一般是不可以选择b中所谓的查询条件。但如果是统计操作,并且是覆盖索引,则优化器还是会选择使用该索引,如下

#联合索引userid_2(userid,buy_date),一般情况,我们按照buy_date是无法使用该索引的,但特殊情况下:
查询语句是统计操作,且是覆盖索引,则按照buy_date当做查询条件时,也可以使用该联合索引
mysql> explain select count(*) from base_purchate_log where buy_date >= '2011-01-01' and buy_date < '2011-02-01';
+----+-------------+---------+-------+---------------+----------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+----------+---------+------+------+--------------------------+
| 1 | SIMPLE | base_purchate_log | index | NULL | userid_2 | 8 | NULL | 7 | Using where; Using index |
+----+-------------+---------+-------+---------------+----------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

九、查询优化神器-explain

强调rows是核心指标,绝大部分rows小的语句执行一定很快(有例外,下面会讲到)。所以优化语句基本上都是在优化rows。

参考官网explain-output

执行计划:让mysql预估执行操作(一般正确)
all < index < range < index_merge < ref_or_null < ref < eq_ref < system/const
id,email 慢:
select * from userinfo3 where name='alex' explain select * from userinfo3 where name='alex'
type: ALL(全表扫描)
select * from userinfo3 limit 1;
快:
select * from userinfo3 where email='alex'
type: const(走索引)

重写非SELECT查询

mysql explain只能解释select查询,并不会对存储程序调用和insert、update、delete或其他语句做解释。

如果重写某些非select查询以利用explain,只需要将该语句转化成一个等价的访问所有相同列的select,任何体积的列都必须在select列表,关联子句,或者where子句中。

假如重写update语句使其可以利用explain

# update语句
UPDATE test.actor
INNER JOIN test.film_actor USING (actor_id)
SET actor.last_update=film_actor.last_update; # 这条explain 语句并不等于上面的update,因为它并不要求服务器从任何一个表上获取last_update列 explain select film_actor.actor_id
from test.actor
inner join test.film_actor USING(actor_id)\G; # 这个差别非常重要。例如,输出结果显示mysql将使用覆盖索引,但是,当检索更新last_updated列时,就无法使用覆盖索引了,下面这种改写法就更接近原来的语句: explain select film_actor.last_update, actor.last_update
from test.actor
inner join test.film_actor USING (actor_id)\G;

MySQL 5.6将允许解释非SELECT查询,一个SELECT查询只需要找到数据的一份副本并返回。

而任何修改数据的查询必须在所有索引上查找并修改其所有副本,这常常比看起来等价的SELECT查询的消耗要高得多。

EXPLAIN中的列

  • 【id列】

这一列总是包含一个编号,识别select所属的行,如果在语句当中没有子查询或联合,那么只会有唯一的select,于是每一行在这个列中都将显示一个1,否则,内层的select语句一般会顺序编号,对应于其在原始语句中的位置。

# 简单的子查询。子句中的子查询和联合给id列增加了更多的复杂性。

explain select (select 1 from test.actor limit 1) from test.film;

# from 子句中的基本子查询。一个匿名的临时表,mysql内部通过别名(der)在外层查询中引用这个临时表,在更复杂的查询中可以看到ref列。

explain select film_id from (select film_id from test.film) as der;

# union 查询。union的结果总是放在一个匿名临时表中,临时表并不在原SQL中出现,因此它的id列为null。
explain select 1 union all select 1;
  • 【select_type列】

显示了对应行是简单还是复杂的select。simple值意味着查询不包括子查询和union,如果查询有任何负责的子部分,则最外层部分标记为primary

  • 【table列】

对应行正在访问哪个表。可以在这一列中从上往下观察mysql的关联优化器为查询选择的关联顺序,mysql的执行计划总是左侧深度优先树,如果把这个计划放倒,就能按顺序读出叶子节点,它们直接对应于explain中的行。

explain select film.film_id
from test.film
inner join test.fillm_actor USING(film_id)
inner join test.actor USING(actor_id);

派生表和联合

mysql创建的匿名临时表仅在查询执行过程中存在.

  • from子句中有子查询 table列是<derivedN>的形式,其中N是子查询的id。这总是“向前引用”——换言之,N指向explain输出中后面的一行。

  • union

union result的table列包含一个参与union的id列表。这总是“向后引用”,因为union result出现在union中所有参与行之后,如果在列表中有超过20个id,table列被截断以防止太长,此时不可能看到所有的值,但是可以推测包括哪些行,因为我们可以看到第一行的id,在这一行和union result之间出现的一切都会以某种方式被包含。

【type列】

这一列显示了“关联类型”,但我们认为更准确的说法是访问类型——即mysql决定如何查找表中的行。

从最差到最优排序:

ALL:index:range:ref:eq_ref:const,system:null

【possible_keys列】

这一列显示了查询可以使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。这个列表是在优化过程的早期创建的,因此有些罗列出来的索引可能对于后续优化过程是没用的。

【key列】

这一列显示了mysql决定采用哪个索引来优化对该表的访问。

如果该索引没有出现在possible_keys列中,那么mysql选用它可能因为其选择了一个覆盖索引,哪怕没有where子句。

换句话说,possible_keys揭示了哪一个索引能有助于高效地进行查找,而key显示的是优化采用哪一个索引可以最小化查询成本。

示例:

explain select actor_id, film_id from film_actor\G;

【key_len列】

该列显示了mysql在索引里使用的字节数

如果mysql正在使用的只是索引里的某些列,那么就可以用这个值来算出具体是哪些列,要记住,mysql 5.5及之前的版本只能使用索引的最左前缀,举例来说,film_actor的主键是两个smallint列,并且每个smallint列是两字节,那么索引中的每项是4字节

mysql并不总是显示一个索引真正使用了多少,例如,如果对一个前缀模式匹配执行like查询,它会显示列的完全宽度正在被使用。

key_len列显示了在索引字段中可能的最大长度,而不是表中数据使用的实际字节数,在前面例子中mysql总是显示13字节,即使a列恰巧只包含一个字符长度。换言之,key_len通过查找表的定义而被计算出,而不是表中的数据。

【ref列】

这一列显示了之前的表在key列记录的索引中查找值所用的列或常量

【rows列】

这一列是mysql为了找到所需的行而要读取的行数。

这个数字是内嵌循环关联计划里的循环数目,它不是mysql认为它最终要从表里读取出来的行数,而是mysql为了找到符合查询的每一点上标准的那些行而必须读取的行的平均数。(这个标准包括sql里给定的条件,以及来自联接次序上前一个表的当前列。)

根据表的统计信息和索引的选用情况,这个估算可能很不精确

要记住这个数字是mysql认为它要检查的行数,而不是结果集里的行数,同时也要认识到有很多优化手段,例如关联缓冲区和缓存,无法影响到行数的显示,mysql可能不必真的读所有它估计到的行,它也不知道任何关于操作系统或硬件缓存的信息。

【Extra列】

这一列包含的是不适合在其他列显示的额外信息。mysql用户手册里记录了大多数可以在这里出现的值。

常见的最重要的值

Using index,Using where,Using temporary,Using filesort,Range checked for each record(index map: N)

树形格式的输出

mysql用户往往更希望把explain的输出格式化成一棵树,更加精确地展示执行计划。

然而,explain查看执行计划的方式确实有点笨拙,树状结构也不适合表格化的输出,当extra列里有大量的值时,缺点更明显,使用union也是这样,union跟mysql能做的其他类型的联接不太一样,它不太适合explain。

MySQL 5.6中的改进

  • 能对类似update、insert等的查询进行解释

尽管可以将dml语句转化为等价的“select”查询并explain,但结果并不会完全反映语句是如何执行的,因而这仍然非常有帮助。在开发使用类似Percona Toolkit中的pt-upgrade时曾尝试使用过那个技术,我们不止一次发现,在将查询转化为select时,优化器并不能按我们预期的代码路径执行。因而explain一个查询而不需要转化为select,对我们理解执行过程中到底发生什么,是非常有帮助的。

  • 对查询优化和执行引擎的一系列修改

允许匿名的临时表尽可能晚地被具体化,而不总是在优化和执行使用到此临时表的部分查询时创建并填充它们,这将允许mysql可以直接解释带子查询的查询语句,而不需要先实际地执行子查询。

  • 在服务器中增加优化跟踪功能的方式改进优化器

这将允许用户查看优化器坐出的抉择,以及输入(例如,索引的基数)和抉择的原因。这对理解服务器选择的执行计划,为什么选择这个计划非常有帮助。

十 、慢查询优化的基本步骤

  • 0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  • 1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
  • 2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
  • 3.order by limit 形式的sql语句让排序的表优先查
  • 4.了解业务方使用场景
  • 5.加索引时参照建索引的几大原则
  • 6.观察结果,不符合预期继续从0分析

十一、 慢日志管理

慢日志
- 执行时间 > 10
- 未命中索引
- 日志文件路径 配置:
- 内存
show variables like '%query%';
show variables like '%queries%';
set global 变量名 = 值
- 配置文件
mysqld --defaults-file='E:\wupeiqi\mysql-5.7.16-winx64\mysql-5.7.16-winx64\my-default.ini' my.conf内容:
slow_query_log = ON
slow_query_log_file = D:/.... 注意:修改配置文件之后,需要重启服务

慢日志的基本操作

MySQL日志管理

  • 错误日志: 记录 MySQL 服务器启动、关闭及运行错误等信息
  • 二进制日志: 又称binlog日志,以二进制文件的方式记录数据库中除 SELECT 以外的操作
  • 查询日志: 记录查询的信息
  • 慢查询日志: 记录执行时间超过指定时间的操作
  • 中继日志: 备库将主库的二进制日志复制到自己的中继日志中,从而在本地进行重放
  • 通用日志: 审计哪个账号、在哪个时段、做了哪些事件
  • 事务日志或称redo日志: 记录Innodb事务相关的如事务执行时间、检查点等

一、bin-log
1. 启用
# vim /etc/my.cnf
[mysqld]
log-bin[=dir\[filename]]
# service mysqld restart
2. 暂停
//仅当前会话
SET SQL_LOG_BIN=0;
SET SQL_LOG_BIN=1;
3. 查看
查看全部:
# mysqlbinlog mysql.000002
按时间:
# mysqlbinlog mysql.000002 --start-datetime="2017-12-05 10:02:56"
# mysqlbinlog mysql.000002 --stop-datetime="2017-12-05 11:02:54"
# mysqlbinlog mysql.000002 --start-datetime="2017-12-05 10:02:56" --stop-datetime="2017-12-05 11:02:54" 按字节数:
# mysqlbinlog mysql.000002 --start-position=260
# mysqlbinlog mysql.000002 --stop-position=260
# mysqlbinlog mysql.000002 --start-position=260 --stop-position=930
4. 截断bin-log(产生新的bin-log文件)
a. 重启mysql服务器
b. # mysql -uroot -p123 -e 'flush logs'
5. 删除bin-log文件
# mysql -uroot -p123 -e 'reset master'
二、查询日志
启用通用查询日志
# vim /etc/my.cnf
[mysqld]
log[=dir\[filename]]
# service mysqld restart
三、慢查询日志

启用慢查询日志

# vim /etc/my.cnf
[mysqld]
log-slow-queries[=dir\[filename]]
long_query_time=n
# service mysqld restart
MySQL 5.6:
slow-query-log=1
slow-query-log-file=slow.log
long_query_time=3
查看慢查询日志
测试:BENCHMARK(count,expr)
SELECT BENCHMARK(50000000,2*3);
四、query_time
  • start_time为slow log中所记录的属性

start_time:看字面意思很容易会被误认为“sql开始的时间”… 但实际上记录的是sql结束的时间。

start_time - query_time 即为sql真正开始的时间。

  • lock_time与query_time为slow log中所记录的两个属性:
  1. lock_time:waiting for xxx lock的时间
  2. query_time:real time + lock time的总时间

实际query_time记录的是lock_time + real time。 query_time ≥ lock_time

tips:有时候一条十分简单的sql也可能执行很长而被记录到slow log,那么可能就需要关注一下lock time是否很大了。

  • long_query_time 为一个MySQL选项参数。

这个参数记录超过执行时间超过该值以上的SQL。

tips:是按真正执行的时间(real time),不包括等待锁的时间。

如果long_query_time设置为1秒 一个insert被lock了10秒,执行只耗了0.5秒,那么不会被记录到慢日志。

五、选项参数
  • log_output

枚举型,动态参数。 用于设置slow log和general log的输出对象。

可以设置为none,table,file,分别代表:不输出,存于表,存于文件。

并且也可以组合设置: 比如SET GLOBAL log_output='table,file'; 则代表同时输出到表和文件中。

如果设置SET GLOBAL log_output='none,file' 或 'none,table' 或 'table,file,none' 均代表'none'

  • slow_query_log与slow_query_log_file

slow_query_log 布尔型,动态参数,默认为OFF。 用于控制是否开启slow log。

slow_query_log_file 动态参数,指定slow log文件的名称和路径。

若未设置,则slow log的文件名取默认值$host_name-slow.log,存放于$datadir下。

  • long_query_time

动态参数,默认值为10。 记录执行时间(real time)超过该值以上的SQL。

  • log_queries_not_using_indexes

布尔型,动态参数,默认为OFF。

若开启,则表示记录所有未使用索引的SQL,无论是否超过long_query_time所设置的值。

不遵循long_query_time。

  • log_throttle_queries_not_using_indexes

整型,动态参数,默认为0。

如果log_queries_not_using_indexes开启, 那么log_throttle_queries_not_using_indexes用于限制每分钟所记录的slow log数量。

设置为0则表示“不限制”。

  • log_slow_admin_statements

布尔型,动态参数,默认为OFF。5.7后新增的参数。

可用于控制slow log是否记录数据库管理的SQL。

若开启,则表示记录这些SQL。

  • log_slow_slave_statements

布尔型,动态参数,默认为OFF。5.7后新增的参数。

开启后,在slave上将会记录超过long_query_time的日志记录。

即便开启了这个选项,也不会立刻生效,新的变更需要再一次START SLAVE后生效。

  • min_examined_row_limit

整型,动态参数,默认为0。

设置该值,则表示返回行数大于等于该值的sql,将会被记录到slow log中。

  • log-short-format 默认为FLASE,该选项仅仅为启动时选项,并不支持系统变量。

如果该选项被激活,则表示在slow log中记录更少的信息。

  • log_timestamps

枚举型,动态,默认为UTC,5.7.2后出现。

这个参数是用于控制记录在error log、general log、slow log中,对应日期时区的选项。

【参考文档】(MySQL 5.7 Reference Manual - 5.1.3 Server Option and Variable Reference)

over