MySQL 的查询流程具体是?or 一条SQL语句在MySQL中如何执行的
客户端请求 —> 连接器(验证用户身份,给予权限) —> 查询缓存(存在缓存则直接返回,不存在则执行后续操作) —> 分析器(对SQL进行词法分析和语法分析操作) —> 优化器(主要对执行的sql优化选择最优的执行方案方法) —> 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口) —> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
InnoDB、 MEMORY、 MyISAM 是其中常见的引擎有三种,其中InnoDB为默认存储引擎。
对比项 | MyISAM | InnoDB |
---|---|---|
主外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
行表锁 | 最小粒度为表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 | 最小粒度为行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作 |
缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
索引类型 | 非聚簇索引 | 聚簇索引 |
表空间 | 小 | 大 |
关注点 | 性能 | 事务 |
原子性(Atomicity):原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。即要么转账成功,要么转账失败,是不存在中间状态(不能出现A账户钱扣了,B账户没增加钱)
InnoDB保证原子性是通过redo log以及undo log来进行的。
redo log负责重做,undo log负责回滚。redo log记录了事务完成后的数据状态,undo log记录了事务开始前的数据状态。
一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态;
**例如:**A有五百元,B有一百元,此时A向B转账一百元,则正确结果是A有四百元,B有两百元,这就是一致性。如果不是这个结果,说明一致性被破坏。
undo log可以保证事务的一致性
隔离性(Isolation):事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性:一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来即使数据库发生故障也不应该对其有任何影响。
**innodb中的redo log可以保证持久性。**Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。
redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和bin log内容决定回滚数据还是提交数据。
**读未提交(Read uncommitted):**如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据
举例说明:你在修改表中数据的时候,你可以看到另一个人没有提交的数据。这是一种安全级别最低的隔离级别,目前这种级别只是理论存在,因为目前基本没有数据库采用这种隔离方式,这种隔离级别会产生的问题就是dirty read(脏读)
**读提交(Read committed):**如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
**举例说明:**AB两个事物同时操作一条数据,如果B事物对数据进行了修改,并没有提交。这时候A事物再次查询到的数据还是原数据,当B事物修改完后输入commit提交后,此时A事物查询才能得到修改后的数据。读提交,只能读取当已提交的数据。这个隔离级别解决了脏读的问题,但是缺点就是不可重复读
可重复度(Repeatable read): 是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。
举例说明:AB两个事物,A事物第一次查询查出5条数据(事物未完成),此时B事物新增加了一条数据,并且已经commit提交事物。A事物继续查询仍然是5条数据,只有A事物commit提交后继续查询,才显示6条数据。这就解决了不可重复度,但是又出现了新的问题幻读,明明刚刚查询的是5条数据,不是6条数据,这就是幻读。
序列化读(Serializable):最安全的默认隔离级别,但是并不支持并发,相当于单线程,不允许并发操作数据库,事务开启后,只允许一个人进行操作,直到提交后下一个人才可以进入。
Undo.log日志文件
数据库事物具备原子性,如果事物提交失败,需要数据回滚。
事物同时还具备持久性,事物所对数据做的变更就完全保存在数据库,不能因为故障而丢失。
原子性可以使用undo.log日志实现
**Undo.log的原理:**为了满足事物的原子性,在操作任何数据之前,首先把数据备份到undo.log中,然后进行数据的修改。如果出现了错误或者执行了回滚(rollback)语句,系统可以利用undo.log中的备份数据进行恢复。
数据库写入数据到磁盘之前,会把数据先缓存在内存中,事物提交时才会写入磁盘
**缺陷:**每个事物提交之前将数据写入磁盘和Undo.log中,这样会导致大量的IO操作,因此性能很低。
如果数据缓存一段时间,就能减少IO提高性能。但是这样就会丧失事物的持久性,因此引入了另外一种事物的持久化机制,即Redo.log
Redo.log日志文件
Redo.log和Undo.log相反,redo.log记录的是新增数据的备份。在事物提交前,将redo.log会进行持久化即可,不需要将数据持久话,减少了IO操作。
Redo.log的基本原理:
Undo.log + Redo.log事物的简化过程
前者是undo.log和数据库数据写入磁盘。现在是undo.log和redo.log写入磁盘,两者对比,似乎没有减少IO次数
1、数据库数据写入磁盘是随机IO,性能很差
2、redo.log在初始化是会开辟一块连续的空间,写入是顺序IO,性能很好
从数据结构角度
从物理存储角度
从逻辑角度
唯一索引 --/-- 非唯一索引
主键索引: 特殊的唯一索引,在不允许重复的基础上也不允许为空
单列索引(普通索引) --/-- 多列索引(复合索引、联合索引)
空间索引: 只能在MyISAM引擎中创建
首先要明白索引(index)是在存储引擎(storage engine)层面(第三层)实现的,而不是server层面(第二层)。不是所有的存储引擎都支持所有的索引类型。即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。
哈希索引原理其实就是hash表,搜索时间效率O(1),搜索效率好,也意味着磁盘IO花费少,mysql底层使用的是链式哈希表,结构如下,每一个bucket就是一个个哈希桶,也就是哈希链表的头结点。哈希结构天然的需要耗费空间资源,是一种用空间换时间的做法
hash要点:
说白了就是用的拉链法去解决的哈希冲突,也正是这个结构造成了哈希索引的一些特性
B-Tree是为磁盘等外存储设备设计的一种平衡查找树。系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取到内存中。在Mysql存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。Mysql存储引擎中默认每个页的大小为16KB,查看方式:
mysql> show variables like 'innodb_page_size';
我们也可以将它修改为4K、8K、16K。系统一个磁盘块的存储空间往往没有16K,因此Mysql每次申请磁盘空间时都会将若干地址连续磁盘块来达到页的大小16KB。Mysql在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
在B-Tree的基础上大牛们又研究出了许多变种,其中最常见的是B+Tree,MySQL就普遍使用B+Tree实现其索引结构。B+Tree是对B-tree的优化,主要是想每一页(每个节点)多存放键值和指针,减少tree深度,降低磁盘IO,提高查找效率。
与B-Tree相比,B+Tree做了以下一些改进:
1、非叶子节点,只存储键值信息,这样极大增加了存放索引的数据量。
2、 所有叶子节点之间都有一个链指针。对于区间查询时,不需要再从根节点开始,可直接定位到数据。
3、 数据记录都存放在叶子节点中。根据二叉树的特点,这个是顺序访问指针,提升了区间访问的性能。
通过这样的设计,一张千万级的表最多只需要3次磁盘交互就可以找出数据。
拓展问题:为什么mysql选择了B+Tree数据结构?
二叉树,平衡二叉树,红黑树,B树等,他们都是二分查找,找数也快,但是不管是平衡二叉树还是优化后的红黑树,说到底他们都是二叉树,当节点多了的时候,它们的高度就会高呀,我找一个数据。根节点不是,那就找下一层,下一层还没有我就再去找下一层,这样造成的后果就是我找一个数据可能要找好几次,而每一次都是执行了一次磁盘的io,而我们的索引的目的就是要减少磁盘io呀,这样设计可不行。那我们是不是把高度变矮就可以了呢?
深入问题:B+Tree如何将高度变矮了?
乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题
**乐观锁:**会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式
**悲观锁:**会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
记录锁(Record Locks): 单个行记录上的锁。对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项
-- 它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行
SELECT * FROM table WHERE id = 1 FOR UPDATE;
-- 在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;
在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
间隙锁(Gap Locks): 当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。对于键值在条件范围内但并不存在的记录,叫做“间隙”。InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。为了防止同一事务的两次当前读,出现幻读的情况。间隙锁基于Next-Key Locking
算法:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;
即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。
临键锁(Next-key Locks): 是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。(临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC(读已提交),临键锁则也会失效。
使用where条件语句限制要查询的数据,避免返回多余的行
-- 查询学生张三的成绩的年纪是否为18岁 优化前:
select * from student where age = 18;
-- 再从查询到的结果种判断是否包含张三 优化后:
select name from student where age = 18 and name = "张三";
*尽量避免select ,改使用select 列名,避免返回多余的列
-- 查询所有18岁的学生姓名 优化前:
select * from student where age = 18;
-- 优化后:
select name from student where age = 18;
若插入数据过多,考虑批量插入
-- 批量插入学生数据 优化前:
-- 伪代码
for(User u :list){
insert into student(age, name, height, weight) values(#{age}, #{name}, #{height}, #{weight});
}
-- 优化后:
insert into student(age, name, height, weight) values
<foreach collection="list" separator="," index="index" item="item">
(#{age}, #{name}, #{height}, #{weight})
</foreach>
避免在where 子句中的 “=” 左边进行内置函数、算术运算或其他表达式运算
-- 优化前:
select * from student where Date_ADD(updated_time,Interval 7 DAY) >=now();
select * from student where age + 1 = 18;
select * from student where substring(name,1,1) = `z`;
-- 优化后:
select * from student where updated_time >= Date_ADD(NOW(),INTERVAL - 7 DAY);
select * from student where age = 18 - 1;
select * from student where name like = "z%";
-- 原因:将导致系统放弃使用索引而进行全表扫描。
避免在 where 子句中使用 != 或 <> 操作符
-- 查询年龄不是18岁的学生 优化前:
select * from student where age != 18;
select * from student where age <> 18;
-- 优化后
select * from student where age < 18 union all select * from student where age > 18;
-- 原因:将导致系统放弃使用索引而进行全表扫描。
避免在 where 子句中使用or操作符
-- 查询年龄为17和18岁的学生信息 优化前:
select * from student where age = 18 or age = 17;
-- 优化后:
select * from student where age = 18 union all select * from student where age = 17;
-- 原因:将导致系统放弃使用索引而进行全表扫描。
where子句中考虑用默认值代替null
-- 查询未填写城市的学生 优化前:
select * from student where city is null;
-- 优化后
select * from student where city = "0";
-- 原因:不用is null 或者 is not null 不一定不走索引了,这个跟mysql版本以及查询成本有关。把null值,换成默认值,很多时候让走索引成为可能。
不要在where字句中使用not in
-- 查询年龄不是17岁的学生信息 优化前:
select * from student where age not in (17);
-- 优化后
select * from student a left join (select * from student where age = 17 ) b on a.id = b.id where b.id is null;
-- 原因:not in 不走索引,建议使用not exsits 和 left join优化语句。
合理使用exist & in
select * from A where id in (select id from B)
select * from A where exists (select 1 from B where B.id=A.id)
-- 当 B 表的数据集小于 A 表的数据集时,用 in 优于 exists, 当 B 表的数据集大于 A 表的数据集时,用 exists优于用 in
谨慎使用distinct关键字
-- 查询所有不重复的用户年龄 优化前:
select distinct * from student
-- 优化后
select distinct name from student;
-- 原因:当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较,过滤的过程会占用系统资源,cpu时间。
like语句优化
-- 查询姓名包含张三的学生信息 优化前:
select * from student where name like "%张三%";
-- 优化后
select * from student where name like "张三%";
-- 原因:like语句后跟"%%"走不了索引。