CS-Notes/docs/notes/MySQL.md
2019-06-09 19:28:54 +08:00

429 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- GFM-TOC -->
* [索引](#一索引)
* [B+ Tree 原理](#b-tree-原理)
* [MySQL 索引](#mysql-索引)
* [索引优化](#索引优化)
* [索引的优点](#索引的优点)
* [索引的使用条件](#索引的使用条件)
* [查询性能优化](#二查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [存储引擎](#三存储引擎)
* [InnoDB](#innodb)
* [MyISAM](#myisam)
* [比较](#比较)
* [数据类型](#四数据类型)
* [整型](#整型)
* [浮点数](#浮点数)
* [字符串](#字符串)
* [时间和日期](#时间和日期)
* [切分](#五切分)
* [水平切分](#水平切分)
* [垂直切分](#垂直切分)
* [Sharding 策略](#sharding-策略)
* [Sharding 存在的问题](#sharding-存在的问题)
* [复制](#六复制)
* [主从复制](#主从复制)
* [读写分离](#读写分离)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 索引
## B+ Tree 原理
### 1. 数据结构
B Tree 指的是 Balance Tree也就是平衡树平衡树是一颗查找树并且所有叶子节点位于同一层
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现它具有 B Tree 的平衡性并且通过顺序访问指针来提高区间查询的性能
B+ Tree 一个节点中的 key 从左到右非递减排列如果某个指针的左右相邻 key 分别是 key<sub>i</sub> key<sub>i+1</sub>且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/33576849-9275-47bb-ada7-8ded5f5e7c73.png" width="350px"> </div><br>
### 2. 操作
进行查找操作时首先在根节点进行二分查找找到一个 key 所在的指针然后递归地在指针所指向的节点进行查找直到查找到叶子节点然后在叶子节点上进行二分查找找出 key 所对应的 data
插入删除操作会破坏平衡树的平衡性因此在插入删除操作之后需要对树进行一个分裂合并旋转等操作来维护平衡性
### 3. 与红黑树的比较
红黑树等平衡树也可以用来实现索引但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构主要有以下两个原因
更少的查找次数
平衡树查找操作的时间复杂度和树高 h 相关O(h)=O(log<sub>d</sub>N)其中 d 为每个节点的出度
红黑树的出度为 2 B+ Tree 的出度一般都非常大所以红黑树的树高 h 很明显比 B+ Tree 大非常多查找的次数也就更多
利用磁盘预读特性
为了减少磁盘 I/O 操作磁盘往往不是严格按需读取而是每次都会预读预读过程中磁盘进行顺序读取顺序读取不需要进行磁盘寻道并且只需要很短的磁盘旋转时间速度会非常快
操作系统一般将内存和磁盘分割成固定大小的块每一块称为一页内存与磁盘以页为单位交换数据数据库系统将索引的一个节点的大小设置为页的大小使得一次 I/O 就能完全载入一个节点并且可以利用预读特性相邻的节点也能够被预先载入
## MySQL 索引
索引是在存储引擎层实现的而不是在服务器层实现的所以不同存储引擎具有不同的索引类型和实现
### 1. B+Tree 索引
是大多数 MySQL 存储引擎的默认索引类型
因为不再需要进行全表扫描只需要对树进行搜索即可所以查找速度快很多
因为 B+ Tree 的有序性所以除了用于查找还可以用于排序和分组
可以指定多个列作为索引列多个索引列共同组成键
适用于全键值键值范围和键前缀查找其中键前缀查找只适用于最左前缀查找如果不是按照索引列的顺序进行查找则无法使用索引
InnoDB B+Tree 索引分为主索引和辅助索引主索引的叶子节点 data 域记录着完整的数据记录这种索引方式被称为聚簇索引因为无法把数据行存放在两个不同的地方所以一个表只能有一个聚簇索引
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/45016e98-6879-4709-8569-262b2d6d60b9.png" width="350px"> </div><br>
辅助索引的叶子节点的 data 域记录着主键的值因此在使用辅助索引进行查找时需要先查找到主键值然后再到主索引中进行查找
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7c349b91-050b-4d72-a7f8-ec86320307ea.png" width="350px"> </div><br>
### 2. 哈希索引
哈希索引能以 O(1) 时间进行查找但是失去了有序性
- 无法用于排序与分组
- 只支持精确查找无法用于部分查找和范围查找
InnoDB 存储引擎有一个特殊的功能叫自适应哈希索引当某个索引值被使用的非常频繁时会在 B+Tree 索引之上再创建一个哈希索引这样就让 B+Tree 索引具有哈希索引的一些优点比如快速的哈希查找
### 3. 全文索引
MyISAM 存储引擎支持全文索引用于查找文本中的关键词而不是直接比较是否相等
查找条件使用 MATCH AGAINST而不是普通的 WHERE
全文索引使用倒排索引实现它记录着关键词到其所在文档的映射
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引
### 4. 空间数据索引
MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数据存储空间数据索引会从所有维度来索引数据可以有效地使用任意维度来进行组合查询
必须使用 GIS 相关的函数来维护数据
## 索引优化
### 1. 独立的列
在进行查询时索引列不能是表达式的一部分也不能是函数的参数否则无法使用索引
例如下面的查询不能使用 actor_id 列的索引
```sql
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
```
### 2. 多列索引
在需要使用多个列作为条件进行查询时使用多列索引比使用多个单列索引性能更好例如下面的语句中最好把 actor_id film_id 设置为多列索引
```sql
SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;
```
### 3. 索引列的顺序
让选择性最强的索引列放在前面
索引的选择性是指不重复的索引值和记录总数的比值最大值为 1此时每个记录都有唯一的索引与其对应选择性越高每个记录的区分度越高查询效率也越高
例如下面显示的结果中 customer_id 的选择性比 staff_id 更高因此最好把 customer_id 列放在多列索引的前面
```sql
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
```
```html
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049
```
### 4. 前缀索引
对于 BLOBTEXT VARCHAR 类型的列必须使用前缀索引只索引开始的部分字符
前缀长度的选取需要根据索引选择性来确定
### 5. 覆盖索引
索引包含所有需要查询的字段的值
具有以下优点
- 索引通常远小于数据行的大小只读取索引能大大减少数据访问量
- 一些存储引擎例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存因此只访问索引可以不使用系统调用通常比较费时
- 对于 InnoDB 引擎若辅助索引能够覆盖查询则无需访问主索引
## 索引的优点
- 大大减少了服务器需要扫描的数据行数
- 帮助服务器避免进行排序和分组以及避免创建临时表B+Tree 索引是有序的可以用于 ORDER BY GROUP BY 操作临时表主要是在排序和分组过程中创建不需要排序和分组也就不需要创建临时表
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的会将相邻的数据都存储在一起
## 索引的使用条件
- 对于非常小的表大部分情况下简单的全表扫描比建立索引更高效
- 对于中到大型的表索引就非常有效
- 但是对于特大型的表建立和维护索引的代价将会随之增长这种情况下需要用到一种技术可以直接区分出需要查询的一组数据而不是一条记录一条记录地匹配例如可以使用分区技术
# 查询性能优化
## 使用 Explain 进行分析
Explain 用来分析 SELECT 查询语句开发人员可以通过分析 Explain 结果来优化查询语句
比较重要的字段有
- select_type : 查询类型有简单查询联合查询子查询等
- key : 使用的索引
- rows : 扫描的行数
## 优化数据访问
### 1. 减少请求的数据量
- 只返回必要的列最好不要使用 SELECT * 语句
- 只返回必要的行使用 LIMIT 语句来限制返回的数据
- 缓存重复查询的数据使用缓存可以避免在数据库中进行查询特别在要查询的数据经常被重复查询时缓存带来的查询性能提升将会是非常明显的
### 2. 减少服务器端扫描的行数
最有效的方式是使用索引来覆盖查询
## 重构查询方式
### 1. 切分大查询
一个大查询如果一次性执行的话可能一次锁住很多数据占满整个事务日志耗尽系统资源阻塞很多小的但重要的查询
```sql
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
```
```sql
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
```
### 2. 分解大连接查询
将一个大连接查询分解成对每一个表进行一次单表查询然后在应用程序中进行关联这样做的好处有
- 让缓存更高效对于连接查询如果其中一个表发生变化那么整个查询缓存就无法使用而分解后的多个查询即使其中一个表发生变化对其它表的查询缓存依然可以使用
- 分解成多个单表查询这些单表查询的缓存结果更可能被其它查询使用到从而减少冗余记录的查询
- 减少锁竞争
- 在应用层进行连接可以更容易对数据库进行拆分从而更容易做到高性能和可伸缩
- 查询本身效率也可能会有所提升例如下面的例子中使用 IN() 代替连接查询可以让 MySQL 按照 ID 顺序进行查询这可能比随机的连接要更高效
```sql
SELECT * FROM tab
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
```
```sql
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
# 存储引擎
## InnoDB
MySQL 默认的事务型存储引擎只有在需要它不支持的特性时才考虑使用其它存储引擎
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁Next-Key Locking防止幻影读
主索引是聚簇索引在索引中保存了数据从而避免直接读取磁盘因此对查询性能有很大的提升
内部做了很多优化包括从磁盘读取数据时采用的可预测性读能够加快读操作并且自动创建的自适应哈希索引能够加速插入操作的插入缓冲区等
支持真正的在线热备份其它存储引擎不支持在线热备份要获取一致性视图需要停止对所有表的写入而在读写混合场景中停止写入可能也意味着停止读取
## MyISAM
设计简单数据以紧密格式存储对于只读数据或者表比较小可以容忍修复操作则依然可以使用它
提供了大量的特性包括压缩表空间数据索引等
不支持事务
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
可以手工或者自动执行检查和修复操作但是和事务恢复以及崩溃恢复不同可能导致一些数据丢失而且修复操作是非常慢的
如果指定了 DELAY_KEY_WRITE 选项在每次修改执行完成时不会立即将修改的索引数据写入磁盘而是会写到内存中的键缓冲区只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘这种方式可以极大的提升写入性能但是在数据库或者主机崩溃时会造成索引损坏需要执行修复操作
## 比较
- 事务InnoDB 是事务型的可以使用 Commit Rollback 语句
- 并发MyISAM 只支持表级锁 InnoDB 还支持行级锁
- 外键InnoDB 支持外键
- 备份InnoDB 支持在线热备份
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多而且恢复的速度也更慢
- 其它特性MyISAM 支持压缩表和空间数据索引
# 数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间一般情况下越小的列越好
INT(11) 中的数字只是规定了交互工具显示字符的个数对于存储和计算来说是没有意义的
## 浮点数
FLOAT DOUBLE 为浮点类型DECIMAL 为高精度小数类型CPU 原生支持浮点运算但是不支持 DECIMAl 类型的计算因此 DECIMAL 的计算比浮点类型需要更高的代价
FLOATDOUBLE DECIMAL 都可以指定列宽例如 DECIMAL(18, 9) 表示总共 18 9 位存储小数部分剩下 9 位存储整数部分
## 字符串
主要有 CHAR VARCHAR 两种类型一种是定长的一种是变长的
VARCHAR 这种变长类型能够节省空间因为只需要存储必要的内容但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作MyISAM 会将行拆成不同的片段存储 InnoDB 则需要分裂页来使行放进页内
在进行存储和检索时会保留 VARCHAR 末尾的空格而会删除 CHAR 末尾的空格
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATETIME TIMESTAMP
### 1. DATETIME
能够保存从 1000 年到 9999 年的日期和时间精度为秒使用 8 字节的存储空间
它与时区无关
默认情况下MySQL 以一种可排序的无歧义的格式显示 DATETIME 例如2008-01-16 22<span>:</span>37<span>:</span>08这是 ANSI 标准定义的日期和时间表示方法
### 2. TIMESTAMP
UNIX 时间戳相同保存从 1970 1 1 日午夜格林威治时间以来的秒数使用 4 个字节只能表示从 1970 年到 2038
它和时区有关也就是说一个时间戳在不同的时区所代表的具体时间是不同的
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳
默认情况下如果插入时没有指定 TIMESTAMP 列的值会将这个值设置为当前时间
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高
# 切分
## 水平切分
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中
当一个表的数据不断增多时Sharding 是必然的选择它可以将数据分布到集群的不同节点上从而缓存单个数据库的压力
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg" width=""> </div><br>
## 垂直切分
垂直切分是将一张表按列切分成多个表通常是按照列的关系密集程度进行切分也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中例如将原来的电商数据库垂直切分成商品数据库用户数据库等
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg" width=""> </div><br>
## Sharding 策略
- 哈希取模hash(key) % N
- 范围可以是 ID 范围也可以是时间范围
- 映射表使用单独的一个数据库来存储映射关系
## Sharding 存在的问题
### 1. 事务问题
使用分布式事务来解决比如 XA 接口
### 2. 连接
可以将原来的连接分解成多个单表查询然后在用户程序中进行连接
### 3. ID 唯一性
- 使用全局唯一 IDGUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 ( Twitter Snowflake 算法)
# 复制
## 主从复制
主要涉及三个线程binlog 线程I/O 线程和 SQL 线程
- **binlog 线程** 负责将主服务器上的数据更改写入二进制日志Binary log
- **I/O 线程** 负责从主服务器上读取二进制日志并写入从服务器的中继日志Relay log
- **SQL 线程** 负责读取中继日志解析出主服务器已经执行的数据更改并在从服务器中重放Replay
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/master-slave.png" width=""> </div><br>
## 读写分离
主服务器处理写操作以及实时性要求比较高的读操作而从服务器处理读操作
读写分离能提高性能的原因在于
- 主从服务器负责各自的读和写极大程度缓解了锁的争用
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余提高可用性
读写分离常用代理方式来实现代理服务器接收应用层传来的读写请求然后决定转发到哪个服务器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/master-slave-proxy.png" width=""> </div><br>
# 参考资料
- BaronScbwartz, PeterZaitsev, VadimTkacbenko, . 高性能 MySQL[M]. 电子工业出版社, 2013.
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
- [20+ MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策")
- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)
- [SQL Azure Federation Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.")
- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
- [B + ](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91)
<img width="650px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/other/公众号海报1.png"></img>