0
点赞
收藏
分享

微信扫一扫

MySQL索引原理以及SQL优化

诗尚凝寒 2022-04-05 阅读 57

文章目录


一、Mysql索引

索引

索引分类:主键索引、唯一索引、普通索引、组合索引、以及全文索引(elasticsearch);

主键索引

非空唯一索引,一个表都有且只有一个主键索引;在 innodb 中,主键索引的 B+ 树包含表数据信息;
PRIMARY KEY(key)

唯一索引

不可以出现相同的值,可以有NULL值
UNIQUE(Key)

普通索引

允许出现索引的内容
INDEX(key)
– OR
KEY(key[,…])

组合索引

对表上的多个列进行索引
INDEX idx(key1,key2[,…]);
UNIQUE(key1,key2[,…]);
PRIMARY KEY(key1,key2[,…]);

全文索引

将存储在数据库当中的整本书和整篇文章中的任意内容信息查找出来的技术;关键词 FULLTEXT;在短字符串中用 LIKE % ;在全文索引中用match和against ;

主键选择

innodb 中表是索引组织表,每张表有且仅有一个主键;
如果显示设置 PRIMARY KEY ,则该设置的key为该表的主键;
如果没有显示设置,则从非空唯一索引中选择;
只有一个非空唯一索引,则选择该索引为主键;
有多个非空唯一索引,则选择声明的第一个为主键;
没有非空唯一索引,则自动生成一个 6 字节的 _rowid 作为主键;

约束

约束是一个逻辑概念
为了实现数据的完整性,对于innodb,提供了以下几种约束,primary key,unique key,foreign key, default, not null;

外键约束

在项目中尽量不要使用这种约束,外键是具有事务性的
外键用来关联两个表,来保证参照完整性;MyISAM存储引擎本身并不支持外键,只起到注释作用;而innodb完整支持外键;

create table parent ( 
id int not null, 
primary key(id)
) engine=innodb; 
create table child ( 
id int, 
parent_id int,
foreign key(parent_id) references parent(id) ON DELETE CASCADE ON UPDATE CASCADE 
) engine=innodb;
-- 被引用的表为父表,引用的表称为子表;
-- 外键定义时,可以设置行为 ON DELETE 和 ON UPDATE,行为发生时的操作可选择:
-- CASCADE 子表做同样的行为 
-- SET NULL 更新子表相应字段为 NULL
-- NO ACTION 父类做相应行为报错 
-- RESTRICT 同 NO ACTION
INSERT INTO parent VALUES (1); 
INSERT INTO parent VALUES (2); 
INSERT INTO child VALUES (10, 1); 
INSERT INTO child VALUES (20, 2); 
DELETE FROM parent WHERE id = 1;

约束与索引的区别

创建主键索引或者唯一索引的时候同时创建了相应的约束;但是约束时逻辑上的概念;索引是一个数据结构既包含逻辑的概念也包含物理的存储方式;

二、B+树

B+树的全称是,多路平衡搜索树,提供一个稳定搜索时间复杂度,高度平衡,叶子节点都在同一层,每一条链路的高度都是一致的
全称:多路平衡搜索树,减少磁盘访问次数;用来组织磁盘数据,以页为单位,物理磁盘页一般为4K,innodb 默认页大小为 16K;对页的访问是一次磁盘io,缓存中会缓存常访问的页;
特征:非叶子节点只存储索引信息,叶子节点存储具体数据信息;叶子节点之间互相连接,方便范围查询;
每个索引对应着一个B+树;
在这里插入图片描述为什么要使用B+树
树的高度代表遍历时比较的次数,B+树组织的是一个磁盘的数据,树的高就代表了我们访问磁盘的次数,磁盘访问的次数要尽量少。
磁盘与内存的访问的差异:一次磁盘io,大约是10ms左右,访问一次内存大约是100us,数量级相差很大
所以我们要减少对磁盘的访问次数,所以尽量让树变得矮胖,一个节点尽量塞更多的数据
B+怎么映射磁盘
从一个节点出发,对应的是一个16k的数据,平常去调用访问那些io函数,去访问磁盘访问数据的时候,通常是4k或者8k,B+树呢就应该是4k的整数倍,一个节点的大小是固定的,通常是16k,B+树一个节点至少要存储两行数据
非叶子节点只存储索引信息,也会指向下一个物理磁盘的地址,比如说查找磁盘的时候,会执行范围查询,然后根据范围选择下一个磁盘
在这里插入图片描述
所有的叶子节点之间都是相互引用的,为了不用去回溯地去查找,查找失败直接通过索引去查,叶子节点之间存储前后叶子节点的物理地址,叶子节点存储具体的数据信息

