赞
踩
索引是帮助MySQL高效获取数据的排好序的数据结构
myISAM存储引擎文件和数据是分离的(非聚集索引)
# simple例子:
explain select * from film where id = 2;
# primary 、subquery、derived例子
set session optimizer_switch='derived_merge=off'; #关闭mysql5.7新特性对衍生表的合 并优化
explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der; n
set session optimizer_switch='derived_merge=on'; #还原默认配置
# union例子
explain select 1 union all select 1;
explain select min(id) from film;
explain select * from (select * from film where id = 1) tmp;
show warnings;
explain select * from film_actor left join film on film_actor.film_id = film.id;
explain select * from film where name = 'film1';
explain select film_id from film left join film_actor on film.id = film_actor.film_id;
explain select * from actor where id > 1;
explain select * from film;
explain select * from actor;
explain select * from film_actor where film_id = 2;
覆盖索引定义:MySQL执行计划Explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树种获取,这种情况一般可以说是用到了覆盖索引,Extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引找到主键,再通过主键去主键索引树中获取其他字段的值
explain select film_id from film_actor where film_id = 1;
explain select * from actor where name = 'a';
explain select * from film_actor where film_id > 1;
actor.name没有用到索引,此时创建了张临时表来distinct
explain select distinct name from actor;
film.name建立了idx_name索引,此时查询是extra是Using index,没有临时表
explain select distinct name from film;
actor.name:未创建索引,会浏览整个表,保存排序关键字name和对应的id,然后排序name并检索行记录
explain select * from actor order by name;
file.name建立了idx_name索引,此时查询时extra是Using index
explain select * from film order by name;
explain select min(id) from film;
ALTER TABLE `employees` ADD INDEX `idx_age` (`age`) USING BTREE ;
explain select * from employees where age >=1 and age <=2000;
explain select * from employees where age >=1 and age <=1000;
explain select * from employees where age >=1001 and age <=2000;
# 删除索引
ALTER TABLE `employees` DROP INDEX `idx_age`;
like KK%相当于=常量,%KK和%KK% 相当于范围
根据自增且连续主键排序的分页查询的例子:
select * from employees limit 90000,5;
该sql表示查询从第90001行开始的五行数据,没添加单独order by,表示通过主键排序,因为主键是自增且连续的,所以可以改写成按照主键去查询从第90001开始的五行数据,如下:
select * from employees where id > 90000 limit 5;
查询的结果一致,对比查询计划
EXPLAIN select * from employees limit 90000,5;
mysql> EXPLAIN select * from employees where id > 90000 limit 5;
显然改写后的SQL走了索引,而且扫描的行数大大减少,执行效率高,但是这条改写的SQL在很多场景并不实用,因为表中可能某些记录被删后,主键空缺,导致结果不一致,如下图所示:
两条SQL的结果并不一样,因此如果不连续,不能使用上面的描述的优化方法,另外,如果原SQL是order by 非主键的字段,按照上面说的方法改写会导致两条SQL的结果不一致,所以这种改写得满足以下两个条件
根据非主键字段排序的分页查询,SQL如下:
select * from employees ORDER BY name limit 90000,5;
EXPLAIN select * from employees ORDER BY name limit 90000,5;
发现并没有使用name字段的索引(key字段对应的值为null),具体原因:扫描整个索引并查找到没索引的行(可能要遍历多个索引树)的成本比扫描全表的成本更高,所以优化器放弃使用索引,因此优化的关键是让排序时返回的字段尽可能少,所以可以让排序和分页操作先查出主键,然后根据主键查到对应的记录,SQL改写如下:
select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
需要的结果与原SQL一致,执行时间减少了一半以上,对比优化前后SQL的执行计划
原SQL使用的是filesort排序,而优化后的SQL使用的是索引排序
MySQL的表关联常见有两种算法
一次一行循环地从第一张表(驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集
# t1表10000条数据,t2表100条数据
EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;
从执行计划可以看出:
上面SQL的大致流程如下
整个过程会读取t2表的所有数据(扫描100行),然后遍历这每行数据中字段a的值,根据t2表中的a的值索引扫描t1表中的对应行(扫描100次t1表的索引,1次扫描可以认为最终只扫描t1表一行完整数据,也就是总共t1表也扫描了100行),因此整个过程扫描了200行,如果被驱动表的关联字段没索引,使用NLJ算法性能会比较低,MySQL会选择Block Nested-Loop-Join算法
把驱动表的数据读到join buffer中,然后扫描被驱动表,把被驱动表每一行读取出来跟join buffer中的数据做对比
EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;
Extra中的Using join buffer(Block Nested Loop)说明该关联查询使用的是BNL算法
上面SQL大致流程如下:
整个过程对表t1和t2都做了一次全表扫描,因此扫描的总行数为10000(表t1的数据总量)+ 100(表t2中的总数) = 10100。并且join buffer里的数据是无序的,因此对表t1中的每一行,都要做100次判断,所以内存中的判断次数是100 * 10000 = 100万次
这里表t2才100行,如果表t2是一个大表,join buffer放不下怎么办呢?
被驱动表的关联字段没索引为什么要选择使用BNL算法而不使用Nested-Loop-Join呢?
原则:小表驱动大表,即小的数据集驱动大的数据集
select * from A where id in (select id from B)
#等价于:
for(select id from B){
select * from A where A.id = B.id
}
select * from A where exists (select 1 from B where B.id = A.id)
# 等价于:
for(select * from A){
select * from B where B.id = A.id
}
# A表与B表的ID字段应建立索引
- EXISTS只返回TRUE或 FALSE,因此子查询中的SELECT * 也可以用SELECT 1 替换,官方说法是实际执行时,会忽略SELECT 清单,因此没有区别
- EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的对比
- EXISTS子查询往往也可以用join来代替,何种最优需要具体问题具体分析
‐‐ 临时关闭mysql查询缓存,为了查看sql多次执行的真实时间
set global query_cache_size=0;
set global query_cache_type=0;
EXPLAIN select count(1) from employees;
EXPLAIN select count(id) from employees;
EXPLAIN select count(name) from employees;
EXPLAIN select count(*) from employees;
以上4条SQL只有根据某个字段count不会走统计字段为null值的数据行
四个SQL的执行计划一样,说明这四个SQL执行效率应该差不多
count(1)和count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1统计,count(字段)还需要取出字段,所以理论上count(1)比count(字段)会快一点
count()是例外,MySQL并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)来替代count()
为什么对于count(id),MySQL最终选择辅助索引而不是主键的聚集索引?
事务是由一组SQL语句组成的逻辑单元,事务具有以下4个属性,通常简称为事务的ACID属性
show variables like 'tx_isolation'
set tx_isolation='REPEATABLE-READ'
MySQL默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别,默认用MySQL设置的隔离级别,如果Spring设置了就用已经设置了的隔离级别
锁分类
每次操作锁住整张表,开销小,加锁快;不会出现死锁;锁粒度大,发生锁冲突的概率最高,并发度最低,一般用在整表数据迁移的场景
加读锁:
加写锁:
结论:
每次操作锁住一行数据,开销大,加锁慢,会出现死锁,锁的粒度小,发生锁冲突的概率最低,并发最高
InnoDB和MyISAM的最大不同有两点:
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
总结:
行锁分析:
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的竞争情况
show status like 'innodb_row_lock%';
对各个状态量的说明如下:
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多等等待,然后根据分析结果着手定制优化计划
查看INFORMATION_SCHEMA系统库锁相关数据表
# 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX
# 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS
# 查看锁等待
select * from INFORMATION_SCHEMA。INNODB_LOCK_WAITS
# 释放锁,trx_mysql_thread_id可以从InnoDB_TRX表里查看到
kill trx_mysql-thread_id
# 查看所等待详细信息
show engine innodb status\G
间隙锁,锁的是两个值之间的间隙,MySQL默认的级别是repeatable-read,间隙锁在某些情况下可以解决幻读问题
假设account表中数据如下:
那么间隙就有id为(3,10),(10,20),(20,正无穷)三个区间,在session_1下面执行update count set name = ‘zhangsan’ where id > 8 and id <18;则其他session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后的20也包含在内
间隙锁是在可重复度隔离级别下才生效
Next-key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁
无索引行锁会升级为表锁
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁
锁定某一行还可以用lock in share mode(共享锁),和 for update(排它锁),例如:select * from test_innodb_lock where a = 2 for update,这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交
结论:
set tx_isolation = 'repeatable-read'
session1执行:select * from account where id = 1 for update;
session2执行:select * from account where id = 2 for update;
session1执行:select * from account where id = 2 for update;
session2执行:select * from account where id = 1 for update;
查看近期死锁日志信息:show engine innodb status\G
大多数情况MySQL可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况MySQL没法自动检测死锁
锁优化建议:
undo日志版本链是指一行数据被多个事务一次修改后,在每个事务修改完后,MySQL会保留修改前的数据undo回滚日志,并且设置两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录
在可重复读级别,当事务开启,执行任何查询sql时会产生当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询SQL时都会重新生成),这个视图由执行时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果都需要从对应版本链里的最新数据开始逐条read-view作比对从而得到最终的快照结果。
版本链对比规则:
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录,如果delete_flag标记为true,意味着记录已被删除,则不返回数据
注意:
begin/start transaction 命令不是一个事务的起点,在执行到他们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向MySQL申请事务id,MySQL内部是严格按照事务的启动顺序来分配事务id的
总结:
MVCC机制的实现就是通过read-view机制与undo版本链对比机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本的数据
执行update t set name = ‘zhangsan666’ where id = 1,id为1的数据name的原值为zhangsan
来一个请求就是直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据,性能可能相当差
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。