CloudNativeEra
  • Introduction
  • 名词解释
  • Computer Science
    • Computer Organization
      • CPU
      • 二进制、电路、加法器、乘法器
      • 编译、链接、装载
      • 存储器
      • IO
    • Operating System
      • 操作系统基础知识
      • 系统初始化
      • 进程管理
      • Everything about Memory
      • 文件系统
      • 并行编程
      • Linux
        • CPU
        • IO 多路复用
        • DMA IO and Linux Zero Copy
    • Computer Network
      • 网络相关命令
      • 评估系统的网络性能
      • 网络抓包
      • Linux 最多支撑的 TCP 连接
      • 网络虚拟化
      • DHCP 工作原理
    • Data Structure and Algorithm
      • 题目列表
      • Summarize
        • 方法总结
        • 二分思想
        • 树的演化
        • 算法思想总结
      • Data Structure
        • Data Struct - Array
        • Tree
        • Heap
        • Hash
        • 字符串
      • Algorithm
        • Sorting Algorithm
        • 查找
        • 贪心算法
        • 动态规划
        • 位运算
      • Practice Topics
        • Data Struct in SDK
        • Topic - Tree
        • Topic - Graph
        • Topic - 滑动窗口
        • 剑指 Offer 题解
    • 并发编程
      • 并发模式
      • 并发模型
  • 系统设计
    • 软件设计
      • 软件架构
      • 编程范式
      • 系统设计题
      • 设计原则
      • 计算机程序的构造和解释 SICP
    • 领域驱动设计
      • 应用:在线请假考勤管理
      • 应用: library
    • 微服务与云原生
      • Designing and deploying microservices
      • 容器技术
      • Docker
      • Etcd
      • Kubernetes
        • Kubernetes - Mapping External Services
      • Istio
      • 监控
    • 分布式系统
      • 分布式理论
      • 分布式事务
    • 后端存储设计
      • 缓存设计
      • 数据库架构设计
    • CI/CD
    • 设计最佳实践
    • 测试
    • 安全
    • 综合
      • 开发实践
      • 分布式锁
      • 分布式计数服务
      • 弹幕系统设计
      • 消息队列设计
      • 分布式ID生成算法
      • 限流设计
      • 网关设计
      • 通用的幂等设计
      • 分布式任务调度
        • Timer
        • ScheduledExecutorService
        • Spring Task
        • Quartz
      • 交易系统
      • 权限设计
  • 编程语言
    • 编程语言
    • C & C++
    • Java
      • JVM
        • JVM Bytecode
      • Java 核心技术
      • Java 8 新特性
      • Java 集合框架
      • Java NIO
      • 并发编程
        • 线程生命周期与线程中断
        • 三个线程交替打印
        • 两个线程交替打印奇偶
        • 优雅终止线程
        • 等待通知机制
        • 万能钥匙:管程
        • 限流器
        • 无锁方案 CAS
    • Java 源码阅读
      • Unsafe
      • 异步计算 Future
      • Java Queue
      • CoalescingRingBuffer 分析
      • Java Collections
        • PriorityQueue 分析
        • HashMap 分析
        • TreeMap
    • Golang
    • Python
  • 框架/组件/类库
    • Guava
      • Guava Cache
      • Guava EventBus
    • RxJava
    • Apache MINA
    • Netty
      • 网络 IO 模型
      • Netty 生产问题
    • Apache Tomcat
    • MyBatis
    • 限流框架
    • Spring Framework
      • Spring Core
      • Spring 事务管理
    • Spring Boot
    • Spring Cloud
      • Feign & OpenFeign
      • Ribbon
      • Eurake
      • Spring Cloud Config
    • FixJ
    • Metrics
    • Vert.x
  • 中间件
    • Redis
      • Redis 基础
        • Redis 数据结构设计与实现
        • Redis 高性能网络模型
      • Redis checklist
      • 应用案例 - Redis 数据结构
      • 应用案例 - Redis 缓存应用
      • 应用案例 - Redis 集群
      • Redis 客户端
      • Redis 生产案例
        • [译] 在 Redis 中存储数亿个简单键值对
    • MySQL
      • MySQL 基础
      • MySQL Index
      • MySQL Transaction
      • MySQL 优化
      • MySQL 内核
      • MySQL Command
      • MySQL Checklist
      • MySQL Analysis Tool
      • 实现 MySQL
    • State Machine
    • 数据库连接池
    • MQ
      • 高性能内存队列 Disruptor
      • Kafka
      • Pulsar
      • RocketMQ
        • Broker 的设计与实现
      • NSQ
  • 实际案例
    • 线上 Case
      • Request Aborted
      • MySQL - Specified key was too long
      • Java 应用 CPU 100% 排查优化
      • 频繁 GC 导致的 Java 服务不响应
      • 导出优化
  • 大数据
    • 流计算
    • Flink
  • 其他
    • 工具
    • 读书
      • 设计数据密集型应用
      • 实现领域驱动设计
      • 精通比特币
      • 提问的智慧
    • 论文
    • 工程博客
    • 阅读源码
    • 面试
      • 如何在最短的时间里对对方有个全面的了解
    • 分享
    • 软技能
    • Todo
  • Blog
    • #算法
      • 查找
      • 位运算
      • 树
    • #架构
      • 1- 通信
    • Design & Dev & Opt
      • High Performance Data structure Design
  • Tiny Project
    • A Simple WeChat-like Instant Messaging Platform
