update docs

pull/2/head
Zhang Peng 2020-07-16 11:14:07 +08:00
parent 463bef34f9
commit 64963710e8
24 changed files with 729 additions and 543 deletions

View File

@ -22,29 +22,32 @@
> [关系型数据库](docs/sql) 整理主流关系型数据库知识点。
[关系型数据库面试总结](docs/sql/sql-interview.md) 💯
#### [共性知识](docs/sql/common)
[**SQL Cheat Sheet**](docs/sql/sql-cheat-sheet.md) 是一个 SQL 入门教程。
- [关系型数据库面试总结](docs/sql/common/sql-interview.md) 💯
- [SQL Cheat Sheet](docs/sql/common/sql-cheat-sheet.md) 是一个 SQL 入门教程。
- [分布式存储基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-storage-theory.md)
- [分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
![img](http://dunwu.test.upcdn.net/snap/20200115160512.png)
#### [Mysql](docs/sql/mysql) 📚
#### Mysql
![img](http://dunwu.test.upcdn.net/snap/20200716103611.png)
> [Mysql](docs/sql/mysql) 📚 是互联网最流行的关系型数据库。
- [Mysql 应用指南](docs/sql/mysql/mysql-quickstart.md)
- [Mysql 索引](docs/sql/mysql/mysql-index.md)
- [Mysql 锁](docs/sql/mysql/mysql-lock.md)
- [Mysql 事务](docs/sql/mysql/mysql-transaction.md)
- [Mysql 应用指南](docs/sql/mysql/mysql-quickstart.md) ⚡
- [Mysql 工作流](docs/sql/mysql/mysql-index.md) - 关键词:`连接`、`缓存`、`语法分析`、`优化`、`执行引擎`、`redo log`、`bin log`、`两阶段提交`
- [Mysql 索引](docs/sql/mysql/mysql-index.md) - 关键词:`Hash`、`B 树`、`聚簇索引`、`回表`
- [Mysql 锁](docs/sql/mysql/mysql-lock.md) - 关键词:`乐观锁`、`表级锁`、`行级锁`、`意向锁`、`MVCC`、`Next-key 锁`
- [Mysql 事务](docs/sql/mysql/mysql-transaction.md) - 关键词:`ACID`、`AUTOCOMMIT`、`事务隔离级别`、`死锁`、`分布式事务`
- [Mysql 性能优化](docs/sql/mysql/mysql-optimization.md)
- [Mysql 运维](docs/sql/mysql/mysql-ops.md) 🔨
- [Mysql 配置](docs/sql/mysql/mysql-config.md)
- [Mysql 问题](docs/sql/mysql/mysql-faq.md)
#### 其他关系型数据库
- [H2 入门指南](docs/sql/h2.md)
- [SqLite 入门指南](docs/sql/sqlite.md)
- [PostgreSQL 入门指南](docs/sql/postgresql.md)
- [H2 应用指南](docs/sql/h2.md)
- [SqLite 应用指南](docs/sql/sqlite.md)
- [PostgreSQL 应用指南](docs/sql/postgresql.md)
### Nosql 数据库
@ -52,14 +55,12 @@
- [Nosql 技术选型](docs/nosql/nosql-selection.md)
#### Redis
> [Redis](docs/nosql/redis) 📚
#### [Redis](docs/nosql/redis) 📚
![img](http://dunwu.test.upcdn.net/snap/20200713105627.png)
- [Redis 面试总结](docs/nosql/redis/redis-interview.md) 💯
- [Redis 入门指南](docs/nosql/redis/redis-quickstart.md) ⚡ - 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅`
- [Redis 应用指南](docs/nosql/redis/redis-quickstart.md) ⚡ - 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅`
- [Redis 数据类型和应用](docs/nosql/redis/redis-datatype.md) - 关键词:`STRING`、`HASH`、`LIST`、`SET`、`ZSET`、`BitMap`、`HyperLogLog`、`Geo`
- [Redis 持久化](docs/nosql/redis/redis-persistence.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync`
- [Redis 复制](docs/nosql/redis/redis-replication.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`REPLCONF ACK`
@ -115,7 +116,7 @@
- **书籍**
- [《Redis 实战》](https://item.jd.com/11791607.html)
- [《Redis 设计与实现》](https://item.jd.com/11486101.html)
- 源码
- **源码**
- [《Redis 实战》配套 Python 源码](https://github.com/josiahcarlson/redis-in-action)
- **资源汇总**
- [awesome-redis](https://github.com/JamzyWang/awesome-redis)

Binary file not shown.

View File

@ -23,19 +23,32 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu
> [关系型数据库](sql) 整理主流关系型数据库知识点。
- [关系型数据库面试总结](sql/sql-interview.md) 💯
- [SQL Cheat Sheet](sql/sql-cheat-sheet.md)
- [Mysql](sql/mysql) 📚
- [Mysql 应用指南](sql/mysql/mysql-quickstart.md)
- [Mysql 索引](sql/mysql/mysql-index.md)
- [Mysql 锁](sql/mysql/mysql-lock.md)
- [Mysql 事务](sql/mysql/mysql-transaction.md)
- [Mysql 性能优化](sql/mysql/mysql-optimization.md)
- [Mysql 运维](sql/mysql/mysql-ops.md) 🔨
- [Mysql 配置](sql/mysql/mysql-config.md)
- [H2 入门指南](sql/h2.md)
- [SqLite 入门指南](sql/sqlite.md)
- [PostgreSQL 入门指南](sql/postgresql.md)
#### [共性知识](sql/common)
- [关系型数据库面试总结](sql/common/sql-interview.md) 💯
- [SQL Cheat Sheet](sql/common/sql-cheat-sheet.md) 是一个 SQL 入门教程。
- [分布式存储基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-storage-theory.md)
- [分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
#### [Mysql](sql/mysql) 📚
![img](http://dunwu.test.upcdn.net/snap/20200716103611.png)
- [Mysql 应用指南](sql/mysql/mysql-quickstart.md) ⚡
- [Mysql 工作流](sql/mysql/mysql-index.md) - 关键词:`连接`、`缓存`、`语法分析`、`优化`、`执行引擎`、`redo log`、`bin log`、`两阶段提交`
- [Mysql 索引](sql/mysql/mysql-index.md) - 关键词:`Hash`、`B 树`、`聚簇索引`、`回表`
- [Mysql 锁](sql/mysql/mysql-lock.md) - 关键词:`乐观锁`、`表级锁`、`行级锁`、`意向锁`、`MVCC`、`Next-key 锁`
- [Mysql 事务](sql/mysql/mysql-transaction.md) - 关键词:`ACID`、`AUTOCOMMIT`、`事务隔离级别`、`死锁`、`分布式事务`
- [Mysql 性能优化](sql/mysql/mysql-optimization.md)
- [Mysql 运维](sql/mysql/mysql-ops.md) 🔨
- [Mysql 配置](sql/mysql/mysql-config.md)
- [Mysql 问题](sql/mysql/mysql-faq.md)
#### 其他关系型数据库
- [H2 应用指南](sql/h2.md)
- [SqLite 应用指南](sql/sqlite.md)
- [PostgreSQL 应用指南](sql/postgresql.md)
### Nosql 数据库
@ -43,14 +56,12 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu
- [Nosql 技术选型](nosql/nosql-selection.md)
#### Redis
> [Redis](nosql/redis) 📚
#### [Redis](nosql/redis) 📚
![img](http://dunwu.test.upcdn.net/snap/20200713105627.png)
- [Redis 面试总结](nosql/redis/redis-interview.md) 💯
- [Redis 入门指南](nosql/redis/redis-quickstart.md) ⚡ - 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅`
- [Redis 应用指南](nosql/redis/redis-quickstart.md) ⚡ - 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅`
- [Redis 数据类型和应用](nosql/redis/redis-datatype.md) - 关键词:`STRING`、`HASH`、`LIST`、`SET`、`ZSET`、`BitMap`、`HyperLogLog`、`Geo`
- [Redis 持久化](nosql/redis/redis-persistence.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync`
- [Redis 复制](nosql/redis/redis-replication.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`REPLCONF ACK`
@ -106,7 +117,7 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu
- **书籍**
- [《Redis 实战》](https://item.jd.com/11791607.html)
- [《Redis 设计与实现》](https://item.jd.com/11486101.html)
- 源码
- **源码**
- [《Redis 实战》配套 Python 源码](https://github.com/josiahcarlson/redis-in-action)
- **资源汇总**
- [awesome-redis](https://github.com/JamzyWang/awesome-redis)
@ -117,13 +128,6 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu
- [CRUG | Redisson PRO vs. Jedis: Which Is Faster? 翻译](https://www.jianshu.com/p/82f0d5abb002)
- [redis 分布锁 Redisson 性能测试](https://blog.csdn.net/everlasting_188/article/details/51073505)
## 🚪 传送门
◾ 🏠 [LINUX-TUTORIAL 首页](https://github.com/dunwu/linux-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾
## 🚪 传送
◾ 🏠 [DB-TUTORIAL 首页](https://github.com/dunwu/db-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾

View File

@ -49,6 +49,6 @@ TODO: 待补充
- [CRUG | Redisson PRO vs. Jedis: Which Is Faster? 翻译](https://www.jianshu.com/p/82f0d5abb002)
- [redis 分布锁 Redisson 性能测试](https://blog.csdn.net/everlasting_188/article/details/51073505)
## 🚪 传送
## 🚪 传送
◾ 🏠 [DB-TUTORIAL 首页](https://github.com/dunwu/db-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾

View File

@ -24,11 +24,11 @@
> Elastic 技术栈,在 ELK 的基础上扩展了一些新的产品,如:[Beats](https://www.elastic.co/products/beats) 、[X-Pack](https://www.elastic.co/products/x-pack) 。
- [Elastic 技术栈快速入门](nosql/elasticsearch/elastic/elastic-quickstart.md)
- [Beats 入门指南](nosql/elasticsearch/elastic/elastic-beats.md)
- [Beats 应用指南](nosql/elasticsearch/elastic/elastic-beats.md)
- [Beats 运维](nosql/elasticsearch/elastic/elastic-beats-ops.md)
- [Kibana 入门指南](nosql/elasticsearch/elastic/elastic-kibana.md)
- [Kibana 应用指南](nosql/elasticsearch/elastic/elastic-kibana.md)
- [Kibana 运维](nosql/elasticsearch/elastic/elastic-kibana-ops.md)
- [Logstash 入门指南](nosql/elasticsearch/elastic/elastic-logstash.md)
- [Logstash 应用指南](nosql/elasticsearch/elastic/elastic-logstash.md)
- [Logstash 运维](nosql/elasticsearch/elastic/elastic-logstash-ops.md)
## 📚 资料

View File

@ -10,7 +10,7 @@
### [Redis 面试总结 💯](redis-interview.md)
### [Redis 入门指南 ⚡](redis-quickstart.md)
### [Redis 应用指南 ⚡](redis-quickstart.md)
> 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅`

View File

@ -129,7 +129,7 @@ AOF 丢数据比 RDB 少,但文件会比 RDB 文件大很多。
> **_Redis 的事务特性、原理_**
>
> 详情参考:[Redis 入门指南之 事务](redis-quickstart.md#六redis-事务)
> 详情参考:[Redis 应用指南之 事务](redis-quickstart.md#六redis-事务)
**Redis 提供的不是严格的事务Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去**。

View File

@ -1,4 +1,4 @@
# Redis 入门指南
# Redis 应用指南
<!-- TOC depthFrom:2 depthTo:3 -->

View File

@ -4,23 +4,26 @@
## 📖 内容
- [关系型数据库面试题 💯](sql-interview.md)
### [共性知识](common)
### SQL
![img](http://dunwu.test.upcdn.net/snap/20200115160512.png)
- [SQL Cheat Sheet](sql-cheat-sheet.md) - SQL 速查手册
- [关系型数据库面试总结](common/sql-interview.md) 💯
- [SQL Cheat Sheet](common/sql-cheat-sheet.md) 是一个 SQL 入门教程。
- [分布式存储基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-storage-theory.md)
- [分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
### [Mysql](mysql/README.md)
- [Mysql 基本原理](mysql/mysql-theory.md)
- [Mysql 索引](mysql/mysql-index.md)
- [Mysql 锁](mysql/mysql-lock.md)
- [Mysql 事务](mysql/mysql-transaction.md)
![img](http://dunwu.test.upcdn.net/snap/20200716103611.png)
- [Mysql 应用指南](mysql/mysql-quickstart.md) ⚡
- [Mysql 工作流](mysql/mysql-index.md) - 关键词:`连接`、`缓存`、`语法分析`、`优化`、`执行引擎`、`redo log`、`bin log`、`两阶段提交`
- [Mysql 索引](mysql/mysql-index.md) - 关键词:`Hash`、`B 树`、`聚簇索引`、`回表`
- [Mysql 锁](mysql/mysql-lock.md) - 关键词:`乐观锁`、`表级锁`、`行级锁`、`意向锁`、`MVCC`、`Next-key 锁`
- [Mysql 事务](mysql/mysql-transaction.md) - 关键词:`ACID`、`AUTOCOMMIT`、`事务隔离级别`、`死锁`、`分布式事务`
- [Mysql 性能优化](mysql/mysql-optimization.md)
- [Mysql 运维](mysql/mysql-ops.md) 🔨
- [Mysql 配置](mysql/mysql-config.md)
- [Mysql 问题](mysql/mysql-faq.md)
### 其他关系型数据库
@ -43,6 +46,6 @@
- **更多资源**
- [awesome-mysql](https://github.com/jobbole/awesome-mysql-cn)
## 🚪 传送
## 🚪 传送
◾ 🏠 [DB-TUTORIAL 首页](https://github.com/dunwu/db-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾

View File

@ -0,0 +1,34 @@
# 关系型数据库共性知识
## 📖 内容
### [关系型数据库面试题 💯](sql/common/sql-interview.md)
### [SQL Cheat Sheet](sql/common/sql-cheat-sheet.md)
![img](http://dunwu.test.upcdn.net/snap/20200115160512.png)
### [分布式存储基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-storage-theory.md)
![img](http://dunwu.test.upcdn.net/snap/20200716110854.png)
### [分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
## 📚 资料
- **官方**
- [Mysql 官网](https://www.mysql.com/)
- [Mysql 官方文档](https://dev.mysql.com/doc/refman/8.0/en/)
- [Mysql 官方文档之命令行客户端](https://dev.mysql.com/doc/refman/8.0/en/mysql.html)
- **书籍**
- [《高性能 MySQL》](https://item.jd.com/11220393.html) - Mysql 经典
- [《SQL 必知必会》](https://item.jd.com/11232698.html) - SQL 入门
- **教程**
- [runoob.com MySQL 教程](http://www.runoob.com/mymysql-tutorial.html) - 入门级 SQL 教程
- [mysql-tutorial](https://github.com/jaywcjlove/mysql-tutorial)
- **更多资源**
- [awesome-mysql](https://github.com/jobbole/awesome-mysql-cn)
## 🚪 传送
◾ 🏠 [DB-TUTORIAL 首页](https://github.com/dunwu/db-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾

View File

@ -823,7 +823,7 @@ SQL 关键字尽量大写Oracle 默认会将 SQL 语句中的关键字
>
> 在不同环境,不同场景下,应该酌情使用合理的配置。这种优化比较考验 Mysql 运维经验,一般是 DBA 的考量,普通开发接触的较少。
>
> Mysql 配置说明请参考:[Mysql 服务器配置说明](./mysql/mysql-config.md)
> Mysql 配置说明请参考:[Mysql 服务器配置说明](sql/mysql/mysql-config.md)
### 硬件优化

View File

@ -1,4 +1,4 @@
# H2 入门指南
# H2 应用指南
## 概述

View File

@ -1,21 +1,42 @@
# Mysql 教程
![img](http://dunwu.test.upcdn.net/snap/20200716103611.png)
## 📖 内容
- [Mysql 应用指南](mysql-quickstart.md)
- [Mysql 索引](mysql-index.md)
- [Mysql 锁](mysql-lock.md)
- [Mysql 事务](mysql-transaction.md)
- [Mysql 性能优化](mysql-optimization.md)
- [Mysql 运维](mysql-ops.md) 🔨
- [Mysql 配置](mysql-config.md)
### [Mysql 应用指南](mysql-quickstart.md)
------
### [Mysql 索引](mysql-index.md)
关系型数据库基本知识:
> 关键词:`Hash`、`B 树`、`聚簇索引`、`回表`
![img](http://dunwu.test.upcdn.net/snap/20200715172009.png)
### [Mysql 锁](mysql-lock.md)
> 关键词:`乐观锁`、`表级锁`、`行级锁`、`意向锁`、`MVCC`、`Next-key 锁`
![img](http://dunwu.test.upcdn.net/snap/20200716064947.png)
### [Mysql 事务](mysql-transaction.md)
> 关键词:`ACID`、`AUTOCOMMIT`、`事务隔离级别`、`死锁`、`分布式事务`
![img](http://dunwu.test.upcdn.net/snap/20200716074533.png)
### [Mysql 性能优化](mysql-optimization.md)
### [Mysql 运维](mysql-ops.md) 🔨
### [Mysql 配置](mysql-config.md) 🔨
---
相关扩展知识:
- [关系型数据库面试总结](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/sql-interview.md) 💯
- [SQL Cheat Sheet](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/sql-cheat-sheet.md)
- [分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
## 📚 资料
@ -27,7 +48,8 @@
- [《高性能 MySQL》](https://book.douban.com/subject/23008813/) - 经典,适合 DBA 或作为开发者的参考手册
- [《MySQL 必知必会》](https://book.douban.com/subject/3354490/) - 适合入门者
- **教程**
- [runoob.com MySQL 教程](http://www.runoob.com/mysql/mysql-tutorial.html) - 入门级 SQL 教程
- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139)
- [runoob.com MySQL 教程](http://www.runoob.com/mysql/mysql-tutorial.html)
- [mysql-tutorial](https://github.com/jaywcjlove/mysql-tutorial)
- **更多资源**
- [awesome-mysql](https://github.com/jobbole/awesome-mysql-cn)

View File

@ -0,0 +1,40 @@
# Mysql FAQ
> **📦 本文以及示例源码已归档在 [db-tutorial](https://github.com/dunwu/db-tutorial/)**
<!-- TOC depthFrom:2 depthTo:3 -->
- [参考资料](#参考资料)
<!-- /TOC -->
## 为什么表数据删掉一半,表文件大小不变
【问题】数据库占用空间太大,我把一个最大的表删掉了一半的数据,怎么表文件的大小还是没变?
表数据既可以存在共享表空间里,也可以是单独的文件。这个行为是由参数 `innodb_file_per_table` 控制的:
1. 这个参数设置为 OFF 表示的是,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
2. 这个参数设置为 ON 表示的是,每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中。
从 MySQL 5.6.6 版本开始,它的默认值就是 ON 了。
我建议你不论使用 MySQL 的哪个版本,都将这个值设置为 ON。因为一个表单独存储为一个文件更容易管理而且在你不需要这个表的时候通过 drop table 命令,系统就会直接删除这个文件。而如果是放在共享表空间中,即使表删掉了,空间也是不会回收的。
所以,**将 innodb_file_per_table 设置为 ON是推荐做法我们接下来的讨论都是基于这个设置展开的。**
我们在删除整个表的时候,可以使用 drop table 命令回收表空间。但是,我们遇到的更多的删除数据的场景是删除某些行,这时就遇到了我们文章开头的问题:表中的数据被删除了,但是表空间却没有被回收。
**插入和删除操作可能会造成空洞**。
- 插入时,如果插入位置所在页已满,需要申请新页面。
- 删除时,不会删除所在页,而是将记录在页面的位置标记为可重用。
所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。
要达到收缩空洞的目的,可以使用重建表的方式。
## 参考资料
- [《高性能 MySQL》](https://book.douban.com/subject/23008813/)
- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139)

View File

@ -1,27 +1,28 @@
# Mysql 索引
索引是提高 MySQL 查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。应当尽量避免事后才想起添加索引,因为事后可能需要监控大量的 SQL 才能定位到问题所在,而且添加索引的时间肯定是远大于初始添加索引所需要的时间,可见索引的添加也是非常有技术含量的。
> 索引是提高 MySQL 查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。应当尽量避免事后才想起添加索引,因为事后可能需要监控大量的 SQL 才能定位到问题所在,而且添加索引的时间肯定是远大于初始添加索引所需要的时间,可见索引的添加也是非常有技术含量的。
>
> 接下来将向你展示一系列创建高性能索引的策略,以及每条策略其背后的工作原理。但在此之前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。
>
接下来将向你展示一系列创建高性能索引的策略,以及每条策略其背后的工作原理。但在此之前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。
![img](http://dunwu.test.upcdn.net/snap/20200715172009.png)
<!-- TOC depthFrom:2 depthTo:3 -->
- [一、索引简介](#一索引简介)
- [索引的优缺点](#索引的优缺点)
- [何时使用索引](#何时使用索引)
- [二、索引的类型](#二索引的类型)
- [三、索引的数据结构](#三索引的数据结构)
- [B 树索引](#b-树索引)
- [二、索引的数据结构](#二索引的数据结构)
- [哈希索引](#哈希索引)
- [B 树索引](#b-树索引)
- [全文索引](#全文索引)
- [空间数据索引](#空间数据索引)
- [四、聚簇索引](#四聚簇索引)
- [五、索引的策略](#五索引的策略)
- [三、索引的类型](#三索引的类型)
- [四、索引的策略](#四索引的策略)
- [索引基本原则](#索引基本原则)
- [独立的列](#独立的列)
- [前缀索引](#前缀索引)
- [覆盖索引](#覆盖索引)
- [使用索引来排序](#使用索引来排序)
- [前缀索引](#前缀索引)
- [最左前缀匹配原则](#最左前缀匹配原则)
- [= 和 in 可以乱序](#-和-in-可以乱序)
- [参考资料](#参考资料)
@ -30,7 +31,7 @@
## 一、索引简介
***索引优化应该是查询性能优化的最有效手段***。
**_索引优化应该是查询性能优化的最有效手段_**。
### 索引的优缺点
@ -67,72 +68,26 @@ B+ 树索引,按照顺序存储数据,所以 Mysql 可以用来做 ORDER BY
- 列名不经常出现在 `WHERE` 或连接(`JOIN`)条件中 - 索引就会经常不命中,没有意义,还增加空间开销。
- 对于特大型表,建立和使用索引的代价将随之增长。可以考虑使用分区技术或 Nosql。
## 二、索引的类型
## 二、索引的数据结构
主流的关系型数据库一般都支持以下索引类型:
### 哈希索引
从逻辑类型上划分(即一般创建表时设置的索引类型
> Hash 索引只有精确匹配索引所有列的查询才有效。
#### 普通索引(`INDEX`
哈希表是一种以键 - 值key-value存储数据的结构我们只要输入待查找的值即 key就可以找到其对应的值即 Value。哈希的思路很简单把值放在数组里用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。
普通索引:最基本的索引,没有任何限制
对于每一行数据,对所有的索引列计算一个 `hashcode`。哈希索引将所有的 `hashcode` 存储在索引中,同时在 Hash 表中保存指向每个数据行的指针
```sql
CREATE TABLE `table` (
...
INDEX index_name (title(length))
)
```
哈希索引的**优点**
#### 唯一索引(`UNIQUE`
- 因为索引数据结构紧凑,所以**查询速度非常快**。
唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
哈希索引的**缺点**
```sql
CREATE TABLE `table` (
...
UNIQUE indexName (title(length))
)
```
#### 主键索引(`PRIMARY`
主键索引:一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。
```sql
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`id`)
)
```
#### 组合索引
组合索引:多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
```sql
CREATE TABLE `table` (
...
INDEX index_name (title(length), title(length), ...)
)
```
#### 全文索引
全文索引:主要用来查找文本中的关键字,而不是直接与索引中的值相比较。
全文索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 WHERE 语句的参数匹配。全文索引配合 `match against` 操作使用,而不是一般的 WHERE 语句加 LIKE。它可以在 `CREATE TABLE``ALTER TABLE` `CREATE INDEX` 使用,不过目前只有 `char`、`varchar``text` 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用 `CREATE INDEX` 创建全文索引,要比先为一张表建立全文索引然后再将数据写入的速度快很多。
```sql
CREATE TABLE `table` (
`content` text CHARACTER NULL,
...
FULLTEXT (content)
)
```
## 三、索引的数据结构
- 哈希索引数据不是按照索引值顺序存储的,所以**无法用于排序**。
- 哈希索引**不支持部分索引匹配查找**。如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A无法使用该索引。
- 哈希索引**只支持等值比较查询**,不支持任何范围查询,如 `WHERE price > 100`
- 哈希索引有**可能出现哈希冲突**,出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。
### B 树索引
@ -140,138 +95,51 @@ CREATE TABLE `table` (
`B+Tree`中的 B 是指`balance`意为平衡。需要注意的是B+树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后得到要查找的数据。
#### 二叉查找
#### 二叉搜索
在介绍`B+Tree`前,先了解一下二叉查找树,它是一种经典的数据结构,其左子树的值总是小于根的值,右子树的值总是大于根的值,如下图 ①。如果要在这课树中查找值为 5 的记录,其大致流程:先找到根,其值为 6大于 5所以查找左子树找到 3而 5 大于 3接着找 3 的右子树,总共找了 3 次。同样的方法,如果查找值为 8 的记录,也需要查找 3 次。所以二叉查找树的平均查找次数为 $$(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3$$ 次,而顺序查找的话,查找值为 2 的记录,仅需要 1 次,但查找值为 8 的记录则需要 6 次,所以顺序查找的平均查找次数为:$$(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3$$ 次,因此大多数情况下二叉查找树的平均查找速度比顺序查找要快
二叉搜索树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。其查询时间复杂度是 $$O(log(N))$$
![img](https:////upload-images.jianshu.io/upload_images/175724-272c1245eba594f5.png?imageMogr2/auto-orient/strip|imageView2/2/w/618/format/webp)
#### 平衡二叉树
由于二叉查找树可以任意构造,同样的值,可以构造出如图 ② 的二叉查找树显然这棵二叉树的查询效率和顺序查找差不多。若想二叉查找数的查询性能最高需要这棵二叉查找树是平衡的也即平衡二叉树AVL 树)。
平衡二叉树首先需要符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度差不能大于 1。显然图 ② 不满足平衡二叉树的定义,而图 ① 是一课平衡二叉树。平衡二叉树的查找性能是比较高的(性能最好的是最优二叉树),查询性能越好,维护的成本就越大。比如图 ① 的平衡二叉树,当用户需要插入一个新的值 9 的节点时,就需要做出如下变动。
![img](https:////upload-images.jianshu.io/upload_images/175724-c806af2d9defcbab.png?imageMogr2/auto-orient/strip|imageView2/2/w/538/format/webp)
##### 平衡二叉树旋转
通过一次左旋操作就将插入后的树重新变为平衡二叉树是最简单的情况了,实际应用场景中可能需要旋转多次。至此我们可以考虑一个问题,平衡二叉树的查找效率还不错,实现也非常简单,相应的维护成本还能接受,为什么 MySQL 索引不直接使用平衡二叉树?
当然为了维持 $$O(log(N))$$ 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 $$O(log(N))$$。
随着数据库中数据的增加,索引本身大小随之增加,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘 I/O 消耗相对于内存存取I/O 存取的消耗要高几个数量级。可以想象一下一棵几百万节点的二叉树的深度是多少?如果将这么大深度的一颗二叉树放磁盘上,每读取一个节点,需要一次磁盘的 I/O 读取,整个查找的耗时显然是不能够接受的。那么如何减少查找过程中的 I/O 存取次数?
一种行之有效的解决方法是减少树的深度,将二叉树变为 m 叉树(多路搜索树),而`B+Tree`就是一种多路搜索树。理解`B+Tree`时,只需要理解其最重要的两个特征即可:第一,所有的关键字(可以理解为数据)都存储在叶子节点(`Leaf Page`),非叶子节点(`Index Page`)并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,所有的叶子节点由指针连接。如下图为高度为 2 的简化了的`B+Tree`。
![img](https:////upload-images.jianshu.io/upload_images/175724-52306456815a0919.png?imageMogr2/auto-orient/strip|imageView2/2/w/993/format/webp)
一种行之有效的解决方法是减少树的深度,将**二叉树变为 N 叉树**(多路搜索树),而 **B+ 树就是一种多路搜索树**
#### B+ 树
##### B+ 树特性
B+ 树索引适用于**全键值查找**、**键值范围查找**和**键前缀查找**,其中键前缀查找只适用于最左前缀查找。
B+ 树索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
理解`B+Tree`时,只需要理解其最重要的两个特征即可:
InnoDB 的 B+Tree 索引分为主索引和辅助索引。
- 第一,所有的关键字(可以理解为数据)都存储在叶子节点,非叶子节点并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。
- 其次,所有的叶子节点由指针连接。如下图为简化了的`B+Tree`。
主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
![img](http://dunwu.test.upcdn.net/snap/20200304235424.jpg)
<div align="center">
<img src="http://upload-images.jianshu.io/upload_images/3101171-28ea7c1487bd12bb.png"/>
</div>
根据叶子节点的内容,索引类型分为主键索引和非主键索引。
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
- **聚簇索引clustered**:又称为主键索引,其叶子节点存的是整行数据。因为无法同时把数据行存放在两个不同的地方,所以**一个表只能有一个聚簇索引**。**InnoDB 的聚簇索引实际是在同一个结构中保存了 B 树的索引和数据行**。
- 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为**二级索引secondary**。数据存储在一个位置,索引存储在另一个位置,索引中包含指向数据存储位置的指针。可以有多个,小于 249 个。
<div align="center">
<img src="http://upload-images.jianshu.io/upload_images/3101171-96c6c85468df0f89.png"/>
</div>
**聚簇表示数据行和相邻的键值紧凑地存储在一起,因为数据紧凑,所以访问快**。因为无法同时把数据行存放在两个不同的地方,所以**一个表只能有一个聚簇索引**。
定义一条数据记录为一个二元组 [key, data]B-Tree 是满足下列条件的数据结构:
**聚簇索引和非聚簇索引的查询有什么区别**
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
- 一个节点中的 key 从左到右非递减排列;
- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1且不为 null则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。
- 如果语句是 `select * from T where ID=500`,即聚簇索引查询方式,则只需要搜索 ID 这棵 B+ 树;
- 如果语句是 `select * from T where k=5`,即非聚簇索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500再到 ID 索引树搜索一次。这个过程称为**回表**。
##### B+ 树原理
也就是说,**基于非聚簇索引的查询需要多扫描一棵索引树**。因此,我们在应用中应该尽量使用主键查询。
> B+ 树查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data否则在相应区间的指针指向的节点递归进行查找。
>
> 由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作以保持 B-Tree 性质。
**显然,主键长度越小,非聚簇索引的叶子节点就越小,非聚簇索引占用的空间也就越小。**
MySQL 将每个节点的大小设置为一个页的整数倍(原因下文会介绍),也就是在节点空间大小一定的情况下,每个节点可以存储更多的内结点,这样每个结点能索引的范围更大更精确。所有的叶子节点使用指针链接的好处是可以进行区间访问,比如上图中,如果查找大于 20 而小于 30 的记录,只需要找到节点 20就可以遍历指针依次找到 25、30。如果没有链接指针的话就无法进行区间查找。这也是 MySQL 使用`B+Tree`作为索引存储结构的重要原因。
自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。从性能和存储空间方面考量自增主键往往是更合理的选择。有没有什么场景适合用业务字段直接做主键的呢还是有的。比如有些业务的场景需求是这样的
MySQL 为何将节点大小设置为页的整数倍,这就需要理解磁盘的存储原理。磁盘本身存取就比主存慢很多,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度往往是主存的几百万分之一,为了尽量减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读即使只需要一个字节磁盘也会从这个位置开始顺序向后读取一定长度的数据放入内存预读的长度一般为页的整数倍。
- 只有一个索引;
- 该索引必须是唯一索引。
> 页是计算机管理存储器的逻辑块,硬件及 OS 往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(许多 OS 中,页的大小通常为 4K。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时会触发一个缺页异常此时系统会向磁盘发出读盘信号磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中然后一起返回程序继续运行
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题
MySQL 巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次 I/O。假设`B+Tree`的高度为 h一次检索最多需要`h-1`次 I/O根节点常驻内存复杂度 $$O(h) = O(logmN)$$。实际应用场景中M 通常较大,常常超过 100因此树的高度一般都比较小通常不超过 3。
最后简单了解下`B+Tree`节点的操作,在整体上对索引的维护有一个大概的了解,虽然索引可以大大提高查询效率,但维护索引仍要花费很大的代价,因此合理的创建索引也就尤为重要。
仍以上面的树为例,我们假设每个节点只能存储 4 个内节点。首先要插入第一个节点 28如下图所示。
![img](https:////upload-images.jianshu.io/upload_images/175724-a862bb909a8ed6a0.png?imageMogr2/auto-orient/strip|imageView2/2/w/950/format/webp)
1leaf page 和 index page 都没有满
接着插入下一个节点 70在 Index Page 中查询后得知应该插入到 50 - 70 之间的叶子节点,但叶子节点已满,这时候就需要进行也分裂的操作,当前的叶子节点起点为 50所以根据中间值来拆分叶子节点如下图所示。
![img](https:////upload-images.jianshu.io/upload_images/175724-ce47c776928bc6b8.png?imageMogr2/auto-orient/strip|imageView2/2/w/928/format/webp)
2Leaf Page 拆分
最后插入一个节点 95这时候 Index Page 和 Leaf Page 都满了,就需要做两次拆分,如下图所示。
![img](https:////upload-images.jianshu.io/upload_images/175724-33cee181795b3dcc.png?imageMogr2/auto-orient/strip|imageView2/2/w/909/format/webp)
3Leaf Page 与 Index Page 拆分
拆分后最终形成了这样一颗树。
![img](https:////upload-images.jianshu.io/upload_images/175724-5289c6ec5d11216e.png?imageMogr2/auto-orient/strip|imageView2/2/w/986/format/webp)
4最终树
`B+Tree`为了保持平衡,对于新插入的值需要做大量的拆分页操作,而页的拆分需要 I/O 操作,为了尽可能的减少页的拆分操作,`B+Tree`也提供了类似于平衡二叉树的旋转功能。当 Leaf Page 已满但其左右兄弟节点没有满的情况下,`B+Tree`并不急于去做拆分操作,而是将记录移到当前所在页的兄弟节点上。通常情况下,左兄弟会被先检查用来做旋转操作。就比如上面第二个示例,当插入 70 的时候,并不会去做页拆分,而是左旋操作。
![img](https:////upload-images.jianshu.io/upload_images/175724-bafd2fbc93cf45ae.png?imageMogr2/auto-orient/strip|imageView2/2/w/739/format/webp)
5左旋操作
通过旋转操作可以最大限度的减少页分裂,从而减少索引维护过程中的磁盘的 I/O 操作,也提高索引维护效率。需要注意的是,删除节点跟插入节点类似,仍然需要旋转和拆分操作,这里就不再说明。
通过上文,相信你对`B+Tree`的数据结构已经有了大致的了解,但 MySQL 中索引是如何组织数据的存储呢?以一个简单的示例来说明,假如有如下数据表:
```sql
CREATE TABLE People(
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum(`m`,`f`) not null,
key(last_name,first_name,dob)
);
```
对于表中每一行数据,索引中包含了 last_name、first_name、dob 列的值,下图展示了索引是如何组织数据存储的。
![img](https:////upload-images.jianshu.io/upload_images/175724-3ba760afbae4a52d.png?imageMogr2/auto-orient/strip|imageView2/2/w/1006/format/webp)
可以看到,索引首先根据第一个字段来排列顺序,当名字相同时,则根据第三个字段,即出生日期来排序,正是因为这个原因,才有了索引的“最左原则”。
### 哈希索引
> Hash 索引只有精确匹配索引所有列的查询才有效。
对于每一行数据,对所有的索引列计算一个 `hashcode`。哈希索引将所有的 `hashcode` 存储在索引中,同时在 Hash 表中保存指向每个数据行的指针。
哈希结构索引的优点:
- 因为索引数据结构紧凑,所以查询速度非常快。
哈希结构索引的缺点:
- 哈希索引数据不是按照索引值顺序存储的,所以无法用于排序。
- 哈希索引不支持部分索引匹配查找。如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A无法使用该索引。
- 哈希索引只支持等值比较查询,不支持任何范围查询,如 WHERE price > 100。
- 哈希索引有可能出现哈希冲突,出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。
这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
### 全文索引
@ -287,24 +155,84 @@ MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数
必须使用 GIS 相关的函数来维护数据。
## 四、聚簇索引
## 三、索引的类型
聚簇索引不是一种单独的索引类型,而是一种数据存储方式。聚簇表示数据行和相邻的键紧凑地存储在一起。具体细节依赖于实现方式。如 **InnoDB 的聚簇索引实际是在同一个结构中保存了 B 树的索引和数据行**
主流的关系型数据库一般都支持以下索引类型:
**聚簇表示数据行和相邻的键值紧凑地存储在一起,因为数据紧凑,所以访问快**。因为无法同时把数据行存放在两个不同的地方,所以**一个表只能有一个聚簇索引**。
#### 主键索引(`PRIMARY`
若没有定义主键InnoDB 会隐式定义一个主键来作为聚簇索引。
主键索引:一种特殊的唯一索引,不允许有空值。一个表只能有一个主键(在 InnoDB 中本质上即聚簇索引),一般是在建表的时候同时创建主键索引。
- **聚集索引**(`Clustered`):表中各行的物理顺序与键值的逻辑(索引)顺序相同,每个表只能有一个。
- **非聚集索引**(`Non-clustered`):非聚集索引指定表的逻辑顺序,也可以视为二级索引。数据存储在一个位置,索引存储在另一个位置,索引中包含指向数据存储位置的指针。可以有多个,小于 249 个。
```sql
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`id`)
)
```
![img](http://dunwu.test.upcdn.net/snap/20200304235424.jpg)
#### 唯一索引(`UNIQUE`
如上图所示InnoDB 的聚簇索引,其叶子节点包含了行的全部数据,而非叶子节点则包含了索引列。
唯一索引:**索引列的值必须唯一,但允许有空值**。如果是组合索引,则列值的组合必须唯一
如果没有定义主键InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引InnoDB 会隐式定义一个主键来作为聚簇索引。
```sql
CREATE TABLE `table` (
...
UNIQUE indexName (title(length))
)
```
## 五、索引的策略
#### 普通索引(`INDEX`
普通索引:最基本的索引,没有任何限制。
```sql
CREATE TABLE `table` (
...
INDEX index_name (title(length))
)
```
#### 全文索引(`FULLTEXT`
全文索引:主要用来查找文本中的关键字,而不是直接与索引中的值相比较。
全文索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 WHERE 语句的参数匹配。全文索引配合 `match against` 操作使用,而不是一般的 WHERE 语句加 LIKE。它可以在 `CREATE TABLE``ALTER TABLE` `CREATE INDEX` 使用,不过目前只有 `char`、`varchar``text` 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用 `CREATE INDEX` 创建全文索引,要比先为一张表建立全文索引然后再将数据写入的速度快很多。
```sql
CREATE TABLE `table` (
`content` text CHARACTER NULL,
...
FULLTEXT (content)
)
```
#### 联合索引
组合索引:多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
```sql
CREATE TABLE `table` (
...
INDEX index_name (title(length), title(length), ...)
)
```
## 四、索引的策略
假设有以下表:
```sql
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
```
### 索引基本原则
@ -318,6 +246,8 @@ MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数
**“独立的列” 是指索引列不能是表达式的一部分,也不能是函数的参数**。
**对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。**
如果查询中的列不是独立的列,则数据库不会使用索引。
❌ 错误示例:
@ -327,50 +257,86 @@ SELECT actor_id FROM actor WHERE actor_id + 1 = 5;
SELECT ... WHERE TO_DAYS(current_date) - TO_DAYS(date_col) <= 10;
```
### 覆盖索引
**覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。**
【示例】范围查询
```sql
create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
select * from T where k between 3 and 5
```
需要执行几次树的搜索操作,会扫描多少行?
1. 在 k 索引树上找到 k=3 的记录,取得 ID = 300
2. 再到 ID 索引树查到 ID=300 对应的 R3
3. 在 k 索引树取下一个值 k=5取得 ID=500
4. 再回到 ID 索引树查到 ID=500 对应的 R4
5. 在 k 索引树取下一个值 k=6不满足条件循环结束。
在这个过程中,**回到主键索引树搜索的过程,我们称为回表**。可以看到,这个查询过程读了 k 索引树的 3 条记录(步骤 1、3 和 5回表了两次步骤 2 和 4
如果执行的语句是 select ID from T where k between 3 and 5这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。索引包含所有需要查询的字段的值,称为覆盖索引。
**由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。**
#### 使用索引来排序
Mysql 有两种方式可以生成排序结果:通过排序操作;或者按索引顺序扫描。
**索引最好既满足排序,又用于查找行**。这样,就可以通过命中覆盖索引直接将结果查出来,也就不再需要排序了。
这样整个查询语句的执行流程就变成了:
1. 从索引 (city,name,age) 找到第一个满足 city='杭州’条件的记录,取出其中的 city、name 和 age 这三个字段的值,作为结果集的一部分直接返回;
2. 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
3. 重复执行步骤 2直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。
### 前缀索引
有时候需要索引很长的字符列,这会让索引变得大且慢。
解决方法是:可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。
这时,可以使用前缀索引,即只索引开始的部分字符,这样可以**大大节约索引空间**,从而**提高索引效率**。但这样也**会降低索引的选择性**。对于 `BLOB`/`TEXT`/`VARCHAR` 这种文本类型的列,必须使用前缀索引,因为数据库往往不允许索引这些列的完整长度
**索引的选择性**是指:不重复的索引值和数据表记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高。
**索引的选择性**是指:不重复的索引值和数据表记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高。如果存在多条命中前缀索引的情况,就需要依次扫描,直到最终找到正确记录。
对于 `BLOB`/`TEXT`/`VARCHAR` 这种文本类型的列,必须使用前缀索引,因为数据库往往不允许索引这些列的完整长度。
**使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。**
要选择足够长的前缀以保证较高的选择性,同时又不能太长(节约空间)。
那么,如何确定前缀索引合适的长度呢?
❌ 低效示例:
可以使用下面这个语句,算出这个列上有多少个不同的值
```sql
SELECT COUNT(*) AS cnt, city FROM sakila.city_demo
GROUP BY city ORDER BY cnt DESC LIMIT 10;
select count(distinct email) as L from SUser;
```
✔ 高效示例:
然后,依次选取不同长度的前缀来看这个值,比如我们要看一下 4~7 个字节的前缀索引,可以用这个语句
```sql
SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref FROM sakila.city_demo
GROUP BY city ORDER BY cnt DESC LIMIT 10;
select
count(distinct left(email,4)as L4,
count(distinct left(email,5)as L5,
count(distinct left(email,6)as L6,
count(distinct left(email,7)as L7,
from SUser;
```
### 覆盖索引
索引包含所有需要查询的字段的值。
具有以下优点:
- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
- 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
### 使用索引来排序
Mysql 有两种方式可以生成排序结果:通过排序操作;或者按索引顺序扫描。
**索引最好既满足排序,又用于查找行**。这样,就可以使用索引来对结果排序。
当然,使用前缀索引很可能会损失区分度,所以你需要预先设定一个可以接受的损失比例,比如 5%。然后,在返回的 L4~L7 中,找出不小于 L * 95% 的值,假设这里 L6、L7 都满足,你就可以选择前缀长度为 6。
### 最左前缀匹配原则
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
MySQL 会一直向右匹配直到遇到范围查询 `(>,<,BETWEEN,LIKE)` 就停止匹配。
- 索引可以简单如一个列(a),也可以复杂如多个列(a, b, c, d),即**联合索引**。
@ -404,10 +370,11 @@ customer_id_selectivity: 0.0373
**不需要考虑 `=`、`IN` 等的顺序**Mysql 会自动优化这些条件的顺序,以匹配尽可能多的索引列。
例子:如有索引(a, b, c, d),查询条件 c > 3 and b = 2 and a = 1 and d < 4 a = 1 and c > 3 and b = 2 and d < 4 MySQL a = 1 and b = 2 and c > 3 and d < 4abc
【示例】如有索引 (a, b, c, d),查询条件 `c > 3 and b = 2 and a = 1 and d < 4``a = 1 and c > 3 and b = 2 and d < 4` 等顺序都是可以的MySQL 会自动优化为 a = 1 and b = 2 and c > 3 and d < 4,依次命abcd
## 参考资料
- [《高性能 MySQL》](https://book.douban.com/subject/23008813/)
- [数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79)
- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139)

View File

@ -1,23 +1,25 @@
# Mysql 锁
![img](http://dunwu.test.upcdn.net/snap/20200716064947.png)
<!-- TOC depthFrom:2 depthTo:3 -->
- [乐观锁和悲观锁](#乐观锁和悲观锁)
- [锁粒度](#锁粒度)
- [读写锁](#读写锁)
- [意向锁](#意向锁)
- [MVCC](#mvcc)
- [基本思想](#基本思想)
- [一、悲观锁和乐观锁](#一悲观锁和乐观锁)
- [二、表级锁和行级锁](#二表级锁和行级锁)
- [三、读写锁](#读写锁)
- [四、意向锁](#意向锁)
- [五、MVCC](#mvcc)
- [MVCC 思想](#mvcc-思想)
- [版本号](#版本号)
- [Undo 日志](#undo-日志)
- [ReadView](#readview)
- [快照读与当前读](#快照读与当前读)
- [Next-key 锁](#next-key-锁)
- [六、Next-key 锁](#next-key-锁)
- [参考资料](#参考资料)
<!-- /TOC -->
## 乐观锁和悲观锁
## 一、悲观锁和乐观锁
确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性,**乐观锁和悲观锁是并发控制主要采用的技术手段。**
@ -28,7 +30,21 @@
- 在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定
- 实现方式:**使用 version 版本或者时间戳**。
## 锁粒度
【示例】乐观锁示例
商品 goods 表中有一个字段 statusstatus 为 1 代表商品未被下单status 为 2 代表商品已经被下单,那么我们对某个商品下单时必须确保该商品 status 为 1。假设商品的 id 为 1。
```sql
select (status,status,version) from t_goods where id=#{id}
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
```
> 更详细的乐观锁说可以参考:[使用mysql乐观锁解决并发问题](https://www.cnblogs.com/laoyeye/p/8097684.html)
## 二、表级锁和行级锁
从数据库的锁粒度来看MySQL 中提供了两种封锁粒度:行级锁和表级锁。
@ -41,7 +57,7 @@
`InnoDB` 中,**行锁是通过给索引上的索引项加锁来实现的**。**如果没有索引,`InnoDB` 将会通过隐藏的聚簇索引来对记录加锁**。
## 读写锁
## 三、读写锁
- 独享锁Exclusive简写为 X 锁,又称写锁。使用方式:`SELECT ... FOR UPDATE;`
- 共享锁Shared简写为 S 锁,又称读锁。使用方式:`SELECT ... LOCK IN SHARE MODE;`
@ -50,7 +66,7 @@
**`InnoDB` 下的行锁、间隙锁、next-key 锁统统属于独享锁**。
## 意向锁
## 四、意向锁
**当存在表级锁和行级锁的情况下,必须先申请意向锁(表级锁,但不是真的加锁),再获取行级锁**。使用意向锁Intention Locks可以更容易地支持多粒度封锁。
@ -81,21 +97,19 @@
- 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁;
- 这里兼容关系针对的是表级锁,而表级的 IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)
## MVCC
## 五、MVCC
不仅是 Mysql包括 Oracle、PostgreSQL 等其他数据库都实现了各自的 MVCC实现机制没有统一标准。
多版本并发控制Multi-Version Concurrency Control, MVCC可以视为行级锁的一个变种。它在很多情况下都避免了加锁操作因此开销更低。
**多版本并发控制Multi-Version Concurrency Control, MVCC可以视为行级锁的一个变种。它在很多情况下都避免了加锁操作因此开销更低**。不仅是 Mysql包括 Oracle、PostgreSQL 等其他数据库都实现了各自的 MVCC实现机制没有统一标准。
MVCC 是 `InnoDB` 存储引擎实现隔离级别的一种具体方式,**用于实现提交读和可重复读这两种隔离级别**。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
### 基本思想
### MVCC 思想
加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的。
MVCC 的思想是:
- 保存数据在某个时间点的快照。**写操作DELETE、INSERT、UPDATE更新最新的版本快照而读操作去读旧版本快照没有互斥关系**这一点和 `CopyOnWrite` 类似。
- **保存数据在某个时间点的快照写操作DELETE、INSERT、UPDATE更新最新的版本快照而读操作去读旧版本快照没有互斥关系**。这一点和 `CopyOnWrite` 类似。
- 脏读和不可重复读最根本的原因是**事务读取到其它事务未提交的修改**。在事务进行读取操作时,为了解决脏读和不可重复读问题,**MVCC 规定只能读取已经提交的快照**。当然一个事务可以读取自身未提交的快照,这不算是脏读。
### 版本号
@ -107,7 +121,7 @@ InnoDB 的 MVCC 实现是:在每行记录后面保存两个隐藏列,一个
### Undo 日志
MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 `ROLL_PTR` 把一个数据行的所有快照连接起来。
例如在 MySQL 创建一个表 t包含主键 id 和一个字段 x。我们先插入一个数据行然后对该数据行执行两次更新操作。
@ -123,7 +137,17 @@ UPDATE t SET x="c" WHERE id=1;
### ReadView
MVCC 维护了一个 `ReadView` 结构,主要包含了当前系统未提交的事务列表 `TRX_IDs {TRX_ID_1, TRX_ID_2, ...}`,还有该列表的最小值 `TRX_ID_MIN``TRX_ID_MAX`
MVCC 维护了一个一致性读视图 `consistent read view` ,主要包含了当前系统**未提交的事务列表** `TRX_IDs {TRX_ID_1, TRX_ID_2, ...}`,还有该列表的最小值 `TRX_ID_MIN``TRX_ID_MAX`
![](http://dunwu.test.upcdn.net/snap/20200715135809.png)
这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id有以下几种可能
1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3. 如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
在进行 `SELECT` 操作时,根据数据行快照的 `TRX_ID``TRX_ID_MIN``TRX_ID_MAX` 之间的关系,从而判断数据行快照是否可以使用:
@ -162,7 +186,7 @@ SELECT * FROM table WHERE ? lock in share mode;
SELECT * FROM table WHERE ? for update;
```
## Next-key 锁
## 六、Next-key 锁
Next-Key 锁是 MySQL 的 `InnoDB` 存储引擎的一种锁实现。
@ -183,3 +207,4 @@ MVCC 不能解决幻读问题,**Next-Key 锁就是为了解决幻读问题**
- [《高性能 MySQL》](https://book.douban.com/subject/23008813/)
- [数据库系统原理](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/数据库系统原理.md)
- [数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79)
- [使用mysql乐观锁解决并发问题](https://www.cnblogs.com/laoyeye/p/8097684.html)

View File

@ -174,6 +174,38 @@ vim /etc/my.cnf
## 二、基本运维
### 客户端连接
语法:`mysql -h<主机> -P<端口> -u<用户名> -p<密码>`
如果没有显式指定密码,会要求输入密码才能访问。
【示例】连接本地 Mysql
```shell
$ mysql -h 127.0.0.1 -P 3306 -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 13501
Server version: 8.0.19 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
```
### 查看连接
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 `show processlist` 命令中看到它。客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 `wait_timeout` 控制的,默认值是 8 小时。
![](http://dunwu.test.upcdn.net/snap/20200714115031.png)
### 创建用户
```sql

View File

@ -2,35 +2,24 @@
<!-- TOC depthFrom:2 depthTo:3 -->
- [一、MySQL 查询过程](#一mysql-查询过程)
- [1客户端/服务端通信协议](#1客户端服务端通信协议)
- [2查询缓存](#2查询缓存)
- [3语法解析和预处理](#3语法解析和预处理)
- [4查询优化](#4查询优化)
- [5查询执行引擎](#5查询执行引擎)
- [6返回结果](#6返回结果)
- [小结](#小结)
- [二、数据结构优化](#二数据结构优化)
- [一、数据结构优化](#一数据结构优化)
- [数据类型优化](#数据类型优化)
- [表设计](#表设计)
- [范式和反范式](#范式和反范式)
- [三、索引优化](#三索引优化)
- [何时使用索引](#何时使用索引)
- [索引优化策略](#索引优化策略)
- [四、SQL 优化](#四sql-优化)
- [索引优化](#索引优化)
- [二、SQL 优化](#二sql-优化)
- [优化 COUNT() 查询](#优化-count-查询)
- [优化关联查询](#优化关联查询)
- [优化 GROUP BY 和 DISTINCT](#优化-group-by-和-distinct)
- [优化 LIMIT](#优化-limit)
- [优化 UNION](#优化-union)
- [优化查询方式](#优化查询方式)
- [五、执行计划](#五执行计划)
- [三、执行计划](#三执行计划)
- [参考资料](#参考资料)
- [传送门](#传送门)
<!-- /TOC -->
## 、数据结构优化
## 、数据结构优化
良好的逻辑设计和物理设计是高性能的基石。
@ -78,20 +67,20 @@
在真实世界中,很少会极端地使用范式化或反范式化。实际上,应该权衡范式和反范式的利弊,混合使用。
## 三、索引优化
### 索引优化
> 索引优化应该是查询性能优化的最有效手段。
>
> 如果想详细了解索引特性请参考:[Mysql 索引](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/mysql/mysql-index.md)
### 何时使用索引
#### 何时使用索引
- 对于非常小的表,大部分情况下简单的全表扫描更高效。
- 对于中、大型表,索引非常有效。
- 对于特大型表,建立和使用索引的代价将随之增长。可以考虑使用分区技术。
- 如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。
### 索引优化策略
#### 索引优化策略
- **索引基本原则**
- 索引不是越多越好,不要为所有列都创建索引。
@ -104,28 +93,10 @@
- **最左匹配原则** - 将选择性高的列或基数大的列优先排在多列索引最前列。
- **使用索引来排序** - 索引最好既满足排序,又用于查找行。这样,就可以使用索引来对结果排序。
- `=`、`IN` 可以乱序 - 不需要考虑 `=`、`IN` 等的顺序
- **覆盖索引**
- **自增字段作主键**
#### 覆盖索引
假设我们只需要查询商品的名称、价格信息,我们有什么方式来避免回表呢?我们可以建立一个组合索引,即商品编码、名称、价格作为一个组合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。
从辅助索引中查询得到记录而不需要通过聚族索引查询获得MySQL 中将其称为覆盖索引。使用覆盖索引的好处很明显,我们不需要查询出包含整行记录的所有信息,因此可以减少大量的 I/O 操作。
#### 自增字段作主键
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为不需要重新移动数据,因此这种插入数据的方法效率非常高。
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
#### 前缀索引
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。
减小索引字段大小,可以增加一个页中存储的索引项,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
不过,前缀索引是有一定的局限性的,例如 order by 就无法使用前缀索引,无法把前缀索引用作覆盖索引。
## 四、SQL 优化
## 二、SQL 优化
SQL 优化后,可以通过执行计划(`EXPLAIN`)来查看优化效果。
@ -284,7 +255,7 @@ SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
## 、执行计划
## 、执行计划
如何检验修改后的 SQL 确实有优化效果?这就需要用到执行计划(`EXPLAIN`)。

View File

@ -2,11 +2,12 @@
<!-- TOC depthFrom:2 depthTo:3 -->
- [存储引擎](#存储引擎)
- [一、SQL 执行过程](#一sql-执行过程)
- [二、存储引擎](#二存储引擎)
- [选择存储引擎](#选择存储引擎)
- [MyISAM](#myisam)
- [InnoDB](#innodb)
- [数据类型](#数据类型)
- [三、数据类型](#数据类型)
- [整型](#整型)
- [浮点型](#浮点型)
- [字符串](#字符串)
@ -14,156 +15,27 @@
- [BLOB 和 TEXT](#blob-和-text)
- [枚举类型](#枚举类型)
- [类型的选择](#类型的选择)
- [索引](#索引)
- [锁](#锁)
- [事务](#事务)
- [性能优化](#性能优化)
- [复制](#复制)
- [四、索引](#索引)
- [五、锁](#锁)
- [六、事务](#事务)
- [七、性能优化](#性能优化)
- [八、复制](#复制)
- [主从复制](#主从复制)
- [读写分离](#读写分离)
- [九、分布式事务](#九分布式事务)
- [十、分库分表](#十分库分表)
- [参考资料](#参考资料)
- [传送门](#传送门)
<!-- /TOC -->
## 一、Mysql 查询过程
## 一、SQL 执行过程
SQL 语句在 Mysql 中是如何执行的?
学习 Mysql最好是先从宏观上了解 Mysql 工作原理。
MySQL 可以分为 Server 层和存储引擎层两部分。
> 参考:[Mysql 工作流](docs/sql/mysql/mysql-index.md)
![img](http://dunwu.test.upcdn.net/snap/20200227201908.jpg)
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
### 1连接
MySQL 客户端/服务端通信是半双工模式:即任一时刻,要么是服务端向客户端发送数据,要么是客户端向服务器发送数据。
客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置`max_allowed_packet`参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。
### 2查询缓存
> **不建议使用数据库缓存,因为往往弊大于利**。
解析一个查询语句前,如果查询缓存是打开的,那么 MySQL 会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。
MySQL 将缓存存放在一个引用表(不要理解成`table`,可以认为是类似于`HashMap`的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。
**如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql 库中的系统表,其查询结果**
**都不会被缓存**。比如函数`NOW()`或者`CURRENT_DATE()`会因为不同的查询时间,返回不同的查询结果,再比如包含`CURRENT_USER`或者`CONNECION_ID()`的查询语句会因为不同的用户而返回不同的结果,将这样的查询结果缓存起来没有任何的意义。
既然是缓存就会失效那查询缓存何时失效呢MySQL 的查询缓存系统会跟踪查询中涉及的每个表如果这些表数据或结构发生变化那么和这张表相关的所有缓存数据都将失效。正因为如此在任何的写操作时MySQL 必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗,甚至导致系统僵死一会儿。而且查询缓存对系统的额外消耗也不仅仅在写操作,读操作也不例外:
- 任何的查询语句在开始之前都必须经过检查,即使这条 SQL 语句永远不会命中缓存
- 如果查询结果可以被缓存,那么执行完成后,会将结果存入缓存,也会带来额外的系统消耗
基于此,我们要知道并不是什么情况下查询缓存都会提高系统性能,缓存和失效都会带来额外消耗,只有当缓存带来的资源节约大于其本身消耗的资源时,才会给系统带来性能提升。但要如何评估打开缓存是否能够带来性能提升是一件非常困难的事情,也不在本文讨论的范畴内。如果系统确实存在一些性能问题,可以尝试打开查询缓存,并在数据库设计上做一些优化,比如:
- 用多个小表代替一个大表,注意不要过度设计
- 批量插入代替循环单条插入
- 合理控制缓存空间大小,一般来说其大小设置为几十兆比较合适
- 可以通过`SQL_CACHE`和`SQL_NO_CACHE`来控制某个查询语句是否需要进行缓存
最后的忠告是不要轻易打开查询缓存,特别是写密集型应用。如果你实在是忍不住,可以将`query_cache_type`设置为`DEMAND`,这时只有加入`SQL_CACHE`的查询才会走缓存,其他查询则不会,这样可以非常自由地控制哪些查询需要被缓存。
当然查询缓存系统本身是非常复杂的,这里讨论的也只是很小的一部分,其他更深入的话题,比如:缓存是如何使用内存的?如何控制内存的碎片化?事务对查询缓存有何影响等等,读者可以自行阅读相关资料,这里权当抛砖引玉吧。
### 3语法解析和预处理
MySQL 通过关键字将 SQL 语句进行解析,并生成一颗对应的语法解析树。这个过程解析器主要通过语法规则来验证和解析。比如 SQL 中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据 MySQL 规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。
### 4查询优化
经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成执行计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL 使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在 MySQL 可以通过查询当前会话的 `last_query_cost` 的值来得到其计算当前查询的成本。
```ruby
mysql> select * from t_message limit 10;
...省略结果集
mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+
```
示例中的结果表示优化器认为大概需要做 6391 个数据页的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布情况等等。
有非常多的原因会导致 MySQL 选择错误的执行计划比如统计信息不准确、不会考虑不受其控制的操作成本用户自定义函数、存储过程、MySQL 认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但 MySQL 值选择它认为成本小的,但成本小并不意味着执行时间短)等等。
MySQL 的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:
- 重新定义表的关联顺序(多张表关联查询时,并不一定按照 SQL 中指定的顺序进行,但有一些技巧可以指定关联顺序)
- 优化`MIN()`和`MAX()`函数(找某列的最小值,如果该列有索引,只需要查找 B+Tree 索引最左端,反之则可以找到最大值,具体原理见下文)
- 提前终止查询(比如:使用 Limit 时,查找到满足数量的结果集后会立即终止查询)
- 优化排序(在老版本 MySQL 会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于 I/O 密集型应用,效率会高很多)
随着 MySQL 的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个非常常用且容易理解的优化策略,其他的优化策略,大家自行查阅吧。
### 5查询执行引擎
在完成解析和优化阶段以后MySQL 会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为`handler API`。查询过程中的每一张表由一个`handler`实例表示。实际上MySQL 在查询优化阶段就为每一张表创建了一个`handler`实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。
### 6返回结果
查询过程的最后一个阶段就是将结果返回给客户端。即使查询不到数据MySQL 仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。
如果查询缓存被打开且这个查询可以被缓存MySQL 也会将结果存放到缓存中。
结果集返回客户端是一个增量且逐步返回的过程。有可能 MySQL 在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足 ① 中所描述的通信协议的数据包发送,再通过 TCP 协议进行传输,在传输过程中,可能对 MySQL 的数据包进行缓存然后批量发送。
### 小结
回头总结一下 MySQL 整个查询执行过程,总的来说分为 6 个步骤:
1. 客户端向 MySQL 服务器发送一条查询请求。
2. MySQL 服务器首先检查查询缓存,如果命中缓存,则立刻返回结果。否则进入下一阶段
3. MySQL 服务器进行 SQL 解析、预处理。
4. MySQL 服务器用优化器生成对应的执行计划。
5. MySQL 服务器根据执行计划,调用存储引擎的 API 来执行查询。
6. MySQL 服务器将结果返回给客户端,同时缓存查询结果。
## 二、Mysql 更新过程
Mysql 更新过程和 Mysql 查询过程类似。主要区别在于:
新流程还涉及两个重要的日志模块它们正是我们今天要讨论的主角redo log重做日志和 binlog归档日志
### redo log
**redo log 是 InnoDB 引擎特有的日志**。
**redo log 即重做日志**。redo log 是物理日志,记录的是“在某个数据页上做了什么修改”。
**redo log 用于保证 crash-safe 能力**。`innodb_flush_log_at_trx_commit` 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1这样可以保证 MySQL 异常重启之后数据不丢失。
**redo log 是基于 WAL 技术**。WAL 的全称是 **Write-Ahead Logging**,它的关键点就是**先写日志,再写磁盘**。
具体来说当有一条记录需要更新的时候InnoDB 引擎就会先把记录写到 redo log 里并更新内存这个时候更新就算完成了。同时InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写。
![img](http://dunwu.test.upcdn.net/snap/20200630180342.png)
有了 redo logInnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为**crash-safe**。
### bin log
**bin log 即归档日志**。binlog 是逻辑日志,记录的是这个语句的原始逻辑。
binlog 是可以追加写入的,即写到一定大小后会切换到下一个,并不会覆盖以前的日志。
**binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用**。
`sync_binlog` 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1这样可以保证 MySQL 异常重启之后 binlog 不丢失。
## 存储引擎
## 二、存储引擎
在文件系统中Mysql 将每个数据库(也可以成为 schema保存为数据目录下的一个子目录。创建表示Mysql 会在数据库子目录下创建一个和表同名的 `.frm` 文件保存表的定义。因为 Mysql 使用文件系统的目录和文件来保存数据库和表的定义大小写敏感性和具体平台密切相关。Windows 中大小写不敏感;类 Unix 中大小写敏感。**不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在 Mysql 服务层统一处理的。**
@ -238,7 +110,7 @@ InnoDB 是基于聚簇索引建立的,与其他存储引擎有很大不同。
支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## 数据类型
## 三、数据类型
### 整型
@ -306,23 +178,23 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提
- 应该尽量避免用字符串类型作为标识列,因为它们很消耗空间,并且通常比数字类型慢。对于 `MD5`、`SHA`、`UUID` 这类随机字符串,由于比较随机,所以可能分布在很大的空间内,导致 `INSERT` 以及一些 `SELECT` 语句变得很慢。
- 如果存储 UUID ,应该移除 `-` 符号;更好的做法是,用 `UNHEX()` 函数转换 UUID 值为 16 字节的数字,并存储在一个 `BINARY(16)` 的列中,检索时,可以通过 `HEX()` 函数来格式化为 16 进制格式。
## 索引
## 四、索引
详见:[Mysql 索引](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/mysql/mysql-index.md)
> 详见:[Mysql 索引](mysql-index.md)
## 锁
## 五、
详见:[Mysql 锁](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/mysql/mysql-lock.md)
> 详见:[Mysql 锁](mysql-lock.md)
## 事务
## 六、事务
详见:[Mysql 事务](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/mysql/mysql-transaction.md)
> 详见:[Mysql 事务](mysql-transaction.md)
## 性能优化
## 七、性能优化
详见:[Mysql 性能优化](https://github.com/dunwu/db-tutorial/blob/master/docs/sql/mysql/mysql-optimization.md)
> 详见:[Mysql 性能优化](mysql-optimization.md)
## 复制
## 八、复制
### 主从复制
@ -340,7 +212,6 @@ Mysql 支持两种复制:基于行的复制和基于语句的复制。
<img src="http://dunwu.test.upcdn.net/cs/database/mysql/master-slave.png" />
</div>
### 读写分离
主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。
@ -356,7 +227,17 @@ MySQL 读写分离能提高性能的原因在于:
<div align="center">
<img src="http://dunwu.test.upcdn.net/cs/database/mysql/master-slave-proxy.png" />
</div>
------
(分割线)以下为高级特性,也是关系型数据库通用方案
## 九、分布式事务
> 参考:[分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
## 十、分库分表
> 参考:[分布式存储基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-storage-theory.md)
## 参考资料

View File

@ -1,5 +1,11 @@
# Mysql 事务
> 不是所有的 Mysql 存储引擎都实现了事务处理。支持事务的存储引擎有:`InnoDB` 和 `NDB Cluster`。不支持事务的存储引擎,代表有:`MyISAM`。
>
> 用户可以根据业务是否需要事务处理(事务处理可以保证数据安全,但会增加系统开销),选择合适的存储引擎。
![img](http://dunwu.test.upcdn.net/snap/20200716074533.png)
<!-- TOC depthFrom:2 depthTo:3 -->
- [一、事务简介](#一事务简介)
@ -15,12 +21,9 @@
- [串行化](#串行化)
- [隔离级别小结](#隔离级别小结)
- [五、分布式事务](#五分布式事务)
- [两阶段提交](#两阶段提交)
- [柔性事务](#柔性事务)
- [事务方案对比](#事务方案对比)
- [六、事务最佳实践](#六事务最佳实践)
- [优化事务](#优化事务)
- [避免死锁](#避免死锁)
- [死锁](#死锁)
- [参考资料](#参考资料)
<!-- /TOC -->
@ -43,10 +46,6 @@ T<sub>1</sub> 和 T<sub>2</sub> 两个线程都对一个数据进行修改T<s
![img](http://dunwu.test.upcdn.net/cs/database/RDB/数据库并发一致性-丢失修改.png)
不是所有的 Mysql 存储引擎都实现了事务处理。支持事务的存储引擎有:`InnoDB` 和 `NDB Cluster`
用户可以根据业务是否需要事务处理(事务处理可以保证数据安全,但会增加系统开销),选择合适的存储引擎。
## 二、事务用法
### 事务处理指令
@ -161,7 +160,8 @@ ACID 是数据库事务正确执行的四个基本要素。
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题:
- **数据丢失**
- **丢失修改**
- **脏读**
- **不可重复读**
- **幻读**
@ -251,35 +251,28 @@ T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为 **本地事务**。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。
**分布式事务** 是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
**分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。**
### 两阶段提交
分布式事务的常见方案如下:
两阶段提交XA对业务侵入很小。 它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用基于 XA 协议的分布式事务。 XA 协议能够严格保障事务 `ACID` 特性。
- **两阶段提交2PC** - 将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
- **三阶段提交3PC** - 与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。将二阶段的准备阶段拆分为 2 个阶段,插入了一个 preCommit 阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
- **补偿事务TCC**
- **Try** - 操作作为一阶段,负责资源的检查和预留。
- **Confirm** - 操作作为二阶段提交操作,执行真正的业务。
- **Cancel** - 是预留资源的取消。
- **本地消息表** - 在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
- **MQ 事务** - 基于 MQ 的分布式事务方案其实是对本地消息表的封装。
- **SAGA** - Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
严格保障事务 `ACID` 特性是一把双刃剑。 事务执行在过程中需要将所需资源全部锁定,它更加适用于执行时间确定的短事务。 对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务系统并发性能衰退明显。 因此,在高并发的性能至上场景中,基于 XA 协议的分布式事务并不是最佳选择。
分布式事务方案分析:
### 柔性事务
- 2PC/3PC 依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
- TCC 适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
- 本地消息表/MQ 事务 都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
- Saga 事务 由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作导致补偿动作的实现比较麻烦例如业务是发送短信补偿动作则得再发送一次短信说明撤销用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。
如果将实现了`ACID` 的事务要素的事务称为刚性事务的话,那么基于`BASE`事务要素的事务则称为柔性事务。 `BASE`是基本可用、柔性状态和最终一致性这三个要素的缩写。
- 基本可用Basically Available保证分布式事务参与方不一定同时在线。
- 柔性状态Soft state则允许系统状态更新有一定的延时这个延时对客户来说不一定能够察觉。
- 而最终一致性Eventually consistent通常是通过消息传递的方式保证系统的最终一致性。
在`ACID`事务中对隔离性的要求很高,在事务执行过程中,必须将所有的资源锁定。柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。通过放宽对强一致性要求,来换取系统吞吐量的提升。
基于`ACID`的强一致性事务和基于`BASE`的最终一致性事务都不是银弹,只有在最适合的场景中才能发挥它们的最大长处。 可通过下表详细对比它们之间的区别,以帮助开发者进行技术选型。
### 事务方案对比
| | 本地事务 | 两(三)阶段事务 | 柔性事务 |
| :------- | :--------------- | :--------------- | --------------- |
| 业务改造 | 无 | 无 | 实现相关接口 |
| 一致性 | 不支持 | 支持 | 最终一致 |
| 隔离性 | 不支持 | 支持 | 业务方保证 |
| 并发性能 | 无影响 | 严重衰退 | 略微衰退 |
| 适合场景 | 业务方处理不一致 | 短事务 & 低并发 | 长事务 & 高并发 |
> 分布式事务详细说明、分析请参考:[分布式事务基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/distributed-transaction.md)
## 六、事务最佳实践
@ -305,15 +298,20 @@ T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插
如上图中的操作,虽然都是在一个事务中,但锁的申请在不同时间,只有当其他操作都执行完,才会释放所有锁。因为扣除库存是更新操作,属于行锁,这将会影响到其他操作该数据的事务,所以我们应该尽量避免长时间地持有该锁,尽快释放该锁。又因为先新建订单和先扣除库存都不会影响业务,所以我们可以将扣除库存操作放到最后,也就是使用执行顺序 1以此尽量减小锁的持有时间。
**在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。**
知道了这个设定,对我们使用事务有什么帮助呢?那就是,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
### 死锁
死锁是指两个或多个事务竞争同一资源,并请求锁定对方占用的资源,从而导致恶性循环的现象。
**死锁是指两个或多个事务竞争同一资源,并请求锁定对方占用的资源,从而导致恶性循环的现象**
产生死锁的场景:
当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。
- 当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。
- 多个事务同时锁定同一个资源时,也会产生死锁。
多个事务同时锁定同一个资源时,也会产生死锁。
#### 死锁的原因
@ -349,15 +347,24 @@ InnoDB 存储引擎的主键索引为聚簇索引,其它索引为辅助索引
#### 避免死锁
避免死锁最直观的方法就是在两个事务相互等待时,当一个事务的等待时间超过设置的某一阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。这种方法简单有效,在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的。
另外,我们还可以将 order_no 列设置为唯一索引列。虽然不能防止幻读,但我们可以利用它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会抛出异常。
我们还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。
#### 解决死锁
为了解决死锁问题不同数据库实现了各自的死锁检测和超时机制。InnoDB 的处理策略是:**将持有最少行级排它锁的事务进行回滚**。
当出现死锁以后,有两种策略:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 `innodb_lock_wait_timeout` 来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 `innodb_deadlock_detect` 设置为 on表示开启这个逻辑。
在 InnoDB 中innodb_lock_wait_timeout 的默认值是 50s意味着如果采用第一个策略当出现死锁以后第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候确实很快就可以解开但如果不是死锁而是简单的锁等待呢所以超时时间设置太短的话会出现很多误伤。
所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 `innodb_deadlock_detect` 的默认值本身就是 on。为了解决死锁问题不同数据库实现了各自的死锁检测和超时机制。InnoDB 的处理策略是:**将持有最少行级排它锁的事务进行回滚**。
主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。你可以想象一下这个过程:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
## 参考资料

View File

@ -0,0 +1,199 @@
# MySQL 工作流
<!-- TOC depthFrom:2 depthTo:3 -->
- [一、基础架构](#一基础架构)
- [二、查询过程](#二查询过程)
- [(一)连接](#一连接)
- [(二)查询缓存](#二查询缓存)
- [(三)语法分析](#三语法分析)
- [(四)查询优化](#四查询优化)
- [(五)查询执行引擎](#五查询执行引擎)
- [(六)返回结果](#六返回结果)
- [三、更新过程](#三更新过程)
- [redo log](#redo-log)
- [bin log](#bin-log)
- [redo log vs. bin log](#redo-log-vs-bin-log)
- [两阶段提交](#两阶段提交)
- [参考资料](#参考资料)
<!-- /TOC -->
## 一、基础架构
大体来说MySQL 可以分为 Server 层和存储引擎层两部分。
**Server 层包括连接器、查询缓存、分析器、优化器、执行器等**,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
**存储引擎层负责数据的存储和提取**。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
![img](http://dunwu.test.upcdn.net/snap/20200227201908.jpg)
## 二、查询过程
SQL 语句在 MySQL 中是如何执行的?
MySQL 整个查询执行过程,总的来说分为 6 个步骤:
1. 客户端和 MySQL 服务器建立连接;客户端向 MySQL 服务器发送一条查询请求。
2. MySQL 服务器首先检查查询缓存,如果命中缓存,则立刻返回结果。否则进入下一阶段。
3. MySQL 服务器进行 SQL 分析:语法分析、词法分析。
4. MySQL 服务器用优化器生成对应的执行计划。
5. MySQL 服务器根据执行计划,调用存储引擎的 API 来执行查询。
6. MySQL 服务器将结果返回给客户端,同时缓存查询结果。
### (一)连接
使用 MySQL 第一步自然是要连接数据库。
MySQL 客户端/服务端通信是**半双工模式**:即任一时刻,要么是服务端向客户端发送数据,要么是客户端向服务器发送数据。客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置`max_allowed_packet`参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。
MySQL 客户端连接命令:`mysql -h<主机> -P<端口> -u<用户名> -p<密码>`。如果没有显式指定密码,会要求输入密码才能访问。
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 `show processlist` 命令中看到它。客户端如果太长时间没动静,连接器就会自动将它断开。**客户端连接维持时间是由参数 `wait_timeout` 控制的,默认值是 8 小时**。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: `Lost connection to MySQL server during query`。这时候如果你要继续,就需要重连,然后再执行请求了。
建立连接的过程通常是比较复杂的,建议在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。为了在程序中提高数据库连接的服用了,一般会使用数据库连接池来维护管理。
但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来可能导致内存占用太大被系统强行杀掉OOM从现象看就是 MySQL 异常重启了。
怎么解决这个问题呢?你可以考虑以下两种方案。
- **定期断开长连接**。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
- 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 `mysql_reset_connection` 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
### (二)查询缓存
> **不建议使用数据库缓存,因为往往弊大于利**。
解析一个查询语句前,如果查询缓存是打开的,那么 MySQL 会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。
MySQL 将缓存存放在一个引用表(不要理解成`table`,可以认为是类似于`HashMap`的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。
**如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql 库中的系统表,其查询结果**
**都不会被缓存**。比如函数`NOW()`或者`CURRENT_DATE()`会因为不同的查询时间,返回不同的查询结果,再比如包含`CURRENT_USER`或者`CONNECION_ID()`的查询语句会因为不同的用户而返回不同的结果,将这样的查询结果缓存起来没有任何的意义。
**不建议使用数据库缓存,因为往往弊大于利**。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 `query_cache_type` 设置成 `DEMAND`,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 `SQL_CACHE` 显式指定,像下面这个语句一样:
```sql
select SQL_CACHE * from T where ID=10;
```
> 注意MySQL 8.0 版本直接将查询缓存的整块功能删掉了。
### (三)语法分析
如果没有命中查询缓存就要开始真正执行语句了。首先MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。MySQL 通过关键字对 SQL 语句进行解析,并生成一颗对应的语法解析树。这个过程中,分析器主要通过语法规则来验证和解析。比如 SQL 中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据 MySQL 规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。
- 分析器先会先做“**词法分析**”。你输入的是由多个字符串和空格组成的一条 SQL 语句MySQL 需要识别出里面的字符串分别是什么代表什么。MySQL 从你输入的"select"这个关键字识别出来这是一个查询语句。它也要把字符串“T”识别成“表名 T”把字符串“ID”识别成“列 ID”。
- 接下来,要做“**语法分析**”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对就会收到“You have an error in your SQL syntax”的错误提醒比如下面这个语句 select 少打了开头的字母“s”。
### (四)查询优化
经过了分析器MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成执行计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL 使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在 MySQL 可以通过查询当前会话的 `last_query_cost` 的值来得到其计算当前查询的成本。
```ruby
mysql> select * from t_message limit 10;
...省略结果集
mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+
```
示例中的结果表示优化器认为大概需要做 6391 个数据页的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布情况等等。
有非常多的原因会导致 MySQL 选择错误的执行计划比如统计信息不准确、不会考虑不受其控制的操作成本用户自定义函数、存储过程、MySQL 认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但 MySQL 值选择它认为成本小的,但成本小并不意味着执行时间短)等等。
MySQL 的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:
- 重新定义表的关联顺序(多张表关联查询时,并不一定按照 SQL 中指定的顺序进行,但有一些技巧可以指定关联顺序)
- 优化`MIN()`和`MAX()`函数(找某列的最小值,如果该列有索引,只需要查找 B+Tree 索引最左端,反之则可以找到最大值,具体原理见下文)
- 提前终止查询(比如:使用 Limit 时,查找到满足数量的结果集后会立即终止查询)
- 优化排序(在老版本 MySQL 会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于 I/O 密集型应用,效率会高很多)
随着 MySQL 的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个非常常用且容易理解的优化策略,其他的优化策略,大家自行查阅吧。
### (五)查询执行引擎
在完成解析和优化阶段以后MySQL 会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为`handler API`。查询过程中的每一张表由一个`handler`实例表示。实际上MySQL 在查询优化阶段就为每一张表创建了一个`handler`实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。
### (六)返回结果
查询过程的最后一个阶段就是将结果返回给客户端。即使查询不到数据MySQL 仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。
如果查询缓存被打开且这个查询可以被缓存MySQL 也会将结果存放到缓存中。
结果集返回客户端是一个增量且逐步返回的过程。有可能 MySQL 在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足 ① 中所描述的通信协议的数据包发送,再通过 TCP 协议进行传输,在传输过程中,可能对 MySQL 的数据包进行缓存然后批量发送。
## 三、更新过程
MySQL 更新过程和 MySQL 查询过程类似,也会将流程走一遍。不一样的是:**更新流程还涉及两个重要的日志模块redo log重做日志和 binlog归档日志**。
### redo log
**redo log 是 InnoDB 引擎特有的日志**。**redo log 即重做日志**。redo log 是物理日志,记录的是“在某个数据页上做了什么修改”。
**redo log 是基于 WAL 技术**。WAL 的全称是 **Write-Ahead Logging**,它的关键点就是**先写日志,再写磁盘**。具体来说当有一条记录需要更新的时候InnoDB 引擎就会先把记录写到 redo log 里并更新内存这个时候更新就算完成了。同时InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写。
![img](http://dunwu.test.upcdn.net/snap/20200630180342.png)
有了 redo logInnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为**crash-safe**。
### bin log
**bin log 即归档日志**。binlog 是逻辑日志,记录的是这个语句的原始逻辑。
binlog 是可以追加写入的,即写到一定大小后会切换到下一个,并不会覆盖以前的日志。
**binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用**。
`sync_binlog` 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1这样可以保证 MySQL 异常重启之后 binlog 不丢失。
### redo log vs. bin log
这两种日志有以下三点不同。
- redo log 是 InnoDB 引擎特有的binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志记录的是“在某个数据页上做了什么修改”binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的空间固定会用完binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2. 执行器拿到引擎给的行数据,把这个值加上 1比如原来是 N现在就是 N+1得到新的一行数据再调用引擎接口写入这行新数据。
3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
4. 执行器生成这个操作的 binlog并把 binlog 写入磁盘。
5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交commit状态更新完成。
这里我给出这个 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
![img](http://dunwu.test.upcdn.net/snap/20200714133806.png)
### 两阶段提交
redo log 的写入拆成了两个步骤prepare 和 commit这就是"两阶段提交"。为什么日志需要“两阶段提交”。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog或者采用反过来的顺序。我们看看这两种方式会有什么问题。
- **先写 redo log 后写 binlog**。假设在 redo log 写完binlog 还没有写完的时候MySQL 进程异常重启。由于我们前面说过的redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。
但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0与原库的值不同。
- **先写 binlog 后写 redo log**。如果在 binlog 写完之后 crash由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
## 参考资料
- [《高性能 MySQL》](https://book.douban.com/subject/23008813/)
- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139)

View File

@ -1,4 +1,4 @@
# PostgreSQL 入门指南
# PostgreSQL 应用指南
> [PostgreSQL](https://www.postgresql.org/) 是一个关系型数据库RDBM
>