MySQL学习 - 索引

Balance-Tree & Balance+Tree 

为何索引这么快,一个好的索引能将检索速度提高几个量级,这种效率离不开这个数据结构node


1.1 门路清

为何须要"索引" ? 

咱们总得依据什么才能去找你想查的东西,那么咱们就依据 id=1去寻找一条记录,怎么找呢? 难道是"顺序检索" ?  sql

数据库里的东西如今大都大到分布在不少个磁盘页上。若是顺序检索基本就是噩梦。你知道"二分法" 比较快,那是由于数据已经被排过序了。 一样的若是如今存在一种排过序的数据结构,使得咱们能快速去找出咱们想要的东西在哪儿,这样必定很方便对吧。数据库

因此归根到底,如今最大的问题的就是"这条记录到底存在哪"。 解决办法是这样的: 
  • 依据这个键好比ID,去产生一个序号,这样就组成了 Key[序号]-Value[记录的位置] 的形式
  • 这种K-V结构,会变成数据结构的一个节点。到时候咱们拿着ID,计算出序号,根据这个序号,在这种数据结构里畅游快速找到这个节点
  • 找到对应节点后查找出记录的位置,去内存里取,完成" select * from users where id = 1"


为何叫它"索引" ? 

在上面的介绍中咱们已经看到了,实现快速找到内容的关键就是这个 "序号" 。这个所谓的序号就很是像"索引"这个名词bash


为何不用二分法查找?

理论上,二分法已是 O(logN), 很是快了。 实际上索引可能不少,多到你都没办法把索引所有加载到内存里,因此这些索引基本上都在磁盘里,依靠磁盘IO,分批次加载到内存里。数据结构

既然提到磁盘IO,就应该知道磁盘IO效率很是低,低到实际上就,若是谁能减小磁盘IO次数,谁就会是最好的索引实现方案。优化

  • 二分法一次两个节点
    • 整个树很是的 "瘦高"  -> 
    • 咱们须要向下遍历不少层 -> 
    • 咱们须要向下加载不少次磁盘IO -> 效率不高
  • 咱们想要把树变的矮胖,减小层数



1.2 Balance-Tree


1.2.1 Balance-Tree中的节点为何是这样子 ?    查询数据的过程ui

  • 一个节点中会包含不少个键值对, 以及不少个 "指针p" 咱们以 { 2 6 } 节点举例看里面有什么
  • 节点内部是这样安排的    {    [p1]     [序号=2]     [p2]     [序号=6]     [p3]      }
    • 假设一个如今 序号[6] 来了,正好能匹配上,前往6对应位置取回数据
    • 假设一个如今 序号[3] 来了,[3] 不匹配任何,可是位于2~6之间,前往p2继续寻找
    • 假设一个如今 序号[1] 来了,[1] 不匹配任何,且小于2,前往p1继续寻找
  • 节点内的全部元素都已经排过序了,所以拿着[序号]来节点里查找的时候效率也比较高

刚刚,咱们就走完了一个节点,咱们完成了一轮查找。每一轮开始以前咱们会先执行磁盘IO,把下一个节点对应内容从磁盘加载到内存里,而后再在组内查找,找获得就退出,找不到继续spa

根据上述结论,咱们也能发现一个重要结论: 既然咱们不能一次性把全部索引都加载到内存里,既然咱们要分批次作磁盘IO。那树的高度其实就是咱们IO的次数,那么矮树就会是最快的方案3d

func balance_tree_search (node *Node , key int) (*Data,error) {

    if node == nil {
        return nil,fmt.Errorf("最终也没能找到对应序号")
    }

    for _, pair := range node {

        if (pair.Key == key){
            return pair.Data,nil
        }

        if (pair.Key > key) {
            // 考虑到节点内键值对是已经排序,从小到大的
            // 那既然上个键值对不知足,这个键值对又过于大
            // 那说明没有符合的,前往下一个节点
            return balance_tree_search( pair.NextNode, key )
        }
    }

    // 比节点内全部键值对都大,直接前往下一个
    return balance_tree_search( pair.NextNode, key )
}  
复制代码


1.2.2 Balance-Tree的插入过程指针



  • 如今是这样,由于BT不能容许一个节点里的有过多的键值对,所以当 [序号4] 过来的时候
  • 实际节点 { 3 5 } 不能容纳 [序号4] 
  • 若是不能容纳,那么咱们会一路向上找到能容纳的下的节点,因而咱们找到根节点
  • 相对应的,根节点变成两个,多出一个位置用于存放指针,相对应的树结构作出调整
  •  {   [p1]  [序号=4]  [p2]   }                   ->                 {   [p1] [序号=4] [p2] [序号=9] [p3]   }

自调整的过程虽然很漫长,看起来也很麻烦,可是这个刚好是知足了BT的自调整性质


