MySQL이 인덱스를 사용하지 않습니까? 엉터리

오늘의 기사는 "왜 100,000 프로그래머"라는 기사의 이전 시리즈입니다 .

인덱싱에 없는 MySQL에 대한 인터뷰 질문이 자주 있습니까? 가끔 동료가 인덱스 성능이 나쁘고 not in의 성능은 해당 필드의 판별과 관련이 있기 때문에 not in을 사용하지 말라고 하는데 이것이 사실입니까?

오늘 Xiaojiang은 이 문제를 깊이 있게 이해하도록 안내할 것입니다.우선, 우리는 Explain 키워드를 사용해야 하므로 이 키워드를 이해해야 합니다. Explain은 MySQL 문장의 실행 정보를 출력할 수 있는 실행 계획으로 인덱스 적중 여부와 최적화가 필요한지 판단할 수 있다.

기사 개요

  1. 자세히 설명하다
  2. 인덱싱 원리
  3. MySQL 문 쿼리 원칙
  4. 원칙적으로
  5. 결론적으로

먼저 테이블을 만들고 다음 테스트를 용이하게 하기 위해 일부 데이터를 삽입합니다.

CREATE TABLE test (
    id INT NOT NULL AUTO_INCREMENT,
    second_key INT,
    text VARCHAR(100),
    PRIMARY KEY (id),
    KEY idx_second_key (second_key)
) Engine=InnoDB CHARSET=utf8;
复制代码
INSERT INTO test VALUES
    (1, 10, 't1'),
    (2, 20, 't2'),
    (3, 30, 't3'),
    (4, 40, 't4'),
    (5, 50, 't5'),
    (6, 60, 't6'),
    (7, 70, 't7'),
    (8, 80, 't8');
复制代码

Explain 명령을 실행하면 다음을 얻습니다.

mysql> explain select * from test \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 13
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
复制代码

여기에는 많은 콘텐츠가 있지만 이러한 필드에만 관심을 기울일 필요가 있습니다.

  • 유형
  • 추가의


현재 문장을 실행할 때 MySQL이 실행하는 타입을 하나씩 설명 하자면, system, const, eq_ref, ref, fulltext, ref_or_null, index_merge, unique_subquery, index_subquery, range, index, ALL 값이 여러 개 존재한다.

  1. 시스템은 상대적으로 드물다.엔진이 MyISAM 또는 메모리이고 레코드가 하나뿐인 경우 시스템 수준에서 정확하게 액세스할 수 있음을 의미하는 시스템입니다. 드문 경우이므로 무시할 수 있습니다.
  2. const 쿼리는 기본 키 또는 고유한 보조 인덱스가 일치할 때 적중합니다. 예를 들어where id = 1
  3. eq_ref가 테이블을 연결할 때 동일한 값 일치를 위해 기본 키 또는 고유 인덱스를 사용할 수 있습니다.
  4. ref 및 ref_or_null, 고유하지 않은 인덱스와 상수가 동등하게 일치하는 경우. ref_or_null은 쿼리 조건이where second_key is null
  5. 전체 텍스트, index_merge는 일반적으로 건너뛰지 않습니다.
  6. unique_subquery 和 index_subquery 表示联合语句使用 in 语句的时候命中了唯一索引或者普通索引的等值查询。
  7. range 表示使用索引的范围查询,比如 where second_key > 10 and second_key < 90
  8. index 我们命中了索引,但是需要全部扫描索引。
  9. All,这个太直观了,就是说没有使用索引,走的是全表扫描。

接下来说一下 rows,MySQL 在执行语句的时候,评估预计扫描的行数。

最后就是关键的内容 Extra,别看他是扩展。但是它很重要,因为他更好的辅助你定位 MySQL 到底如何执行的这个语句。我们选择一些重点说一说。

  1. Using index,当我们查询条件和返回内容都存在索引里面,就可以走覆盖索引,不需要回表,比如 select second_key from test where second_key = 10
  2. Using index condition,经典的索引下推,虽然命中了索引,但是并不是严格匹配,需要使用索引进行扫描对比,最后再进行回表,比如 select * from test where second_key > 10 and second_key like '%0';
  3. Using where,当我们使用全表扫描时,并且 Where 中有引发全表扫描的条件时,会命中。比如 select * from test where text = 't'
  4. Using filesort,查询没有命中任何索引,需要在内存或者硬盘中排序的,比如 select * from test where text = 't' order by text desc limit 10

你也可以发现,无论是 type 还是 Extra,他们都是从前往后性能越来越差的,所以我们在优化 SQL 的时候,要尽量往前面的优化。好了到这里我们就简单介绍了完了关键词了,但是到我们可以分析 not in 是否命中索引还差点内容。我们需要了解一下 MySQL 的索引原理。下面是一个 B+ Tree 的索引图,也是 MySQL 索引的原理。

MySQL 每一个索引都会构建一棵树,我们也要做能做心中有“树”。那么我心中的两棵树是这个样子。

为了快速讲述本文重点,图片适当的忽略的一些 B+ 树的细节。

  1. 第一棵树是主键索引,每一个 Page 就是 B+树中最重要的概念——页,这里我们也叫它节点。非叶子节点不存储数据,只存储指向子节点的指针,叶子节点存储主键和其他所有列值。其中每个节点通过双向指针链接左右节点组成了双向链表,页内部每个块可以理解为一条记录,页内多条记录通过单向指针链接,组成单链表,所有的页和页内的记录都是根据主键从左到右递增的。
  2. 第二棵树是二级索引,非叶子节点不存储数据,只存储指向子节点的指针,叶子节点存储二级索引和主键,所有的页和页内的记录都是根据二级索引从左到右递增的,这些是和主键索引最大的不同,其余的一样。

