目录
【二叉树】
【B树和B+树】
【Hash索引】
【MySQL的存储原理】
⾮聚簇索引(MyISAM)
聚簇索引(InnoDB)
【MySQL的底层存储原理】
【redo log 和 binlog】
【SQL语句的执行流程】
【存储引擎的选择】
【mysql基本设置相关命令】
在之前的一篇文章 MySQL关于索引的理解_浮.尘的博客-CSDN博客 中提到了MySQL的MyISAM和InnoDB存储引擎的索引底层实际上是一个B+TREE(B+树),那么什么是B+树,今天就来简单说说。
在说B+树之前,先说说什么是二叉树,因为B+树是从二叉树演变过来的。
二叉树(Binary Tree):二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树(Binary Tree)。二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树是完全二叉树的一种特殊情况。
再比如下面的图就是一个简单的二叉树:
其中1是根节点,2和3分别是1的左子树和右子树,4和5分别是2的左子树和右子树,6是3的右子树,3没有左子树。
一般遍历二叉树可以使用 前序遍历、中序遍历、后序遍历,其中,前、中、后序,表示的是节点与它的左右子树节点遍历的先后顺序。用递归代码来实现遍历的时间复杂度是 O(n)。(关于时间复杂度后面再讲)
可以用代码实现一下做这个二叉树,这里用PHP代码实现一下,代码如下:
左子树 ---> 右子树
*/
function preorder($root) {
if (empty($root)) {
return;
}
echo $root->value . ' ';//输出根节点
preOrder($root->left);
preOrder($root->right);
}
/**
* 中序遍历: 左子树---> 根节点 ---> 右子树
*/
function inorder($root) {
if (empty($root)) {
return;
}
inorder($root->left);
echo $root->value . ' ';//输出根节点
inorder($root->right);
}
/**
* 后序遍历: 左子树 ---> 右子树 ---> 根节点
*/
function tailorder($root) {
if (empty($root)) {
return;
}
tailorder($root->left);
tailorder($root->right);
echo $root->value . ' ';//输出根节点
}
//测试
/*
1
/ \
2 3
/ \ \
4 5 6
*/
$a = new BinaryTree1();
$b = new BinaryTree1();
$c = new BinaryTree1();
$d = new BinaryTree1();
$e = new BinaryTree1();
$f = new BinaryTree1();
$a->value = '1';
$b->value = '2';
$c->value = '3';
$d->value = '4';
$e->value = '5';
$f->value = '6';
$a->left = $b;
$a->right = $c;
$b->left = $d;
$b->right = $e;
$c->right = $f;
echo "前序遍历:";
preorder($a); //1 2 4 5 3 6
echo "\n";
echo "中序遍历:";
inorder($a);//4 2 5 1 3 6
echo "\n";
echo "后序遍历:";
tailorder($a);//4 5 2 6 3 1
echo "\n";
完整代码可以参考:https://gitee.com/rxbook/php_algo/blob/master/code18_BinaryTree2.php
虽然二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。原因是二叉树每个节点保存的数据较少,会增加IO操作。索引不止存在内存中,还要写到磁盘上。为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么我们就不应该使用二叉树,而是要使用“N叉”树。这里,“N叉”树中的“N”取决于数据块的大小。
二叉树的基本原理就先说到这里,关于数据结构与算法的更多内容,后面再细说。接下来看看什么是B树和B+树。
B树是为了磁盘或其它存储设备⽽设计的⼀种多叉平衡查找树,B树是⾮叶⼦节点和叶⼦节点都会存储数据。B树的⾼度⼀般都是在2-4这个⾼度,树的⾼度直接影响IO读写的次数。下面演示一下生成一颗B树的过程:
B树生成过程
生成的B树可能是下面的结构:
B+树是只有叶⼦节点才会存储数据,⽽且存储的数据都是在⼀⾏上,⽽且这些数据都是有指针指向的。下面演示一下生成一颗B+树和查找的过程:
B+树生成过程
怎么样,是不是很有趣,大家可以点这个链接自己去试一试:B+ Tree Visualization
数据库索引的常见模型:哈希表、有序数组和搜索树。
哈希表这种结构适用于只有等值查询的场景,比如 Memcached、Redis 及其他一些 NoSQL 引擎。而有序数组在等值查询和范围查询场景中的性能就都非常优秀。如果仅仅看查询效率,有序数组就是最好的数据结构了。但是在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。所以,有序数组索引只适用于静态存储引擎(比如历史数据,不再变动的)
采用 Hash 进行检索效率非常高,基本上一次检索就可以找到数据,而 B+ 树需要自顶向下依次查找,多次访问节点才能找到数据,中间需要多次 I/O 操作,从效率来说 Hash 比 B+ 树更快。既然Hash索引这么好,为什么mysql没有用Hash索引呢?因为Hash索引存在如下缺陷:
之前我们说了MySQL的InnoDB底层使用的就是B+树,现在来看看它的存储原理:B+ 树的节点存储在磁盘上,每个节点存储 1000 多个数据,这样树的深度最多只要 4 层, 就可存储数亿的数据。如果将树的根节点缓存在内存中,则最多只需要三次磁盘访问就可以检索到需要的索引数据,是不是效率比二叉树要高了好多个量级。
下面再来深入分析一下 聚簇索引 和 非聚簇索引,以及之前说的“回表”是什么意思。
#主键查询,只需要搜索id这棵B+树
select * from user where id=1; #应该尽量使用主键查询
#普通索引查询,需要先搜索name索引树,得到id的值为1,再到上面的id索引树搜索一次,这个过程称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。
select * from user where name='renxing'; #回表查询,检索两次:⾮主键索引 —> 主键索引 —> 数据。
# 不需要回表,在辅助索引树上就可以查询到了,叫做覆盖索引,因此要多⽤组合索引。
select id,name from user where name='renxing';
再来分下一下为什么mysql不建议使⽤过⻓的字段作为主键?
因为InnoDB的每个索引保存的都是主索引的值,过⻓的主索引会令辅助索引变得过⼤。
ALTER TABLE table_name ADD INDEX index_name('name','age','sex');
select name,age,sex from table_name where name='xxx'; #实现覆盖索引,不需要回表。
## 组合索引遵循最左前缀原则:like a%能用到索引,like %a 用不到索引;
关于MySQL的B+树和索引底层原理大概就是这样了,下面总结一下:
1.索引的作用:提高数据查询效率
2.常见索引模型:哈希表、有序数组、搜索树
3.哈希表:键-值(key - value)。哈希思路:把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。哈希冲突的处理办法:链表或一致性哈希算法。哈希表适用场景:只有等值查询的场景。
4.有序数组:按顺序存储。查询用二分法就可以快速查询,时间复杂度是:O(log(N)),有序数组查询效率高,更新效率低。有序数组的适用场景:静态存储引擎。
5.二叉搜索树:每个节点的左子树小于父节点,父节点又小于右子树;查询时间复杂度O(log(N)),更新时间复杂度O(log(N))。数据库存储大多不适用二叉树,因为树太高。
6.InnoDB中的索引模型:B+Tree,索引类型:主键索引、非主键索引。主键索引的叶子节点存的是整行的数据(聚簇索引),非主键索引的叶子节点内容是主键的值(二级索引)
7.主键索引和普通索引的区别:主键索引只要搜索ID这个B+Tree即可拿到数据。普通索引先搜索索引拿到主键值,再到主键索引树搜索一次(回表)
8.一个数据页满了,按照B+Tree算法,新增加一个数据页,叫做页分裂,会导致性能下降。空间利用率降低大概50%。当相邻的两个数据页利用率很低的时候会做数据页合并,合并的过程是分裂过程的逆过程。
9.从性能和存储空间方面考量,自增主键往往是更合理的选择。
10.由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
11.在建立联合索引的时候,如何安排索引内的字段顺序?第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
12.索引下推:在MySQL5.6之前,只能从根据最左前缀查询到ID开始一个个回表。到主键索引上找出数据行,再对比字段值。MySQL5.6引入的索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
13.有的时候明明有索引却不能命中的原因是,数据库 在对物理执行计划优化的时候,评估发现不走索引,直接全表扫描是更优的选择。
mysql 落在磁盘上的文件名称(假设表名是users):
【问】mysql为什么不建议用uuid当主键?为什么建议主键ID是递增的,和B+ Tree有什么关系?
【答】因为 mysql一个 page 放满数据之后才会放到下一个 page ,使用递增可以有效利用存储空间。uuid是没有规律的。
物理日志redo log:先说说mysql数据库的WAL机制,WAL 的全称是 Write-Ahead Logging,它的关键点是先写日志再写磁盘。redo log是保证事务持久性的重要机制,当mysql服务器意外崩溃或者宕机后,为了保证已经提交的事务,确定持久化到磁盘中的一种措施。当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写。有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
对任意页面进行修改的操作都会生成redo日志,在事务提交时,只要保证生成的redo日志成功落盘即可,这样即使MySQL发生故障导致内存中的数据丢失,也可以根据已落盘的redo日志恢复数据。一个事务生成的redo日志是按顺序写入磁盘的,是顺序IO。
归档日志 binlog:redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog。最开始 MySQL 里并没有 InnoDB 引擎,早期MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统(也就是 redo log)来实现 crash-safe 能力。
binlog 记录了所有的DDL和DML(除了数据查询语句)语句,还包含语句所执行的消耗的时间,MySQL的binlog是事务安全型的。因为有了数据更新的binlog,所以可以用于实时备份与master/slave主从复制结合。回放 Binlog,就相当于把之前对数据库所有数据更新操作按照顺序重新执行了一遍,回放完成之后数据自然就恢复了。这就是 Binlog 增量备份的基本原理。(后面再讲)
如果是一天一备份,假设误删了数据表,先用全量备份的sql文件恢复到当天0点,然后再用 Binlog 把数据恢复到删表之前的那个时刻。通过定期的全量备份,配合 Binlog 就可以把数据恢复到任意一个时间点。MySQL 中无论是复制还是备份恢复依赖的都是全量备份和 Binlog,全量备份相当 于备份那一时刻的一个数据快照,Binlog 则记录了每次数据更新的变化,也就是操作日志。
【区别和说明】
一个 SQL 提交到数据库, 经过连接器将 SQL 语句交给语法分析器,生成一个抽象语法树 AST(Abstract Syntax Tree),AST 经过语义分析与 优化器,进行语义优化,使计算过程和需要获取的中间数据尽可能少,然后得到数据库执行计划;执行计划提交给具体的执行引擎进行计算,将结果通过连接器再返回给应用程序。
一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。简单地说就是:SQL 语句→缓存查询→解析器→优化器→执行器。(注意:MySQL8.0已经把缓存部分删掉了)
SELECT语句的执行顺序:FROM > WHERE > GROUP BY > HAVING > 字段 > DISTINCT > ORDER BY > LIMIT。
比如对于这样一个 SQL 语句,其语义是表示从 users 表中取出每一个 id 和 order 表当前记录比较是否相等。
mysql> select f.id from orders f where f.user_id = (select id from users);
事实上,这个 SQL 语句在语义上等价于下面这条 SQL 语句,表间计算关系更加清晰。
mysql> select f.id from orders f join users u on f.user_id = u.id;
数据库里面的长连接是指连接成功后,如果客户端持续有请求则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。但是需要注意:定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续使用mysql,就需要重连,然后再执行请求。
如果查询命中缓存,MySQL 不需要执行后面的复杂操作就可以直接返回结果,这个效率会很高。查询缓存可以看做是SQL文本和查询结果的映射,第二次查询的SQL和第一次查询的SQL完全相同,则会使用缓存。表的结构或数据发生改变时,查询缓存中的数据不再有效。mysql中关于缓存的配置和命令如下:
query_cache_type #查询缓存类型,有0、1、2三个取值。0则不使用查询缓存。1表示始终使用查询缓存。2表示按需使用查询缓存。
query_cache_type #为1时,也可以手动关闭查询缓存:SELECT SQL_NO_CACHE * FROM my_table WHERE condition;
query_cache_type #为2时,按需查询缓存:SELECT SQL_CACHE * FROM my_table WHERE condition;
query_cache_size #默认情况下值为0,表示为查询缓存预留的内存为0,则无法使用查询缓存。
SHOW STATUS LIKE 'Qcache_hits'; #查看缓存命中次数
FLUSH QUERY CACHE; #清理查询缓存内存碎片
RESET QUERY CACHE; #从查询缓存中移出所有查询
FLUSH TABLES; #关闭所有打开的表,同时该操作将会清空查询缓存中的内容
但是大多数情况下不要使用查询缓存,因为查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。可以用 SQL_CACHE 显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from T where ID=10;
需要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。
优化器 是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
MySQL的存储引擎有:MyISAM、InnoDB、Memory、Archive、Blackhole、CSV。
InnoDB的特点:
MyISAM:
应用场景:
【问】为什么MyISAM会比Innodb 的查询速度快?
【答】InnoDB 在做SELECT的时候,要维护的东西比MYISAM引擎多很多:
(1)InnoDB 要缓存数据和索引,MyISAM只缓存索引块,这中间还有换进换出的减少
(2)innodb寻址要映射到块,再到行,MyISAM记录的直接是文件的OFFSET,定位比INNODB要快
(3)InnoDB 还需要维护MVCC一致;就算你没有用到,但还是需要去检查和维护
Memory:如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果。数据的处理速度很快但是安全性不高。一般用得不多,可以使用redis来代替。
Archive:如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive。
# 查看mysql提供什么存储引擎:
show engines;
# 查看默认的存储引擎:
show variables like '%storage_engine%'; 或 SELECT @@default_storage_engine;
# 修改默认的存储引擎:
SET DEFAULT_STORAGE_ENGINE=MyISAM; 或者修改 my.cnf
# 修改数据表存储引擎:
ALTER TABLE engine_demo_table ENGINE = InnoDB;
# 查询MySQL数据库服务器的性能参数、执行频率:
SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
#常用的性能参数如下:
Connections //连接MySQL服务器的次数。
Uptime //MySQL服务器的上 线时间。
Slow_queries //慢查询的次数。
Innodb_rows_read:Select //查询返回的行数
Innodb_rows_inserted //执行INSERT操作插入的行数
Innodb_rows_updated //执行UPDATE操作更新的行数
Innodb_rows_deleted //执行DELETE操作删除的行数
Com_select //查询操作的次数。
Com_insert //插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
Com_update //更新操作的次数。
Com_delete //删除操作的次数。
# 统计SQL的查询成本:
SHOW STATUS LIKE 'last_query_cost';
# 开启慢查询日志参数:
set global slow_query_log='ON'; #开启slow_query_log
show variables like '%long_query_time%'; #修改long_query_time阈值
# 查询当前系统中有多少条慢查询记录:
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
# 慢查询日志分析工具:
mysqldumpslow
# 查看 SQL 执行成本:
show variables like 'profiling';
# 开启 show profile:
set profiling = 'ON';
# 当前会话都有哪些 profiles:
show profiles;
# 慢查询日志是否打开
show variables like '%slow'
# 查看MySQL允许的最大连接数
show variables like 'max_connections'
# 系统当前状态,com_xxx表示xxx语句执行的次数,例如com_select.
show global status
# 查看慢查询的条数
show global status like '%slow'
# 查看索引的使用情况:
show status like 'Handler_read%'
# 显示当前所有连接的工作状态
show processlist;
慢查询相关:
mysql > show variables like '%slow_query_log'; #先看下慢查询是否已经开启
mysql > set global slow_query_log='ON'; #把慢查询日志打开,注意设置变量值的时候需要使用 global
mysql > show variables like '%long_query_time%'; #查看慢查询的时间阈值设置
mysql > set global long_query_time = 3; #如果想把时间缩短,比如设置为 3 秒
可以使用 MySQL 自带的 mysqldumpslow 工具统计慢查询日志: perl mysqldumpslow.pl -s t -t 2 "/tmp/slow.log”
能看到开启了慢查询日志,并设置了相应的慢查询时间阈值之后,只要大于这个阈值的 SQL 语句都会保存在慢查询日志中,然后就可以通过 mysqldumpslow 工具提取想要查找的 SQL 语句了。
MySQL 对一条 SQL 语句的执行时间进行分析:
【日拱一卒,再小的坚持也是进步!】