由 GitBook 提供支持
在本页
  • B+ Tree
  • 索引选择与优化

这有帮助吗?

  1. 中间件
  2. MySQL

MySQL Index

分析 MySQL 的 InnoDB 存储引擎的索引机制

上一页MySQL 基础下一页MySQL Transaction

最后更新于4年前

这有帮助吗?

索引是一种可以提升查询效率的数据结构(索引的本质是数据结构, 目的是提高查询效率)。

MySQL 的 InnoDB 存储引擎是使用 B+ Tree 来组织数据的,也就是说 B+ Tree 就是 InnoDB 使用的索引数据结构。

先来搞明白一个事情,为什么查询数据要借助数据库和数据库的索引?通常情况下,我们可以使用操作系统提供的 awk / grep 等来查询数据,顺序遍历文件中的每一行数据,直到找到为止;当数据量比较小时,比如几十兆、几百兆甚至上 GB,这种方式也许还可以应付,但是更多的数据量呢?我们会面临一些问题:

  1. 计算机磁盘存在 IO 性能问题,如果把要搜索的数据都加载到内存,是需要时间的

  2. 普通的做法需要对数据做全量扫描,全量意味着随着数据量的增长,查询速度会越来越慢

  3. 一些常识:a、磁盘读取数据要寻址,要花费几ms级别,当然顺序读写就不需要额外的寻址时间;b、 磁盘的IO,也就是吞吐量,一般的磁盘吞吐在几百M每秒;c、 和内存相比,内存的操作时间是 100纳秒左右,差了 10万倍左右,当然ssd硬盘会更快一点,也会相差几万倍

总结一下,普通的原始做法是把数据顺序存储在文件中,等需要的时候,从头到尾全量遍历直到找到满足要求的数据,缺点就是需要做全量扫描的IO操作,而且还不支持丰富多样的过滤查询,一个字“慢“,无法应对日益增长的数据量。而数据库管理系统提供了一种解决方案。

A database needs to do two things: when you give it some data, it should store the data, and when you ask it again later, it should give the data back to you.

-- from << Designing Data-Intensive Applications>>

怎么解决呢?应对海量数据的问题,就是利用分治思想,比如说操作系统管理文件系统的方式,磁盘被分成很多的数据块,每个数据块 4 KB 大小(有的是16 K),一个文件就可以被分成很多个 4 K 大小的数据页,实际的数据就被存放在这些数据页中;但是一般磁盘都非常大,比如说一个 1 TB 的磁盘就会有 2 亿个左右的数据页,维护这么大的数据页就需要借助索引来完成,建立数据页的索引,用来标识哪个数据存在哪个数据页里,索引就是来加速查找的数据结构,索引也是文件,叫索引文件,我们可以建立两级索引,第一级索引在内存中,第二级索引在磁盘上,在查询时,首先获取二级索引,然后根据二级索引加载数据页,最后取出数据。这样即避免了数据的全量IO(因为只加载部分数据),又提高了查询速度。

实质上,InnoDB 的 B+ Tree 索引实现利用了类似的思想:分治 + 多级索引 + 顺序磁盘扫描 + 少量的随机磁盘扫描。

为什么选择使用 B+ Tree 来做数据库存储引擎的索引呢?涉及到:局部性原理,文件系统和磁盘的管理,数据结构的复杂度分析(能够应对高效查询的数据结构有哪些?avl / 红黑树 / hash 表 / 跳表),参考 ;B-Tree 和 B+Tree 是结合了磁盘访问特性和平衡二叉搜索树的满足特定需求的树结构,特别适合做大量数据的存储。

B+ Tree 的定义

  1. 节点中的数据索引从左到右递增排列;

  2. 非叶子节点不存储data,只存储索引(冗余),可以放更多的索引

  3. 叶子节点包含所有索引字段

  4. 叶子节点用指针连接,提高区间访问的性能

InnoDB 如何使用 B+ Tree 来组织数据,又是如何结合磁盘的局部性原理和顺序读写呢?

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