B+树层高问题

B+树的一个节点对应一个数据页;B+树的层越高,那么要读取到内存的数据页越多,io次数越多;
innodb一个节点16kB;
假设:
key为10byte且指针大小6byte,假设一行记录的大小为1kB;
那么一个非叶子节点可存下16kB/16byte=1024个(key+point);每个叶子节点可存储1024行数据;
结论:
2层B+树叶子节点1024个,可容纳最大记录数为: 1024 * 16 = 16384;
3层B+树叶子节点1024 * 1024,可容纳最大记录数为:1024 * 1024 * 16 = 16777216;
4层B+数叶子节点1024 * 1024 * 1024,可容纳最大记录数为:1024 * 1024 * 1024 * 16 =17179869184;
我们项目正常的数据就是2到4行,超过500w的数据就要考虑分表分库了

关于自增id

超过类型最大值会报错;
类型 bigint 范围: -2的63次方到2的63次方再减去1;
假设采用 bigint 1秒插入1亿条数据,大概需要5849年才会用完索引;
所以不用担心会超过最大值

聚集索引

按照主键构造的B+树;叶子节点中存放数据页;数据也是索引的一部分;
#table id name
select * from user where id >= 18 and id < 40;
myisam,由这三个文件构成
frm 表信息文件
myd 数据文件(用堆表进行组织的)
myi 索引文件(B+树进行组织的)
myisam的B+树叶子节点:索引+行所在数据文件的地址
比如说select * from table where id=9;回表查询,首先通过这个id=9,查找到那个B+树,找到那个索引,通过那个索引找到那个行所在数据文件的地址,找到那个地址以后,我们回到那个数据文件,从那个myd数据文件中找到那个数据。
InnoDB中的数据文件的数据也是B+树进行组织的
比如说我们再回到这一句,这一句是怎样查找到我们数据的呢
select * from user where id >= 18 and id < 40;
首先是根节点,第一次io,查询大于18,就能定位到p2所对应的地址,然后找到页3,然后找到页8,在所有叶子节点里边的数据都是有序的,所以采用的是二分查找,如果没找到或者找到了想找下一条,不用回溯到上一层,而是直接通过索引去寻找下一个叶子节点。
在这里插入图片描述

辅助索引

叶子节点不包含行记录的全部数据;辅助索引的叶子节点中,除了用来排序的 key 还包含一个bookmark ;该书签存储了聚集索引的 key;
– 某个表 包含 id name lockyNum; id是主键,lockyNum存储辅助索引;
select * from user where lockyNum = 33;
每一个索引会对应一个B+树,lockynum不是主键,走辅助索引,通过索引信息去找主键的id,通过主键id走聚集索引找一行数据,叶子节点的数据,索引+主键id。通过主键id找聚集索引叫回表查询
如果是查一行数据,都是要进行回表查询,不管where后边跟的是不是主键
InnoDB,通过主键查询只会走聚集索引,如果走辅助索引,就要查找到主键id,如果查一行完整的数据,再走回表查询聚集索引
在这里插入图片描述

索引存储

innodb由段、区、页组成;段分为数据段、索引段、回滚段等;区大小为 1 MB(一个区由64个连续页构成);页的默认值为16k;页为逻辑页,磁盘物理页大小一般为 4K 或者 8K;为了保证区中的页的连续,存储引擎一般一次从磁盘中申请 4~5 个区;
在这里插入图片描述

innodb 体系结构

左边是内存的块,右边是磁盘的一个块
内存有一个重要的叫Buffer Pool,指的是这些具体的缓存信息,就是刚刚磁盘io访问数据的时候,会把这些信息报存到缓冲池Buffer Pool中去,为了下一次再去访问相同数据的时候,会直接访问内存,不用再去访问磁盘。
内存中还有一个Change Buffer,缓存对非唯一索引DML操作,数据修改的时候,会先放到这个change buffer中,如果是唯一索引的修改,会直接写到磁盘当中,如果是非唯一索引,先记录到缓存当中,我们会有io线程异步地把数据刷到磁盘中去。

