导学:索引什么时候失效?为什么类型转换索引会失效?不满足最左匹配原则?
- 我们都知道,MySQL它主要有2大模快组成,第一块就是我们的MySQL服务,里面包含了像连接管理、解析器、预处理、优化器、执行器,这个会把一条SQL语句执行的前置工作会做完,然后会生成一个执行计划,给到我们的存储引擎组件。
- 存储引擎负责数据的存储、操作以及解锁。MySQL支持的存储引擎有很多,比如InnoDB、MyISAM、Memory等等,不同的存储引擎,存储与操作的方式也不一样。
今天要研究的是 InnoDB 存储引擎,因为 InnoDB是我们MySQL默认的存储引擎 ,索引也是研究InnoDB 存储索引。
官网:MySQL :: MySQL 8.0 Reference Manual :: 16 Alternative Storage Engines
- 存储引擎是MySQL组件,可以为不同的表类型处理SQL操作。InnoDB是默认的,也是最通用的存储引擎,Oracle推荐使用它来存储表,除了特殊的用例。(MySQL 8.0中的CREATE TABLE语句默认创建InnoDB表)
- MySQL服务器使用可插拔的存储引擎架构,允许存储引擎在运行的MySQL服务器中加载和卸载。
- 要确定服务器支持哪些存储引擎,请使用SHOW engines语句。
- Support列的值表示是否可以使用该引擎:“YES”、“NO”和“DEFAULT”表示该引擎存在、不可用或可用,当前设置为默认的存储引擎。
-- 用于显示当前数据库可用的存储引擎列表,并显示它们的状态、默认值以及是否支持事务等信息
show engines;
- Comment:注释
- Transactions:是否支持事务
- XA:表示是否支持XA分布式事务协议(涉及到两阶段提交)
- Savepoints:表示该存储引擎是否支持Savepoints
Savepoints
- Savepoints:它是一种在事务中创建的检查点(checkpoint),通过创建和使用SavePoints,可以在事务执行期间对事务进行部分回滚或回滚到先前的状态。
- 在事务中,Savepoints可以用来标记一个临时的保存点,以保留事务中的某个特定状态,这样,如果事务后续出现错误或需要回滚,可以根据需要回滚到先前的Savepoints,而不需要回滚整个事务。
- 通过Savepoints,可以更细粒度的控制事务的回滚操作,提供更灵活的事务管理能力。
- 使用"SAVEPOINT"命令来创建Savepoints,并使用"ROLLBACK TO SAVEPOINT"命令回滚到特定的Savepoint。
- 注意:Savepoints只在某些支持事务的存储引擎中可用,比如InnoDB。
MySQL 8.0支持的存储引擎
1. InnoDB: MySQL 8.0默认的存储引擎。
- InnoDB是一个事务安全(兼容ACID)的MySQL存储引擎,具有提交、回滚和崩溃恢复功能,以保护用户数据。
- InnoDB行级别的锁(没有升级到更粗粒度的锁)和oracle风格一致的非锁读取提高了多用户并发性和性能。
- 非锁读取(non-locking read)是指在读取数据时不对数据进行加锁的操作,也就是说其它事务仍然可以对数据进行修改操作。
- InnoDB将用户数据存储在聚集索引中,以减少常见的基于主键的查询的I/O。为了维护数据的完整性,InnoDB还支持外键引用完整性约束。
2. MyISAM:这些表占用空间很小。
- 表级锁限制了读/写工作负载的性能,因此它经常用于Web和数据仓库配置中的只读或以读为主的工作负载中。
3. Meomry - 内存:
- 将所有数据存储在RAM内存中,以便在需要快速查找非关键数据的环境中快速访问。这个引擎以前被称为堆引擎。
- 它的用例正在减少;InnoDB的缓冲池内存区提供了一种通用且持久的方式来将大部分或所有数据保存在内存中,而NDBCLUSTER为大型分布式数据集提供了快速的键值查找。
那么InnoDB存储引擎的数据怎么存的呢?
- 肯定会存到磁盘,磁盘里面会有文件,也就是表空间。
表空间最后都是一个page页的数据结构,然后page页里面是我们的row(行数据 / 数据行) => MySQL最小的数据单元。 page页是内存跟磁盘交互的最小单位,page页的大小默认是16K。- 每次查询都去跟磁盘交互太慢了,所以多了一个Buffer-Pool内存区间的概念。
- 有了Buffer-Pool以后,每次查询数据,先去内存查,如果没有,那么再去从磁盘读取,读取后,放到内存,所以Buffer-Pool提升了性能,尽可能的减少跟磁盘的实时IO。因为我内存更改了以后,我所有的数据是异步同步到磁盘的。
- 内存跟磁盘的同步最小单位是page页。
- 一个page页里面到底能存多少数据,根据我的page页的大小以及行大小来决定的。
索引
我们可以按照四个角度来分类索引。
- 按「数据结构」分类:B+Tree索引、Hash索引、Full-text索引。
- 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
- 按「字段个数」分类:单列索引、联合索引。
从本质上来说,索引的类型就只有两种:主键索引和二级索引!
B+Tree中的B => Blance平衡~!
索引的概念
- 索引(Index)是帮助MySQL(存储引擎)高效获取数据的一种数据结构。
- 形象的说就是索引是数据的目录!
- 索引存在的意义:提高查询效率,加快数据访问。
存储引擎说白了就是如何存储数据、如何为存储的数据建立索引以及如何更新、查询数据等技术的实现方法。
MySQL的存储引擎有InnoDB、MyISAM、Memory等,其中InnoDB是在MySQL5.5之后成为默认的存储引擎。
下图是 MySQL 的结构图,索引和数据就是位于存储引擎中:
- MySQL的数据存在什么地方? => 磁盘
MySQL的索引数据存储在什么地方?
- 也是放在磁盘里面,如果索引数据放在内存里面就会异常情况的出现,比如说你的系统断电了,难道你的索引数据都要重新去创建吗?
- 之所以误以为放到内存里面,是因为有要把数据从磁盘加载到内存的这样一个过程 => IO。
提高IO效率有两种方式:
- 减少IO量
- 减少IO次数
- MySQL在存储具体文件的时候,如果存储引擎为InnoDB,则文件的后缀为.ibd,ibd文件里面放的是我们的数据文件和索引文件,数据文件和索引文件它统一放在一个文件里面。.frm文件存储的是表结构相关的数据。
- 再比如MyISAM存储引擎,.MYD存放的是数据文件,.MYI存放的是索引数据。
- 所以在不同的存储引擎中,文件会有不同的组织形式,数据文件与索引文件会放到不同的文件里面。
索引的实现方式 - 不同数据格式:不同的实现方式会匹配不同的应用场景。
Key-Value → K(索引列的值)-V(文件名+offset偏移量,即从哪儿开始读+长度),因为只有知道这些准确信息之后,才能把某一个固定区间的数据给读取回来。比如大数据里面的hive(风潮),它的索引在进行创建的时候用的就是这种方式。。
K - 索引列的值 → V - 行记录 —> 条件或要求:数据跟索引必须要放在一起才可以~!比如InnoDB存储引擎!
1. 一张表中可以有多少个索引?
- 理论上没有限制,跟列的个数相关,但是索引不是越多越好,索引越多,维护的成本越高!在某些特定的情况下有可能会导致效率更低!
2. 一个索引对应一颗树,还是所有的索引对应一颗树?
- 一个索引对应一颗树!
3. 索引的叶子节点存储的是数据,那么当索引比较多的时候,数据存几份儿?
- 数据有且仅有一份儿!
4. 当存在多个索引的时候,叶子节点中存储的是什么值?
- Primary Key - 聚簇索引对应的列值
- 在InnoDB存储引擎中,MySQL在进行数据插入的时候必须要跟某一个索引列绑定在一起存储,如果有主键,那么选择主键;如果没有主键,那么选择唯一键;如果没有唯一键,那么系统会生成一个6字节的row_id来进行存储!
- 聚簇索引:数据跟索引绑定在一起存储的就叫做聚簇索引!
- 非聚簇索引:没有数据绑定在一起的就是非聚簇索引!
5. 非聚簇索引一般情况下都是普通索引,聚簇索引不一定是主键索引,因为有可能是唯一索引!
6. 一条SELECT语句执行耗时0.104s,快还是慢?
- 先别急着回答问题,先问问表的数据量多少?
索引创建
- 官网:MySQL :: MySQL 8.0 Reference Manual :: 13.1.15 CREATE INDEX Statement
- 在MySQL当中,index_type:USING{BTREE | HASH}
创建索引:
-- 创建索引 index_name命名规范:idx_表名_字段名 create [unique | FULLTEXT | SPATIAL] index 索引名 USING [BTREE | HASH] on 表名(字段名1,字段名2,...);
CREATE INDEX idx_price USING BTREE ON product_new (product_price ASC); -- 建立多个字段联合索引 CREATE INDEX idx_age_name USING BTREE ON gp_teacher (teacher_age ASC, teacher_addr(4) ASC);
创建、修改table创建索引
官网:MySQL :: MySQL 8.0 Reference Manual :: 13.1.20 CREATE TABLE Statement
CREATE TABLE huihui_test ( `id` BIGINT NOT NULL AUTO_INCREMENT KEY, `name` varchar(100) NOT NULL COMMENT '商品名称', `image` varchar(100) NOT NULL COMMENT '商品图片', `title` varchar(100) NOT NULL COMMENT '商品标签', INDEX (`name`) ) ENGINE = INNODB CHARACTER SET utf8mb4;
MySQL :: MySQL 8.0 Reference Manual :: 13.1.9 ALTER TABLE Statement
ALTER table huihui_test ADD INDEX idx_title(title)
索引作用
-
索引的作用只有一个,提升查询性能!
-
SQL语句查询一般要求不能超过10毫秒或者20毫秒(1秒 = 1000毫秒),如果超过就是进行优化。
索引原理
MySQL :: MySQL 8.0 Reference Manual :: 8.3.1 How MySQL Uses Indexes
- 索引用于快速查找具有特定列值的行。
- 在没有索引的情况下,MySQL必须从第一行开始,然后遍历整张表来找到相关的行。表越大,开销也就越大,花费的时间就越多。
- 如果表中有相关列的索引,MySQL可以快速确定要查找的数据文件的中间位置,而无需查看所有数据。这比顺序读取每一行要快得多。
Hash索引 - 基于哈希表
- InnoDB和MyISAM存储引擎都不支持Hash索引(Key-Value结构),只有Memory支持Hash索引,Full Text全文索引意义不大。
- 哈希表本质上是散列表,要将数据均匀的分布,那么就会导致数据无序,当需要进行范围查询的就是就必须要挨个对比,效率比较低。
哈希表会有哈希冲突或哈希碰撞的问题,需要设计优良的哈希算法。
- Hash索引(Key-Value结构),根据单条记录/数据去查询速度会非常的快,也就是Hash在做等值查询的时候效率贼快,时间复杂度为O(1),所以Hash索引如果只查单条数据,那么它的速度是要比B+Tree快很多的;但是由于Hash索引是K-V结构,一个Key存一个Value,所以不支持范围查询,比如你去查大于1,你没有给我一个明确的Key,我怎么查?
- InnoDB和MyISAM之所以不支持Hash索引,因为InnoDB和MyISAM大多数都是一些关系型事务数据,场景大多数是一些范围查询而不是精准查询。
- InnoDB支持自适应哈希,InnoDB支持的哈希索引是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,是由MySQL的Server层来进行控制的,不是人为可以干预的。
主键索引
- 在一张表里面,我们一般会有一个主键,那么这个主键我们不需要去建索引,它去查询的时候也会非常的快。
Page页存储
Page页是Buffer-Pool跟磁盘交互的最小单位,Page页里面存的是Record ,也就是我们的行数据,Page默认大小是16KB。 MySQL中的每个数据库都存储为文件系统上的目录,每个表存储一个单独文件。 行格式:按行记录数据的格式。 行溢出:一个Page数据页的默认大小上限是16KB,大于这个值就是行溢出。 一行数据也是有大小的,并且最大不能超过page页大小的一定比例。 元素的最大行大小默认innodb_page_size为16KB,约为8000字节。 所以, 一个page页里面存放的数据是有限的。 一个extent-区里面肯定会有很多很多的page-页,这些page-页为了去区分开来,我们每一个页都会有一个页ID,每一个page页的页ID肯定是唯一的,并且page页里存的是row行数据,如图:
为了在page页里面进行更快的查询(能够更快的找到相应的行数据),page页里面的数据都是有序进行排序的,并且数据跟数据之间形成了一个单向链表,这样有助于进行范围查询。 这个有序,可以根据不同的字段来,但是每个表最少有一个排序字段! 该字段默认是主键,默认是根据主键排序,如果主键不存在,就是非空的唯一索引,如果唯一索引也没有,那么每个行数据都会有一个默认的row_id。 二分法的前提是有序!如果一个Page页里面放不下了,会再来一个page页。Page页链表
- 由于一个Page页里面存放row行数据是有限的,所以一个extent区里面肯定会有很多很多的page页,假如我们的数据越来越多,我们就需要新起一个page页,并且page页是以LRU链表保存的,所以page页跟page页之间就形成了一个双向链表,并且还必须要满足一个条件:链表的下一个页的最小数据必须要大于上一个页的最大数据,因为我们查询数据时很有可能要跨页查询,所以这一个原则可以使得页与页之间有关联,是为了更好的去查到我想要的数据。
现在,我们知道了 在页链表的数据中,去查询一条数据,比如查询ID=38的数据,我们需要{遍历页链表} => 链表的查询,查询时间为O(n),并且随着数据量越大,遍历的页数越多,那么查询性能会越来越慢 ===> 这也是HashMap为什么要链表转成红黑树的原因,因为HashMap如果链表过长,你去查询数据会非常慢,性能会有所影响,查询速度会越来越慢! Page页它是内存跟磁盘交互的最小单位! 这种页链表的数据结构很像一本没有目录的书!
类比书目录
- 现在,就相当于你有一本厚厚的书,但是没有目录。
- 比如水浒传,现在你想去找那个潘金莲棍打西门庆的那个章节,但是由于没有目录,你能做的就是一页一页的去翻,那么随着书的厚度查询到的时间也可能会越来越慢!
- 所以,书才有了一个目录,这个目录无非就是用额外的页,来记录你想看的数据在哪里,那么,你去找你你想要的数据,只需要遍历目录即可,不需要遍历整本书。
我们发现, 目录由2个部分组成 :
这个章节大致讲什么,通过这个知道来知道这个章节里面会有哪些内容- 这个章节在哪一页,通过页码可以找到对应的位置,大大提升了我们去查询我们想要看的章节的速度
参考书目录的索引
同样是查询我们想要的数据,我们可以去参考书的目录,去提升我们查询数据的性能。 书的目录是额外的书纸来记录的 , 简单点, 就是用空间换时间 ,那么 我们的MySQL里面也能用额外的一些page页来记录我每个page的目录,所以目录都不会有真实的内容,它只是一个目录,但是它 可以 / 能 帮你快速的找到你想要的数据,原来需要遍历所有的page页,现在只需要遍历目录所在的page页即可。- 为每个页建立一个目录后,这个目录也是一条数据,只不过这个数据是目录数据,目录所在的页叫目录页(就这个页我只去记录目录),为了确保Page页的唯一性,目录页也会有一个页ID,record行会有个字段,record_type,0代表真实的用户数据,而1代表的是目录数据。
因为目录数据比真实数据要小,所以,一个page页能放的数据也就更多。 目录页也是页,它默认也是有最大大小的,16KB,如果一个目录页放不下,就会新起另一个目录页 ,假如,一页能放4条目录数据, 目录数据,包含2个部分:
这个page页最小的数据,用来判断是否要在这个页进行查询 这个page页的页码,通过页码找到页的位置目录建好后,如图:
- 因为一个页只能放4条数据,那么目录页就会有2个,那么如果数据页越来越多,那我要查询的数据还是不知道在哪;
- 所以我们又可以参照书的二级目录或者三级目录思想,目录上面再 加/建 一层目录,也就是目录的目录,即父目录,直到根目录的位置能在一个page页放下!
- 我们发现,慢慢的形成了一个树形结构,并且这个树跟二叉树是不一样的,一个节点可以有多个节点,并且只有叶子节点才有真实的数据,并且叶子节点与叶子节点之间形成了一个有序的双向链表,这个树就叫做B+Tree。
- 数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。
索引效果
- 在没有索引树之前,我们要去查询主键为43的数据,我们需要一条一条的去遍历,需要从1开始,直到找到43。
现在有了索引树后,我们查询43的数据:根目录 => 子目录 => 叶子节点中的数据页 所以,我们发现,不管查询什么数据,我们只需要查询/遍历3个page , 那么这个查询/遍历的page页数量是根据树的高度来决定的 , 只跟这个树的层级有关,并且层级越低,我的磁盘IO次数越少,性能也就越高 , 所以也是为什么没有用二叉树来做索引结构的原因 , 因为二叉树的层级会很高(二叉树的每个节点最多支持两个子节点),层级越高,跟磁盘的IO次数也就越多,性能也就越低。所以,如果选择二叉查找树作为MySQL的索引结构,会存在以下缺点:
- 当主键顺序插入时,会形成一个单向链表,查询性能大大降低;
- 大数据量的情况下,层级较深,检索速度慢!
BST二叉查找树在二叉树的基础上增加了一个规则,就是左子树的所有子节点都要小于它的根节点,而右侧子节点要大于它的根节点;二叉查找树有可能出现斜树问题,会退化成链表,导致时间复杂度增加,因此又引入了一种叫做平衡二叉树(AVL)的机制,它具有二叉查找树的所有特点,同时还增加了一个规则,它的规则就是左右两个子树的高度差的绝对值不能超过1,为了达到这样的一个平衡,它会引入一个左旋和右旋的机制来实现树的平衡。为什么索引用B+Tree,而不用红黑树?
首先InnoDB引擎的数据都是存储在磁盘上的,所以选择数据结构的第一优先级肯定是考虑从磁盘查询数据的成本,如果树的高度越高,意味这磁盘I/O次数就越多,这样就会影响查询的性能。
当树的高度变高的时候,会导致IO次数的增加。
MySQL之所以能索引用B+Tree,而不用红黑树的主要原因是随着数据量的增多,红黑树的高度会被B+Tree树高度高很多,红黑树是一颗自平衡二叉树,因此红黑树本质上是二叉树,一个节点最多支持两个分叉,而B+Tree是多叉树,存储相同数据量的情况下,红黑树的高度会比B+Tree高很多,由于InnoDB引擎的数据都是存储在磁盘上的,如果树的高度越高,意味着磁盘I/O次数就越多,会影响查询的性能,所以,InnoDB引擎的索引就选择了B+Tree。
因为Page页是Buffer-Pool跟磁盘的交互的最小单位,你要查找/遍历的Page页越多,可能跟磁盘的IO次数也就越多。树的高度/层级就意味着磁盘IO的数量。所以, 索引树通过B+树的数据结构大大提升了查询性能。BTree VS B+Tree:
- BTree的数据存储在每个节点上,BTree的每个节点都会存储数据,因此BTree出现了一个最大的问题,叫数据冗余;而B+Tree中的数据只存储在叶子节点上,非叶子节点只存储索引,并且叶子节点中的数据通过双向链表的方式进行关联,可以方便的进行范围查询;而BTree的叶子节点之间没有指针连接,每个叶子节点独立存储数据项。
- B+Tree:B+Tree的第一层跟第二层没有真实的数据,它只有一个目录,真实的数据必须在第三层的叶子节点当中。
为什么树的高度变高,就会导致IO次数的增加?换句话说为什么一层就是一次IO?
- 我们在进行数据读取的时候,是要按照页的整数倍进行读取的,在InnoDB里面,默认页的大小是16KB。
- 因为树的每一层对应着磁盘上的一个块或一个Page页,在访问树的每一层时,需要进行一次磁盘读取或写入操作,这被称为一次IO。
- 树的分支不就是树的指针吗(地址值)。
为什么MySQL中的索引结构要采用B+Tree而不是BTree?
- 从磁盘的I/O效率方面来看:B+Tree的非叶子节点不存储数据,所以树的每一层就能够存储更多的索引数量。也就是说,在层高相同的情况下/同样的层级,B+Tree要比BTree存储的数据量更多,因为在同样的一个Page页里面的数据,BTree要的空间更多,也就是同样的数据量,BTree的层级要比B+Tree更高,这间接增加了磁盘I/O的次数,但是不一定BTree就比B+Tree慢,BTree它只是整体慢,因为整体而言BTree的层级要越高。那什么情况下BTree要比B+Tree查询数据快呢?BTree可能只查一两次就查到对应的数据了,就不用向下查了,也就是在根节点或二级目录如果就找到了你想要的数据,就比B+Tree要快;而B+Tree必须到叶子节点,BTree可能在非叶子节点就查到对应的数据了,BTree可能会因为非叶子节点上有数据而减少查询次数。
- 从范围查询的效率来看:在MySQL中,范围查询是一个比较常用的操作,而B+Tree的所有存储在叶子节点上的数据使用了双向链表来进行关联,所以B+Tree在查询的时候只需要查询两个节点(page页)并进行遍历,通过遍历叶子节点形成的双向链表来获取连续的数据项,而不需要回溯到非叶子节点;而BTree需要获取所有节点,B树是不支持顺序访问的,因此B+Tree在范围查询上效率更高。
- 从全表扫描方面来看:因为B+Tree是叶子节点存储所有数据,所以B+Tree的全表扫描能力更强一些,它只需要扫描叶子节点,而BTree需要遍历整个树。
- 从查询稳定性方面来看:B+Tree不管查询什么数据它都是比较稳定的,因为它都需要经过树的层级,它必须到要叶子节点才可以查询数据,也就是说查找必须到叶子节点才能结束,因此查找的路径长度更加均匀,查找效率更稳定;而BTree可能在非叶子节点就能查到对应的数据了,查询是非常不稳定的。
- MongoDB使用的是B树。
- B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次,一般情况下,3~4层的B+Tree足以支撑千万级的数据存储。索引列的Key值占用的空间越小越好!
B+Tree的特性:这个字段必须是排好序的!
主键索引 & 主键索引树
- 我们刚才索引树里面的排序默认是根据主键ID来排序的,主键ID是一定可以有序的,因为它是唯一的,所以每个表都会基于主键ID去创建一个这样的索引树,所以这个索引树也叫做主键索引树或者聚簇索引树 / 聚集索引树,并且这个树下的叶子节点会有我们完整的数据,也就是在创建表时,如果添加了主键,数据库就会默认创建主键索引,并且主键索引是所有索引当中性能最高的,因此我们现在根据主键去查询就很快了!
- 主键索引树是每个表都会有的,任何一个表,它都会有一个主键索引树,如果存在主键,主键索引就是聚集索引;就算在创建表时没有添加主键,那么主键索引树也一定会有,如果没有主键,InnoDB默认会用第一个非空的唯一字段 / 唯一索引(UNIQUE)去做我们的主键索引树!
- 如果非空的唯一字段(唯一索引)也没有,InnoDB将会自动生成一个隐藏的6字节的字段,叫row_id(行ID),用它去做我们的主键索引树,这个字段它是递增的,随着新行插入单调递增。
- 所以主键可以没有,但是主键索引树每个表它必有!
- 总结:每个InnoDB表都有一个特殊的索引,称为聚集索引,用于存储行数据。
- 在InnoDB中,聚集索引 / 主键索引指的是按照每张表的主键构建的一种索引方式,它是将表数据按照主键的顺序存储在磁盘上的一种方式。
唯一索引和主键的区别?
- 唯一索引列允许为空值,而主键列不允许为空值;
- 主键列在创建时,已经默认为非空值 + 唯一索引了;
- 主键可以被其它表引用为外键,而唯一索引不能;
- 一张表最多只能创建一个主键,但可以创建多个唯一索引。
唯一约束和唯一索引的区别?
- 唯一约束和唯一索引,都可以实现列数据的唯一,列值可以为NULL;
- 创建唯一约束,会自动地创建一个同名的唯一索引,该索引不能单独删除,删除约束会自动删除索引,唯一约束是通过唯一索引来实现列数据唯一;
- 创建一个唯一索引,这个索引是独立的索引,可以单独被删除;、
- 如果一个列上想有约束和索引,且两者可以单独的删除,可以先建立唯一索引,再建立同名的唯一约束。
聚集索引如何加速查询?
因为索引搜索直接指向包含行数据的页,聚簇索引这种索引方式保证了行的物理存储顺序和主键的逻辑顺序相同,因此通过聚集索引访问行数据会非常的快!
主键索引 / 聚集索引 / 聚簇索引!
B+索引Tree只有两种:主键索引和二级索引!
二级索引与聚集索引有何关系?
但是, 如果我们查询条件不是主键,而是其他的字段,那么就需要用到我们的二级索引树。- 聚集索引 / 主键索引之外的索引称为辅助索引(二级索引或非聚簇索引)。
- 构建的主键索引和二级索引默认使用的是B+Tree索引。
二级索引 & 二级索引树
除了主键索引之外的所有索引树,都是二级索引树!
一个索引树它一定是要有排序的,可以去指定排序规则,倒排或正排。
利用索引的前提是索引里的 key 是有序的。
二级索引 / 辅助索引 / 非聚簇索引 / 非主键索引!
在InnoDB中,二级索引 / 非聚簇索引 / 辅助索引是指根据非主键字段创建的索引。
在InnoDB中,辅助索引 / 二级索引 / 非聚簇索引它不影响表中数据的物理存储顺序,而是单独创建一张索引表,辅助索引中的B+Tree的叶子节点中每条记录都包含了辅助索引中指定的列(索引字段的值或索引列的值)以及这一行对应的主键索引/聚集索引中的主键值(即对应行的指针,也就是主键索引树的排序字段的值),InnoDB使用这个主键值在主键索引/聚集索引中搜索/获取完整的行(数据)。
在二级索引的叶子节点中,上面存储的是索引列的值,而下面存储的是对应的主键ID。
对于二级索引(非唯一索引)来说,叶子节点中的索引列的值可以重复,在二级索引的叶子节点中,索引列的值对应一个或多个行的主键ID,如果多个行具有相同的索引列值,它们会按照主键ID的顺序存储在叶子节点中;而对于唯一索引,叶子节点中的索引列的值必须是唯一的!
为什么二级索引的叶子节点要加主键ID呢?
二级索引的叶子节点只有我指定的索引字段的值 + 主键索引树的排序字段的值!
因为二级索引中索引列的值可以重复,所以如果不加主键ID是没有办法确定唯一性的!
不光二级索引的叶子节点有主键ID,在索引页也会有主键ID,因为要确保唯一性。
索引按物理存储分类,什么是聚簇索引和非聚簇索引?
回表是什么?为什么二级索引需要回表?回表 => 动作
-
聚簇索引:通过这棵树能找到所有的数据!
- 聚簇索引只有在InnoDB存储引擎中才存在,而在MyISAM当中是不存在的!
- 聚簇索引默认使用主键,即聚簇索引默认就是主键索引!
- 在InnoDB存储引擎中,按照物理存储分类或根据索引的存储形式,MySQL索引可分为两种类型: 主键索引/聚集索引/聚簇索引 和 非聚簇索引/二级索引/辅助索引!
- 聚簇索引和非聚簇索引它们的组织和存储数据时有不同的方式。 主键索引的B+Tree和二级索引的B+Tree区别如下:
- 主键索引 / 聚簇索引的B+Tree是将数据与索引放到了一起,找到了索引也就找到了数据,也就是说,对于聚簇索引来说,它的非叶子节点上存储的是索引字段的值,而它的叶子节点上存储的是这条记录的整行数据,也就是说主键索引的B+Tree的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+Tree的叶子节点当中。
- 非聚簇索引 / 二级索引的B+Tree是将数据与索引分开存储,它的非叶子节点上存储的都是索引值,而它的叶子节点上存储的都是主键的值(就是指向page页中数据行的逻辑指针),而不是实际数据。·
- 所以,通过非聚簇索引的查询或在查询时使用了二级索引,需要进行一次回表,因为二级索引的B+Tree需要先找到对应的叶子节点,查到对应的主键ID,然后再通过主键值到主键索引中的B+Tree查询到对应的叶子节点,最终获取到整行数据,这个过程就叫回表,也就是说要查两个B+Tree才能查到数据。
需要通过主键索引的排序字段去主键索引树拿到响应的数据,这个过程叫做:回表! 回表效率低,尽量减少回表的可能!-- 回表 -- 假设现在我有个表,有id、a、b、c四个字段,建立一个联合索引(a,b) select * from table where a = 1; -- 能走到联合索引 -- 由于查询返回的条件是*,即查询返回所有字段 -- 在联合二级索引树中有id(主键ID)、a和c这三个字段,但是没有c这个字段 -- 所以我们要根据主键id再回到主键索引树去查询字段c,这就是回表 -- 这也是为什么在开发中不建议使用select * 的原因,因为有可能会发生回表
走二级索引一定要回表吗?什么是覆盖索引(查询)?
- 覆盖索引它不是索引,覆盖索引不如叫做索引覆盖(在你的索引树里面覆盖了你想要的内容),它避免了回表,所以它不是索引的优化方式,它算是一种减少了回表的优化方式。
- 当查询的数据直接就是主键ID时,比如 select id from,当一条查询语句复合覆盖索引条件时,也就是当查询的数据直接能在二级索引中的B+Tree的叶子节点当中查询到,只需要通过索引就可以返回查询所需要的数据,这时就不需要回表,就不用再去主键索引的B+Tree去查询,避免了查到索引后再返回表操作,减少磁盘IO,提升查询效率。
- 这种在二级索引中的B+Tree直接就能查询到结果的过程或场景就叫做覆盖索引,也就是只需要查一个B+Treee就能找到数据,也可以称之为实现了索引覆盖。
- 索引覆盖效率高,可以作为优化点存在!
为什么不建议使用UUID去做主键?
- UUID(全局唯一标识符),是指在一台机器上生成的数字,它的目标是保证在同一时空中的所有机器都是唯一的。
- 由于UUID是由随机数、时间戳等信息生成的,因此UUID具有极高的唯一性。
- UUID由32位16进制数表示,通常以字符串形式表示。
- UUID它的优点就是全局唯一,性能比较高,不依赖网络,本地就可以生成,使用起来也比较简单,并且随机生成的UUID很难被猜测出来,对于需要保密性的应用场景较为适用,因此UUID具有不可预测性;它的缺点就是长度过长。
如果使用UUID这种随机ID作为主键ID,它有很多个缺点:
- 它的存储空间是比较大,因为UUID它很长,通常以字符串形式存储,所以索引的大小会变得非常大,大的索引会占用更多的磁盘空间,导致行数据变大,在相同的page页里面能存的行数据也就越少,导致相同的数据需要更多的page页,叶子节点的page页越多,那就可能导致我的树的层级越高,树的层级越高,磁盘IO次数也就越多,树的层级变高无形中增加了磁盘IO次数,导致查询性能变慢/降低!并且MySQL官网里面有说到:如果主键很长,那么辅助索引会占用更多的空间,所以短的主键是很有利的。=>因为主键字段长度越小,意味着二级索引的叶子节点越小,因为二级索引的叶子节点存放的数据是主键值,这样二级索引占用的空间也就越小,
- 因为UUID它不是趋势递增的,如果我们使用UUID作为非自增主键,当使用UUID进行排序时,由于每次插入主键的索引值都是随机的,那么新的UUID值通常会插入到叶子节点的中间位置,即插入到现有数据页中间的某个位置,这就导致后面所有的数据以及目录数据都会进行变更,导致了树的分裂以及页的分裂合并,从而影响查询效率/查询性能,因为每次分裂以及分裂后的合并都牵扯到数据的重新排序和移动(索引的维护=>索引的维护成本是非常高的!),因此会影响查询的性能;并且页分裂还有可能会增加写入的开销,因为页分裂有可能会造成大量的内存碎片,导致索引结构不紧凑,因此增加了写入的开销。
- 不适合做范围查询:因为UUID这种随机ID它不是自增的,所以它不支持做范围查询。
- 不方面展示:主键ID有的时候需要在系统间或者前台页面展示,如果是UUID的话,因为比较长,并且没有任何的业务含义,所以它不方便展示。
- 如果使用UUID这种随机ID作为主键,那么在频繁插入数据时,就会导致随机磁盘I/O,从而导致性能较低。
- 补充:索引的维护成本是非常高的 ⇒ 在做数据迁移的时候,一般会把索引全部都删掉,等数据都迁移完成之后,把数据全部迁移完成之后,再重新去构建索引!如果边迁移数据边构建索引,效率极低,因为你要频繁的去改树的结构!
为什么建议使用自增ID作为主键? => 大字段不建议建立索引
- 在MySQL当中,可以通过设置AUTO_INCREMENT属性实现ID的自增长。
在满足业务需求的情况下,主键尽量 / 尽可能自增!
索引的维护成本是非常高的 ⇒ 补充:在做数据迁移的时候,一般会把索引全部都删掉,等数据都迁移完成之后,再重新去构建索引!
使用自增ID做主键的好处:
- 因为自增ID本身具有连续性,使得对应的数据会按照顺序存储在磁盘上,即每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面,写入性能和检索性能都很高。
- 其次,自增ID是数字,所以占用的位数要比UUID小很多,所以在存储空间上也节省很多。
- 并且因为自增ID比较短,因此方便展示。
- 因此,一般建议使用自增ID作为主键!
但是,使用自增ID做主键也存在一定的问题:
- 分库分表:当我们做分库分表时,就没办法依赖一张表的自增ID来做主键了,这样会发生重复导致冲突问题。
- 可预测:因为ID是顺序自增的,所以是可预测的,这就给系统带来了一定的安全风险。
- 可能用尽:自增ID的话可能是int、bigint等,但是它们都是有范围的,有可能会用尽。
索引按字段个数分类
从字段个数的角度来看,索引分为单列索引和联合索引(复合索引)!
建立在单列上的索引称为单列索引,比如主键索引!
建立在多列上的索引称为联合索引!说白了就是通过将多个字段组合成一个索引,该索引就被称为联合索引!
单列二级索引:即一个索引只包含单个列
单列二级索引:我基于某个字段去建的索引树,也是根据某个字段排序的索引树。
二级索引(非唯一索引)中索引列的值可以重复。
联合二级索引:即一个索引包含了多个列 & 最左匹配原则{索引失效}
- 通过将多个字段组合成一个索引,该索引就被称为联合索引(复合索引 / 组合索引)。
- 假如查询条件为多个字段的时候,通过联合二级索引有可能会大大提升我们的查询性能
- 比如:你建立了一个(a,b,c)的联合索引,相当于建立了(a),(a,b)和(a,b,c)三个索引!
- 联合二级索引是基于多个字段的,并且多个字段是有先后顺序的!所以它也会根据多个字段排序,先根据第一个字段排,第一个字段如果相等,再根据第二个字段排,以此类推!
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no,name),创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
- 使用联合索引时,存在最左[前缀]匹配原则,也就是按照最左优先的方式进行索引的匹配,最左前缀匹配是指在查询中利用索引的最左边的一部分来进行匹配。
- 一句话说明白什么是遵循了最左前缀匹配原则:查询语句的where条件中,必须包含最左边的索引的值,即查询从索引的最左列开始,否则连这个索引树都走不到,不满足最左前缀匹配!
- 在使用联合索引进行查询的时候,如果不遵循「最左前缀匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
比如,如果创建了一个(a,b,c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
- where a=1;
- where a=1 and b=2 and c=3;
- where a=1 and b=2;
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要,即最左前缀匹配和查询条件的顺序没有关系,但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
- where b=2;
- where c=3;
- where b=2 and c=3;
上面这些查询条件之所以会失效,是因为(a,b,c)联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到联合索引的。
需要注意的是,创建一个联合索引,MySQL创建的它只是一颗B+Tree树,只不过这颗树中,它是先按照第一个字段进行排序,在第一个字段相同时再按照第二个字段进行排序,以此类推。
为什么要遵循最左前缀匹配?
- 我们都知道,在MySQL的InnoDB引擎中,索引是通过B+Tree树来实现的。
- 不管是单列二级索引还是联合二级索引,都需要构造一颗B+Tree的索引结构。
如果是单列二级索引的存储结构,B+Tree的每个非叶子节点记录的是索引的值,叶子节点上记录的是聚簇索引树/主键索引树的排序字段值或主键ID值:
如果是联合二级索引的B+Tree,非叶子节点记录的是多个字段的值,叶子节点记录的是多个字段的值以及主键ID的值:
- 可以看到,联合二级索引的底层是一个B+Tree,并且在构造B+Tree的时候,当左边的key不同时,会先按照左边的key进行排序,当左边的key相同时,再一次按照右边的key进行排序。
- 所以,在通过索引查询的时候,也要遵守最左前缀匹配的原则,也就是需要从联合索引的最左边开始进行匹配,这时候就要求查询语句的where条件中,必须包含最左边的索引的值。
为什么MySQL用B+Tree,而MongoDB用BTree?
- InnoDB是MySQL的存储引擎,MySQL是一种关系型数据库;而MongoDB是非关系型数据库。
- 关系型数据库和非关系型数据库比较大的区别就是关系型数据库有大量的范围查询,而非关系型数据库基本上都是单条查询。
- 还有一个区别就是,InnoDB引擎下的MySQL的数据是存储在磁盘上的,而MongoDB的数据是存储在内存上的。
- 由于B+Tree的叶子节点之间通过双向指针链接进而形成了双向链表,而BTree的叶子节点之间没有指针连接,每个叶子节点独立存储数据项,所以B+Tree在范围查询时更加高效,因为B+Tree在范围查询时可以通过顺序遍历叶子节点来获取连续的数据,从而提高了查询的效率。
- 而BTree更适合内存存储和随机访问的场景,因为BTree的特点是叶子节点和非叶子借点都包含键值和数据信息,这种设计使得BTree在内存中的访问更加高效,因为BTree可能直接在根节点或二级目录中就找到了你想要的数据,也就是BTree可能在非叶子节点就查到对应的数据了,而B+Tree必须到叶子节点才可以。
- MongoDB在旧版本中用的确实是BTree,但是在MongoDB 3.2以后,已经采用WiredTiger作为默认的存储引擎了。
索引使用面试题
1. 我们在做InnoDB表结构设计的时候,会把一些大字段拆分,比如varchar(5000), 会独立成附表,为什么?
- 因为我们的数据都是存在我们主键索引树的叶子节点的,叶子节点是一个一个的page页组成的双向链表,如果字段过大,将大字段放在主表里面。就会导致行数据变大,那么每个page页能存放的数据也就越少,同样的数据需要更多的page页来保存,叶子节点的页越多,那么树的层级就越高,树的层级越高,磁盘IO次数也就越多,导致查询性能就会变慢!
- 并且很多的场景,我只需要去查基本信息,像附表的信息,一般不会去查。
所有的技术是为业务服务的!
- 比如:数据库三范式,没意义,因为我们一般在开发时不会去遵循三范式!
2. 索引字段不是越多越好!=> 避免创建过多的索引
要在适合的场景去建立索引,并且尽量使用联合索引!
因为索引大小是有限制的,如果无限制的添加索引,那么就会占用大量的磁盘空间,影响写入性能。
官网对InnoDB索引限制的定义:
一个表最多1017列,二级索引最多只能有64个。
并且因为索引越多就需要去维护更多的索引树,一个索引会对应一个索引树,在数据新增或删除时,也就是在数据变更时是需要对索引树进行维护的,需要维护的索引树越多,操作数据也会越慢。
3. 什么是分库分表?什么时候应该分库分表,做数据拆分?(三层索引树到底能支撑多少数据?)
- 分库分表,是企业里面比较常见的针对高并发、数据量大的场景下的一种技术优化方案,所谓"分库分表",根本就不是一件事,而是三件事儿,它们要解决的问题也都不一样!
- 这三个事分别是:"只分库不分表表"、"只分表不分库"以及"既分库由分表"!
分库
- 分库主要解决的是并发量大的问题。
- 因为并发量一旦上来了,那么数据库就可能成为瓶颈,因为数据库的连接数是有限的,虽然可以调整,但是也不是无限调整的,所以,当你的数据库的读或者写的QPS过高,导致你的数据库连接数不足了的时候,就需要考虑分库了,通过增加数据库实例的方式来提供更多的可用数据库连接,从而提升系统的并发量。
- 比较典型的分库的场景就是我们在做微服务拆分的时候,就会按照业务边界,把各个业务的数据从单一的数据库中拆分开,分别把订单、物流、商品、会员等单独放到单独的数据库中。
分表
- 分表主要解决的是数据量大的问题。通过将数据拆分到多张表中,来减少单表的数据量,从而提升查询速度。
- 假如你的单表数据量非常大,因为并发不高,数据库连接可能还够,但是存储和查询的性能遇到了瓶颈,你做了很多优化之后还是无法提升效率的时候,就需要考虑做分表了。
- 阿里巴巴开发规约中规定单表行数超过500万行或者单表容量超过2GB之后,才推荐进行分库分表,小于这个数据量,遇到性能问题仙剑一大家通过其它优化来解决。
- 阿里巴巴开发规约给出的数据偏保守,根据实际经验来说,单表抗2000万数据量问题不大,但具体的数据还是要看存储引擎设置、硬件配置等。
既分库又分表
- 当你的数据库连接不够了,并且单表数据量也很大导致查询比较慢的时候,也就是既需要解决并发量大的问题,又需要解决数据量大的问题的时候,此时就需要做既分库由分表了!
- 通常情况下,高并发和数据量大的问题都是同时发生的,所以,我们会经常遇到分库分表需要同时进行的情况。
横向拆分(水平拆分)和纵向拆分(垂直拆分)
- 谈到分库分表,就要涉及到该如何做拆分的问题。
- 通常在做拆分的时候有两种做法,分别是横向拆分(水平拆分)和纵向拆分(垂直拆分)。
纵向拆分或垂直拆分:
把某一张表当中的某一条记录的多个字段,拆分到多张表中,这种就是纵向拆分。针对不同的业务做拆分,拆分分成多个数据库的这种情况,也是纵向拆分的一种!
横向拆分或水平拆分:
把某一张表中的不同的记录分别放到不同的表中,这种就是横向拆分。
- 横向拆分的结果就是数据库表中的数据会分散到多张分表中。比如我们可以把不同用户的订单表拆分到不同的表中。
你们一般是怎么分库的呢?
垂直分库:
以表为依据,按照业务归属的不同,将不同的表拆分到不同的库中。
水平分库:
- 以表中数据为依据,按照一定的策略,将一个库中的数据拆分到多个库中。
你们是怎么分表的?
水平分表:
- 以表中数据为依据,按照一定的策略,将一个表中的数据拆分到多个表中。、
垂直分表:
- 以字段为依据,按照字段的活跃性,将表中不同的字段拆分到不同的表中。
分库分表的工具:分库分表中间件
- 我们如何可以做到像处理单表一样去处理分库分表的数据呢,此时就需要用到一些分库分表的工具。
- 目前市面上比较不错的分库分表的开源框架主要有三个,分别是sharding-jdbc、TDDL和MyCAT。
Sharding-JDBC(主流)
- 现在叫ShardingSphere(Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这三款相互独立的产品组成)。
- 它定位为轻量级Java框架,在Java的JDBC层提供额外服务,它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
TDDL
- TDDL是淘宝开源的一个用户访问数据库的中间件,它集成了分库分表,读写分离,动态数据源配置等功能,封装了JDBC的DataSource给用户提供统一的基于客户端的使用。
MyCAT(主流)
- MyCAT是一款分布式关系型数据库中间件,它支持分布式SQL查询,兼容MySQL通信协议,以Java生态支持多种后端数据库,通过数据分片提高数据查询的处理能力。
4. 你觉得分库分表会带来什么问题呢?
从分库的角度来讲:
1. 事务的问题
- 使用关系型数据库,有很大一点在于它保证事务完整性。
- 而分库之后单机事务就用不上了,必须使用分布式事务来解决一致性问题。
2. 跨库JOIN问题
- 在一个库中的时候我们还可以利用JOIN来联表查询,而跨库了之后就无法使用JOIN了。
- 解决方法:数据异构,把需要跨库JOIN的数据异构到ES等存储结构中,通过ES进行查询!
从分表的角度来讲:
1. 跨节点的count,order by,group by等聚合函数问题
- 只能由业务代码来实现或者利用中间件将各表中的数据汇总、排序、分页然后返回。
2. ID问题:分表后全局ID如何生成?
- 数据库表被切分后,就会引申出分布式系统中唯一主键ID的生成问题,因为在单表中我们可以用数据库主键来做唯一ID,如果做了分表,多张单表中的自增主键ID就一定会发生冲突,那么就不具备全局唯一性了,所以需要一些手段来生成一个全局唯一的ID从而来保证全局主键唯一性!
- 雪花算法:雪花算法是比较常用的一种分布式ID的生成方式,它是由Twitter研发的一种分布式ID生成算法,它具有全局唯一、递增、高可用的特点~!
5. explain查看SQL执行计划分析的时候,要关注哪些信息?
- 要想知道优化器选择了哪个索引,我们可以在SELECT查询语句的最前面加个explain命令,这样就会输出这条SQL语句的执行计划,然后执行计划中的key就表示执行过程中使用了哪个索引。
- 如果查询语句的执行计划里的key为null说明没有使用索引,那就会全表索引(type = ALL),这种查询扫描的方式是效率最低的;如果key为PRIMARY就是使用了主键索引!
下面是一次explain返回的一条SQL语句的执行计划的内容:
一次执行计划中,共有12个字段:
- id:执行计划中每个操作的唯一标识符,对于一条查询语句,每个操作都有一个唯一的id。
- select_type:操作的类型。常见的类型包括SIMPLE、PRIMARY、SUBQUERY、UNION等。不同类型的操作会影响查询的执行效率。
- table:当前操作涉及的表
- partitions(分区):当前操作所涉及的分区
- type:type字段描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高(性能由差到好)的顺序为:包括ALL(表示全表扫描或全盘扫描,效率最低)、index(全索引扫描,会遍历索引树来查找匹配的行,但是效率并不高)、range(范围扫描,使用索引进行范围扫描,只会扫描索引树中的一个范围来查找匹配的行,ref(非唯一索引扫描,只会扫描索引树中的一部分来查找匹配的行,使用非唯一索引进行查询)、eq_ref(主键索引查询或唯一索引查询,通常用在多表联查中,并且连接操作中使用了主键索引或唯一索引,只会扫描索引树中的一个匹配行)、const(表示查询条件使用了主键,根据主键查询)、system(查询系统中的表,少量数据,往往不需要进行磁盘IO,效率最高)、
NULL(无需关注)。从range这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式(如果type为ALL或index,就代表需要进行优化了)。- possible_keys:表示可能被查询优化器选择使用的索引!possible_keys表示查询语句中可以使用的索引,但不一定实际使用了这些索引。当前SQL可能会使用到的索引。
- key:当前SQL实际命中的索引,表示查询优化器选择使用的索引!kye字段表示实际用于查询的索引,如果在查询中使用了索引,则该字段将显示使用的索引名称。
- key_len:表示索引的长度 => 索引占用的大小。索引的长度越短,查询时的效率越高!
- ref:表示连接操作所使用的索引。
- rows:表示此操作需要扫描的行数,即扫描表中多少行才能得到结果。
- filtered:表示此操作过滤掉的行数占扫描行数的百分比。该值越大,表示查询结果越准确。比如100.
- Extra(额外的):表示其它额外的信息 => Extra - 额外的优化建议,该字段描述了MySQL在执行查询时所做的一些附加操作,包括Using index(表示MySQL使用了覆盖索引优化(也称为索引覆盖),查询的列被索引覆盖,只需要扫描索引,而无需回到数据表中检索行,避免了回表操作)、Using filesort、Using temporary(表示MySQL创建了临时表来存储查询结果,这通常是在order by排序或group by分组时发生的,效率低,要避免这种问题的出现)、Using where(表示MySQL查询时使用了where字句进行条件过滤,这表示MySQL需要在检索的数据中应用where条件,以过滤出符合条件的结果集!)、Using where和Using index可以同时出现!
6. 如何判断一条SQL有没有走索引?
- 首先看查询语句的执行计划里的key是否为null,如果为null表示没有使用索引,如果不为null表示用到了索引树,但是具体是怎么用的,还得看type和Extra。
7. 索引为什么会失效呢?
- 因为我们的索引树它是根据某个字段排序的,如果要快速的根据某个字段找到我想要的数据,那肯定首先是要走索引树,所以如果要能走索引树,必须这个索引字段是有序的!如果索引字段是无序的,那就走不到索引树!
8. 索引失效问题如何排查?索引失效有哪些种情况?什么时候会索引失效?
索引存储结构长什么样?
- 我们先来看看索引存储结构长什么样,因为只有知道索引的存储结构,才能更好的理解索引失效的问题。
- 索引的存储结构跟MySQL使用哪种存储引擎有关,因为存储引擎就是负责将数据持久化在磁盘中,而不同的存储引擎采用的索引数据结构也会不相同。
- MySQL默认的存储引擎是InnoDB,它采用B+Tree作为索引的数据结构。
- 在创建表时,InnoDB存储引擎默认会创建一个主键索引,也就是聚簇索引,其它索引都属于二级索引。
- MySQL的MyISAM存储引擎支持多种索引数据结构,比如B+Tree索引、Full-Text全文索引。MyISAM存储引擎在创建表时,创建的主键索引默认使用的是B+Tree索引。
虽然InnoDB和MyISAM都支持B+Tree索引,但是它们的数据的存储结构实现方式不同,不同之处在于:
- InnoDB存储引擎的B+Tree索引的叶子节点保存数据本身;
- MyISAM存储引擎的B+Tree索引的叶子节点保存数据的物理地址,即用户数据的指针。
InnoDB存储引擎根据索引类型的不同,分为聚簇索引和二级索引,它们的区别在于:
- 聚簇索引的叶子节点存放的是实际数据,所有完整的用户数据都存放在聚簇索引的叶子节点;
- 而二级索引的叶子节点存放的是主键值,而不是实际数据。
MySQL的索引失效是一个比较常见的问题,这种情况一般会在慢SQL发生时需要考虑,考虑是否存在索引失效的问题。
在SQL语句的查询条件中用上了索引列,并不意味着查询过程就一定都用上了索引,有些情况会导致索引失效,而发生全表扫描!
- 在排查索引失效的时候,第一步一定是找到要分析的SQL语句,然后通过explain查看它的执行计划。
- 我们需要通过key + type + Extra来判断一条SQL语句是否用到了索引。如果有用到索引,那么是走了覆盖索引呢,还是索引下推呢?还是扫描了整颗索引树呢?
索引失效的情况有很多,从底层原理上理解,以不变应万变,无论索引失效情况有多少种,都在这两种范围内:
- 查询从数据结构上根本走不了索引
- MySQL优化器根据自己的推断走索引的性能可能还不如直接走全表扫描,因此就不走索引了,这样对外的表现形式就是看起来索引失效了。
可能导致索引失效的情况:
1. 联合索引非最左匹配:不符合最左前缀匹配原则会导致(联合)索引失效!
- 对主键字段建立的索引叫做聚簇索引,对普通字段建立的索引叫做二级索引。
- 将多个普通字段组合在一起创建的索引就叫做联合索引,也叫做组合索引。
- 创建联合索引时,我们需要注意创建时的顺序问题,比如联合索引(a,b,c)和(c,b,a),注意因为有查询优化器,所以字段在where子句的顺序并不重要。
- 联合索引要想正确使用必须遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配!
为什么联合索引不遵循最左匹配原则就会失效?
- 原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序,以此类推。
- 也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中各个列必须是联合索引中从最左边开始连续的列,如果是仅仅按照第二列搜索,肯定无法走索引。
2. 索引列字段参与计算,也就是对索引进行表达式计算,就会导致索引失效!
- select * from table where age + 1 = 12;=> 索引失效!
- select * from table where age = 12 - 1; => 还是可以走索引
为什么对索引进行表达式计算,就无法走索引了呢?
- 因为索引保存的是索引字段的原始值,而不是 id + 1 表达式计算后的值,所以无法走索引!
3. 对索引列字段增加 / 进行函数操作,就会索引失效!
- select * from table where create_time = '2023-04-01 00:00:00';
- select * from table where YEAR(create_time) = 2022;===> 索引失效
为什么对索引使用函数,就无法走索引了呢?
- 因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了!
4. where子句中使用OR:
如果使用OR,并且OR的两边存在>或<的时候,就会索引失效,如:
- select * from table where name = 'OD' or age > 18; => 索引失效
但是如果使用OR两边都是=判断,并且两个字段都有索引,那么也是可以走索引的,如:
- select * from table where name = 'MySQL' OR age = 18; => 正常走索引
- 在where子句中,如果在OR前面和后面只有一个条件列是索引列,那么索引就会失效,就会进行全表扫描,因为OR的含义就是两个只要满足一个即可,因此要求OR前后两个条件列都是索引列,只有一个条件列是索引列是没有意义的。
- 举例:比如下面的查询语句,id是主键,age是普通列,从执行计划的结果看,是走了全表扫描:select * from user where id = 1 or age = 18;
5. 对索引使用左或者左右模糊匹配 => 头部模糊匹配导致索引失效
- 当我们使用左或者左右模糊匹配的时候,也就是 索引列 like %xx 或者 like %xx%这两种方式都会造成索引失效。我们就可以在执行计划中看到 type 为 null,代表没有走索引,而是全表扫描!
为什么like关键字左或者左右模糊匹配无法走索引呢?
- 因为索引B+Tree是按照[索引值]有序排列存储的,只能根据前缀进行比较。
- 如果使用 name like '%xx'方式来查询,就不知道走哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
6. 对索引隐式类型转换
- 如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型,也会导致索引失效,你会在执行计划的结果发现这条语句会走全表扫描!
比如:name是一个varchar字符串类型,但是我们用int类型(整型)查询,导致索引失效:
- select * from tabel where name = 1;=> 隐式类型转换导致索引失效
- MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。
- 如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会自动发生隐式类型转换,由于隐式类型转换是通过CAST函数实现的,等同于对索引列使用了函数,所以就会导致索引失效!
这种情况会有一个特例,比如索引字段是整型,而查询条件中的输入参数即使为字符串,添加了单引号,是不会导致索引失效的,还是可以走索引扫描,因为MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较!比如:
- select * from table where age = '1'; => 正常走索引
9. 联合索引范围查询
-
建立索引根据自己的业务场景来满足最左匹配原则 !
-
建立联合索引(a,b),select * from table where a = 1; ⇒ 该SQL语句也用了联合索引,只不过用到了联合索引的一部分而已!
场景题:
-
有个Table表,这个表有5个字段:主键ID、a、b、c、d,建立了一个联合索引(a,b,c),select * from table where c = 1 and a = 1 and b > 1,是否用到索引?
- 索引能够快速的根据某个字段找到想要的数据,但是有一个前提条件:就是我去查询的时候这个索引字段一定要有序!
- 联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行查询,也就是可能存在部分字段用到了联合索引的B+Tree,部分字段没有用到联合索引的B+Tree的情况,这种特殊情况就发生在范围查询。
- 联合索引的最左匹配原则会一直向右匹配直到遇到[范围查询]就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段就无法用到联合索引了。
- 首先,联合索引(a,b,c),在索引树里面是先根据字段a来排序,a相同的才根据b排,b相同的再根据c排 => 前面一个字段相等,后面一个字段才有序!
首先, 最左匹配原则:a在where条件中存在,满足最左前缀匹配原则,所以a肯定能走到索引树 ,但是不确定是否所有条件都会走; 继续最左,b在条件中也存在,所以b也能走到索引。但是b是个范围查询,范围查询后,导致下一个索引列失效,c就是无序的,因为在符合 b > 1 条件的二级索引记录的范围里,c字段的值是无序的!所以,如果找c = 1的数据必须扫描遍历b > 1的所有数据,c是不能走索引扫描的! 所以这条SQL查询语句只有a,b两个字段用到了联合索引进行索引查询,而c字段并没有使用到联合索引。Q2:select * from table where a ≥ 1 and b = 2,联合索引(a,b)哪一个字段用到了联合索引的B+Tree?
- 对于联合索引,是先按照a字段的值排序,然后在a字段的值相同的情况下,再按照b字段的值进行排序,所以a字段肯定在联合索引的B+Tree中进行了索引查询;
- 虽然在符合 a ≥ 1条件的二级索引记录的范围里,b字段的值是无序的,但是对于符合 a = 1的二级索引记录的范围里,b字段的值是有序的,也就是b字段可以利用联合索引进行索引查询,因此Q2这条SQL查询语句a和b字段都用到了联合索引进行索引查询。
Q3:select * from table where a between 2 and 8 and b = 2,联合索引(a,b)哪一个字段用到了联合索引的B+Tree?
- Q3查询条件中,a between 2 and 8的意思是查询a字段的值在≥2和≤8之间的记录。
- 不同的数据对between value1 and value2处理方式是有差异的,在MySQL中,between包含了value1和value2的边界值,即≥ and ≤。
- 由于MySQL的BETWEEN...AND包含了value1和value2边界值,因此Q3这条查询语句a字段和b字段都用到了联合索引进行索引查询。
Q4:select * from user where name like 'j%' and age = 22,联合索引(name,age)哪一个字段用到了联合索引的B+Tree?
- 联合二级索引是先按照name字段的值排序的,所以前缀为'j'的name字段的二级索引记录都是相邻的,因此name字段肯定用到了联合二级索引的B+Tree进行索引查询;
- 虽然在符合前缀为'j'的name字段的二级索引记录的范围里,age字段的值是无序的,但是对于符合name = j的二级索引记录的范围里,age字段的值是有序的!,因为对于联合索引,是先按照name字段的值排序,然后在name字段的值相同的情况下,再按照age字段的值进行排序,所以Q4这条SQL查询语句name和age字段都用到了联合索引进行索引查询。
- 通过Q4查询语句我们可以知道,虽然name字段使用了like前缀匹配进行范围查询,但是联合索引的最左匹配原则并没有在遇到name字段的like前缀匹配的范围查询后就停止匹配了,因此age字段还是用到了联合索引。
总结:
- 联合索引的最左匹配原则,在遇到范围查询(如>、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段后面的字段就无法用到联合索引了。注意,对于≥、≤、BETWEEN...AND...、like前缀匹配的范围查询,并不会停止匹配。
联合索引 & explain => 什么是索引跳跃?最左原则索引失效场景一定适用所有场景吗?
Index Skip Scan(索引跳跃扫描)
- MySQL 8.0.13引入的优化,某种程度上可以无视最左前缀原则。
- 假设我们有联合索引(a,b),当我们在 8.0 当中使用 b 字段进行查询的时候,是可以做索引的,使用explain可以看到执行计划里有:Using Index for skip scan(索引跳跃),以下查询语句:select * from table where b = xxx
- 在开启该优化后会被转换为:select from table where a = 1 and b = xxx union select * from table where a = 1 and b = xxx....
- 列出所有索引a的情况,会把a列通过穷举的方式一个个的带到where条件中,拼接到SQL当中,最后再把这些SQL用union合并,达到走索引的目的,但如果 a 这个字段的值很多的话,效率一定是很低的,所以这种索引的优化,主要是针对联合索引当中左边的这个字段的distinct的值比较少,区分度不高的情况,如果取值可能很多的时候呢,MySQL优化器会基于成本考虑,可能就会进行全表扫描了,除此之外还有一些其它的限制条件。
不过有限制条件:
- select选择的字段不能包含非索引字段
- SQL当中不能带group by或者distinct的语句语法
- Skip Scan仅支持单表查询,不能多表join等等。
注意:MySQL只是提供了这种机制,但并不意味着每次查询都会触发,MySQL优化器会根据行数据大小,数据总量,索引树情况去综合选择。
判断SQL语句是否用上了联合索引?
- where查询条件满足最左匹配,无论查询的字段能否被联合索引覆盖,肯定用上了联合索引;
- where查询条件没有满足最左匹配,判断是否需要回表,如果不需要回表,可以直接在联合二级索引树中找到查询所返回的字段 => 索引覆盖,则认为用上了联合索引;
- 如果不仅没有满足最左匹配,并且还需要回表,那么它一定没有用上联合索引,而是走的是全表扫描!
-- 结论:当你表里面的全部列都是索引列的时候,一定会用到联合索引,哪怕不符合最左匹配原则,它也会用到联合索引!
-- 创建表
CREATE TABLE tb_demo (id int primary key, a int, b int, c int);
-- 给表添加abc联合索引
alter table tb_demo
add index idx_a_b_c (a, b, c);
-- 查询表中索引信息
show index from tb_demo;
-- 分析:联合索引 => Key-a,b,c & Value-id 主键索引=> Key-id & Value-a,b,c
-- 分析下面语句是否有用到联合索引! => key:idx_a_b_c Extra中都显示Using index,表示使用了索引覆盖!
-- key:idx_a_b_c type:ref-扫描类型为非唯一索引扫描 Extra:Using index-表示MySQL使用了索引覆盖优化
explain select * from tb_demo where a = 1 and b = 2 and c = 3;
-- key:idx_a_b_c type:index-全索引扫描,遍历索引树,但效率并不高 Extra:Using where;Using index
explain select * from tb_demo where b = 2 and c = 3;
-- key:idx_a_b_c type:index-全索引扫描,遍历索引树,但效率并不高 Extra:Using where;Using index
explain select * from tb_demo where c = 3;
-- 结论:当你表里面的全部列都是索引列的时候,一定会用到联合索引,哪怕不符合最左匹配原则,它也会用到联合索引!
-- ===========================================================================
-- 创建表
create table tb_demo2(id int primary key, a int, b int, c int, d int);
-- 给表添加a,b,c联合索引
alter table tb_demo2 add index idx_a_b_c(a,b,c);
-- 查询表中索引信息
show index from tb_demo2;
-- 分析:联合索引 => Key-a,b,c & Value-id 主键索引 => Key-id & Value-a,b,c,d
-- 分析下面语句是否有用到联合索引
-- 结论:
-- 满足最左匹配,肯定用上了联合索引;
-- 没有满足最左匹配,判断是否有回表,如果不需要回表,可以直接在联合二级索引树中找到查询所返回的字段,则认为用上了联合索引;
-- 如果不仅没有满足最左匹配,并且还需要回表,那么它一定没有用上联合索引,而是走的是全表扫描!
-- key:idx_a_b_c type:ref-扫描类型为非唯一索引扫描 Extra:null
-- 虽然该SQL语句回表了,但是是在联合索引过滤了一次的基础上才进行回表的,因此可以认为它用上了联合索引
explain select * from tb_demo2 where a = 1 and b = 2 and c = 3;
-- key:idx_a_b_c type:index-全索引扫描,遍历索引树,效率较低 Extra:Using where;Using index
-- 该SQL语句不用回表,直接可以在联合二级索引树中找到查询所返回的字段,整个过程没有去聚簇索引树上进行遍历,因此认为用上了联合索引
explain select a,b,c,id from tb_demo2 where b = 2 and c = 3;
-- key:null type:ALL-全表扫描 Extra-Using where-表示查询时使用了where子句进行条件过滤
explain select * from tb_demo2 where b = 2 and c = 3;
-- key:null type:ALL-全表扫描 Extra:Using where-表示查询时使用了where子句进行条件过滤
explain select * from tb_demo2 where c = 3;
10. 离散度比较低的不适合建立索引或不适合排在联合索引列的靠前的位置
离散度比较低的字段或区分度不高的字段不适合建立索引,因为它的利用率不高,效率比较低。
所谓离散度,意思就是指区分度不高的字段,就是字段是否是相同的几个值,比如性别,这种就不适合建立索引!区分度越高,使用索引的效率越高。
建立联合索引时的字段顺序,对索引效率也有很大影响,越靠前的字段被用于索引过滤的概率越大,因此在实际开发中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的SQL使用到。
离散度或区分度就是某个字段column不同值的个数 除以 表的总行数,计算公式如下:
11. 尽量建立联合二级索引而不是单列二级索引
- 因为联合索引本来就是多个字段的联合,比如a、b、c三个字段的联合索引,相当于a、ab、abc,三个查询都能走到索引!
12. 索引按字段特性分类
- 从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。
主键索引
- 主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
在创建表时,创建主键索引的方式如下:
CREATE TABLE table_name ( .... PRIMARY KEY (index_column_1) USING BTREE );
唯一索引
- 唯一索引是建立在UNIQUE字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
在创建表时,创建唯一索引的方式如下:
CREATE TABLE table_name ( .... UNIQUE KEY(index_column_1,index_column_2,...) );
建表后,如果要创建唯一索引,可以使用这面这条命令:
CREATE UNIQUE INDEX index_name ON table_name(index_column_1,index_column_2,...);
普通索引
- 普通索引就是建立在普通字段上的索引,什么是普通字段呢?
- 既不要求字段为主键,也不要求字段为UNIQUE。
在创建表时,创建普通索引的方式如下:
CREATE TABLE table_name ( .... INDEX(index_column_1,index_column_2,...) );
建表后,如果要创建普通索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(index_column_1,index_column_2,...);
前缀索引
- 前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为char、varchar、text、binary、varbinary的列上。
- 使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
在创建表时,创建前缀索引的方式如下:
CREATE TABLE table_name( column_list, INDEX(column_name(length)) );
建表后,如果要创建前缀索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(column_name(length));
-
限流是保证高可用!
13. 什么时候需要创建索引?索引创建原则有哪些?
- 针对于数据量较大(单表超过10w条数据),且查询比较频繁的表建立索引!
- 经常用于where查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
经常用于group by分组 和order by排序的字段,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在B+Tree中的记录都是排序好的。
字段有唯一性限制的,比如商品编码;
如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引;
尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高;
尽量使用联合索引,减少单列索引,这样可以节省存储空间,查询时,联合索引很多时候可以索引覆盖,避免回表,提高查询效率。
要控制索引的数量,索引并不是越多越好,因为所以越多,维护索引结构的代价也就越大,会影响增删改的效率;
如果索引列不能存储NULL值,则需要在创建表时使用NOT NULL约束它。
14. 什么时候不需要创建索引?
- where条件、group by、oder by里用不到的字段不需要创建索引,因为索引的价值是快速定位,如果起不到定位的字段是不需要创建索引的,因为毕竟索引是需要占用物理空间的。
- 离散度或区分度较低的字段:字段中存在大量重复数据,这样的字段也不需要创建索引,因为它的利用率不高,效率比较低,比如性别字段,只有男女。
- 表数据太少的时候,不需要创建索引!
- 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
15. 有什么优化索引的方法?
前缀索引优化
- 前缀索引优化就是使用某个字符串字段中的前几个字符来建立索引。
- 使用前缀索引是为了减少索引字段大小,从而可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减少索引列的大小。
- 不过,前缀索引也有一定的局限性,比如order by就无法试用前缀索引。
覆盖索引优化
- 覆盖索引是指SQL语句中查询的所有字段,在二级索引的B+Tree的叶子节点上就可以找到,而不需要再去聚簇索引的B+Tree中去查询获得,从而避免回表的操作,也就减少了大量的磁盘IO操作。
主键索引使用自增ID
- 如果我们使用自增主键,那么每次插入一条新数据时就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,因此这种插入数据的方法效率非常高。
- 如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就有可能会插入到现有数据页中间的某个位置,这就导致后面所有的数据以及目录数据都会进行变更,这就导致了树的分裂以及页的分裂合并,从而影响查询效率,因为每次分裂以及分裂后的合并都牵扯到数据的重新排序和移动;并且页分裂还有可能会增加写入的开销,因此页分裂有可能会造成大量的内存碎片,导致索引结构不紧凑。
主键字段长度不要太大
- 因为如果主键很长,那么辅助索引就会占用更多的空间,因此主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(因为二级索引的叶子节点存放的数据是主键值)。
- 如果没有特别的业务要求,建议使用自增ID作为主键!
索引最好设置为NOT NULL
为了更好的利用索引,索引列要设置为NOT NULL 非空约束,有两个原因:
- 索引列存在NULL值会导致优化器在做索引选择时更加复杂,更加难以优化。
- NULL值不仅是一个没意义的值,并且它还会占用物理空间。
防止索引失效
查询语句中用上了索引并不意味着查询时使用到了索引,这里简单说一下,发生索引失效的情况:
- 当我们在使用左或者左右模糊匹配时,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;
- 当我们在查询条件中对索引列做了计算、函数、类型转换操作,也会造成索引失效;
- 联合索引没有最左匹配原则,也就是没有按照最左优先的方式进行索引的匹配,这也会导致索引失效;
- 在where子句中,如果在OR前面的条件列是索引列,而在OR后面的条件列不是索引列时,就会导致索引失效。
16. 如果用到索引之后还是很慢,可能是什么原因?
- 走了索引也并不一定快,是因为MySQL的优化器有自己的索引选择的规则,但并不是每一次索引选择的规则都是对的,因为它的规则是固定死的,所以才会有一个操作:force index - 强制索引,当MySQL自己选择的索引不是我们想要的时候,可以使用force index强制指定自己想要的索引!
- 数据分布不均匀:索引的效率与数据的分布密切相关,如果数据分布不均匀,就可能导致某些索引节点的数据量很大,而另外一些节点的数据量很少,从而使查询性能下降!
- SQL语句存在问题:查询语句的优化非常重要,一些常用的优化技巧包括避免使用select * ,尽量避免多表join等,如果查询语句没有进行优化,就可能导致查询性能较差!
- 数据库设计不合理:如果表结构设计不合理,就可能导致查询需要扫描大量的数据才能得到结果,从而影响查询性能!
- 查询语句可能受到系统硬件或网络环境等因素的影响,如果系统硬件或网络环境存在问题,就可能导致查询性能较差!
16. 索引下推优化?
- 索引下推是MySQL 5.6 引入的一种优化技术,默认开启。
- 可以使用SET optimizer_switch = 'index_condition_pushdown = off',可以将关闭!
- 索引下推(Index Condition Pushdown),简称ICP,它的目的就是为了 / 索引下推能够 减少二级索引在查询时的回表次数以及Server层跟存储层之间的数据交互,提高了查询的效率,因为它将Server层部分负责的事情,交给或下推到了存储引擎层去处理了,把本来要在Server层执行器里过滤的数据移动到了二级索引树。
- MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合二级索引遍历过程中,对联合二级索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
- 联合索引遇到范围查询时(>、<)就会停止匹配!
索引下推的生效场景:
- 仅对二级索引生效!
- 仅对InnoDB跟MyISAM生效!
比如:建立一个联合索引(age,score),现在有这样一条SQL查询语句:
- select * from user where age > 20 and score = 99;
- 我们知道联合索引遇到范围查询时(>、<)就会停止匹配,也就是age字段能用到联合索引,但是score字段无法利用到联合索引。
如果不使用索引下推(MySQL 5.6之前的版本),执行器与存储引擎层的执行流程时这样的:
- Server层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到age > 20的第一条记录;
- 存储引擎根据二级索引的B+Tree快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给Server层; Server层再判断该记录的score是否等于99,如果成立则返回给客户端;否则跳过该记录;
- 接着,继续向存储引擎要下一条记录,存储引擎在二级索引定位到下一条记录后,获取主键值,然后进行回表操作,将完整的记录返回给Server层;
- 如此往复,直到存储引擎把表中的所有记录读完。
- 可以看到,如果没有索引下推,每查询到一条二级索引的记录的主键值后,都要进行回表操作,然后将记录返回给Server层,接着Server层再判断该记录的socre是否等于99。
而如果使用索引下推后,判断记录的score是否等于99的工作就交给了存储引擎层,过程如下:
- Server层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到age > 20的第一条记录;
- 存储引擎定位到二级索引后,先不执行回表操作,而是先判断以下该索引中包含的列(score列)的条件(score是否等于99)是否成立,如果条件不成立,则直接跳过该二级索引;如果成立,则执行回表操作,将查询到的整条记录返回给Server层,Server层将其发送给客户端,然后向存储引擎索要下一条记录。
- 如此往复,直到存储引擎把表中的所有记录读完。
- 可以看到,使用了索引下推后,虽然score列无法使用到联合索引,但是因为它包含在联合索引(age,score)中,所以直接在存储引擎过滤出满足 score = 99的记录后,才去执行回表操作,获取整个记录,相比没有使用索引下推,节省了很多回表操作。
- 当你发现执行计划里的Extra部分显示了"Using index condition",说明使用了索引下推!
17. 慢SQL的问题如何排查?MySQL如何定位慢查询?
- 慢查询是值数据库中查询时间超过指定阈值的SQL,这个阈值根据不同的业务来说一般是不一样的,在阿里内部,这个阈值是1s,也就是说,如果一条SQL执行超过1s,就认为是一个慢SQL。
- 现象:页面加载过慢,接口压测响应时间过长(超过1s).
方案一:开源工具
- 调试工具:Arthas
- 运维工具:Prometheus、Skywalking
一般来说,慢SQL的问题,是比较容易能够发现的,如果系统有部署很成熟的运维监控体系的话,会把慢SQL进行统计,然后以报警的形式推送出来!
方案二:MySQL自带慢查询
- 如果,项目中没有这种运维的监控系统,我们可以在MySQL的系统配置文件中开启慢查询日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,这样我们就可以在日志文件中找到执行比较慢的SQL了。
- 慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
- 如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
配置完毕之后,通过重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息:
慢日志文件记录的文件地址:/var/lib/mysql/localhost-slow.log
一般来说,导致一个SQL慢的常见原因有以下几个:
- 索引失效问题(比如没有遵守最左前缀匹配)、索引设计不合理(比如索引区分度不高):一般是先通过explain执行计划分析是否走了索引,以及走的索引是否复合预期,如果因为索引设计的不合理或者索引失效导致的,那么就可以修改索引或者修改SQL语句,或者强制执行使用某个索引。
- 多表join:尽量避免联表查询,都走单表查询性能最高!
- 查询字段太多:这个有的时候是因为我们错误的用到了select * 导致的,一般来说,查询字段小于100个,都不是特别大的问题,除非真的是字段数特别多,这时候有两种解决办法:第一个就是不要查询那些你不关心的字段,只查询少部分字段;第二个就是做分表,垂直分表,把数据拆分到多张表中,但是这么做可能会带来需要多表join的问题,所以拆分的是也需要考虑冗余。
- 表中数据量过大查询:一般来说,单表数据量超过1000w,就会导致查询效率降低,即使使用索引可能也会比较慢,所以表中数据量太大的话,这时候可能通过建立索引并不一定就能完全解决了,可以通过做分库分表、数据归档(把历史数据移出去,比如只保留最近半年的数据,半年前的数据做归档),或者使用搜索引擎,如ES等。
- 回表次数多
- 深度分页查询
- 聚合查询(使用聚合函数来完成数据的聚合操作,比如Count、Sum、Max、Min等)
- 其它...
18. 为什么大厂不建议使用多表join?
- 之所以不建议使用join查询,最主要的原因就是join的效率比较低。
- MySQL是使用了嵌套循环(Nested-Loop Join)的方式来实现关联查询的,简单点儿说就是要通过两层循环,用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出。
- 如果有2张表join的话,复杂度最高是O(n^2),3张表join则是O(n^3),随着表越多,表中的数据量越大,JOIN的效率会呈指数级下降。
- Alibaba开发规约中规定:超过三个表禁止join,即使双表join也要注意表索引以及SQL性能!
Hash Join算法
- MySQL 8.0中新增了hash join算法,hash join是MySQL 8.0.18版本中新推出的一种多表join的算法,在这之前,MySQL是使用了嵌套循环(Nested-Loop Join)的方式来实现关联查询的,而嵌套查询的算法性能是比较差的,因此Hash Join的出现就是要优化Nested-Loop Join的。
- 所谓Hash Join,其实是因为它底层用到了Hash表。
- Hash Join是一种针对equal-join场景的优化,它的基本思想是将驱动表数据加载到内存,并建立hash表,这样只要遍历一遍非驱动表,然后再去通过哈希查找在哈希表中寻找匹配的行,就可以完成join操作了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在MySQL中,可以使用JOIN在两个表或多个表中进行联合查询,join有三种,分别是inner join、left join和right join:
- inner join(内连接或等值连接):取两个表的交集部分
- left join(左连接):获取左表所有记录,即使右表没有对应匹配的记录。取两个表的交集部分 + 左表中的数据
- right join(右连接):与left join相反,用于获取右表所有记录,即使左表没有对应匹配的记录。取两个表的交集部分 + 右表中的数据。
在配合join一起使用的还有on关键字,用来指明关联查询的一些条件。
19. 什么是Full Text全文索引?
- 全文索引是一种用于实现全文检索的索引技术,但是这种场景,我们有更好的替代品,如ElacticSearch,所以实际使用并不多。
- MySQL的全文索引使用了一种特殊的数据结构,称为倒排索引,全文索引是基于倒排的方式来实现的。
- 全文索引只有InnoDB和MyISAM存储引擎支持吗,并且仅支持char、varchar和text字段。
count(1)、count(*) 与 count(字段/列名) 的区别
- count(1)和count(*)表示的是直接查询符合条件的数据库表的行数,说白了count(1)与count(*)的统计结果中,会包含值为NULL的行数。
- 而count(列名)表示的是查询符合条件的列的值不为NULL的行数。
- 除了查询得到的结果集有区别之外,在性能方面count(*)是约等于count(1)的,但是count(*)是SQL92定义的标准统计行数的语法,因为它是标准语法,所以MySQL数据库对它进行过很多优化。
COUNT(*)和COUNT(1)
- 关于这两者的区别,网上的说法众说纷纭。
- 有的说count(*)在执行时会转换成count(1),所以count(1)少了转换步骤,所以更快。
- 还有的说,因为MySQL针对count(*)做了特殊优化,所以count(*)更快。
其实,这两种说法都不对,我们来看看MySQL官方文档是怎么说的:
- InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way.There is no performance difference.
- InnoDB处理SELECT COUNT(*)和SELECT COUNT(1)操作的方式相同。没有性能上的差异。
既然MySQL对于COUNT(*)和COUNT(1)的优化是完全一样的,那建议用哪个呢?
- 建议使用COUNT(*)!因为这个是SQL92定义的标准统计行数的语法。
COUNT(字段/列名)
- COUNT(字段/列名)的查询就比较简单粗暴了,它是进行全表扫描,然后判断指定字段的值是不是为NULL,不为NULL则累加。
- 相比COUNT(*),COUNT(字段)多了一个步骤就是判断所查询的字段是否为NULL,所以它的性能要比COUNT(*)慢。