MySQL-进阶篇-索引

温馨提示:PC端浏览体验感最佳~~

  • 索引

    • 索引概述

      • 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
      • 索引的优缺点
    • 索引结构

      • MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的结构,主要包含以下几种:
      • 不同存储引擎对索引的支持情况
      • 注意:我们平常所说的索引,如果没有特别指明,都是指B+树结构组织的索引。
      • 数据结构
        • 二叉树
          • 二叉树缺点:顺序插入时,会形成一个链表,查询性能大大降低。大数据量情况下,层级较深,检索速度慢。
        • 红黑树(自平衡的二叉树)
          • 红黑树缺点:大数据量情况下,层级较深,检索速度慢。
        • B-Tree(多路平衡查找树)
          • 以一颗最大度数(max-degree)为5(5阶)的b-tree为例(每个节点最多存储4个key,5个指针):
            • 说明:树的度数指的是一个节点的子节点个数。
          • B-Tree构建过程示例
        • B+Tree
          • 以一颗最大度数(max-degree)为4(4阶)的b+tree为例:
          • B+Tree构建过程示例
          • B+Tree相对于B-Tree的区别
            • 所有的数据都会出现在叶子节点
            • 叶子节点形成一个单向链表
          • MySQL索引数据结构中B+Tree的应用
            • MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能。
        • Hash
          • 哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
          • 如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
          • Hash索引特点
            • Hash索引只能用于对等比较(=,in),不支持范围查询(between,>,<,...)
            • 无法利用索引完成排序操作
            • 查询效率高,通常只需要一次检索就可以了,效率通常要高于B+tree索引
          • 存储引擎支持
            • 在MySQL中,支持hash索引的是Memory引擎,而InnoDB中具有自适应hash功能,hash索引是存储引擎根据B+Tree索引在指定条件下自动构建的。
      • InnoDB存储引擎选择使用B+tree索引结构的原因
        • 相对于二叉树,层级更少,搜索效率高;
        • 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少(Page的大小固定为16K),指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
        • 相对Hash索引,B+tree支持范围匹配及排序操作;
    • 索引分类

      • 在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:

        • 聚集索引选取规则:

          • 如果存在主键,主键索引就是聚集索引。
          • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
          • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。
        • 聚集索引与二级索引示例

            • 说明:其中id为主键,根据id建立的主键索引就是聚集索引,聚集索引的叶子节点下面挂的是这一行的数据。而以name字段建立的索引是二级索引,二级索引的叶子节点下面挂的是这一行数据的主键(id)。
        • 回表查询

          • 假如执行以下SQL语句:

            sql 复制代码
              select * from user where name='Arm';
          • 则SQL语句的执行过程为:先根据name字段对应的二级索引找到数据对应的主键值,再根据主键值在聚集索引中拿到这一行的行数据。

        • InnoDB主键索引的B+tree不同高度可存储的数据量的计算

          • 假设:
            • 一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB的指针占用6个字节的空间,主键即使为bigint,占用字节数为8。
          • 高度为2:
            • n * 8 + (n + 1) * 6 = 16 * 1024,算出n约为 1170
              • 其中n为非叶子节点存储key的数量
            • 所以高度为2的B+Tree能够存储的数据量大概为:1171 * 16= 18736
          • 高度为3:
            • 高度为3的B+Tree能够存储的数据量大概为:1171 * 1171 * 16 = 21939856
    • 索引语法

      • 创建索引

        sql 复制代码
          CREATE [UNIQUE | FULLTEXT] INDEX index_name ON table_name(index_col_name, ...);
        • 补充:一个索引既可以只关联一个字段(称为单列索引),也可以关联多个字段(称为联合索引或组合索引)。
      • 查询索引

        sql 复制代码
          SHOW INDEX FROM table_name;
      • 删除索引

        sql 复制代码
          DROP INDEX index_name ON table_name;
    • SQL性能分析

      • SQL执行频率

        • MySQL客户端连接成功后,通过show [session | global] status命令可以提供服务器状态信息。通过如下指令,可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次:

          sql 复制代码
            SHOW GLOBAL STATUS LIKE 'Com_______';
          • 执行指令后的结果如图:

      • 慢查询日志

        • 慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。

          • 查看慢查询日志开关的语法:

            sql 复制代码
              show variables like 'slow_query_log';
        • MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:(这部分实操时可能跟课程有出入,例如MySQL配置文件的位置、慢日志文件的位置以及名称)

          sql 复制代码
            # 开启MySQL慢日志查询开关
            slow_query_log=l
            # 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
            long_query_time=2
          • 配置完毕之后,通过 systemctl restart mysqld 指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息 /var/lib/mysql/localhost-slow.log。
      • profile详情

        • show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling参数,能够看到当前MySQL是否支持profile操作:

          sql 复制代码
            SELECT @@have_profiling;
        • 默认profiling是关闭的,可以通过set语句在session/global级别开启profiling:

          sql 复制代码
            SET profiling=1;
        • 执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:

          sql 复制代码
            #查看每一条SQL的耗时基本情况
            show profiles;
            #查看指定query_id的sQL语句各个阶段的耗时情况
            show profile for query query_id;
            #查看指定query_id的SQL语句CPU的使用情况
            show profile cpu for query query_id;
      • explain执行计划

        • EXPLAIN或者DESC命令获取MYSQL如何执行SELECT语句的信息,包括在SELECT语句执行过程中表如何连接和连接的顺序。

        • 语法:

          sql 复制代码
            #直接在select语句之前加上关键字explain/desc
            EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
          • 执行指令后的结果:

          • EXPLAIN执行计划各字段含义

            • id
              • select查询的序列号,表示查询中执行select子句或者是操作表的顺序(id相同,执行顺序从上到下;id不同,值越大,越先执行)。
            • select_type
              • 表示SELECT的类型,常见的取值有SIMPLE(简单表,即不使用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等。
            • type
              • 表示连接类型,性能由好到差的连接类型为NULL(不访问任何表,在实际业务开发中不可能)、system(访问系统表)、const(根据主键或唯一索引进行访问)、eq_ref、ref(使用非唯一性的索引进行查询时)、range、index(虽然用了索引,但会对索引进行扫描,遍历整个索引树)、all(全表扫描)。(在优化SQL语句时尽量将type往前优化)
            • possible_key
              • 显示可能应用在这张表上的索引,一个或多个。
            • key
              • 实际使用的索引,如果为NULL,则没有使用索引。
            • key_len
              • 表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好。
            • rows
              • MySQL认为必须要执行查询的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的。
            • filtered
              • 表示返回结果的行数占需读取行数的百分比,filtered的值越大越好。
    • 索引使用规则

      • 最左前缀法则

        • 如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。

        • 如果跳跃某一列,索引将部分失效(后面的字段索引失效)。

        • 可以通过类似以下SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
            #且建立联合索引时的字段列表为(profession, age, status)
            
            #满足最左前缀法则,通过索引进行查询,profession、age、status三个字段的索引都生效
            explain select * from tb_user where profession='软件工程' and age=31 and status='0';
            #满足最左前缀法则,通过索引进行查询,profession、age两个字段的索引都生效
            explain select * from tb_user where profession='软件工程' and age=31;
            #满足最左前缀法则,通过索引进行查询,profession字段的索引生效
            explain select * from tb_user where profession='软件工程';
            #满足最左前缀法则,通过索引进行查询,profession字段的索引生效,但status字段的索引失效(因为中间跳过了age字段)
            explain select * from tb_user where profession='软件工程' and status='0';
            
            #注意:最左前缀法则与SQL语句的书写顺序没有关系,只要最左列字段存在即可,例如下面这条SQL语句是满足最左前缀法则的(其他字段也是如此)
            explain select * from tb_user where age=31 and status='0' and profession='软件工程';
            
            #不满足最左前缀法则,通过全表扫描进行查询(因为查询没有从索引的最左列------profession开始)
            explain select * from tb_user where age=31 and status ='0';
            #不满足最左前缀法则,通过全表扫描进行查询(因为查询没有从索引的最左列------profession开始)
            explain select * from tb_user where status ='0';
      • 范围查询

        • 联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效

        • 可以通过类似以下SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
            #且建立联合索引时的字段列表为(profession, age, status)
            
            #age字段使用了>进行范围查询,所以age右侧的status字段的列索引失效
            explain select * from tb_user where profession='软件工程' and age>30 and status='0';
            
            #age字段使用的是>=进行范围查询,其右侧的status字段的列索引不会失效(使用联合索引时,在业务允许的情况下,尽量使用>=或<=这样的运算符)
            explain select * from tb_user where profession='软件工程' and age>=30 and status='0';
            
            #注意:范围查询右侧的列索引是由创建索引时的字段列表的顺序决定的,跟SQL语句的书写顺序无关,
            #例如以下SQL语句中,status字段的列索引仍会失效
            explain select * from tb_user where profession='软件工程' and status='0' and age>30;
      • 索引列运算

        • 尽量不要在索引列上进行运算操作,否则索引将失效。

        • 可以使用类似以下SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的phone字段建立了索引
            
            #因为对phone字段进行了运算操作,所以phone字段的索引失效
            explain select * from tb_user where substring(phone,10,2) = '15';
      • 字符串不加引号

        • 字符串类型的字段使用时,不加引号,索引将失效。

        • 可以使用类似以下的SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
            #且建立联合索引时的字段列表为(profession, age, status),其中status的字段类型为字符串类型
            
            #因为status=0存在隐式类型转换,所以status字段的索引失效(注意:其他两个字段的索引仍生效)
            explain select * from tb_user where profession='软件工程' and age=31 and status=0;
      • 模糊查询

        • 如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

        • 可以使用类似以下的SQL语句进行验证:

          sql 复制代码
          #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
          #且建立联合索引时的字段列表为(profession, age, status)
          
          #尾部模糊匹配,且符合最左前缀法则,索引生效
          explain select * from tb_user where profession like '软件%';
          
          #头部模糊匹配,索引失效
          explain select * from tb_user where profession like '%工程';
          #头部模糊匹配,索引失效
          explain select * from tb_user where profession like '%工%';
      • or连接的条件

        • 用or分割开的条件,如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。

        • 可以使用类似以下的SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
            #且建立联合索引时的字段列表为(profession, age, status)
            #id为主键(主键索引)
            
            #age的联合索引不生效(不满足最左前缀法则),此时or的一侧(id字段)有索引,另一侧(age字段)没有索引,故id和age的索引都失效
            explain select * from tb_user where id=10 or age=23;
          • 由于age没有索引,所以即使id有索引,索引也会失效。所以需要针对于age也要建立索引。
      • 数据分布影响

        • 如果MySQL评估使用索引比全表更慢,则不使用索引。

        • 可以使用类似以下的SQL语句进行验证:

          sql 复制代码
            #假设已为tb_user表的phone字段建立了索引
            #tb_user表的数据请看下面的第一张图
            
            #此时表中的大部分数据都满足条件,MySQL评估使用索引比全表更慢,所以不使用索引,直接全表扫描
            select * from tb_user where phone>='17799990005';
            
            #此时表中只有少部分数据满足条件,MySQL评估使用索引比全表更快,所以使用索引
            select * from tb_user where phone>='17799990015';
      • SQL提示

        • SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。

        • user index(建议MySQL使用指定的索引,MySQL可以接受建议,也可以不接受)

          sql 复制代码
            explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';
        • ignore index(让MySQL忽略指定的索引)

          sql 复制代码
            explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';
        • force index(强制MySQL必须使用指定的索引)

          sql 复制代码
            explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';
      • 覆盖索引

        • 尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到),减少select *。

        • 观察以下SQL语句执行结果中的Extra列:

          sql 复制代码
            #假设已为tb_user表的profession、age、status三个字段建立了联合索引idx_user_pro_age_sta,
            #且建立联合索引时的字段列表为(profession, age, status)
            #id为主键(主键索引)
            #name没有索引
            
            explain select id, profession from tb_user where profession='软件工程' and age=31 and status='0';
            explain select id, profession, age, status from tb_user where profession='软件工程' and age=31 and status ='0';
            explain select id, profession, age, status, name from tb_user where profession='软件工程' and age=31 and status='0';
            explain select * from tb_user where profession='软件工程' and age=31 and status='0';
          • 说明

            • using index condition:查找使用了索引,但是需要回表查询数据。
            • using where; using index:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据。(效率更高)
      • 前缀索引

        • 当字段类型为字符串(varchar,text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO,影响查

          询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。

        • 语法

          sql 复制代码
            create index idx_xxxx on table_name(column(n));
        • 前缀长度

          • 可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,

            唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

            sql 复制代码
              #计算整个email字段的选择性
              select count(distinct email) / count(*) from tb_user;
              
              #计算email字段只截取前5个字符的选择性
              select count(distinct substring(email,1,5) / count(*) from tb_user;
        • 前缀索引查询流程

            • 说明:假设我们将email字段的前5个字符建立了前缀索引,当执行上图中的SQL语句时,会先截取email的前五个字符去二级索引进行查找,找到相同的前缀后再拿着id去聚集索引进行查找,查找到对应的数据后,不会直接返回,而是会将数据中的email字段拿出来与SQL语句中的email进行比对,如果相同,则证明这条数据是符合查询条件的数据,并且,比对成功后还会继续接着查找辅助索引的下一个,比对下一个前缀是否也相同(例如图中定位到lvbu6并且数据也比对成功后,还会继续看xiaoy是否符合条件,这里显然不符合),如果相同,则继续拿着id进行回表查询(重复之前的步骤);若不相同,直接组装结果并返回数据。
      • 单列索引与联合索引

        • 单列索引:即一个索引只包含单个列。

        • 联合索引:即一个索引包含了多个列。

        • 在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。

        • 单列索引情况:

          sql 复制代码
            explain select id, phone, name from tb_user where phone='17799990010' and name='韩信';
          • 执行以上SQL语句的结果:

              • 说明:多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询。
        • 联合索引的结构

            • 说明:对于上图中的联合索引,phone在前,name在后,会优先根据phone进行排序,如果phone相同,再根据name进行排序。
    • 索引设计原则

      • 1、针对于数据量较大,且查询比较频繁的表建立索引。
      • 2、针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
      • 3、尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
      • 4、如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
      • 5、尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
      • 6、要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
      • 7、如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
相关推荐
庞轩px1 小时前
Redis工具类重构——从臃肿到优雅的门面模式实践
数据库·redis·设计模式·重构·门面模式·可扩展性·可维护性
m0_609160491 小时前
SQL如何通过窗口函数简化年度报表逻辑_SQL开发技巧
jvm·数据库·python
m0_733565461 小时前
JavaScript中原型链的查找机制与终点null的意义
jvm·数据库·python
weixin_444012931 小时前
HTML怎么区分正文与广告_HTML aside与广告位语义【技巧】
jvm·数据库·python
zjy277771 小时前
Go语言如何用定时器_Go语言time.Ticker定时器教程【详解】
jvm·数据库·python
CLX05051 小时前
Layui弹出层layer.open如何实现窗口在指定时间后自动最大化
jvm·数据库·python
m0_624578591 小时前
如何在Bootstrap中制作一个响应式的团队介绍页面
jvm·数据库·python
心流时间1 小时前
读书笔记-PostgreSQL实战
数据库·postgresql
X56611 小时前
golang如何实现表单验证_golang表单验证实现方法
jvm·数据库·python