在这里插入图片描述
对数据修改,修改唯一的,先写日志,再异步刷盘,为啥要先写日志呢,B+树是离散的,这里涉及到一个随机io与顺序io的差别
随机io通过旋转来定位我们所找的位置,位置是不连续的,我们这里使用的是B+树去存储,访问这种不连续的磁盘,我们才能够找到具体的位置
顺序io会有一个日志表文件,这个日志表是一行一行往后面辅加,磁道是连续的,所以用顺序io非常快,先写日志再异步刷盘
非唯一修改,先写缓存,再异步刷盘
磁盘是由磁道和扇区组成
在这里插入图片描述

Buffer pool
Buffer pool缓存表和索引数据;采用 LRU算法(原理如下图)让Buffer pool只缓存比较热的数据 ;
说到LRU算法,这里的比我们所认知的有点不一样,以往的LRU算法都是最先执行的排到最前边,这里不一样这里是把最先执行的插入到最中间去,如果证明是经常访问的数据最热的数据,那么这个数据就会从中间往前面爬。
在这里插入图片描述
Change buffer
Change buffer缓存非唯一索引的数据变更(DML操作),Change buffer中的数据将会异步merge到磁盘当中;
在这里插入图片描述

三、最左匹配原则与覆盖索引

对于组合索引,从左到右依次匹配,遇到 > < between like 就停止匹配;
有多个key来构成一个索引,key1有序,key2不一定有序,key1相同的情况下key2才是有序的
在这里插入图片描述
我们来看看以下语句,我们来思考一下

ROP TABLE IF EXISTS `left_match_t`;
CREATE TABLE `left_match_t` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(255) DEFAULT NULL,
	`cid` INT(11) DEFAULT NULL,
	`age` SMALLINT DEFAULT 0,
	PRIMARY KEY (`id`),
	KEY `name_cid_idx` (`name`, `cid`)
)ENGINE = INNODB AUTO_INCREMENT=0 DEFAULT CHARSET = utf8;

INSERT INTO `left_match_t` (`name`, `cid`, `age`)
VALUES
	('mark', 10001, 12),
	('darren', 10002, 13),
	('vico', 10003, 14),
	('king', 10004, 15)

SHOW INDEX FROM `left_match_t`;
# 作用优化器
EXPLAIN SELECT * FROM `left_match_t` WHERE `name` = 'mark';
# 优化器
EXPLAIN SELECT * FROM `left_match_t` WHERE `cid` = 1 AND `name` = 'mark';
EXPLAIN SELECT * FROM `left_match_t` WHERE `cid` = 1;

EXPLAIN SELECT * FROM left_match_t WHERE name = ‘mark’;这一句走的哪一个索引,我们主要看这四个地方,我们要看是哪些列
在这里插入图片描述最左匹配原则,按照我们声明的顺序,从最左侧开始匹配,从name开始匹配,我们来看结果
在这里插入图片描述EXPLAIN主要作用在优化器阶段
我们再来看看,这一句
EXPLAIN SELECT * FROM left_match_t WHERE cid = 1;
会不会走,很明显不会走,因为不符合我们的最左匹配原则
在这里插入图片描述我们再来看看这一句
EXPLAIN SELECT * FROM left_match_t WHERE cid = 1 AND name = ‘mark’;
会不会走呢,结果是会走的,我们的优化器会自动帮我们优化,把顺序调换一下,不会影响最终结果

在这里插入图片描述

覆盖索引

从辅助索引中就能找到数据,而不需通过聚集索引查找;利用辅助索引树高度一般低于聚集索引树;较少磁盘 io;意思就是说不需要回表查询
我们来举一个例子,还是用上边那个表
我们来看看这一句
SHOW INDEX FROM left_match_t;
在这里插入图片描述使用select语句查询时,如果只想获取某一些字段的数据,可以不用*,这个是获取一行的意思,要获取一行就会走回表查询,如何不走回表查询呢
我们可以这样选择字段把星号去掉
EXPLAIN SELECT name,cid,id FROM left_match_t WHERE name = ‘mark’;
这句不用走回表查询,因为这些数据在辅助索引当中就能找到,所以不用回表查询
我们再来看看这句
EXPLAIN SELECT ‘id’, ‘name’ FROM covering_index_t WHERE cid = 1;
走的是那条索引呢,走的是辅助索引
在这里插入图片描述
这里Mysql认为成本是最低的,mysql优化器主要针对IO和CPU会计算语句的成本;

