首页 > 技术文章 > <MySQL>索引原理

shuimohei 2020-07-26 15:56 原文

  • 索引介绍

    • 需求:一般的应用系统,读写比例大概在10:1左右,急需优化读操作,MySQL里的读操作表现形式就是查询语句,优化查询语句就是目的。
    • 索引:相当于图书的目录,可以帮助用户快速的找到需要的内容。
    • 本质:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。
  • 索引方法 

    • B+树索引:由平衡树二叉查找树结合产生的一种平衡查找树。
      • 特点:所有的记录节点都是按键值大小顺序存放在同一层的叶节点中,叶节点间用指针相连,构成双向循环链表,非叶节点(根节点、枝节点)只存放键值,不存放实际数据。
      • 例子
      • 查找过程:
        • 小知识:系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一磁盘块中的数据会被一次性读取出来,而不是按需读取。InnoDB 存储引擎使用页作为数据读取单位,页是其磁盘管理的最小单位,默认 page 大小是 16kB。
        • 目标:查找数字30
          • 1.首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定30在28和65之间,锁定磁盘块1的P2指针,内存时间非常短(相比磁盘的IO)可以忽略不计。
          • 2.通过磁盘块1的P2指针的磁盘地址把磁盘块由磁盘加载到内存,发生第二次IO,30在28和35之间,锁定当前磁盘块的P1指针。
          • 3.通过指针加载磁盘块到内存,发生第三次IO,同时内存中做二分查找找到30,结束查询,总计三次IO。   
        • 真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
    • HASH 索引
      • hash就是特殊形式的键值对,允许多个key对应相同的value,但不允许一个key对应多个value。
      • 索引建立方式
        • 为某一列或几列建立hash索引,就会利用这一列或几列的值通过一定的算法计算出一个hash值,对应一行或几行数据. 
        •  hash索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率.
  • 索引类型 

    • 普通索引
      • 功能:加速查询
      • 创建表,表中添加索引
        • #创建表同时添加name字段为普通索引
          create table tb1(
             id int not null auto_increment primary key,
             name varchar(100) not null,
             index idx_name(name)  
          );
      • 单独创建索引
        • create index idx_name on tb1(name);
      • 删除索引
        • drop index idx_name on tb1;
          
      •  查看索引 
        • show index from tb1;
    • 唯一索引
      • 功能:加速查询 和 唯一约束(不可含null)
      • 表中创建唯一索引
        • create table tb2(
            id int not null auto_increment primary key,
            name varchar(50) not null,
            age int not null,
            unique index idx_age (age)   
          )
      • 语句创建唯一索引
        • create unique index idx_age on tb2(age);
    • 主键索引:一个表中最多只能有一个主键索引
      • 功能:加速查询 和 唯一约束(不可含null) 
      • 表中创建主键
        • #方式一:
          create table tb3(
             id int not null auto_increment primary key,
             name varchar(50) not null,
             age int default 0 
          );
          
          #方式二:
          create table tb3(
             id int not null auto_increment,
             name varchar(50) not null,
             age int default 0 ,
             primary key(id)
          );
      • 单独创建
        • alter table tb3 add primary key(id);
      • 删除主键
        • #方式一
          alter table tb3 drop primary key;
          
          #方式二:
          #如果当前主键为自增主键,则不能直接删除.需要先修改自增属性,再删除
          
          alter table tb3 modify id int ,drop primary key;
    • 组合索引:组合索引是将n个列组合成一个索引
      • 应用场景::频繁的同时使用n列来进行查询,如:where n1 = 'alex' and n2 = 666。
      • 表中创建组合索引
        • create table tb4(
            id int not null ,
            name varchar(50) not null,
            age int not null,
            index idx_name_age (name,age)   
          )
      • 单独创建
        • create index idx_name_age on tb4(name,age);
    • 索引应用场景
      • 举个例子来说,比如你在为某商场做一个会员卡的系统。
        
        这个系统有一个会员表
        有下列字段:
        会员编号 INT
        会员姓名 VARCHAR(10)
        会员身份证号码 VARCHAR(18)
        会员电话 VARCHAR(10)
        会员住址 VARCHAR(50)
        会员备注信息 TEXT
        
        那么这个 会员编号,作为主键,使用 PRIMARY
        会员姓名 如果要建索引的话,那么就是普通的 INDEX
        会员身份证号码 如果要建索引的话,那么可以选择 UNIQUE (唯一的,不允许重复)
        
  • 聚合索引和辅助索引

    • 数据库中的B+树索引可以分为聚集索引和辅助索引. 

      • 聚集索引:表中数据按主键B+树存放,叶子节点直接存放整条数据,每张表只能有一个聚集索引。

        • 当你定义一个主键时,InnnodDB存储引擎则把它当做聚集索引。

        • 如果你没有定义一个主键,则InnoDB定位到第一个唯一索引,且该索引的所有列值均飞空的,则将其当做聚集索引。

        • 如果表没有主键或合适的唯一索引INNODB会产生一个隐藏的行ID值6字节的行ID聚集索引。 

      • 辅助索引:(也称非聚集索引)是指叶节点不包含行的全部数据,叶节点除了包含键值之外,还包含一个书签连接,通过该书签再去找相应的行数据。

    • 何时使用聚集索引或非聚集索引

  • 测试索引  

    1. 创建表

      • CREATE TABLE userInfo(
            id int NOT NULL,
            name VARCHAR(16) DEFAULT NULL,
            age int,
            sex char(1) not null,
            email varchar(64) default null
        )ENGINE=MYISAM DEFAULT CHARSET=utf8;
    2. 创建存储过程,插入数据

      • delimiter$$
        CREATE PROCEDURE insert_user_info(IN num INT)
        BEGIN
            DECLARE val INT DEFAULT 0;
            DECLARE n INT DEFAULT 1;
            -- 循环进行数据插入
            WHILE n <= num DO
                set val = rand()*50;
                INSERT INTO userInfo(id,name,age,sex,email)values(n,concat('alex',val),rand()*50,if(val%2=0,'女','男'),concat('alex',n,'@qq.com'));
                set n=n+1;
            end while;
        END $$
        delimiter;
    3. 调用存储过程,插入500万条数据

      • call insert_user_info(5000000);
        
    4. 修改引擎为INNODB

      • ALTER TABLE userinfo ENGINE=INNODB;
    5. 测试索引

      1. 没索引查询速度

        • SELECT * FROM userinfo WHERE id = 4567890;
        • 注意:无索引情况,mysql根本就不知道id等于4567890的记录在哪里,只能把数据表从头到尾扫描一遍,此时有多少个磁盘块就需要进行多少IO操作,所以查询速度很慢.

      2. 在表中已经存在大量数据的前提下,为某个字段段建立索引,建立速度会很慢  
        • CREATE INDEX idx_id on userinfo(id);
      3. 在索引建立完毕后,以该字段为查询条件时,查询速度提升明显
        • select * from userinfo where id  = 4567890;
      4. 注意
        • 1.  mysql先去索引表里根据b+树的搜索原理很快搜索到id为4567890的数据,IO大大降低,因而速度明显提升

          2. 我们可以去mysql的data目录下找到该表,可以看到添加索引后该表占用的硬盘空间多了 

          3.如果使用没有添加索引的字段进行条件查询,速度依旧会很慢(如图:)

  • 正确使用索引 

    • 数据库表中添加索引后确实会让查询速度起飞,但前提必须是正确的使用索引来查询,如果以错误的方式使用,则即使建立索引也会不奏效。即使建立索引,索引也不会生效:  

    • #1. 范围查询(>、>=、<、<=、!= 、between...and)
          #1. = 等号
          select count(*) from userinfo where id = 1000 -- 执行索引,索引效率高
          
          #2. > >= < <= between...and 区间查询
          select count(*) from userinfo where id <100; -- 执行索引,区间范围越小,索引效率越高
          
          select count(*) from userinfo where id >100; -- 执行索引,区间范围越大,索引效率越低
          
          select count(*) from userinfo where id between 10 and 500000; -- 执行索引,区间范围越大,索引效率越低
          
         #3. != 不等于
         select count(*) from userinfo where id != 1000;  -- 索引范围大,索引效率低
         
         
      #2.like '%xx%'
          #为 name 字段添加索引
          create index idx_name on userinfo(name);
          
          select count(*) from userinfo where name like '%xxxx%'; -- 全模糊查询,索引效率低
          select count(*) from userinfo where name like '%xxxx';   -- 以什么结尾模糊查询,索引效率低
        
          #例外: 当like使用以什么开头会索引使用率高
          select * from userinfo where name like 'xxxx%'; 
      
      #3. or 
          select count(*) from userinfo where id = 12334 or email ='xxxx'; -- email不是索引字段,索引此查询全表扫描
          
          #例外:当or条件中有未建立索引的列才失效,以下会走索引
          select count(*) from userinfo where id = 12334 or name = 'alex3'; -- id 和 name 都为索引字段时, or条件也会执行索引
      
      #4.使用函数
          select count(*) from userinfo where reverse(name) = '5xela'; -- name索引字段,使用函数时,索引失效
          
          #例外:索引字段对应的值可以使用函数,我们可以改为一下形式
          select count(*) from userinfo where name = reverse('5xela');
      
      #5.类型不一致
          #如果列是字符串类型,传入条件是必须用引号引起来,不然...
          select count(*) from userinfo where name = 454;
              
          #类型一致
          select count(*) from userinfo where name = '454';
      
      #6.order by
          #排序条件为索引,则select字段必须也是索引字段,否则无法命中  
          select email from userinfo ORDER BY name DESC; -- 无法命中索引
      
          select name from userinfo ORDER BY name DESC;  -- 命中索引
              
          #特别的:如果对主键排序,则还是速度很快:
          select id from userinfo order by id desc;
  • 注意事项

    • 1. 避免使用select *
      2. 其他数据库中使用count(1)或count(列) 代替 count(*),而mysql数据库中count(*)经过优化后,效率与前两种基本一样.
      3. 创建表时尽量时 char 代替 varchar
      4. 表的字段顺序固定长度的字段优先
      5. 组合索引代替多个单列索引(经常使用多个条件查询时)
      6. 使用连接(JOIN)来代替子查询(Sub-Queries)
      7. 不要有超过4个以上的表连接(JOIN)
      8. 优先执行那些能够大量减少结果的连接。
      9. 连表时注意条件类型需一致
      10.索引散列值不适合建索引,例:性别不适合
  • 大数据分页优化  

    • 大数据分页

      • select * from userinfo limit 3000000,10;
    • 优化方案:

      • 一. 简单粗暴,就是不允许查看这么靠后的数据,比如百度就是这样的

        • 最多翻到72页就不让你翻了,这种方式就是从业务上解决;

      • 二.在查询下一页时把上一页的行id作为参数传递给客户端程序,然后sql就改成了

        • select * from userinfo where id>3000000 limit 10;  
        • 这条语句执行也是在毫秒级完成的,id>300w其实就是让mysql直接跳到这里了,不用依次在扫描全面所有的行。

        • 如果你的table的主键id是自增的,并且中间没有删除和断点,那么还有一种方式,比如100页的10条数据

          • select * from userinfo where id>100*10 limit 10;
      • 三.最后第三种方法:延迟关联

        • 我们在来分析一下这条语句为什么慢,慢在哪里。

          • select * from userinfo limit 3000000,10;
        • 玄机就处在这个 * 里面,这个表除了id主键肯定还有其他字段  比如 name  age  之类的,因为select  *  所以mysql在沿着id主键走的时候要回行拿数据,走一下拿一下数据;

        • 如果把语句改成 :

          • select id from userinfo limit 3000000,10;
        • 你会发现时间缩短了一半;然后我们在拿id分别去取10条数据就行了;

        • 语句就改成这样了:

          • select table.* from userinfo inner join ( select id from userinfo limit 3000000,10 ) as tmp on tmp.id=userinfo.id;
      •  这三种方法最先考虑第一种 其次第二种,第三种是别无选择                       

          

                               

                                 

推荐阅读