【mysql】- 索引使用篇


回顾

  • 每个索引都对应一棵B+树,B+树分为好多层,最下边一层是叶子节点,其余的是内节点。所有用户记录都存储在B+树的叶子节点,所有目录项记录都存储在内节点。
  • InnoDB 存储引擎会自动为主键(如果没有它会自动帮我们添加)建立聚簇索引,聚簇索引的叶子节点包含完整的用户记录。
  • 我们可以为自己感兴趣的列列建立二级索引 ,二级索引 的叶子节点包含的用户记录由索引列列+主键组成,所以如果想通过 二级索引来查找完整的用户记录的话,需要通过回表操作,也就是在通过二级索引找到主键值之后再到聚簇索引中查找完整的用户记录。
  • B+树中每层节点都是按照索引列列值从小到大的顺序排序而组成了了双向链表,而且每个页内的记录(不不论是用户记录还是目录项记录)都是按照索引列列的值从小到大的顺序而形成了了一个单链表。如果是联合索引的话,则页面和记录按照先按照联合索引前边的列列排序,如果该列列值相同,再按照联合索引后边的列列排序。
  • 通过索引查找记录是从B+树的根节点开始,一层一层向下搜索。由于每个页面都按照索引列列的值建立了了Page Directory(页目录),所以在这些页面中的查找非常快。

代价

  • 空间上的代价
    • 这个是而易见的,每建立一个索引都为要它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB的存储空间,一棵很大的B+树由许多数据页组成,那可是很大的一片存储空间
  • 时间上的代价
    • 每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了了双向链表。不不论是叶子节点中的记录,还是内节点中的记录(也就是不不论是用户记录还是目录项记录)都是按照索引列列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收啥的操作来维护好节点和记录的排序。如果我们建了了许多索引,每个索引对应的B+树都要进行相关的维护操作,这还能不不给性能拖后腿么?
  • 所以说,一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。为了了能建立又好又少的索引,我们先得学学这些索引在哪些条件下起作用的。

