Index
MySQL
1. MySQL基本架构
1.1. Server层基本架构
1.2. 引擎
2. 索引
2.1. Hash索引
2.2. 稠密索引、稀疏索引
2.3. 主键索引、唯一索引、普通索引
2.3.1. 主键索引与普通索引区别
2.3.2. 唯一索引与普通索引
2.4. 索引数据结构
2.4.1. B树
2.4.2. B+树
2.4.3. 为什么使用B+树数据结构
2.5. 增删操作与索引
2.6. 索引规则
2.6.1. 覆盖索引
2.6.2. 最左前缀原则
2.6.3. 索引下推
2.7. 索引选择规则
2.7.1. 导致索引失效情况
2.7.2. 采样统计储存导致走错索引
2.8. 添加索引技巧
2.8.1. 前缀索引
2.8.2. 倒序存储
2.8.3. hash字段存储
2.8.4. 上述添加索引方法比较
2.8.5. 其他
2.9. MySql 查询分析
2.9.1. 基于扫描索引树的分析
2.9.2. 基于索引统计的分析
2.9.3. 基于全表扫描
2.9.4. 索引统计优化
2.9.4.1. 索引统计结果的基础信息
2.9.4.2. 索引统计表
2.9.5. 参考资料
3. 执行计划
3.1. 执行计划列信息
3.2. 如何优化SQL
4. MySQL的锁
4.1. 全局锁
4.2. 表锁
4.3. 行锁
4.3.1. 两阶段锁
4.4. 死锁
4.4.1. 死锁策略
4.5. 幻读(间隙锁)
4.5.1. 如何解决幻读?
4.5.2. 加锁原则
4.6. 悲观锁
4.7. 乐观锁
5. 事务
6. 锁与隔离级别
6.1. 串行化
6.2. RR可重复读(Repeatable Read)TODO
6.2.1. 不可重复读问题解决
6.2.2. 幻读解决(间隙锁+行锁)
6.3. RC读已提交(Read Committed)
6.3.1. 脏读解决
6.3.2. 不可重复读问题出现
6.4. RU读未提交(Read Uncommitted)
6.4.1. 脏读问题出现
6.5. 多版本并发控制(Multi-Version Concurrency Control,MVCC)
6.5.1. 前置知识
6.5.2. MVCC原理
6.5.2.1. 版本链
6.5.2.2. undo log
6.5.2.3. Read View(读视图)
6.5.3. 隔离级别与MVCC
6.5.4. 参考资料
7. redo log、undo log、binlog
7.1. undo log
7.2. redo log
7.2.1. redo log 写入机制
7.3. binlog
7.3.1. binlog写入机制
7.3.2. binlog 格式
7.4. crash-safe的设置
7.5. 两阶段提交
7.6. 两种日志区别
7.7. 两阶段提交的组提交机制执行流程
8. 实际sql的执行
8.1. count(*)实现
8.2. order by 处理流程
8.2.1. 全字段排序
8.2.2. rowId排序
8.3. join的执行过程
8.3.1. Index Nested-Loop Join
8.3.2. Simple Nested-Loop Join
8.3.3. Block Nested-Loop Join
8.3.4. join语句mysql的优化
8.3.5. 总结
8.4. union执行流程
8.5. group by 执行流程
8.5.1. 优化-索引
8.5.2. 优化-直接排序
8.5.3. 小结
9. MySQL中的组件
9.1. Buffer Pool
9.1.1. 内存管理策略
9.1.2. change buffer
9.1.3. change buffer 和 redo log
9.2. sort buffer
9.3. 内存临时表
9.4. 磁盘临时表
9.5. binlog cache
9.6. redo log buffer
9.7. net_buffer
9.8. join buffer
10. 数据库设计
10.1. 数据库设计原则
10.1.1. 第一范式(列不可再分)
10.1.2. 第二范式(确保表中的每列都和主键相关)
10.1.3. 第三范式(确保每列都和主键列直接相关,而不是间接相关)
10.1.4. 相关资料
10.2. 表字段设计
10.2.1. 乐观锁字段
10.2.2. 通用字段
10.2.3. 日期字段的选择
10.3. 分区与分表
10.3.1. 分区
10.3.2. 分表
10.4. 实际设计问题 —— 设计部门表
11. MySQL架构
11.1. 主从模式(读写分离)
11.2. 分库分表
11.2.1. 分库分表场景
11.2.2. 垂直切分
11.2.3. 水平切分
11.2.4. 分库分表的查询
11.3. MGR(MySQL Group Replication)
11.4. 主从数据同步
11.4.1. MySQL异步复制
11.4.2. MySQL半同步复制
11.4.3. MySQL组复制
12. 其他面试题
12.1. 主键索引是否使用自增
12.2. 临时表与内存表
12.3. 重建索引是否合理
12.4. mysql数据库抖动
12.5. 读已提交和可重复读是如何实现的
12.6. 读已提交和可重复读区别?
12.7. 数据库数据库一致性是如何实现的?
12.8. redoLog、undoLog、binlog区别?
12.8.1. 两种日志有以下三点不同
12.9. 一个事务开启什么时候产生三种日志?
12.10. 可重复读隔离级别下,事务中select一条记录巨慢
12.11. 数据库什么情况会出现死锁?如何处理死锁?
12.11.1. 由于索引导致的死锁
12.11.2. order by 优化
12.11.3. sub-query 子查询
12.11.4. limit 优化 - 延迟关联
12.11.4.1. 相关资料
12.11.5. Or/And Condition
12.11.6. join 优化
12.12. 数据库连接池、数据库连接线程安全的吗?
12.13. InnoDB 和 MyIsam 数据库引擎的区别
12.14. MySQL数据页(储存页)
12.15. 高并发数据库读写压力大怎么处理?
12.16. 其他
12.16.1. 什么是覆盖索引跟回表?
12.16.2. left join,right join,inner join
,表表关联什么区别?
12.16.3. 大事务会有什么影响?
MySQL可以分为Server层和存储引擎层两部分
MySQL的一个设计思想:如果内存够用,就要多利用内存,尽量减少磁盘访问。
mysql -h$ip -P$port -u$user -p
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。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择效率高的方案。
mysql> select * from T where ID=10;
比如我们这个例子中的表T中,ID字段没有索引,那么执行器的执行流程是这样的:
- 调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
关于 MyISAM 和 InnoDB 的选择问题
大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊!)。
《MySQL 高性能》上面有一句话这样写到:
不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
一般情况下InnoDB都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择 MyISAM 也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。
因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由再使用 MyISAM 作为自己的 MySQL 数据库的存储引擎。
索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。 一本500页的书,对于数据库的表而言,索引其实就是它的“目录”。
若一张表中无主键索引,mysql会默认创建一个长度为6字节的rowid主键。
除了B-Tree 索引,MySQL还提供了如下索引:
Hash索引
只有Memory引擎支持,场景简单R-Tree索引
MyISAM的一个特殊索引类型,主要用于地理空间数据类型Full-text
MyISAM的一个特殊索引,主要用于全文索引,从MySQL 5.6开始InnoDB支持全文索引索引 / 存储引擎 | MyISAM | InnoDB | Memory |
---|---|---|---|
B-Tree索引 | 支持 | 支持 | 支持 |
HASH索引 | 不支持 | 不支持 | 支持 |
R-Tree索引 | 支持 | 支持 | 不支持 |
Full-text索引 | 支持 | 支持 | 不支持 |
Hash索引适于key-value查询,通过Hash索引比B-tree索引查询更加迅速。但Hash索引不支持范围查找例如!>
,<=
,>=
等。 Memory只有在"="的条件下才会使用hash索引
值存在数组,用一个hash函数把key转换成一个确定的内存位置,然后把value放在数组的该位置。使用 hash 自然会有哈希冲突可能,MySQL 采取拉链法解决。
Hash索引基于Hash表实现,只有查询条件精确匹配Hash索引中的列时,才能够使用到hash索引。对于Hash索引中的所有列,存储引擎会为每行计算一个hashcode,Hash索引中存储的就是hashcode。
Hash索引缺点:
稠密索引:线性索引中,文件的每个记录对应一个索引项。在稠密索引中,索引项总是按关键码顺序排列。只要内存空间允许,通常把稠密索引存储在内存中,提高记录的查找效率。
由于索引表是有序的,并且每个索引想都包含一个关键码以及指向该记录存储位置的指针,所以可以实现对数据库记录有效的查找。(采用折半查找)
稠密索引缺点:
稀疏索引:在文件中,只为某些查找键值建立索引记录。稀疏索引则需要先定位到搜索值>索引值的最小的那个,然后在通过起始位置去定位具体的偏移量。
稀疏索引占用空间少,但是在查询的精确率上还是相对于稠密索引还是比较慢的,因为不需要顺序查找,还有回表。
索引分类:唯一索引,主键(聚集)索引,非聚集索引(普通索引)
回到主键索引树搜索的过程,我们称为回表。
图解索引结构
主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。 在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。 通过这种方式就能保证这个数据逻辑的正确性
change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。
- 访问这个数据页会触发merge。
- 系统有后台线程会定期merge。
- 在数据库正常关闭(shutdown) 的过程中,也会执行merge操作。
change Buffer与redo log 区别
merge的执行流程是这样的:
这时候,数据页和内存中change buffer对应的磁盘位置都还没有修改,属于脏页,之后各自刷回自己的物理数据。
InnoDB里面索引对应一棵B+树
B树和平衡二叉树稍有不同的是,B树属于多叉树又名平衡多路查找树
规律:
与B树的区别
特点
B+树的插入与删除:
参考资料:B树和B+树的插入、删除图文详解
使用B+树而不是二叉搜索树或者红黑树的原因是,由于存储介质的特性,磁盘本身存取就比主存慢很多,每次搜索的磁盘IO的开销过大,而B+树可以使用较少次的磁盘IO搜索到对象。
对于删除操作,InnoDB引擎只会将记录标记为删除,新数据的插入如果索引的位置刚好在删除记录,那么插入记录就会复用该位置。
如果我们用delete命令把整个表的数据删除呢?结果就是,所有的数据页都会被标记 为可复用。但是磁盘上,文件不会变小。
对于插入操作,如果数据页刚好进行了页分裂,那么分裂完的数据页的中都是会存在空洞的。
解决空洞问题,对表进行重建。
InnoDB存储引擎支持覆盖索引,即从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。
select id, b from t where b = xxx (id为主键,b为索引)
B+树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
mysql 5.6 后引入索引下推。
优化器选择索引的目的是找一个最优方案,用最小的代价执行语句。其中扫描行数、是否使用临时表、是否排序等因素都会影响优化器对索引的选择判断。
若or的条件中包含非索引,就会只用全表扫描的。如果or的条件两边都是索引,那么会使用index_merge的优化技术。
负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。
EXPLAIN SELECT * from container_load where materiel_id+1 = 152899293852729344
EXPLAIN SELECT *, SUBSTR(materiel_id,10) from container_load where SUBSTR(materiel_id,10) = 852729344
默认为Null的列,存在Null值会导致mysql优化器处理起来比较复杂,当命中结果数量小于40%的时候,会走索引。
(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引
select * from t where k2=2;
select * from t where k3=3;
slect * from t where k2=2 and k3=3;
// 以下这条只会部分走索引
slect * from t where k1=1 and k3=3;
扫描行数判断,mysql使用采样统计的方式来获取索引的统计信息基数。采样统计的方式可以减少磁盘的IO次数
InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
analyze table t
命令,可以用来重新统计索引信息。
解决索引选择异常方案
在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent
的值来选择:
由于是采样统计,所以不管N是20还是8,这个基数都是很容易不准的。
定义:定义字符串的一部分作为索引。
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。如使用邮箱的前六位作为索引,索引的区分度好。
mysql> alter table SUser add index index2(email(6));
使用前缀索引就用不上覆盖索引对查询性能的优化了,对于已经找到类似字段的记录,都需要回表进行扫描。这也是你在选择是否使用前缀索引时需要考虑的一个因素。
倒序存储:
mysql> select field_list from t where id_card = reverse('input_id_card_string');
定义:使用Hash字段。每次插入新记录的时候,都同时用crc32()这个函数得到校验码填到一个新字段.
mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
MySQL基于索引的查询成本分析主要包含两个方式:扫描索引树和索引统计
在MySQL中的查询成本主要包含下面两部分:
MySQL中定义加载一条记录到内存花费的成本常数是1.0,即一条记录的I/O成本为1.0。检查一条记录是否满足查询条件的成本常数是0.2,即一条记录的CPU成本为0.2。
在MySQL中有一个配置参数eq_range_index_dive_limit
,eq_range_index_dive_limit
参数的默认值在5.7版本更新为200。它的作用如下:
等值条件数大于等于该配置参数
,则查询成本分析使用索引统计的方式完成。而小于的情况使用扫描索引树的方式。基于索引的查询成本分析:
进入方法 | 退出方法 |
---|---|
扫描索引树分析 | 效率低,但分析结果比较准确 |
索引统计分析 | 效率高,但分析结果不准确 |
全表扫描分析 | 效率很低,但分析结果准确 |
index dives
:方案的效率低是由于必须扫描索引树来确定查询成本,如果索引树的分叉很多,势必会降低扫描的效率,从而降低了查询成本计算的效率。index statistics
:方案的计算不准确是由于MySQL是通过采样部分索引树的节点,然后对这些节点做相关计算,从而生成索引统计结果,最后,得出查询成本的。SELECT * FROM user WHERE age >= 16 AND age < 25 ORDER BY age LIMIT 0, 20;
ALTER TABLE user add index index_age_birth(age,birthday);
使用的范围查询,通过扫描索引树的方式,完成查询成本分析。由于[16,25)区间内的记录数为4,成本计算如下:
4 * 0.2 + 0.01 + 4 * 0.2 = 1.61
(在辅助索引中搜索满足查询条件的记录的总成本 + 微调参数 + 在聚簇索引中搜索满足查询条件的记录的总成本 + 0.01为MySQL对CPU成本的微调参数。)1.0 + 4 * 1.0 = 5.0
(范围区间的数量 + 聚簇索引中满足条件的记录的加载成本)范围区间的数量:由于MySQL在做索引树扫描分析前,需要读取查询条件到内存中,再做分析,所以,该案例中,在扫描
index_age_birth
索引树,需要读取一次条件区间[16,25)的上下限到内存中,这个读取次数就叫做范围区间的数量。
最终,[16,25)的查询总成本为CPU成本 + I/O成本: 1.61 + 5.0 = 6.61
SELECT * FROM user WHERE id IN (1, 5, 3, '...', 2);
ALTER TABLE user add index index_age_birth(age,birthday);
id字段值个数超过eq_range_index_dive_limit,为300个
mysql> show index from user;
+-------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| user | 1 | index_age_birth | 1 | age | A | 3 | NULL | NULL | YES | BTREE | | |
| user | 1 | index_age_birth | 2 | birthday | A | 3 | NULL | NULL | YES | BTREE | | |
+-------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
主要关注Cardinality这一列,它表示索引列中不重复值的数量。
假设我们定义user表的总记录数为total_rows,某一个索引列的Cardinality值为cardinality,那么,我们可以计算出平均一个值在该列重复多少次。即
total_rows/cardinality
案例中的SQL是主键id查询,一定命中主键索引,即表格中的聚簇索引PRIMARY,结合上面的公式,假设user表中包含记录数为8,我们就可以得出平均一个值在id这一列重复的次数,即 8/8 = 1
查询条件id值的个数为300,这条SQL查询成本估算如下:
300 * 1.0 = 300
300 * 0.2 = 60
在InnoDB中全表扫描的时间为聚簇索引的所有节点的数量。
1.1为MySQL微调固定参数。
内存中节点读成本
内存中节点读成本 = 内存中节点大小 * MEMORY_BLOCK_READ_COST
MEMORY_BLOCK_READ_COST是一个常量,默认值为0.25:
UPDATE mysql.engine_cost
SET cost_value=0.5
WHERE cost_name="memory_block_read_cost"
内存中节点大小 = 总节点大小(包含内存和磁盘) * 内存中节点占用百分比
内存中节点占用百分比:
总节点大小 < buffer_pool_size的20%
,说明所有节点都在内存中,内存中节点占用百分比 = 1.0
buffer_pool_size的20% < 总节点大小(table_size) < buffer_pool_size
情况,内存中节点占用百分比 = 1.0 - (总节点占用buffer_pool_size的百分比 - 0.2)/(1.0 - 0.2)
总节点大小 > buffer_pool_size
,说明内存中放不下所有节点,只能将节点放到磁盘中,内存中无节点,所以内存中节点占用百分比 = 0.0
磁盘中节点读成本
磁盘中节点读成本 = 磁盘中节点大小 * IO_BLOCK_READ_COST
IO_BLOCK_READ_COST为一个常量,默认为1.0,可以使用如下命令修改:
UPDATE mysql.engine_cost
SET cost_value=0.5
WHERE cost_name="io_block_read_cost"
磁盘中节点大小 = 总节点大小 - 内存中节点大小
(总节点大小 - 内存中节点大小) * IO_BLOCK_READ_COST
+ 内存中节点大小 * MEMORY_BLOCK_READ_COST
INFORMATION_SCHEMA,它是MySQL生成索引统计结果时需要的基础数据来源。
索引统计结果的基础信息主要来源于2张字典表:INNODB_SYS_TABLES和INNODB_SYS_INDEXES
表名 | 说明 |
---|---|
INNODB_SYS_TABLES | 存放了MySQL中所有表的基础信息 |
INNODB_SYS_INDEXES | 存放了MySQL中所有索引的基础信息 |
为了保证索引统计结果的准确性,MySQL在构建索引统计结果时,为了获取基础数据,必须从磁盘上频繁读取这两张表,此时,磁盘IO会增加,导致MySQL性能下降。所以,为了解决这个问题,MySQL对InnoDB数据字典做了缓存,即InnoDB字典缓存。
两张核心的数据字典表缓存:INNODB_SYS_TABLES缓存和INNODB_SYS_INDEXES缓存
缓存名 | 说明 |
---|---|
INNODB_SYS_TABLES缓存 | 主要包含3个结构:table_hash、table_id_hash和table_LRU |
INNODB_SYS_INDEXES缓存 | MySQL为每张表的索引维护了一个双向链表 |
核心的数据字典表缓存的构建过程:
构建方式 | 说明 |
---|---|
创建表时构建 | 创建表时触发构建INNODB_SYS_TABLES缓存 |
创建索引时构建 | 创建索引时触发构建INNODB_SYS_INDEXES缓存 |
MySQL重启时构建 | 在MySQL重启时,会全量加载INNODB_SYS_TABLES和INNODB_SYS_INDEXES表数据到缓存中 |
索引统计表innodb_index_stats的数据:
mysql> SELECT * FROM mysql.innodb_index_stats WHERE table_name = 'user';
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| database_name | table_name | index_name | last_update | stat_name | stat_value | sample_size | stat_description |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| tms | user | PRIMARY | 2022-07-31 15:38:00 | n_diff_pfx01 | 2 | 1 | id |
| tms | user | PRIMARY | 2022-07-31 15:38:00 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| tms | user | PRIMARY | 2022-07-31 15:38:00 | size | 1 | NULL | Number of pages in the index |
| tms | user | index_age_birth | 2022-07-31 15:38:00 | n_diff_pfx01 | 1 | 1 | age |
| tms | user | index_age_birth | 2022-07-31 15:38:00 | n_diff_pfx02 | 1 | 1 | age,birthday |
| tms | user | index_age_birth | 2022-07-31 15:38:00 | n_diff_pfx03 | 2 | 1 | age,birthday,id |
| tms | user | index_age_birth | 2022-07-31 15:38:00 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| tms | user | index_age_birth | 2022-07-31 15:38:00 | size | 1 | NULL | Number of pages in the index |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
MySQL采用两种方式更新索引统计表:
结合索引统计表定时更新方式,我们看一下这个场景:
如果INNODB_SYS_INDEXES链表随着表越来越多,早期更新的表可能要花很长时间才能扫描到,导致该表更新索引统计表周期变长,该表对应的索引统计表统计项更新变慢,最终影响该表相关SQL的查询的性能。
recalc_pool
引入可以解决这个问题,可以让MySQL总是从最早变化的表取出索引的基础信息,然后,用这些信息计算,得到索引统计结果。
recalc_pool
:一个存储table->id的列表,将最近变更的table->id添加到recalc_pool
尾部,定时任务从recalc_pool
头部取最早变更的table->id进行索引统计。
调整参数,增加Mysql采样叶子节点数量
采样叶子节点的数量是由MySQL参数innodb_stats_persistent_sample_pages
决定的,所以,如果我们可以调大这个参数,就可以保证精确计算索引统计表中的各统计项,从而使得MySQL能够更加正确地选择执行计划。采样的叶子节点数参数默认为20。
SET GLOBAL innodb_stats_persistent_sample_pages=30;
mysql> EXPLAIN SELECT * FROM country WHERE country = 'China';
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | country | NULL | ALL | NULL | NULL | NULL | NULL | 109 | 10.00 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set (0.02 sec)
EXPLAIN 的各列信息如下:
id
:每个 SELECT 关键字都对应一个唯一的id。查询语句中每出现一个 SELECT 关键字,MySQL 就会为它分配一个唯一的id值。select_type
:查询的类型,table
:查询的表(别名),这一列显示了对应行正在访问哪个表,一般情况下它就是访问的那个表,或是该表的别名。可以从这一列中从上往下观察SQL执行的关联顺序。partitions
:分区信息。一般情况下查询语句的执行计划的 partitions 列的值都是 NULL。type
:访问方法possible_keys
:可能用到的索引,显示了查询可以使用哪些索引。key
:实际上使用的索引key_len
:实际使用到的索引长度,表示使用的索引的最大长度(字节数)ref
:使用索引列等值查询时,与索引列进行等值匹配的对象信息rows
:预估需要读取的记录条数filtered
:某个表经过搜索条件过滤后剩余记录条数的百分比Extra
:一些额外的信息select_type这一列显示了对应的行是简单查询还是复杂查询,MySQL官方文档中显示有如下类型。
SIMPLE
:SIMPLE 就是非常简单的查询,意味着不包含 UNION 或子查询,连接查询也算是 SIMPLE 类型。PRIMARY
:如果查询有任何复杂的子部分,如 UNION、UNION ALL、子查询,则最外层部分标记为 PRIMARY。UNION
:在 UNION 或 UNION ALL 子句中的第二个之后的 SELECT 都会被标记为 UNION。UNION RESULT
:UNION 查询需要使用内存临时表来去重,针对该临时表的查询的 select_type 就是 UNION RESULT,上面介绍 UNION 时可以看到这个类型。SUBQUERY
:包含在 SELECT 列表中的子查询中的 SELECT 会标记为 SUBQUERY,不包含 FROM 子查询。DERIVED
:用来表示包含在 FROM 子句的子查询中的 SELECT,MySQL 会将子查询的结果物化,放到一个临时表中,也称为 派生表,因为该临时表是从子查询中派生来的。type访问方法
下面是访问方法,依次从最优到最差。
NULL
:这种访问方式意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。system
:当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如 MyISAM、Memory,那么对该表的访问方法就是 system。const
:当根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const。出现 const 表明查询是极为快速的。eq_ref
:在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref,被驱动表最多只会返回一条符合条件的记录。ref
:当通过普通二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是 ref。这是一种索引访问方式,相比 eq_ref,ref 可能返回多条符合条件的记录。ref_or_null
:当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是 ref_or_null。index_merge
:一般情况下对于某个表的查询只会用到一个索引,但也可能同时使用多个索引,使用索引合并的方式来执行查询,这时访问方法就可能是 index_merge。unique_subquery
:在一些包含 IN 子查询的SQL语句中,如果查询优化器决定将 IN 子查询转换为 EXISTS 子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询使用的方法就是 unique_subquery。index_subquery
:类似于 unique_subquery
,只不过访问子查询中的表时如果使用的是普通索引,则访问方法是 index_subquery。range
:range 表示基于索引的范围扫描,返回索引某些范围区间的记录。这比 index 全索引扫描好一些,因为它不用遍历全部索引。index
:index 类似于 ALL 全表扫描,不过 index 是顺序扫描索引的全部记录。它的主要优点是避免了排序;但如果要回表的话,开销可能比 ALL 更大。ALL
:ALL 就是全表扫描,通常意味着MySQL必须扫描整张表,应尽量避免这种方法的出现。主要常见的为const、eq_ref、ref、ALL
rows:表示使用指定的查询方式(type)会读出来的行数,注意它并不是最终要从表里读取出来的行数,只是一个预估值。
filtered:表示针对读出来的行数(rows),再用上其它的查询条件(WHERE子句或连接条件),又可以过滤出百分之几的数据来。
Extra
Using Index
:表示使用了 覆盖索引,没有 回表 查询,就是仅仅涉及到一个二级索引。Using where
: 一般在没有使用到索引,执行全表扫描时,并且 WHERE 子句中有针对该表的搜索条件,就会提示 Using where。或者用到了索引,但还有其它 WHERE 条件,也可能会提示 Using where。Using temporary
: 出现 Using temporary 意味着MySQL内部使用了临时表来完成一些功能,比如去重、排序之类的Using filesort
: 有些排序可以直接使用索引的排序,但很多时候可能无法使用索引排序,这时就需要将数据查到内存中,或者在数据了较大时放到磁盘中,再进行排序。这种在内存中或者磁盘上进行排序的方式统称为文件排序(filesort),在 Extra 列就会显示 Using filesort。Using join buffer (Block Nested Loop)
: 在连接查询执行过程中,当被驱动表没有索引或者不能有效的利用索引加快访问速度时,MySQL一般会为其分配一块名叫 join buffer 的内存块,然后将数据读到这块内存块中,再基于块中的数据做嵌套循环查询,以此来加快查询速度。问题:如何从执行计划中看出是否有回表?
执行计划的Extra信息只有一个Using Index
,表示使用了覆盖索引,没有回表查询。
explain 分析例子
+----+-------------+-------+------------+--------+------------------+------------------+---------+----------------------------------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+------------------+------------------+---------+----------------------------------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | ct | NULL | ALL | NULL | NULL | NULL | NULL | 353 | 100.00 | Using where |
| 1 | SIMPLE | mo | NULL | eq_ref | PRIMARY | PRIMARY | 8 | ct.organization_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | cm | NULL | eq_ref | PRIMARY | PRIMARY | 8 | ct.container_master_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | cei | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | ctp | NULL | eq_ref | PRIMARY | PRIMARY | 8 | cei.transfer_plan_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | mw | NULL | eq_ref | PRIMARY | PRIMARY | 8 | ct.current_warehouse_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | cl | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | mm | NULL | eq_ref | PRIMARY | PRIMARY | 8 | cl.materiel_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | qq | NULL | ALL | NULL | NULL | NULL | NULL | 123 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | pg | NULL | eq_ref | PRIMARY | PRIMARY | 8 | qq.product_grade_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | cli | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | ctr | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | Using where |
| 1 | SIMPLE | cso | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | tsb | NULL | eq_ref | PRIMARY | PRIMARY | 8 | cso.tran_sea_bill_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | tspn | NULL | eq_ref | PRIMARY | PRIMARY | 8 | tsb.sea_plan_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | suser | NULL | ref | uk_user_name | uk_user_name | 63 | tspn.book_space_user | 1 | 100.00 | Using where; Using index |
| 1 | SIMPLE | tspc | NULL | ALL | idx_container_id | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | tspd | NULL | eq_ref | PRIMARY | PRIMARY | 8 | tspc.tran_sub_plan_detail_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | tsp | NULL | eq_ref | PRIMARY | PRIMARY | 8 | tspd.tran_sub_plan_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | csm | NULL | ref | idx_container_id | idx_container_id | 9 | ct.container_id | 1 | 100.00 | NULL |
| 1 | SIMPLE | yjtsp | NULL | eq_ref | PRIMARY | PRIMARY | 8 | csm.sea_plan_id | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+------------------+------------------+---------+----------------------------------+------+----------+----------------------------------------------------+
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、 表级锁和行锁三类。
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。
MySQL里面表级别的锁有两种: 一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables …read/write。
另一类表级的锁是MDL(metadata lock)。
如何安全地给小表加字段?
MySQL的行锁是在引擎层由各个引擎自己实现的。 MyISAM引擎就不支持行锁,InnoDB是支持行锁的。
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁
添加行锁:
select * from t lock in share mode; // lock in share mode是加共享锁的形式。
两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。 事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。
策略一:直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。在InnoDB中,innodb_lock_wait_timeout的默认值是50s
策略二:发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。 将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
对“幻读”做一个说明:
session A里执行了三次查询,分别是Q1、 Q2和Q3。 它们的SQL语句相同,都是select * from t where d=5 for update。 表示查所有d=5的行,而且使用的是当前读,并且加上写锁。
其中,Q3读到id=1这一行的现象,被称为“幻读”。 也就是说,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行
幻读会导致数据一致性的问题。 锁的设计是为了保证数据的一致性。 而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
尝试解决幻读,把所有语句都上锁,查询语句改成
select * from t for update
。但是仍然无法解决插入新语句出现的幻读现象。
InnoDB引入新的锁,也就是间隙锁(Gap Lock)。在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。
间隙锁之间的冲突:跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。 间隙锁之间都不存在冲突关系。
间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。
select * from t for update
要把整个表所有记录锁起来,就形成了7个next-key lock,分别是 (-∞,0]、 (0,5]、 (5,10]、 (10,15]、 (15,20]、 (20, 25]、 (25, +supremum]。间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
总结的加锁规则里面,包含了两个“原则”、 两个“优化”和一个“bug”。
添加一行字段当版本号字段,更新的时候带版本号。如:
update xx set name = aaa and version = 3 where id = 'xxx' and version = 2'
事务ACID:
从本质上来说,原子性、隔离性、持久性,最终目的都是为了保证一致性。即一致性是最终目标,原子性、隔离性、持久性可以说都是为了实现这一目标的手段。
MySQL中有undo log(用于回滚)和redo log(用于数据写入)
⚠️脏读、不可重复读、幻读本质上是由于各种锁在不同加锁时间上⚠️组合应用所产生的结果。以下的介绍将从锁的角度,而不是数据的现象说明四个隔离级别及相关问题。
串行化访问提供了强度最高的隔离性,如果不考虑性能优化的话,对事务所有读、写的数据全都加上读锁、写锁和范围锁即可做到可串行化
但数据库不考虑性能肯定是不行的,并发控制理论(Concurrency Control)决定了隔离程度与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低。
因为添加了范围锁,也就不会出现幻读问题了。
见幻读章节
有三种情况:
可重复读对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。
可重复读实现,添加一个贯穿事务周期的读锁:
Innodb 为解决幻读问题引入了间隙锁+行锁充当范围锁
见上述幻读章节
读已提交对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。
隔离级别是读已提交,两次重复执行的查询结果就会不一样,原因是读已提交的隔离级别缺乏贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化。
读已提交对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。读已提交比可重复读弱化的地方在于不可重复读问题
例子:
事务B处理数据T1必须等事务A结束,解决了读未提交破坏了事务隔离性问题,实现了读取的数据都是已提交的数据,不会被回滚,即读已提交。
例子:
读未提交对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。
注意⚠️:写锁禁止其他事务施加读锁,而不是禁止事务读取数据,如果事务 T1 读取数据并不需要去加读锁的话,就会导致事务 T2 未提交的数据也马上就能被事务 T1 所读到。
例子:
多版本并发控制MVCC: 是一种无锁读取的优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。
数据库并发场景
当前读 :它读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。
如下操作都是当前读:
快照读 :快照读的实现是基于多版本并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。
MVCC用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。
读操作只读取该事务开始前的数据库快照。
MVCC的实现原理:主要是版本链,undo日志 ,Read View 来实现的
实现机制:InnoDB在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录删除的版本号。
我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段,分别是db_trx_id、db_roll_pointer、db_row_id。
实际还有一个删除flag隐藏字段, 记录被更新或删除并不代表真的删除,而是删除flag变了
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
见undolog 章节
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
ReadView是一个数据结构,包含4个字段
m_ids
: 当前活跃的事务编号集合(未提交事务)low_limit_id
: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。up_limit_id
: 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。creator_trx_id
: ReadView创建者的事务编号在RR隔离级别下,分析隔离级别,此时已有一个事务ID 99,其他三个事务如下,事务开启时刻,带上已生效的版本号ID
事务A视图版本100,事务B视图版本101,事务C视图版本102
RC隔离级别读提交事务隔离的例子,事务A读取的结果为2。
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;
具体分析有三种情况,undolog
: trx_id
, 读视图: low_limit_id
、up_limit_id
:
trx_id > low_limit_id
up_limit_id<=trx_id<= low_limit_id && (txn_id in trx_ids)
trx_id < up_limit_id
或 up_limit_id<=trx_id<= low_limit_id && (txn_id not in trx_ids)
或 trx_id = creator_trx_id
读提交(RC)的逻辑和可重复读(RR)的逻辑类似,它们最主要的区别是:
下述场景中,读提交(RC)隔离级别下:
事务A查询语句返回的是k=2。事务B查询结果k=3。
undo log主要有两个作用:回滚和多版本控制(MVCC)
在数据修改的时候,不仅记录了redo log,还记录undo log,如果因为某些原因导致事务失败或回滚了,可以用undo log进行回滚。
MySQL里经常说到的WAL技术,WAL的全称是WriteAheadLogging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
redo log buffer :redo log buffer就是一块内存,用来先存redo日志的。 在执行事务的时候,如insert、update会先存在buffer中。等事务commit,再一起写入redo log
这三种状态分别是:
为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:
InnoDB写盘的三种情况:
MySQL整体来看,其实就有两块: 一块是Server层,它主要做的是MySQL功能层面的事情; 还有一块是引擎层,负责存储相关的具体事宜。 上面我们聊到的粉板redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog(归档日志) 。
每个线程有自己binlog cache,但是共用同一份binlog文件。
write 和fsync的时机,是由参数sync_binlog控制的:
binlog 三种格式,主要区别于在存储binlog的格式区别
insert
语句调用now()
函数,log中会先记录一条set timestamp
的信息。现在越来越多的场景要求把MySQL的binlog格式设置成row,有利于数据恢复。
- 如执行完一条delete语句以后,发现删错数据了,可以直接把 binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了。
- 如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。
而使用statement,需要注意语句的上下文信息,以防恢复有误。同样的mixed模式下,部分语句转成statement也会导致恢复的过程较难操作的情况
redo log用于保证crash-safe能力。innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘,建议设置成1,这样可以保证 MySQL异常重启之后数据不丢失。
sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。这个参数也建议你设置成1,这样可以保证MySQL异常重启之后binlog不丢失。
这就是通常说的MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设 置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。
两阶段提交:主要用于保证redo log 与binlog 的状态保持逻辑上一致。
图中 两个“commit”的概念:
“commit语句”,是指MySQL语法中,用于提交一个事务的命令。一般跟begin/start transaction配对使用。
图中用到的这个“commit步骤”,指的是事务提交过程中的一个小步骤,也是最后一步。 当这个步骤执行完成后,这个事务就提交完成了。
“commit语句”执行的时候,会包含“commit 步骤
崩溃后的数据恢复阶段:
如果在更新或写入数据的过程中,机器出现崩溃。那么在机器在重启后,MySQL会首先去验证redo log的完整性,如果redo log中没有prepare状态的记录,则记录是完整的,就日记提交。如果redolog中存在prepare记录,那么就去验证这条redolog对应的binlog记录,如果这条binlog是完整的,那么完整提交redo log,否则执行回滚逻辑
崩溃恢复时的判断规则:
一个事务的binlog是有完整格式的:
statement格式的binlog,最后会有COMMIT;
row格式的binlog,最后会有一个XID event。
为何设计两阶段提交?
redo log 日志在innodb引擎端,而binlog在server层。涉及到两个不同服务的交互,又要保证数据一致性问题,这就是一个典型的分布式问题。
2PC保证了事务在引擎层(redo)和server层(binlog)之间的原子性。其中binlog作为XA协调器,即以binlog是否成功写入磁盘作为事务提交的标志(innodb commit标志并不是事务成功与否的标志)
为何需要两个日志
binlog没有能力恢复“数据页”。InnoDB引擎使用的是WAL技术,执行事务的时候,写完内存和日志,事务就算完成了。 如果之后崩溃,要依赖于日志来恢复数据页。
binlog write这个位置发生崩溃的话,之前的事务1也是可能丢失了的,而且是数据页级的丢失。此时,binlog里面并没有记录数据页的更新细节,是补全丢失的数据。
binlog作为MySQL一开始就有的功能,被用在了很多地方。其中,MySQL系统高可用的基础,就是binlog复制
很多公司有异构系统(比如一些数据分析系统) ,这些系统就靠消费MySQL的binlog来更新自己的数据。
参考文章:
crash-safe的参数设置会导致频繁的刷盘,在高TPS下容易达到磁盘能力的瓶颈。
MySQL为了让组提交的效果更好,把redo log做fsync的时间拖到了binlog write 之后,整体的提交流程改成如下:
WAL机制主要得益于两个方面:
IOPS
消耗。一个sql 语句mysql的执行顺序:
from -- on -- JOIN -- where -- group by -- having -- select -- distinct -- order by -- limit
(8) SELECT (9) DISTINCT
(1) FROM
(3) JOIN
(2) ON
(4) WHERE
(5) GROUP
(6) WITH {CUBE|ROLLUP}
(7) HAVING
(10) ORDER BY
(11) LIMIT
执行查询的语句是 select id from T where k=5
。这个查询语句在索引树上查找的过程:
先是通过B+树从树根开始,按层搜索到叶子节点,也就是对应的数据页,然后可以认为数据页内部通过二分法来定位记录。
不同引擎中的实现
InnoDB中Mysql对于count()的优化:InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。 所以,普通索引树比主键索引树小很多。普通索引树比主键索引树小很多。对于count()这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
show table status 命令也可以显示行数,这里的行数是基于采样统计的,并不准确。
不同count的用法:count(*)、 count(主键id)、 count(字段)
和count(1)
count()
是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值count(主键id)
: InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。count(1)
: InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。count(字段)
:
count(*)
是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。按照效率排序的话,count(字段)
全字段排序,按字面意思就是按字段进行排序的一种处理方式。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
select city,name,age from t where city='杭州' order by name limit 1000 ;
Extra这个字段中的
Using filesort
表示的就是需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer。
sort_buffer
,确定放入name、city、age
这三个字段;city='杭州’
条件的主键id,也就是图中的ID_X;name、city、age
三个字段的值,存入sort_buffer
中;这里的排序使用的是归并排序
sort_buffer_size
: 是MySQL为排序开辟的内存(sort_buffer)的大小。 如果要排序的数据量小于sort_buffer_size
,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
sort_buffer_size大于了需要排序的数据量的大小,number_of_tmp_files就是0,排序直接在内存完成。
optimizer_trace 是一个跟踪功能,跟踪执行的语句的解析优化执行的过程,比explain更详细。
/* 打开optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on';
max_length_for_sort_data
: 是MySQL中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL就认为单行太大,要换一个算法。
与全字段排序的区别,主要体现在内存排序完毕之后要多一次查询。因为排序的字段超过了
max_length_for_sort_data
的限制。
对于InnoDB表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。在“InnoDB表”中,对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。
优化器会优先考虑的,就是用于排序的行越少越好。
内存临时表与磁盘临时表
tmp_table_size这个配置限制了内存临时表的大小,默认值是16M。 如果临时表大小超过了tmp_table_size,那么内存临时表就会转成磁盘临时表
order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序
方法。
直接使用order by rand(),这个语句需要Using temporary和 Using filesort,查询的执行代价往往是比较大的
以下为三种join算法,主要的区别在于连接的字段是否有索引
select * from t1 straight_join t2 on (t1.a=t2.a);
t1与t2的a字段均有索引。
执行流程:
实际执行的时候使用了BAK优化,将尽可能多的驱动表数据取出放Join Buffer中,再关联查询。
BAK优化: 见下部分
流程中:
结论:
注意,这个结论的前提是“可以使用被驱动表的索引”。
select * from t1 straight_join t2 on (t1.a=t2.b);
表t2的字段b上没有索引,关联查询使用全表扫描。
查询过程与上述index join
相同,从t1拿定位一行数据后,接着就去t2表根据连接字段查询,SQL请求就要扫描表t2多达100次,总共扫描100*1000=10万行。
执行过程:
将驱动表数据读入线程内存join_buffer
中,同样以全表扫描,但是因为使用内存操作,速度比Simple join
的方法快。
select *
,因此是把整个表t1放入了内存;如果t1的数据超过了join buffer的限制,那整个执行流程如下,处理完部分数据先放到结果集中:
Multi-Range Read
优化
Batched Key Access
(BAK)
Block Nested-Loop Join
算法,这样的语句就尽量不要使用;(select 1000 as f) union (select id from t1 order by id desc limit 2);
执行流程:
union all:
(select 1000 as f) union all(select id from t1 order by id desc limit 2);
与上面union
的区别为union all
不需要除重,因此直接把查询结果放在结果集中返回。
select id%10 as m, count(*) as c from t1 group by m;
使用explain分析sql
Using index
,表示这个语句使用了覆盖索引,选择了索引a,不需要回表;Using temporary
,表示使用了临时表;Using filesort
,表示需要文件排序。这个语句的执行流程:
group by语句默认都会对语句进行排序,可以使用order by null 避免group by 排序。
select id%10 as m, count(*) as c from t1 group by m order by null;
索引保证了数据有序,在group by时候,分组计数计算时一片区域的id都是连续的,整个表扫描结束时便可以拿到结果,不需要临时表也不需要排序。
确保数据量确实超过了sort buffer
,可以直接强制mysql直接使用磁盘文件排序。
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
mysql的数据都是存放在磁盘中的,如果每次查询数据都从磁盘读取,性能较低,Buffer Pool
就是一层缓存池,来优化查询效率的。
内存的数据页是在Buffer Pool(BP)中管理的,在WAL里
Buffer Pool
起到了加速更新的作用。而 实际上,Buffer Pool
还有一个更重要的作用,就是加速查询。
在change buffer存储了一条更新操作后,如果刚好有一条查询,change buffer会把修改应用到内存页上,这时候内存数据页的结果是最新的,直接读内存页就可以了。
Buffer Pool对查询的加速效果,依赖于一个重要的指标,即:内存命中率。
InnoDBBuffer Pool
的大小是由参数innodb_buffer_pool_size
确定的,一般建议设置成可用物理 内存的60%~80%。
InnoDB内存管理用的是最近最少使用(Least RecentlyUsed, LRU)算法。
InnoDB管理Buffer Pool的LRU算法,是用链表来实现的。在InnoDB实现上,按照5:3的比例把整个LRU链表分成了young区域和old区域。
这个策略,就是为了处理类似全表扫描的操作量身定制的。防止一个对历史数据大表的全表扫描,而导致Buffer Pool的内存命中率急剧下降
执行流程:
change buffer(写缓存):在MySQL5.5之前,叫插入缓冲(insert buffer),只针对insert做了优化;现在对delete和update也有效,叫做写缓冲(change buffer)。
作用场景:
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。
通过这种方式就能保证这个数据逻辑的正确性。需要说明的是,虽然名字叫作change buffer
,实际上它是可以持久化的数据。也就是说,change buffer在内存中有拷贝,也会被写入到磁盘上。
Merge过程:
将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge操作。
merge的时候是真正进行数据更新的时刻,而change buffer的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做merge之前,change buffer记录的变更越多(也就是这个页面 上要更新的次数越多),收益就越大。
对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
change buffer
用的是buffer pool
里的内存,因此不能无限增大。change buffer的大小,可以通 过参数innodb_change_buffer_max_size来动态设置。这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。
一个更新的记录不在内存页中,这时,InnoDB的处理流程如下:
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。
change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
图中涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间 (t.ibd)、系统表空间(ibdata1)。
sort_buffer_size: 是MySQL为排序开辟的内存(sort_buffer
) 的大小。 如果要排序的数据量小于sort_buffer_size
,排序就在内存中完成。 但如果排序数据量太大内存放不下,则不得不利用磁盘临时文件辅助排序。
sort_buffer_size大于了需要排序的数据量的大小,number_of_tmp_files就是0,排序直接在内存完成。
具体可见order by排序章节
使用explain
关键字分析中:Extra
字段显示Using temporary
,表示的是需要使用临时表;Using filesort
,表示的是需要执行 排序操作。
在order by 排序中,若数据超过sort buffer的大小,那么就会考虑使用内存临时表
select word from words order by rand() limit 3;
内存临时表切换成磁盘临时表的参数:tmp_table_size
这个配置限制了内存临时表的大小,默认值是16M。如果临时表大 小超过了tmp_table_size,那么内存临时表就会转成磁盘临时表。
磁盘临时表使用的引擎默认是InnoDB,是由参数internal_tmp_disk_storage_engine
控制的。 当使用磁盘临时表的时候,对应的就是一个没有显式索引的InnoDB表的排序过程。
当使用磁盘临时表的时候,对应的就是一个没有显式索引的InnoDB表的排序过程。而临时文件的排序过程,就是归并排序算法。
系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程 内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
定义:当MySQL开始产生可以返回的结果集,会在通过网络返回给客户端请求线程之前,会先暂存在通过 net_buffer_size 所设置的缓冲区中,等满足一定大小的时候才开始向客户端发送,以提高网络传输效率。
这块内存的大小是由参数net_buffer_length定义的,默认是 16k。
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
join_buffer的大小是由参数join_buffer_size设定的,默认值是256k。如果放不下驱动表的所有数据,策略很简单就是分段放。
第一范式是最基本的范式(确保每列保持原子性)。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储。
第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
如一个订单的商品明细表,不应该订单主表的ID放到商品明细表中,会导致数据冗余
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
如一个订单表都会有客户字段,可以把客户编码作为一个外键跟订单表建立响应关系。
数据库设计三大范式
规则:用尽量少的存储空间来存储一个字段的数据
常用的类型:int、float、double、 decimal、varchar、char、 date、datetime
等
使用version作为乐观锁的控制。
创建人、创建时间、修改人、修改时间
数据库日期的存储主要有三种:字符串、DataTime与Timestamp、时间戳
使用字符串存储时间会有如下问题:
Datetime 和 Timestamp,通常会首选 Timestamp
set time_zone='+8:00';
时间戳存储:
表分区:分区是将数据分段划分在多个位置存放,可以是同一块磁盘也可以在不同的机器。分区后,表面上还是一张表,但数据散列到多个位置了。
mysql数据库中的数据是以文件的形式存在磁盘上的,默认放在/mysql/var下面(可以通过my.cnf中的datadir来查看), 对存储引擎为myisam来说,一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,一个是myi存表索引。
如果一张表的数据量太大的话,那么myd,myi就会变的很大,查找数据就会变的很慢,这个时候我们可以利用mysql的分区功能,在物理上将这一张表对应的三个文件,分割成许多个小块,这样呢,我们查找一条数据时,就不用全部查找了,只要知道这条数据在哪一块,然后在那一块找就行了。如果表的数据太大,可能一个磁盘放不下,这个时候,我们可以把数据分配到不同的磁盘里面去。
分区的好处是:
分区的限制和缺点:
分表:分表是将一个大表按照一定的规则分解成多张具有独立存储空间的实体表。
详细可见sharding-jdbc 分库分表
设计一张部门表应该有哪些必要字段?
读写分离的主要目标就是分摊主库的压力
客户端直连
上述的结构是客户端(client)主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说,由客户端来选择后端数据库进行查询。
客户端连接代理proxy
在MySQL和客户端之间有一个中间代理层proxy,客户端只连接proxy,由proxy根据请求类型和上下文决定请求的分发路由。
读写数据不一致问题:
由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
垂直分表定义:将一个表按照字段分成多表,每个表存储其中一部分字段。通常我们按以下原则进行垂直拆分:
垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
将表按照功能模块、关系密切程度划分出来,部署到不同的库上。
例如,我们会建立定义数据库 workDB、商品数据库 payDB、用户数据库 userDB、日志数据库 logDB 等,分别用于存储项目数据定义表、商品定义表、用户数据表、日志数据表等。
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
如将店铺ID为单数的和店铺ID为双数的商品信息分别放在两个库中。
水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。
一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大 表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。一般情况下,这种分库分表系统都有一个中间层proxy。
select v from ht where f=N;
通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上。这种语句只需要访问一个分表
select v from ht where k >= M order by t_modified desc limit 100;
由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有 行,然后统一做order by的操作。
思路1:在proxy层的进程代码中实现排序。
缺点:
思路2:把各个分库拿到的数据,汇总到一个MySQL实例的一个表中,然后在这个汇总实例上做逻辑操作。
该思路用到了临时表的使用。
MGR(MySQL Group Replication)是 MySQL 自带的一个插件,可以灵活部署。
在MGR出现之前,用户常见的MySQL高可用方式,无论怎么变化架构,本质就是Master-Slave架构。MySQL 5.7版本开始支持无损半同步复制(lossless semi-sync replication),从而进一步提高数据复制的强一致性。
MySQL MGR 集群是多个 MySQL Server 节点共同组成的分布式集群,每个 Server 都有完整的副本,它是基于 ROW 格式的二进制日志文件和 GTID 特性。
参考资料:
MySQL 的主从复制工作过程大致如下:
请求流程:
MySQL 建立请求的主从的详细流程如下:
master事务的提交不需要经过slave的确认。
master不关心slave是否接收到master的binlog。slave接收到master的binlog后先写relay log,最后异步地去执行relay log中的sql应用到自身。
由于master的提交不需要确保slave relay log是否被正确接受,当slave接受master binlog失败或者relay log应用失败,master无法感知。
在master事务的commit之前,必须确保一个slave收到relay log并且响应给master以后,才能进行事务的commit。但是slave对于relay log的应用仍然是异步进行的
基于传统异步复制和半同步复制的缺陷——数据的一致性问题无法保证
使用自增好处:
从性能和存储空间方面考量,自增主键往往是更合理的选择。
适合用业务字段直接做主键的场景,典型的KV场景。
比如,有些业务的场景需求是这样的:
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
内存表,指的是使用Memory引擎的表
create table …engine=memory
。临时表,可以使用各种引擎类型。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎。
create temporary table …。
alter table T drop index k;
alter table T add index(k);
索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样 页面的利用率最高,也就是索引更紧凑、更省空间。
但是,重建主键的过程不合理。不论是删 除主键还是创建主键,都会将整个表重建。
这两个语句,你可以用这个语句代替 :
alter table Tengine=InnoDB
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。
在内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
Mysql 数据库抖动可能就是在刷“脏页”。两种触发刷脏页(flush)的方法
为什么不直接淘汰脏页,等新数据读取的时候再应用redo log? 主要为了保证状态统一,内存的数据存在则肯定是最新的,内存没有则文件肯定是最新的。
InnoDB刷脏页的控制策略
举两个事务线程的例子。
通过redoLog 与binlog 两阶段提交 保证事务一致性。
binlog是Mysql数据库的日志,而redoLog与undoLog是innodb引擎才有的日志
binlog的作用?(说的是监控,其实主要是主从复制或者备份)
若事务回滚,根据undo log回滚更新请求。
session A:
start transaction;
// 时刻A
select * from t where id =1; // sql1
select * from t where id = 1 lock in share mode; // sql2
session B:
update t set c=c+1 where id =1 // 执行1万次
session B更新完100万次,生成了100万个回滚日志(undo log)。因此第一条查询sql1需要从一条条的undo log回找,查询效率极低。而第二条sql2是当前读,直接定位到对应的结果。
mysql 两个事物更新条件互斥,进入循环等待。
使用mysql参数innodb_deadlock_detect设置为on,表示开启这个死锁检测逻辑。
show processlist;显示哪些线程正在运行。您也可以使用mysqladmin processlist语句得到此信息
执行kill 命令
InnoDB 引擎,更新操作默认会加行级锁,行级锁会对索引加锁。
如果更新语句使用多个索引,行锁会先锁非聚簇索引,再锁聚簇索引。
如果两个事务中的 SQL 用到了不同的非聚簇索引或者一个用了一个没有使用(即使用索引的情况不同),这样的话就会导致这两个事务加行锁的顺序不一致,形成了多个事务之间资源(行锁)的循环等待,导致死锁。
例子:
事务1用到的 SQL 语句:
UPDATE authorized_user SET status = 1 WHERE username = 'wcy'
事务1获取的锁:X lock on (669,13359,1176) index_username
事务1等待的锁:X lock on (669,937,136) PRIMARY
事务2用到的 SQL 语句:
UPDATE authorized_user SET username = 'wcy100' WHERE id = 1
事务2获取的锁:X lock on (669,937,136) PRIMARY
事务2等待的锁:X lock on (669,13359,1176) index_username
事务1的 update 语句正常的执行步骤如下:
UPDATE authorized_user SET username = 'wcy100' WHERE id = 1,
解决方案:都是用主键更新
Order by查询的两种情况:
优化目标:尽量通过索引来避免额外的排序,减少CPU资源的消耗。
主要优化策略:
当无法避免Filesort操作时,优化思路就是让Filesort的操作更快。
优化策略:
对于子查询,一般的优化策略是做等价改写
sub-query优化总结:
优化策略:等价改写、反嵌套。
例子:
select * from customer where customer_id not in (select customer_id from payment)
改写成:
select * from customer a left join payment b on a.customer_id=b.customer_id where b.customer_id is null
分页查询,就是将过多的结果在有限的界面上分多页来显示。
其实质是每次查询只返回有限行,翻页一次执行一次。
优化目标
优化策略:
延迟优化案例
通过子查询关联,子查询先把对应的主键id查询出来,再进行主表关联
select * from user inner join
(select id from user limit 10000000 10) a on user.id = a.id
小案例:常规查询
select * from test_table
where merchant_id = 43 and status = 'SUCCESS'
order by salary_id desc limit 900000,10;
10 rows in set (0.82 sec)
延迟关联优化:通过id查询减少回表次数
SELECT *
FROM test_table a
INNER JOIN
(SELECT salary_id
FROM test_table
WHERE merchant_id = 43
AND STATUS = 'SUCCESS'
LIMIT 900000,
10) b ON a.salary_id = b.salary_id;
10 rows in set (0.52 sec)
mysql优化:覆盖索引(延迟关联)
处理策略
对于explain中出现的 Using join buffer (Block Nested Loop)
当两张表关联,如果不能够通过索引去做关联条件的匹配,这时候就会产生join_buffer的使用。
关联字段索引的必要性
数据库连接池就用用来保存数据库连接的一个池子。每当我们的业务代码需要和数据库进行交互时,就从这个池子里面取出一个数据库连接,然后在这个连接上进行查增删改操作。使用结束后,业务代码再将这个连接归还给这个池子,然后这个连接就可以被其他业务代码继续使用了。
从过程中可以看出,数据库连接池是可以在多个线程中使用的,每个线程获取不同的数据库连接。因此是线程安全的。
单个数据库连接肯定不是线程安全的,这就是需要实现数据库事务的原因。
.myi
索引文件和.myd
数据文件).idb
数据文件)
- DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
- 索引的支持:InnoDB不支持FULLTEXT类型的索引
- InnoDB表的行锁也不是绝对的,假如在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”
应用场景
默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同:
相关文章: mysql数据页存储与磁盘之间的关系
见索引规则
left join,right join,inner join
,表表关联什么区别?[Top]left join:保留左表所有结果,未匹配的右表记录字段为null
right join:保留右表结果
inner join:两表匹配结果的交集
表表关联:查询结构跟inner join结果一致
因此不要一次性的用delete语句删除太多数据。其实,这就是一个典型的大事务场景。可以使用循环分批删除的方式,减小事务。