小问题:

  • 为什么 InnoDB 的表必须有主键,并且推荐使用整型的自增主键?(首先非聚簇索引需要引用聚簇索引的id,需要有个主键来被引用;其次,如果没有主键如何组织索引树呢,必须得需要一个载体来组织索引和数据。使用自增主键,是考虑到B+树的特点和局部性原理,叶子节点存放的是数据,如果是随机的值作为主键,在插入数据时有可能会频繁发生页分裂,或页利用率低的问题,即降低了性能也浪费了一定空间; 利用顺序读写效率高于随机读写;)

  • 为什么非主键索引结构叶子节点存储的是主键值?(因为数据一致性的考虑,如果有多份数据拷贝,维护多个索引树之间的数据一致性就变得比较麻烦;还有节省存储空间的考虑)

索引选择与优化

索引分析的两大利器

  • explain 执行计划

结论:

  • InnoDB 以数据页来读取数据,默认 16KB

  • change buffer,是在内存中的一块特殊的内存区域,用来缓存更新操作,目的是提高更新性能(因为change buffer 能够减少数据页的随机 IO,然后利用后台线程定期merge,merge时一般是会尽可能利用数据页的顺序 IO);change buffer 在写多读少的业务效果会更好,比如账单类、日志类的系统;

  1. 尽量使用普通索引,不是说不能使用唯一索引,关键是要思考真的需要唯一索引吗

  2. 如果是写后立即读的场景比较频繁,应该关闭change buffer

  3. 对于非常少出现写后立即读的场景,change buffer 可以提升性能,特别是数据量大的表;对于日志类的这类应用,可以把 change buffer 调大一些

  • 重建索引

-- 重建普通索引
alter table T drop index k;
alter table T add index(k);

-- 重建主键索引
alter table T engine = InnoDB;
  • 索引并不总是最好的工具,只有当索引帮助存储引擎快速查找记录带来的好处大于其带来的额外工作时,索引才有效。对于非常小的表,大部分情况下简单的全表扫描更高效。对于中到大型的表,索引就非常有效。但对于特大型的表,建立和使用索引的代价将随之增长。

  • 尽量在InnoDB上采用自增字段做主键

  • 关于索引最左前缀,我们会建立多列的联合索引

  1. 全列匹配,就是精确查找联合索引中的所有列

  2. 最左前缀匹配(和where条件中列出现的顺序无关),精确匹配联合索引中最左前缀

  3. 查询条件没有指定索引第一列,不会命中索引

  4. 匹配前缀字符串,比如索引中使用了 like 'abc%',可以命中索引,但是 like '%abc', 无法命中索引

  5. 范围查询,可以命中最左前缀的索引,范围后面的列无法命中索引

  6. 查询条件中有函数或表达式无法命中索引,比如 select * from sakila.actor where actor_id + 1 = 5 无法命中索引;还有就是 select * from t where to_days(current_date) - to_days(current_date) <= 10

  • 索引选择性

既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好

索引选择性是指不重复的索引值和数据表总记录数之间的比值,越接近1,选择性越好

  1. 记录数非常少的表可以考虑不建立索引,做全表扫描也没有多大问题,比如经验值 2000

  2. 选择性比较低的列,可以考虑不建索引;选择性是指不重复的索引值,基数和表记录数的比值(Index Selectivity = Cardinality / #T)

  3. 一般情况下,使用前缀索引也是足以满足查询性能的。对于 BLOB、Text和很长的 Varchar 类型,必须使用前缀索引,

一个例子,employees 表

mysql> EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 299556 |     1.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

使用选择性来决定怎么建索引

mysql> select count(distinct(first_name)) / count(1) as Selectivity from employees;
+-------------+
| Selectivity |
+-------------+
|      0.0042 |
+-------------+
1 row in set (0.20 sec)

mysql> select count(distinct(concat(first_name,last_name))) / count(1) as Selectivity from employees;
+-------------+
| Selectivity |
+-------------+
|      0.9313 |
+-------------+
1 row in set (0.60 sec)

first_name 和 last_name 的长度之和为 30,索引长度能不能小一点?

取 last_name 的前缀

mysql> select count(distinct(concat(first_name,left(last_name,3)))) / count(1) as Selectivity from employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+
1 row in set (0.54 sec)
alter table employees add index `idx_first_last4` (`first_name`, last_name(4));

mysql> explain SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys   | key             | key_len | ref         | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------------+------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | ref  | idx_first_last4 | idx_first_last4 | 76      | const,const |    1 |   100.00 | Using where |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

-- 加索引前
mysql> SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  18454 | 1955-02-28 | Eric       | Anido     | M      | 1988-07-18 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.09 sec)

