目录

数据库索引优化的题型

索引类型【按作用范围】

MySQL索引类型包括1

  • 普通索引:没限制。
    • CREATE INDEX indexName ON mytable(username(length));
    • ALTER mytable ADD INDEX [indexName] ON (username(length))
    • 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;
    • 如果是BLOB和TEXT类型,必须指定 length。(全文索引)
  • 唯一索引:索引列必须唯一,可以有 null 值。UNIQUE
  • 主键索引:特殊的唯一索引,不允许有 null 值。PRIMARY KEY主键索引的叶子节点存的是整行数据
  • 组合索引:MySQL在多个单列索引存在的时候,只会用到它认为似乎最有效的那个。建立组合索引往往比多个单列索引要好,组合索引有一个最左匹配原则,一个组合索引abc相当于三个组合索引abcaba

区别实现索引的方式,也就是索引模型,主要有:有序数组(适用于静态存储引擎,不会再修改的数据)、哈希表(适用于等值查询)、搜索树。数据库发展到现在,跳表、LSM树也被用于引擎设计中。2

在 MySQL 中,索引是在存储引擎层实现的,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样。而即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。InnoDB的索引是B+树2

InnoDB 的索引模型【按结构】

在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+树中的。

每一个索引在 InnoDB 里面对应一棵 B+ 树。

https://cdn.jsdelivr.net/gh/dfface/img0@master/2022/05-08-VVPl2k.png

根据叶子节点的内容,索引类型分为主键索引和非主键索引。

主键索引的叶子节点存的是整行数据在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)

非主键索引的叶子节点内容是主键的值在 InnoDB 里,非主键索引也被称为二级索引(secondary index)

根据上面的索引结构说明,我们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?

如果语句是 select * from T where ID=500 ,即主键查询方式,则只需要搜索 ID 这棵B+ 树;

如果语句是 select * from T where k=5 ,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表

也就是说,**基于非主键索引的查询需要多扫描一棵索引树。**因此,我们在应用中应该尽量使用主键查询。

索引维护

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。

而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。

除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。

当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。

主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。

但也不绝对,如果你只有一个索引并且是唯一索引,那么这就是KV的场景,不用考虑其他索引叶子节点大小问题,而要“尽量使用主键查询”的原则,将这个设为主键索引,避免每次查询搜索两棵树。

索引原则

覆盖索引

如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

https://cdn.jsdelivr.net/gh/dfface/img0@master/2022/05-08-VVPl2k.png

基于上面覆盖索引的说明,我们来讨论一个问题:在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?

我们知道,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了。而再建立一个(身份证号、姓名)的联合索引,是不是浪费空间?

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间

当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这正是业务 DBA,或者称为业务数据架构师的工作。

最左前缀原则

B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。

不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

基于上面对最左前缀索引的说明,我们来讨论一个问题:在建立联合索引的时候,如何安排索引内的字段顺序。

我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b)这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。

那么,如果既有联合查询,又有基于 a、b 各自的查询呢?查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护 (a,b)、(b) 这两个索引。

这时候,我们要考虑的原则就是空间了。比如上面这个市民表的情况,name 字段是比age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。

索引下推

满足最左前缀原则的时候,最左前缀可以用于在索引中定位记录,那不符合最左前缀的部分,会怎么样呢?

我们还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

select * from tuser where name like '张 %' and age=10 and ismale=1;

你已经知道了前缀索引规则,所以这个语句在搜索索引树的时候,只能用 “张”,找到第一个满足条件的记录 ID3。当然,这还不错,总比全表扫描要好。

然后呢?当然是判断其他条件是否满足。

https://cdn.jsdelivr.net/gh/dfface/img0@master/2022/05-08-jLqgtb.png

在 MySQL 5.6 之前,只能从 ID3 开始一个个回表到主键索引上找出数据行,再对比字段值

https://cdn.jsdelivr.net/gh/dfface/img0@master/2022/05-08-VInPve.png

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数

索引失效3

  1. 索引列用函数或表达式,比如这种

    select * from test where  num  +  1 = 5
    

MySQL无法解析这种方程,这完全是用户的行为,应该把索引列当成独立的列,这样索引才会生效

  1. 存在NULL值条件
select * from user where user_id is not null;

在设计数据库表时,应该尽力避免NULL值出现,如果数据有为空的情况可以给一个默认值,比如数值型的可以给0、-1,字符类型的可以给空字符串。

  1. or表达式作为条件,有一个列没有索引,那么其它列的索引将不起作用
select * from user where user_id = 700 or user_name = "老薛";

像这种,如果user_id有加索引,而user_name没有的话,那么执行的时候user_id的索引也是失效的,这也是为什么开发中尽量少用or的原因,除非是两个字段都加了索引。