适用条件

  • 前提:idx_name_date_age为索引

    • 全值匹配

      • 搜索条件中的列列和索引列一致的话,这种情况就称为全值匹配
        • select * from tableName where name = 'xxx' and date = '2020-01-01';,,包含查询中的2个列,查询过程:
          • 因为 B+ 树的数据页和记录先是按照name列的值进行排序的,所以先可以很快定位name列列的值是xxx的记录位置。
          • name列相同的记录里是按照date列的值进行排序的,所以在name列的值是xxx的记录里又可以快速定位 date列的值是 '2020-01-01' 的记录。

      上述条件的顺序不影响索引的命中,因为mysql有个查询优化器,会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定使用哪个搜索条件,后使用哪个搜索条件。

    • 匹配左边的列(最左原则)

      • 搜索语句句中也可以不用包含全部联合索引中的列列,只包含左边的就行,比方说下边的查询语句句:
        • select * from tableName where name = 'xxx';或者select * from tableName where name = 'xxx' and date = 'xxx';
      • 对于下面的语句就用不到索引了
        • select * from date = 'xxx';
        • 原因:因为B+树的数据页和记录先是按照name列的值排序的,在name·列的值相同的情况下才使用date列进⾏行排序,也就是说name列的值不同的记录中date的值可能是无序的。而现在你跳过name列直接根据date`的值去查找,自然是用不上索引。
      • 对于下面只能用到name索引
        • select * from tableName where name = 'xxx' and age = 'xxx';
        • 原因:因为name值相同的记录先按照date的值进行排序,date值相同的记录才按照age值进行排序。
    • 匹配列前缀

      • 索引创建时候会按照索引列的字母开始进行排序,如name的情况
      • aaaa aaab aaac ...
        • 先比较字符串串的第一个字符,第一个字符小的那个字符串就比较小。
        • 如果两个字符串的第一个字符相同,那就再比较第二个字符,第二个字符比较小的那个字符串串就比较小。
        • 如果两个字符串的第二个字符也相同,那就接着比较第三个字符,依此类推。
      • 所以一个排好序的字符串列其实有这样的特点:
        • 先按照字符串串的第一个字符进行排序。
        • 如果第一个字符相同再按照第二个字符进行排序。
        • 如果第二个字符相同再按照第三个字符进行排序,依此类推。
      • 那么查询name为'AB'开头,可以这么写:
        • select * from tableName where name like 'AB%';
      • 假如仅仅是后缀或者中间,如'XXAB'或者'XABX',是不用到索引的,因为这时的'AB'并未排好序。
        • select * from tableName where name like '%AB%'
    • 匹配范围值

      • 语句查询过程为:

        • select * from tableName where name > 'AA' and name < 'BB';
          • 找到name值为AA的记录。
          • 找到name值为BB的记录。
          • 由于所有记录都是由链表连起来的(记录之间用单链表,数据页之间用双链表),所以他们之间的记录都可以很容易易的取出来
          • 找到这些记录的主键值,再到聚簇索引 中回表查找完整的记录。

        在使用联合索引进行范围查找的时候需要注意,如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引

        • select * from tableName where name > 'AA' AND name < 'BB' AND date > '2020-01-01';
        • 上边这个查询可以分成两个部分:
          1. 通过条件 name > 'AA' AND name <'BB'来对name进行范围,查找的结果可能有多条name值不不同的记录;
          1. 对这些name值不不同的记录继续通过date > '2020-01-01条件继续过滤。
        • 这样子对于联合索引idx_name_date_age来说,只能用到name列的部分,而用不到date 列的部分,因为只有 name值相同的情况下才能用date列的值进行排序,而这个查询中通过name进行范围查找的记录中可能并不是按照 date列进行排序的,所以在搜索条件中继续以date列进行查找时是用不到这个B+树索引的。
    • 精确匹配某一列并范围匹配另外一列

      • 对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列列可以进行范围查找
        • select * from tableName where name = 'AA' AND date > '2020-01-01' AND date < '2020-12-31' AND age > '10';
        • 这个查询的条件可以分为3个部分:
          • name = 'AA' ,对name列进行精确查找,当然可以使用 B+ 树索引了。
          • date > '2020-01-01'AND birthday<'2020-12-31',由于name列是精确查找,所以通过name='AA'条件查找后得到的结果的name 值都是相同的,它们会再按照date的值进行排序。所以此时对date列进行范围查找是可以用到B+树索引的。
          • age > '10',通过age的范围查找的记录的date的值可能不同,所以这个条件无法再利用B+树索引了了,只能遍历上一步查询得到的记录。
    • 用于排序

      • select * from table order by name,date,age limit 1;

        • 这个查询的结果集需要先按照name值排序,如果记录的name值相同,则需要按照date来排序,如果date的值相同,则需要按照age排序。

        关于联合索引,order by的子句句后边的列的顺序也必须按照索引列的顺序给出,如果给出order by date, age, name的顺序,那也是用不了B+树索引

    • 不可以使用索引进行排序的几种情况

      • ASC、DESC混用,要么全是ASC,要么全是DESC
      • WHERE子句中出现非排序使用到的索引列
      • 排序列列包含非同一个索引的列
      • 排序列列使用了复杂的表达式
    • 用于分组

      • select * from tableName group by name,date,age;
      • 上述语句原理跟order by一致,分组列的顺序也需要和索引列的顺序一致
  • 回表的代价

    • 关于使用一个二级索引
      • 会使用到两个B+树索引,一个二级索引,一个聚簇索引
      • 访问二级索引使用顺序I/O,访问聚簇索引使用随机I/O
    • 如何减少回表
      • 覆盖索引:查询的列包含索引列
  • 索引的挑选

    • 只为用于搜索、排序或分组的列创建索引
    • 考虑列的基数
    • 索引列的类型尽量小
      • 数据类型越小,在查询时进行的比较操作越快
      • 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更更多的数据页缓存在内存中,从而加快读写效率。

总结

  • B+树索引在空间和时间上都有代价,所以没事儿别瞎建索引。
  • B+树索引适用于下边这些情况:
    • 全值匹配
    • 匹配左边的列
    • 匹配范围值
    • 精确匹配某一列并范围匹配另外一列
    • 用于排序
    • 用于分组
  • 在使用索引时需要注意下边这些事项:
    • 只为用于搜索、排序或分组的列列创建索引
    • 为列列的基数大的列创建索引
    • 索引列列的类型尽量小
    • 可以只对字符串值的前缀建立索引
    • 只有索引列在比较表达式中单独出现才可以适用索引
    • 为了了尽可能少的让 聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。
    • 定位并删除表中的重复和冗余索引
    • 尽量适用覆盖索引进行查询,避免回表带来的性能损耗
posted @ 2020-07-14 18:43  双木l之林  阅读(172)  评论(0编辑  收藏  举报