-- 加索引后
mysql> SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  18454 | 1955-02-28 | Eric       | Anido     | M      | 1988-07-18 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.00 sec)
-- profiling 是否开启
mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling     | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

-- 开启
set profiling = 1;

-- 执行sql

-- 查看执行时间
mysql> show profiles;
+----------+------------+---------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                           |
+----------+------------+---------------------------------------------------------------------------------+
|        1 | 0.00047900 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
+----------+------------+---------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

-- 查看某个sql的 cpu/io/memory/swaps/context switch/source 等使用情况
mysql> show profile cpu, block io, memory,swaps,context switches,source for query 1;
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+-------+-----------------------+----------------------+-------------+
| Status               | Duration | CPU_user | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Swaps | Source_function       | Source_file          | Source_line |
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+-------+-----------------------+----------------------+-------------+
| starting             | 0.000087 | 0.000055 |   0.000008 |                 0 |                   0 |            0 |             0 |     0 | NULL                  | NULL                 |        NULL |
| checking permissions | 0.000007 | 0.000004 |   0.000003 |                 0 |                   0 |            0 |             0 |     0 | check_access          | sql_authorization.cc |         809 |
| Opening tables       | 0.000014 | 0.000013 |   0.000001 |                 0 |                   0 |            0 |             0 |     0 | open_tables           | sql_base.cc          |        5781 |
| init                 | 0.000035 | 0.000030 |   0.000006 |                 0 |                   0 |            0 |             0 |     0 | handle_query          | sql_select.cc        |         128 |
| System lock          | 0.000015 | 0.000008 |   0.000005 |                 0 |                   0 |            0 |             0 |     0 | mysql_lock_tables     | lock.cc              |         330 |
| optimizing           | 0.000014 | 0.000011 |   0.000004 |                 0 |                   0 |            0 |             0 |     0 | optimize              | sql_optimizer.cc     |         158 |
| statistics           | 0.000128 | 0.000084 |   0.000036 |                 0 |                   2 |            0 |             0 |     0 | optimize              | sql_optimizer.cc     |         374 |
| preparing            | 0.000017 | 0.000013 |   0.000004 |                 0 |                   0 |            0 |             0 |     0 | optimize              | sql_optimizer.cc     |         482 |
| executing            | 0.000003 | 0.000002 |   0.000002 |                 0 |                   0 |            0 |             0 |     0 | exec                  | sql_executor.cc      |         126 |
| Sending data         | 0.000062 | 0.000048 |   0.000014 |                 0 |                   0 |            0 |             0 |     0 | exec                  | sql_executor.cc      |         202 |
| end                  | 0.000005 | 0.000003 |   0.000002 |                 0 |                   0 |            0 |             0 |     0 | handle_query          | sql_select.cc        |         206 |
| query end            | 0.000007 | 0.000006 |   0.000001 |                 0 |                   0 |            0 |             0 |     0 | mysql_execute_command | sql_parse.cc         |        4956 |
| closing tables       | 0.000007 | 0.000005 |   0.000002 |                 0 |                   0 |            0 |             0 |     0 | mysql_execute_command | sql_parse.cc         |        5009 |
| freeing items        | 0.000040 | 0.000023 |   0.000016 |                 0 |                   0 |            0 |             0 |     0 | mysql_parse           | sql_parse.cc         |        5622 |
| cleaning up          | 0.000038 | 0.000015 |   0.000020 |                 0 |                   1 |            0 |             0 |     0 | dispatch_command      | sql_parse.cc         |        1931 |
+----------------------+----------+----------+------------+-------------------+---------------------+--------------+---------------+-------+-----------------------+----------------------+-------------+
15 rows in set, 1 warning (0.00 sec)

mysql> show profile for query 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000087 |
| checking permissions | 0.000007 |
| Opening tables       | 0.000014 |
| init                 | 0.000035 |
| System lock          | 0.000015 |
| optimizing           | 0.000014 |
| statistics           | 0.000128 |
| preparing            | 0.000017 |
| executing            | 0.000003 |
| Sending data         | 0.000062 |
| end                  | 0.000005 |
| query end            | 0.000007 |
| closing tables       | 0.000007 |
| freeing items        | 0.000040 |
| cleaning up          | 0.000038 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)

参考资料

使用的表和数据是 MySQL 官方提供的

B+ Tree
一般化到特殊化演变的树
employees 示例数据
trace 分析工具
MySQL 索引背后的数据结构和算法原理
MySQL 索引原理及查询优化
关系型数据库索引设计原则
MySQL 与 InnoDB 存储引擎总结
employees的表结构
employees 的索引
使用 first_name 和 last_name 查询
first_name 的选择性
first_name 和 last_name 的选择性