如果出现OR的一个条件没有索引时,建议使用 union ,拼接多个查询语句

  1. 列与列对比,某个表中,有两列(id和c_id)都建了单独索引,下面这种查询条件不会走索引
select * from test where id = c_id;
  1. 数据类型的转换。如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
create index `idx_user_name` ON user(user_name)
select * from user where user_name = 123;

像上面这种,虽然给user_name建立了索引,但查询的时候条件没有当成字符串,这样的话就不会走索引。

  1. NOT条件:当查询条件为非时,索引定位就困难了,执行计划此时可能更倾向于全表扫描,这类的查询条件有:<>not innot exists
select * from user where user_id<>500;
select * from user where user_id in (1,2,3,4,5);
select * from user where user_id not in (6,7,8,9,0);
select * from user where user_id exists (select 1 from user_record where user_record.user_id = user.user_id);
  1. like查询是以%开头

当使用模糊搜索时,尽量采用后置的通配符,例如要查姓张的人,可以用user_name like ‘张%’,这样走索引时,可以从前面开始匹配索引列,但如果是这样user_name like ‘%张’,那么就会走全表扫描的方式

8、多列索引,遵循最左匹配原则,这个上面说了

时机3

一般来说,应该在这些字段上创建索引:

  • 主键字段,这不用多说了吧;
  • 经常需要搜索的列,比如where条件经常用到的字段;
  • 其他表的外键字段,作为连接表的条件字段,可以有效加快连表查询速度;
  • 查询中作为排序、统计或者是分组的字段;

同样,对于有些字段不应该创建索引,这些列包括

  • 频繁更新的字段不适合创建索引,因为每次更新不单单是更新记录,还会更新索引,保存索引文件
  • where条件里用不到的字段,不创建索引;
  • 表记录太少,不需要创建索引;
  • 对于那些定义为text,image类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少,不利于使用索引;
  • 数据重复且分布平均的字段,因此为经常查询的和经常排序的字段建立索引。注意某些数据包含大量重复数据,这种字段建立索引就没有太大的效果,例如性别字段,只有男女,不适合建立索引。(区分度要高的)

EXPLAIN3

explain是MySQL的关键字,通过该关键字我们可以查看搜索语句的性能。

explain SQL语句

关于explain的几个重要参数,我们有必要了解一些:

id:查询的序列号

select_type:查询的类型,主要是区别普通查询和联合查询、子查询之类的复杂查询。

type

type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:

system > const > eq_ref > ref >fulltext > ref_or_null > index_merge > unique_subquery >index_subquery > range > index > ALL

System效率最高,ALL的话已经是全表扫描了,一般来说,查询至少要达到range级别。

key

显示MySQL实际决定使用的键。如果没有索引被选择,键是NULL

key=primary的话,表示使用了主键;

key=null表示没用到索引。

possible_keys

指出MySQL能使用哪个索引在该表中找到行。如果是空的,没有相关的索引。这时要检查语句中是不是有什么情况导致索引失效。

rows

表示执行计划中估计扫描的行数,是个估计值。

Extra

  • 如果是Only index,这意味着信息只用索引树中的信息检索出的,这比扫描整个表要快。
  • 如果是where used,就是使用上了where限制。
  • 如果是impossible where ,表示用不着where,一般就是没查出来啥。
  • 出现using index就说明我们的索引是生效的。

注意

  • 索引不会包含有NULL值的列:只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

  • 使用短索引:对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就限制前缀长度,不要对整个列进行索引。

  • 索引列排序:MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

  • LIKE语句问题:一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。

  • 不要在列上进行运算:select * from users where YEAR(adddate)<2007; 将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成 select * from users where adddate<‘2007-01-01’;

  • 不使用NOT IN和<>操作。

  • 最左前缀匹配原则, mysql会一只向右匹配直到遇到范围查询(>, <, between, like)就停止匹配(第一个范围是用到了的,后没不管有没有=都要停止),比如a=1 and b=2 and c>3 and d=4 如果建立了(a,b,c,d)顺序的索引, d是用不到索引的, 如果建立(a,b,d,c)的索引, 则都可以使用到, a,b,d的顺序可以任意调整。

  • =in 可以乱序, 比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意顺序, mysql 的查询优化器会帮你优化成索引可以识别的形式。

  • 单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引;(当一个表中查询大的情况下,where条件中有多个,如果使用多个单列索引,根据mysql优化器策略,造成可能只使用一个索引,其他索引会失效,导致会全盘扫描表,具体看下面链接)4

  • 组合索引,即一个索引包含多个列。(当一个表中查询大的情况下,where条件中有多个,那么可以使用组合查询,不会扫描表,直接从索引中获取,查询效率高)