四、索引失效问题

select … where A and B 若 A 和 B 中有一个不包含索引,则索引失效;
索引字段参与运算,则索引失效;例如: from_unixtime(idx) = ‘2022-04-05,这个太复杂了’;
索引字段发生隐式转换,则索引失效;例如: ‘1’ 隐式转换为 1 ;
LIKE 模糊查询,通配符 % 开头,则索引失效;例如: select * from user where name like ‘%Mark’;
在索引字段上使用 NOT <> != 索引失效;如果判断 id <> 0 则修改为 idx > 0 or idx < 0 ;
组合索引中,没使用第一列索引,索引失效;
in + or 索引失效;单独的in 是不会失效的; not in 肯定失效的;
B+树的有序性是通过比较K得出来的

五、索引原则

查询频次较高且数据量大的表建立索引;
索引选择使用频次较高,过滤效果好的列或者组合;
使用短索引;节点包含的信息多,较少磁盘io操作;比如:smallint,tinyint;
对于很长的动态字符串,考虑使用前缀索引;
有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大的节约索引空间,从而提高索引效率,但这会降低索引的区分度,索引的区分度是指不重复的索引值和数据表记录总数的比值。
索引的区分度越高则查询效率越高,因为区分度更高的索引可以让mysql在查找的时候过滤掉更多的行。
对于 BLOB , TEXT , VARCHAR 类型的列,必要时使用前缀索引,因为mysql 不允许索引这些列的完整长度,使用该方法的诀窍在于要选择足够长的前缀以保证较高的区分度

select count(distinct left(name,3))/count(*) as sel3, 
count(distinct left(name,4))/count(*) as sel4, 
count(distinct left(name,5))/count(*) as sel5,
count(distinct left(name,6))/count(*) as sel6,
from user; alter table user add key(name(4)); 
-- 注意:前缀索引不能做 order by 和 group by

推荐创建主键,主键建议使用整数且自增的,因为这样方便比较。
自增的话,在B+树叶子节点上加数据的话,总是往最后面加数据,就会有较少的数据平衡
对于组合索引,考虑最左侧匹配原则和覆盖索引;
尽量选择区分度高的列作为索引;该列的值相同的越少越好;
尽量扩展索引,在现有索引的基础上,添加复合索引;最多6个索引
不要 select * ; 尽量只列出需要的列字段;方便使用覆盖索引;
索引列,列尽量设置为非空;
可选:开启自适应 hash 索引或者调整 change buffer;

优化器成本分析

mysql 优化器主要针对 IO 和 CPU 会计算语句的成本;可能不会按照分析的原理来执行语句;
成本分析步骤
找出所有可能需要使用到的索引;
计算全表扫描的代价;
计算不同索引执行查询的代价;
对比找出代价最小的执行方案;
SQL优化
MySQL :: MySQL 5.7 Reference Manual :: 8 Optimization
EXPLAIN
用来查看SQL语句的具体执行过程。
原理:模拟优化器执行 SQL 查询语句,从而知道mysql是如何处理sql语句的。

六、问题的解决与定位

有问题的sql语句怎么找
1、htop top 看cpu 磁盘
2、sql-slow-log 10s看日志查询
3、线上的时候,有些用户访问特别慢
看完整的语句
slow processlist看哪条连接出问题了
show full processlist就能看到是哪条语句出了问题
再用explain进行分析,可能分析不出什么问题,通过输出那几个字段很难描述问题的复杂性
所以我们还需要把优化器的选择过程弄出来
优化器根据解析树可能会生成多个执行计划,然后选择最优的的执行计划;
SHOW VARIABLES LIKE ‘optimizer_trace’;
– 启用优化器的追踪
SET optimizer_trace=‘enabled=on’;
– 执行一条查询语句
SELECT * FROM information_schema.optimizer_trace;
– 用完关闭
SET optimizer_trace=“enabled=off”; SHOW VARIABLES LIKE ‘optimizer_trace’;

举报

相关推荐

0 条评论