1.2.3 Balance-Tree的删除过程



  • 假设咱们如今想要删除这个红色的节点,可是删除后 [序号12] 左边会缺乏一个位置
  • 因而咱们如今须要调整一下,旋转一下,成为知足要求的Balance-Tree


1.2.4 Balance-Tree的定义

  • 依据自旋转过程,保证树都是同样高的,m (也叫秩m)
  • 每一个节点上元素的数量  [m/2 ~ m]
  • 节点上元素须要排序


1.3 Balance+Tree


  • 在B+T中,中间节点并不会存储任何跟"数据在那儿"相关的信息,只会作数据索引,指引你前往叶子节点。 
    • 不管怎样你都要遍历一下叶子节点,比BT更加稳定
    • 不须要前往中间节点,遍历所有数据的时候比BT快
  • B+T全部节点都会按顺序保存好数据,而且全部叶子节点都是串在一块儿的,这样当数据来的时候咱们只要按照粉色的路径一路寻找就行了



关于索引,你须要知道


2.1 多个列,组合索引

  • 在你 create table 的时候假设你定义出 A+B+C 成为你的组合索引,你须要知道这三列如今开始是不等价的,实际上这里面只有第一列 A 重量级最高
    • 在上面咱们提到了[序号],那么 A+B+C 如今就是这条数据的序号,在进行索引比较的时候会 先比较A,A相同了比较B,B相同了再去比较C
      • 多说一句,咱们不喜欢使用字符串做为索引的缘由,是由于"字符串比较" 会比 "整数比较" 开销更大,那种很长的字符串比较就更是麻烦了
    • A重量级最高:  若是你想真的从索引中受益,那么你的WHERE筛选条件中必定要带上这个重量级最高的A,不然索引没有真的发挥做用,举个例子,你能够这么使用索引
      • WHERE A=1                            (首列精确匹配)
      • WHERE A IS LIKE 1                  (首列的近似匹配)
      • WHERE A=1 AND B IS LIKE 2  (首列精确,二列近似)
    • 若是你尝试在筛选条件里不带上A,那么本次查找索引就根本没有发挥做用
  • 咱们都知道 BT&B+T 能拥有一个很良好的"排序性质",咱们既然能按照排序的方式快速找到一个键值对,那么在BT&B+T为基础的索引上
    • 完成 ORDER BY 很快
    • 完成范围查找很快 ->  B IS IN ( 20,100 )


2.2 哈希索引

假设咱们定义出A+B+C做为索引列,哈希索引就是针对每一条记录计算出hash(A,B,C) 对应的值是这条记录存储的位置,哈希索引很是快,可是也有自身对应的一些弊端

  • 由于已经没有排序结构了,所以ORDER BY 功能已经没有那么快了
  • 哈希功能须要全部索引列参与,你不能在只有B&C的状况下去期望使用上哈希索引

2.2.1 哈希冲突

假设如今两条记录能哈希出同一个值,这种时候:

// 若是只依赖hash 则返回两条记录
SELECT * FROM users WHERE hash(name) = 1;
>> liangxiaohan 23 M
   zhangxiaoming 24 F

// 最好的办法是不只使用hash同时也指定索引列自身的值
// hash冲突下,造成链表,存储引擎遍历链表全部行
SELECT * FROM users WHERE hash(name) = 1 and name = "liangxiaohan"
>> liangxiaohan 23 M
复制代码

2.2.2 自创索引

InnoDB支持哈希,但他的支持是指,它会自优化你的B树索引成为"某种程度上的"哈希索引。针对这一点,你能够本身实现一个简单的哈希索引

// 更新表,新建一列用于存放哈希值
ALTER TABLE ADD COLUMN name_crc VARCHAR(20)

// 关于哈希值,你可使用 TRIGGER 实现自动插入
// 你只负责插入name就好了,关于crc32哈希值它每次会本身计算
CREATE TRIGGER crc_create BEFORE INSERT ON users 
FOR EACH ROW SET NEW.name_crc = crc32(NEW.name)复制代码


2.3 聚簇索引

2.3.1 关于聚簇索引,你须要知道

  • 聚簇索引 并非一种新型的索引,它所表明的只是一种数据存储方式
  • 在聚簇索引,至关于一种变形B+T
    • 它的中间节点一样不负责存储任何数据
    • 它的叶子节点会存下这条记录的全部内容实体,而不是一个指针
    • 叶子节点首位相接
  • 一个表只能有一个聚簇索引:按照聚簇索引的要求,数据会被存在一个指定的位置。可是数据不能既在这里又在哪里,因此只能有一个聚簇索引
  • 它之因此被叫作“聚簇” : 是由于按照索引的要求,数据全都被存储在了指定位置,而且索引上相邻的位置,他们也存储的很近