那么我们开始分析一下索引的查询原理

select * from test where second_key = 40;
复制代码

这条语句的查询流程是:

  1. 因为 second_key 有索引,所以走的是 idx_second_key 二级索引生成的树。
  2. 通过检查 Page 1 发现我们需要查询的记录在 Page 12 所属的叶子节点内。
  3. 通过查询 Page 12 发现我们需要查询的记录在 Page 27 节点内。
  4. 从 Page 27 的节点内从左向右遍历,得到 40 节点
  5. 获取到 40 节点里面存储的主键 ID 4
  6. 因为二级索引里面没有数据,所以需要回表,回表的时候重新通过 ID 4 查找 primary_key 主键索引树。
  7. 依照刚才的顺序,最终找到内容在 Page 27 里面的节点,返回。

同时我们运行一下 explain 验证一下,type 是 ref,走的是非唯一索引的等值匹配。

explain select * from test where second_key = 40 \G;
复制代码
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ref
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
复制代码

上面是一个非常简单的查询,那么我们看一下稍微复杂的。

select * from test where second_key > 10 and second_key < 50;
复制代码

这条语句的查询流程是:

  1. 因为 second_key 有索引,所以走的是 idx_second_key 二级索引生成的树。
  2. 因为索引是从左到右递增的,所以我们先找 second_key > 10,通过前面的讲解,我们会定位到 Page 23 的第 2 个节点。
  3. 因为叶子节点是双向链表,所以我们不需要重新从根节点找其他内容,我们直接从左向右遍历比较,直到内容 >= 50 停止,这样我们会定位到 Page 16 的第 1 个节点停止。
  4. 那么我们拿到的结果就是 Page 23 和 Page 27 的 20,30,40 节点。
  5. 然后回表,分别找到 20,30,40 对应的主键 2,3,4 的内容,返回数据。

我们继续运行一下 explain,type 是 range 表示使用索引的范围查询, Extra 里面有了内容。Using index condition 表示 range 查询的时候使用了索引进行比较以后才进行的回表。

explain select * from test where second_key > 10 and second_key < 50 \G;
复制代码
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
复制代码

好的,那么进入了本文的高潮阶段,下面的语句怎么执行的你知道吗?

select * from test where second_key not in(10,30,50);
复制代码

凭着我们的手感,这次先运行 explain 吧,坏了,果不其然,type 是 ALL,全表扫描,小匠你又骗人?这不是没走索引吗?

           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: ALL
possible_keys: idx_second_key
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 8
     filtered: 75.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
复制代码

好吧,尴尬了。再来,那我们换个语句试试吧。

select second_key from test where second_key not in(10,30,50);
复制代码

再运行一次试试,看能不能搬回来一局。It's Nice。这次就走索引了诶。

           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 6
     filtered: 100.00
        Extra: Using where; Using index
复制代码

那么为什么第一次没有走索引呢?好了不绕弯子了,我们解密吧。
MySQL 会在选择索引的时候进行优化,如果 MySQL 认为全表扫描比走索引+回表效率高, 那么他会选择全表扫描。回到我们这个例子,全表扫描 rows 是 8,不需要回表;但是如果走索引的话,不仅仅需要扫描 6 次,还需要回表 6 次,那么 MySQL 认为反复的回表的性能消耗还不如直接全表扫描呢,所以 MySQL 默认的优化导致直接走的全表扫描。

那么我就是想 select * 还走索引怎么办呢? 好的,安排

select * from test where second_key not in(10,30,50) limit 3;
复制代码
           id: 1
  select_type: SIMPLE
        table: test
   partitions: NULL
         type: range
possible_keys: idx_second_key
          key: idx_second_key
      key_len: 5
          ref: NULL
         rows: 6
     filtered: 100.00
        Extra: Using index condition
复制代码

如释重负啊,这次不就是走索引了吗?因为 limit 的增加,让 MySQL 优化的时候发现,索引 + 回表的性能更高一些。所以 not in 只要使用合理,一定会是走索引的,并且真实环境中,我们的记录很多的,MySQL一般不会评估出 ALL 性能更高。

那么最后还是说一下 not in 走索引的原理吧,这样你就可以更放心大胆的用 not in 了?再次回到我们这个索引图。

select * from test where second_key not in(10,30,50) limit 3;
复制代码

这个语句在真正执行的时候其实被拆解了

select * from test where 
(second_key < 10) 
or 
(second_key > 10 and second_key < 30) 
or 
(second_key > 30 and second_key < 50) 
or 
(second_key > 50);
复制代码

>, <의 경우에 인덱스를 어떻게 사용하는지에 대해서는 이미 이야기를 했으니, 직접 디스어셈블된 문장을 분석해 볼까요? 이 문장의 분해가 완료된 후 시작 노드를 한 번 찾은 다음 인덱스에 따라 검색하는 것은 각각 4개의 열린 간격에 해당하므로 누군가 인덱스를 사용 not in하지 말라고 하면 어떻게 해야 하는지 알 수 있습니까? 말하다?

이 글은 제가 예전에 기획했던 'Why 100,000 Programmers' 의 글 시리즈로 , 더 궁금한 사항이 있으시면 저에게 메시지를 남겨주시거나 제 구독번호인 '코더의 노트'를 확인하시면 시리즈 글을 보실 수 있습니다.

인용하다

Nuggets 소책자 "Understanding MySQL from the Roots" 를 참조하십시오.

рекомендация

отjuejin.im/post/7102968550540705799