MySQL 有哪些索引?#
主键索引、唯一索引、普通索引、前缀索引、联合索引。
- 主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
- 唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
- 普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
- 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
- 联合索引:通过将多个字段组合成一个索引,该索引就被称为联合索引。
回答#
我了解到 MySQL 有主键索引、唯一索引、普通索引、前缀索引、联合索引这几种索引。Innodb 引擎会要求每一张数据库表都必须要有一个主键索引,比如表里的 id 字段就是主键索引。
然后针对查询比较频繁的字段,我们可以对这个字段建立普通索引,如果是多个字段的话,可以考虑建立联合索引,利用索引覆盖的特性提高查询效率。
对于长文本、字符串等类型的字段,比如文章标题、商品名称等,我们可以只对这些字段的前缀部分建立索引,也就是建立前缀索引,这样可以减少索引的存储空间。
MySQL 主键是聚簇索引吗?#
聚簇索引就是按照每张表的主键构造一棵 B+ 树,同时叶子节点中存放的是整张表的行记录数据,就好像把数据和索引聚集在了一棵 B+ 树上,所以这种数据组织形式的索引叫聚簇索引
每张表只能拥有一个聚簇索引,因为数据库表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
- 如果定义了主键,默认会使用主键作为聚簇索引的索引键;
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id (row_id)列作为聚簇索引的索引键;
回答#
是的,主键是聚簇索引,我们创建数据库表的时候,通常都会对 id 字段设置为主键索引,InnoDB 在创建聚簇索引时,默认会使用主键作为聚簇索引的索引键。
主键为什么不推荐有业务含义?#
因为任何有业务含义的列都有改变的可能性,主键一旦带上了业务含义,那么主键就有可能发生变更。主键一旦发生变更,该数据在磁盘上的存储位置就会发生变更,有可能会引发页分裂,产生空间碎片。
还有就是,带有业务含义的主键,不一定是顺序自增的。那么就会导致数据的插入顺序,并不能保证后面插入数据的主键一定比前面的数据大。如果出现了,后面插入数据的主键比前面的小,就有可能引发页分裂,产生空间碎片。
回答#
我觉得原因两个:
- 第一个业务会有变动的可能性,我们谁也无法预测 在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现,等需要变动的时候,去更改主键是成本很高的一件事情,不如设计阶段就规避不用有业务含义的主键。
- 第二个是业务含义的主键可能不是顺序自增的,有可能会发生页分裂问题,从而影响性能
主键是用自增还是UUID?#
通常主键 id 的数据类型有两种选择:字符串或者整数,主键通常要求是唯一的,如果使用字符串类型,我们可以选择 UUID 或者具有业务含义的字符串来作为主键。
对于 UUID 而言,它由 32 个字符+4 个’-‘组成,长度为 36,虽然 UUID 能保证唯一性,但是它有两个致命的缺点:
- 不是递增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
- 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。
对于整数的数字类型,MySQL 中主要有 int 和 bigint 类型。其中 int 占用 4 个字节,bigint 占用 8 个字节。
- 如果使用无符号的 int 类型作为主键,那么主键的最大值为 2^32-1,即 4294967295,这个值不到 43 亿,似乎有点太小了。虽然一张表的数据,我们不可能让其达到 43 亿条(太大会影响性能),但是对于频繁进行插入、删除的表来说,43 亿这个值是可以达到的。
- 而如果使用无符号的 bigint 类型的话,主键的最大值可以达到 2^64-1,这个数足够大了,如果以每秒插入 100 万条数据计算的,58 万年以后才能达到最大值。所以 bigint 作为主键的数据类型,完全不用担心超过最大值的问题。
而强制要求主键 id 是自增的,则是为了在数据插入的过程中,尽可能的避免索引树上页分裂的问题。
回答#
用自增 id 比较好,因为UUID是随机值,在数据插入的过程中,会导致索引树发生页分裂的问题,会影响性能,而且UUID是字符串类型,长度比较长,占用内存比较大,而页的大小是固定的,这样会导致索引树的高度越高,查询的时候会发生的磁盘 IO 次数也越多,性能也就更低。
但是自增 id 在分库分表环境下就不适用了,因为没办法保证全局唯一,这时候就需要考虑用雪花算法来作为主键了。
普通索引和唯一索引有什么区别?哪个更新性能更好?#
普通索引和唯一索引的核心区别在于对数据唯一性的约束以及底层更新机制的不同。
- 普通索引(Normal Index): 没有任何限制,允许在索引列中插入重复的值和空值(NULL)。它的唯一作用就是为了加快查询速度。
- 唯一索引(Unique Index): 索引列的值必须唯一,但允许有空值(NULL)。它的作用不仅是加速查询,更重要的是保证业务数据的完整性,防止产生重复的脏数据。
当数据页在内存中时(性能差异微乎其微)
- 普通索引: 找到位置,直接更新内存中的记录。
- 唯一索引: 找到位置,判断有没有冲突(因为在内存中,这个判断极快),然后更新内存记录。
- 结论: 这种情况两者性能几乎没有区别,因为都是纯内存操作。
当数据页不在内存中时(性能差距被拉开)
这时候需要从磁盘中将数据页读取到内存,而磁盘的随机 I/O 是非常慢的。
- 普通索引: InnoDB 会将这次更新操作直接记录在 Change Buffer 中,然后立刻向客户端返回更新成功。后台会有专门的线程在合适的时机(或者等下次查询访问到这个数据页时),将 Change Buffer 中的记录 merge(合并)到磁盘上。(极大地减少了随机磁盘 I/O,性能极高)
- 唯一索引: InnoDB 无法使用 Change Buffer。因为它必须先判断这个新插入的值是否违反了唯一性约束。为了做这个判断,它必须将数据页从磁盘加载到内存中。(触发了昂贵的随机磁盘 I/O,性能较差)
回答#
我认为普通索引的更新性能会更好,因为普通索引在更新的时候,如果更新的数据页不在内存的话,可以直接把更新操作缓存在 change buffer 中,更新操作就结束了,但是,唯一索引因为需要有唯一性约束,如果更新的数据页不在内存的话,需要从磁盘读取对应的数据页到内存,判断到没有冲突,这里会涉及磁盘随机 IO 的访问。
普通索引因为能使用 change buffer 特性,所以普通索引的更新相比于唯一索引,减少了随机磁盘访问,所以更新性能更好。
主键怎么设置?追问:假如你不设置会怎么样?#
可以在创建表时,将某一列定义为主键(PRIMARY KEY)。例如:
CREATE TABLE table_name (
id INT PRIMARY KEY,
column1 datatype,
column2 datatype,
...
);InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键;
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
回答#
在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
追问回答#
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键,如果这个条件也没有达成的话,InnoDB 将自动生成一个隐式 rowid 列作为聚簇索引的索引键。
介绍一下什么是外键约束?#
先来看看什么是外键?
假设我们有 2 个表,分别是表 A 和表 B,它们通过一个公共字段“id”发生关联关系,我们把这个关联关系叫做 R。如果“id”在表 A 中是主键,那么,表 A 就是这个关系 R 中的主表。相应的,表 B 就是这个关系中的从表,表 B 中的“id”,就是表 B 用来引用表 A 中数据的,叫外键。所以,外键就是「从表」中用来引用「主表」中数据的那个公共字段。
什么是外键约束?
在 MySQL 中,外键是通过外键约束来定义的。外键约束就是约束的一种,它必须在从表中定义,包括指明哪个是外键字段,以及外键字段所引用的主表中的主键字段是什么。MySQL 系统会根据外键约束的定义,监控对主表中数据的删除操作。如果发现要删除的主表记录,正在被从表中某条记录的外键字段所引用,MySQL 就会提示错误,从而确保了关联数据不会缺失,保证了 2 个表中数据的一致性。
回答#
外键就是「从表」中用来引用「主表」中数据的那个公共字段,外键约束确保了数据的引用完整性,也就是「从表」中的外键必须存在于「主表」的主键中,如果发现要删除的主表记录,正在被「从表」中某条记录的外键字段所引用,MySQL 就会提示错误,从而保证了 2 个表中数据的一致性。
外键有什么优劣势#
MySQL 外键最大的作用就是有助于维护数据的一致性和完整性。
- 一致性:如果一个订单表引用了一个客户表的外键,外键可以确保订单的客户 ID 存在于客户表中,从而保持数据的一致性。
- 完整性:外键可以防止在引用表中删除正在被其他表引用的记录,从而维护数据的完整性。
但是,其实在很多大型互联网公司中,很少用外键的,甚至阿里巴巴Java开发手册中明确规定了:「不要使用外键约束,如果数据存在外键关系,请在程序层面实现」
那么,使用外键会带来哪些问题呢?
- 性能问题:定义外键之后,数据库的每次操作都需要去检查外键约束,检查关联表是否已经存在数据。硬性保持数据一致性。这些操作会占用数据库的计算资源,如果一条记录中存在多个外键,这样的buff还将会被叠加(性能损耗加成)。
- 锁竞争问题:在使用外键的情况下,每次修改数据都需要去检查外键关联表里的数据,这需要额外获取读锁,如果是高并发的情况下,更容易造成死锁。
- 无法适用分库分表场景:在大型项目中,当数据量特别大的时候,一般会采取分库分表来存储数据,但在不同的库中使用相同的外键来维护数据一致性和完整性是非常难的操作,外键难以跨越不同数据库来建立关系。所以在分布式、高并发集群的项目数据库中一般看不到外键的存在。
回答#
外键能够保证数据的一致性和完整性,通过设置外键,数据库就会判断数据的完整性,不需要在应用代码里实现。
有了外键之后,每次增删改都需要额外检查外键约束,会占用数据库的计算资源,影响增删改的性能,而且还需要额外获取锁,在高并发场景下很容易发生死锁的问题,另外,外键也不适合分库分表的场景,外键难以跨越不同数据库来建立关系。
因此,基于性能开销、锁竞争、分库分表的考虑,一般项目中,很少用外键约束,都是在应用层面完成检查数据一致性的逻辑,就像大厂会使用 RC(读已提交隔离级别)来替代 RR(可重复读隔离级别)一样,会尽可能的降低锁的发生,一方面提升性能,一方面降低死锁概率。
为什么要建立索引#
考察索引的优点。建索引的三个优点:
- 索引大大减少了 MySQL 需要扫描的数据量;
- 索引可以帮助 MySQL 避免外部排序和使用临时表;
- 索引可以将随机 I/O 变为顺序 I/O;
回答#
如果没有建立索引,我们查询数据的话,搜索时间复杂度是 O(n),这样的查询效率还是比较低的,为了提高查询效率,我们可以建立索引。
建立了索引后数据都会按照顺序存储,这时候我们可以利用类似二分查找的方式快速查找数据,B+ 树索引是多叉树,搜索时间复杂度是 O(logdN),这样就提高了查询速度,除此之外还可以避免外部排序和使用临时表等问题,以及将随机 I/O 变为顺序 I/O。
一般选择什么样的字段来建立索引?#
考察索引的使用场景。
字段有唯一性限制的,比如商品编码;
为何普通索引比唯一索引的性能更好,还要求字段有唯一性索引?
因为业务数据的正确性,永远凌驾于微小的性能优化之上。
性能更好:只有没用在内存时,性能更好,但是为了在高并发下保证数据安全性,还是该字段有唯一性。
经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
不适合索引的场景:
WHERE条件,GROUP BY,ORDER BY里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。- 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。
- 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
回答#
可以对字段有唯一性限制和频繁用于 WHERE 查询条件的字段建立索引,这样能够提高整张表的查询速度,如果查询条件不是一个字段,可以考虑建立联合索引。还有对于经常用于排序、分组的字段建立索引,这样在查询的时候就不需要再去做一次排序了,因为建立索引之后在 B+树中的数据都是排序好的。
不过,对于一些区分度不高的字段,比如性别字段,只有男女,不建议建立索引,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据,在这种情况下,MySQL 的优化器发现某个值在表中出现的比例很高的时候,它一般会忽略索引,进行全表扫描,这时候建立的索引就没有起到作用,反而还占用了存储空间。
索引越多越好吗?#
考察索引的缺点。索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
- 空间代价:需要占用物理空间,数量越大,占用空间越大;
- 时间代价:会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
回答#
不是的,索引虽然能提高查询效率,但是多建立一个索引,就意味着新生成一个 B+ 树索引,是需要占用存储空间的,特别是在表数据量非常大的时候,索引占用的空间越大。
还有,索引越多数据库的写入性能会下降,因为每次对表进行增删改操作的时候,都需要去维护各个 B+ 树索引的有序性。
什么时候不用索引更好?#
回答#
建立了索引,虽然能提升查询效率, 但是它带来了两个代价,第一个是空间代价,因为需要多构建一颗 b+树,会占用磁盘空间。第二个更新时间代价,每次增删改索引,都需要动态维护 b+树,以满足 b+树的有序性。
所以, 我认识到如果一张表经常被增删改的话,也就是写多读少的场景下, 不建立索引会更好,因为这时候维护索引的开销可能会超过索引带来的性能提升。
还有一点,如果表中某个列的值高度重复,那么建了索引也没有用,优化器会选择全表扫描,这样建立的索引会占用存储空间,也会影响增删改的效率,选择不用索引会更好。
字段为什么要定义为NOT NULL?#
来自《高性能MySQL》中有这样一段话:
尽量避免NULL
很多表都包含可为NULL(空值)的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的默认属性。通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。
如果查询中包含可为NULL的列,对MySql来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySql里也需要特殊处理。当可为NULL的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为NULL的列改为NOT NULL带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改掉这种情况,除非确定这会导致问题。但是,如果计划在列上建索引,就应该尽量避免设计成可为NULL的列。
当然也有例外,例如值得一提的是,InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据有很好的空间效率。但这一点不适用于MyISAM。
回答#
- 如果查询中包含可为NULL的列,对MySQL的优化器来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。
- 如果某列存在NULL的情况,可能导致
count()等函数执行不准确,因为 count 不会统计值为 NULL 列。 - NULL 值是一个没意义的值,但是它会占用物理空间,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表
索引怎么优化?#
几种常见优化索引的方法:
覆盖索引优化:假设我们只需要查询商品名称和价格这两个数据,这时候我们可以对这两个字段建立联合索引,即「商品名称、价格」作为一个联合索引,针对
select product_id, product_name, price from table where product_name = "iphone";的语句,这时候就利用覆盖索引优化了, 因为索引中已经包含这两个字段数据了,所以查询将不会再次检索主键索引,从而避免回表,减少了大量的 I/O 操作。主键索引最好是自增的:
- 如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
- 如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
防止索引失效:
- 当我们使用左或者左右模糊匹配的时候,也就是
like %xx或者like %xx%这两种方式都会造成索引失效; - 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
- 当我们使用左或者左右模糊匹配的时候,也就是
前缀索引优化:使用前缀索引可以减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
回答#
我用过这几种优化的方式:
- 对于只需要查询几个字段数据的 SQL 来说,我们可以对这些字段建立联合索引,这样查询方式就变成了覆盖索引,避免了回表,减少了大量的 I/O 操作。
- 我们的主键索引最好是递增的值,因为我们索引是按顺序存储数据的,如果主键的值是随机的值,可能会引发页分裂的现象, 页分裂会导致大量的内存碎片,这样索引结构不紧凑了,就会影响查询效率。
- 我们要避免写出发生索引失效的 SQL 的语句,比如不要对索引进行计算、函数、类型转换操作,联合索引要能正确使用需要遵循最左匹配原则等等。
- 对于一些大字符串的索引,我们可以考虑用前缀索引只对索引列的前缀部分建立索引,节省索引的存储空间,提高查询性能。
建立了索引,查询的时候一定会用到索引吗?#
两个方向回答:
- 索引失效的场景
- 优化器是基于成本考虑,即使查询条件用了索引,如果走索引的查询成本太高,也不会选择走索引。
回答#
不是的。
我了解到即使查询使用到了索引,也是可能不走索引的,比如:
- 当我们查询语句对索引字段进行左模糊匹配、表达式计算、函数、隐式类型转换操作,这时候查询语句就无法走索引了,查询方式就变成了全表扫描的方式。还有我们使用联合索引进行查询的时候,如果没有遵循最左匹配原则,也是会发生索引失效的。
- 优化器是基于成本考虑来选择查询的方式,在使用二级索引进行查询的时候,优化器会计算回表的成本和全表扫描的成本,如果回表的代价太高,优化器会选择不走索引,而是走全表扫描。
隐形转换#
如果我定义了一个 varchar 类型的日期字段,并且有一个数据是‘20230922’,如果这个日期字段上有索引,那如果我查询的wher条件是where time=20230922 不加单引号,还会命中索引吗?为什么?
不会命中索引。
要明白这个原因,首先我们要知道 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。那么这个字符串转为数字的过程,实际上背后会执行 CAST 函数。
//查询语句
select * from t_user where time = 20230922;
//背后的效果执行效果是
select * from t_user where CAST(time AS signed int) = 20230922;而题目中的字符串对象是 time,也就是索引字段,这时候 CAST 函数就会作用到 time 索引字段,相当于对索引字段进行了函数计算,因此就会发生索引失效。
而如果反过来,id 是索引且是整型类型,那么下面这条语句就不会发生索引失效,因为字符串对象是“1”,是在它身上发生函数计算,id 并不会发生函数计算,所以没有问题。
//查询
select * from t_user where id = "1";
//等价于
select * from t_user where id = CAST("1" AS signed int);回答#
不会命中索引。
因为 mysql 在遇到字符串和数字比较的时候,会发生隐式类型转换,会将字符串的对象转为数字,这个转换的过程实际上会涉及到函数。你说的这个查询,日期字段是字符串,那么发生隐式类型转换的时候,就会作用在日期这个索引字段上,对索引进行函数计算的话,是会发生索引失效的。
MySQL 最新版本解决了索引失效的哪些情况了吗?#
MySQL 8.0 新特性:函数索引和索引跳跃扫描机制。
函数索引
MySQL 8.0 索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
举个例子,我通过下面这条语句,对 length(name) 的计算结果建立一个名为
idx_name_length的索引。alter table t_user add key idx_name_length ((length(name)));然后我再用下面这条查询语句,这时候就会走索引了。
explain select * from t_user where length(name)=6;索引跳跃扫描机制
MySQL 8.0 新增索引跳跃扫描机制,支持不符合联合索引最左前缀原则条件下的SQL,依然能够使用联合索引,减少不必要的扫描。
回答#
- 我了解到 MySQL 8.0 可以给字段增加函数索引,这个新特性可以解决对索引使用函数的时候,索引失效的问题。
- 还有一个新特性是索引跳跃式扫描,5.7 版本之前,使用联合索引的时候,如果不满足最左匹配原则,就会发生索引失效,而 8.0 出了索引跳跃式扫描特性之后,即使没有遵循最左匹配原则,部分场景下,依然可以使用联合索引。
什么是最左匹配原则?#
要知道联合索引的结构,才能理解最左匹配原则。
如果创建了一个 (a, b, c) 联合索引,联合索引的索引顺序是这样的,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。
因此,使用联合索引时,存在最左匹配原则。
例如,如果有一个联合索引 (a, b, c),当查询条件为 WHERE a=1 AND b=2 时,MySQL 可以使用这个索引进行查询,因为查询条件匹配了索引的最左边的两个列。但是如果查询条件为 WHERE b=2 AND c=3,则 MySQL 无法使用这个索引进行查询,因为查询条件不匹配索引的最左边的列。
回答#
假设有一个 (a, b, c) 联合索引,它的存储顺序是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。由于这个的特性,在使用联合索引时,存在最左匹配原则,具体的规则:
- MySQL 会从联合索引从最左边的索引列开始匹配查询条件,然后依次从从左到右的顺序匹配,如果查询条件没有使用到某个列,那么该列右边的所有列都无法使用走索引。
- 当查询条件中使用了某个列,但是该列的值包含范围查询,范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
所以,我们在使用联合索引的时候,要遵守最左匹配原则,否则可能会出现部分索引字段走不了索引。
建立联合索引有什么需要注意的?#
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:
$$ 区分度 = \frac{distinct(column)}{count(*)} $$比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是"30%")很高的时候,它一般会忽略索引,进行全表扫描。
回答#
最好把区分度比较大的字段放在联合索引最左侧,有助于提高索引的过滤效果,比如 UUID 这类字段就比较适合排在联合索引列的靠前的位置。
如果区分度很低的字段放在了联合索引最左侧,有可能会导致查询优化器会选择全表扫描,而不走索引了。
了解索引下推吗?什么情况下会下推到引擎去处理?#
索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。
举一个具体的例子,我对 age 和 reward 字段建立了联合索引(age,reward),现在有下面这条查询语句:
select * from t_user where age > 20 and reward = 100000;联合索引当遇到范围查询时就会停止匹配,也就是 age 字段能用到联合索引,但是 reward 字段则无法利用到索引。
那么,不使用索引下推(MySQL 5.6 之前的版本)时,执行器与存储引擎的执行流程是这样的:
- Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
- 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给 Server 层;
- Server 层在判断该记录的 reward 是否等于 100000,如果成立则将其发送给客户端;否则跳过该记录;
- 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层;
- 如此往复,直到存储引擎把表中的所有记录读完。
可以看到,没有索引下推的时候,每查询到一条二级索引记录,都要进行回表操作,然后将记录返回给 Server,接着 Server 再判断该记录的 reward 是否等于 100000。
而使用索引下推后,判断记录的 reward 是否等于 100000 的工作交给了存储引擎层,过程如下 :
- Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
- 存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引中包含的列(reward列)的条件(reward 是否等于 100000)是否成立。如果条件不成立,则直接跳过该二级索引。如果成立,则执行回表操作,将完成记录返回给 Server 层。
- Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
- 如此往复,直到存储引擎把表中的所有记录读完。
可以看到,使用了索引下推后,虽然 reward 列无法使用到联合索引,但是因为它包含在联合索引(age,reward)里,所以直接在存储引擎过滤出满足 reward = 100000 的记录后,才去执行回表操作获取整个记录。相比于没有使用索引下推,节省了很多回表操作。
回答#
索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。
举个例子,联合索引(a,b,c),查询条件为 a=? and c=? 的时候,由于联合索引的最左匹配原则,c 是无法走索引的,在没有索引下推机制之前,查询语句走二级索引的时候,需要回表读取 c 的值,然后在 server 层进行过滤,有了索引下推机制后,即使 c 无法走索引,但是由于 c 在二级索引里,那么将过滤 c 的工作从 server 层下推到存储引擎层,这样直接在二级索引里过滤满足 c 条件的记录,减少了回表的次数。
联合索引的判断#
建立联合索引 (a,b,c),下面的查询语句会不会走索引?具体哪些字段能走?
select * from T where a=1 and b=2 and c=3;select * from T where a=1 and b>2 and c=3;select * from T where c=1 and a=2 and b=3;select * from T where a=2 and c=3;select * from T where b=2 and c=3;select (a,b) from T where a=1 and b>2
回答#
- 遵循最左匹配原则,所以 abc 三个字段都可以走索引,查询方式是在联合索引找到主键值后,会回主键索引找完整的数据行。
- 根据最左匹配原则,范围查询后面的字段无法使用索引,所以 ab 可以走索引,c 无法走索引,不过 c 可以进行索引下推。
- abc都能走索引,因为 where 查询条件字段的顺序并不会影响,MySQL 优化器会帮我们调整字段的查询顺序,所以也是符合最左匹配原则的。
- a 能走索引,根据最左匹配原则,c 无法走索引,但是 c 可以被索引下推
- 根据最左匹配原则,bc都无法走索引。
- a 和 b 都能走索引,查询方式是覆盖查询,不需要回表。
建立索引#
where a>1 and b = 2 and c <3怎么建立索引?#
回答#
我会创建(bac)联合索引或者(bca)联合索引,因为这两种联合索引都可以有 2 个字段走索引。比如
- 创建 (bac) 联合索引,b 和 a 都能走索引,c 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数;
- 创建 (bca) 联合索引,b 和 c 都能走索引,a 字段虽然无法走索引,但是可以进行索引下推,这样会减少回表的次数;
where a=? And b=? order by c 怎么建立索引?#
回答#
这里 a 和 b 的区分度谁高, 在建立索引时,谁前面。
这里有 order by 排序,我们尽量要用索引来避免额外排序的操作,可以考虑建立 (a,b,c) 联合索引,因为 c 有序的前提是建立在 a=? And b =? 的场景下,刚好符合这个查询条件,这样 c 就不需要额外排序了,天然利用了索引的有序性。
where a>100 and b=100 and c=123 order by d 怎么建立联合索引?#
回答#
如果是为了强过滤(范围条件过滤性极佳):建 (等值列1, 等值列2, 范围列),接受内存排序。即 (b,c,a)
如果是为了防 filesort 并配合 LIMIT(范围条件过滤性差):建 (等值列1, 等值列2, 排序列, 范围列)。即 (b,c,d,a)
select b from table where a = 10 and c>20 怎么创建索引?#
回答#
优先考虑能让 where 查询中的字段能走索引,所以可以考虑(a,c) 联合索引,这样查询的时候,a 和 c 都能走联合索引,然后再考虑 select 的列是否能索引覆盖,很明显这个查询场景,只需要查询 b 列,那么我们可以考虑创建(a,c,b) 联合索引,这时候查询的时候,a 和 c 既能走索引,也能索引覆盖,避免了回表。
可以考虑创建(a,c,b)顺序的联合索引,这时候查询的时候,a 和 c 既能都走索引,也能利用索引覆盖的特性,避免了回表。
select id, name from XX where age > 10 and name like ‘xx%’,有联合索引(name,age),说一下查询过程。#
有三点需要说出来:
- 能不能走索引?哪些字段能走索引?能走索引,name 能走索引,age 不能走索引。
- 哪个字段能索引下推? age 字段能索引下推。
- 查询需不需要回表?不需要回表,索引覆盖查询。
回答#
索引定位 (Index Range Scan)
MySQL 优化器首先识别出 name LIKE 'xx%' 是一个范围查询。由于 name 是联合索引的第一列,它符合最左前缀原则。
- 存储引擎利用 B+ 树的查找机制,快速定位到索引树中第一个以
xx开头的name记录。 - 因为 B+ 树的叶子节点是按顺序排列的,所有
name以xx开头的记录都在一段连续的区间内。
索引下推过滤 (Index Condition Pushdown, ICP)
在 MySQL 5.6 及以上版本中,会触发 索引下推 优化:
- 存储引擎在遍历
name以xx开头的索引记录时,会直接判断该索引项中的age是否满足> 10。 - 注意: 虽然在
name是范围查询的情况下,age在全局范围内不是严格有序的,但age的值已经存储在索引树的叶子节点中了。 - 存储引擎直接过滤掉不符合
age > 10的索引项,只将满足条件的记录返回给 Server 层。如果没有 ICP,引擎需要把所有xx%的数据都传给 Server 层去过滤age。
覆盖索引 (Covering Index)
这是这个查询最快的地方:
- 查询要求返回
id和name。 - 在 InnoDB 引擎中,二级索引的叶子节点存储了:索引列的值 (
name,age) 和 主键值 (id)。 - 既然索引里已经包含了
id和name,存储引擎不需要回表(即不需要根据 id 去聚簇索引找整行记录),直接从当前的索引树上就能提取出结果。
结束扫描
当存储引擎扫描到第一个不以 xx 开头的 name 时,扫描停止,将结果集返回。
where id NOT IN (?, ?, ?) 会走索引吗?#
in 能不能走索引,关键是看查询成本,没有绝对说 in 会发生索引失败,也没有绝对说 in 一定能走索引。
回答#
要看查询成本,如果走某个索引花费的随机 I/O 比从聚簇索引顺序查(顺序I/O)的成本都还要高,那还不如直接去全表扫描。
举例: num 字段(非唯一二级索引)只包含 3 个值,1、2、3,3 只有几行,而 1、2 各有 100w 行,如果查询条件是 NOT IN (1, 2) 会走索引,如果查询条件是 NOT IN (3) 不会走索引。
如果查询条件中包含索引列和非索引列,MySQL的具体查询流程是什么样的?#
回答#
假设 a 是索引列,d 是非索引列,select a from test where a = ? and d = ? 查询过程先按索引去查,然后回表再过滤非索引列,会涉及回表的过程。
查询过程先按索引去二级索引B+树查,然后拿到主键 id,回表到主键索引B+树再过滤非索引列,查询过程会查 2 个 b+树,涉及回表的过程。