2.3.2 聚簇索引的优势 & 缺点

  • 优势
    • "聚簇"可能能够发挥出很大的威力: 假设咱们令用户ID成为聚簇索引,那么这个用户可能全部相关的信息全存在一块儿,咱们有可能存在一次IO读取全部邮件
    • 访问更快:  聚簇索引将索引+数据全都保存在同一个BT中,读数据一般更快
  • 缺点
    • 假设一个应用,像是读邮件这种应用,本质上是IO密集应用,若是这种时候咱们能好好利用聚簇索引,效果卓群。可是若是数据全都存在内存中,并不涉及磁盘IO那就没有多少优点了
    • 最大的问题:  执行插入的速度 。  由于在聚簇索引中,全部的数据已是直接存在B+T中而且排序了,那么问题就来了
      • 若是我按照聚簇索引顺序插入 -> 速度很快
      • 若是我随机插入 -> 涉及数据的复制移动等,速度感人
    • 仍是关于聚簇的随机插入: 页分裂。 假设如今一个内存页已经填满了数据,可是如今有一个数据试图在中间插入,存储引擎会将这一页分裂成两页,将第一页的一部分复制到第二页去
      • 效率极低
      • 产生内存碎片
      • 由于内存不连续致使扫描全表变慢
    • 若是真的要随机插入,记得在插入完成之后使用 OPTIOMIZE TABLE 从新整理内存


上手试试? 

1. 一个查询主页很是慢的例子

背景: 获取主页面, 查询前10条记录,耗时2~3秒, 被描述为" 不可忍受"的时间

1.1 咱们目前使用了那些数据库索引?

MySQL[user] > SHOW INDEX FROM jobs;

*************************** 1. row ***************************
        Table: jobs
   Non_unique: 0
     Key_name: PRIMARY     <主键索引>
 Seq_in_index: 1
  Column_name: id
    Collation: A
  Cardinality: 13701
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
*************************** 2. row ***************************
        Table: jobs
   Non_unique: 1
     Key_name: userId      <惟一索引>
 Seq_in_index: 1
  Column_name: user_id
    Collation: A           
  Cardinality: 105
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE复制代码
  • 目前存在两只索引,一只主键索引  一只普通惟一索引,区别:
    • 主键本质上是汇集索引,惟一索引做为一种冗余数据结构由数据库维护 更多
  • Collation = A 说明是升序储存
  • Packed = NULL 说明键未被压缩 <更多字段解释>


1.2 目前语句的查询状况是怎样的? 

MySQL [compile]> 

EXPLAIN SELECT COUNT(j.id) 
FROM 
    (SELECT * FROM jobs WHERE user_id = 123 AND deleted_at is NULL) j 
LEFT JOIN 
    (SELECT * FROM builds WHERE deleted_at is NULL) b 
ON 
    b.id = j.latest_build_id \G

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2
        Extra: NULL
*************************** 2. row *************************** 
           id: 1
  select_type: PRIMARY
        table: <derived3>
         type: ref
possible_keys: <auto_key0>
          key: <auto_key0>
      key_len: 4
          ref: j.latest_build_id
         rows: 1187
        Extra: NULL
*************************** 3. row ***************************
           id: 3
  select_type: DERIVED     -- 内嵌表
        table: builds      -- 表名
         type: ALL         -- 全表扫描,效率 ALL < index < range < ref < const
possible_keys: NULL        -- 可能用到的索引
          key: NULL        -- 实际用到的索引
      key_len: NULL
          ref: NULL
         rows: 118713      -- 预期扫描行数
        Extra: Using where -- 使用Where作过滤,效率 filesort < temp < where < index
*************************** 4. row ***************************
           id: 2
  select_type: DERIVED
        table: jobs
         type: ref
possible_keys: userId
          key: userId
      key_len: 5
          ref: const
         rows: 1
        Extra: Using where

MySQL [compile]> SELECT COUNT(j.id) FROM (SELECT * FROM jobs WHERE user_id = 4 AND deleted_at is NULL) j LEFT JOIN (SELECT * FROM builds WHERE deleted_at is NULL) b ON b.id = j.latest_build_id;

+-------------+
| COUNT(j.id) |
+-------------+
|        1280 |
+-------------+
1 row in set (0.93 sec)

复制代码
  • 顺序由下向上,也就是说咱们先生成了表4, 表3 ,随后才用上了表2 表1
  • 关于每个字段的含义:   这里
  • 光是在数据库里执行就消耗1280行,真是难以忍受的时间


1.3 查询成本估计解读

  • 产生表4 :  预计扫描 1 行     ->   假设产生 a 行
  • 产生表3 :  预期扫描 118713 行  ->  假设产生 b 行
  • LEFT-JOIN 3&4 :  预计扫描 a * b = 1187 行  ->  假设产生 c 行
  • SELECT COUNT(j.id) 预计扫描 2 行
  • 【预测】总共扫描行数  1 + 118713 + 1187 + 2
相关文章
相关标签/搜索