mirror of https://github.com/dunwu/db-tutorial.git
update docs
parent
c9559efa64
commit
4043b110fc
|
@ -18,7 +18,7 @@
|
|||
- :two: [Nosql](docs/nosql/README.md)
|
||||
- :three: [Mysql](docs/sql/mysql/README.md)
|
||||
- [Mysql 命令](docs/sql/mysql/mysql-cli.md)
|
||||
- [Mysql 维护](docs/sql/mysql/mysql-maintain.md)
|
||||
- [Mysql 维护](docs/sql/mysql/mysql-ops.md)
|
||||
- [Mysql 原理](docs/sql/mysql/mysql-theory.md)
|
||||
- :four: [Redis](docs/nosql/redis/README.md)
|
||||
- [Redis 快速入门](docs/nosql/redis/redis-quickstart.md)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
- :two: [Nosql](nosql/README.md)
|
||||
- :three: [Mysql](sql/mysql/README.md)
|
||||
- [Mysql 命令](sql/mysql/mysql-cli.md)
|
||||
- [Mysql 维护](sql/mysql/mysql-maintain.md)
|
||||
- [Mysql 维护](sql/mysql/mysql-ops.md)
|
||||
- [Mysql 原理](sql/mysql/mysql-theory.md)
|
||||
- :four: [Redis](nosql/redis/README.md)
|
||||
- [Redis 快速入门](nosql/redis/redis-quickstart.md)
|
||||
|
|
|
@ -46,4 +46,4 @@ cd /opt/mongodb/mongodb-3.6.3/bin
|
|||
|
||||
## 脚本
|
||||
|
||||
| [安装脚本](https://github.com/dunwu/linux/tree/master/codes/deploy/tool/mongodb) |
|
||||
| [安装脚本](https://github.com/dunwu/linux-tutorial/tree/master/codes/linux/soft) |
|
||||
|
|
|
@ -55,4 +55,4 @@ cd /opt/redis/redis-4.0.8/src
|
|||
|
||||
以上两种安装方式,我都写了脚本去执行:
|
||||
|
||||
| [安装脚本](https://github.com/dunwu/linux/tree/master/codes/deploy/tool/redis) |
|
||||
| [安装脚本](https://github.com/dunwu/linux-tutorial/tree/master/codes/linux/soft) |
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## :memo: 知识点
|
||||
|
||||
- [mysql 维护](mysql-maintain.md)
|
||||
- [mysql 维护](mysql-ops.md)
|
||||
- [mysql 命令](mysql-cli.md)
|
||||
- [mysql 原理](mysql-theory.md)
|
||||
|
||||
|
|
|
@ -4,35 +4,35 @@
|
|||
|
||||
<!-- TOC depthFrom:2 depthTo:3 -->
|
||||
|
||||
- [1. 索引](#1-索引)
|
||||
- [1.1. 索引的优点和缺点](#11-索引的优点和缺点)
|
||||
- [1.2. 索引类型](#12-索引类型)
|
||||
- [1.3. 索引数据结构](#13-索引数据结构)
|
||||
- [1.4. 索引原则](#14-索引原则)
|
||||
- [2. 事务](#2-事务)
|
||||
- [2.1. ACID](#21-acid)
|
||||
- [2.2. 并发一致性问题](#22-并发一致性问题)
|
||||
- [2.3. 事务隔离级别](#23-事务隔离级别)
|
||||
- [3. 并发控制](#3-并发控制)
|
||||
- [3.1. 锁粒度](#31-锁粒度)
|
||||
- [3.2. 锁类型](#32-锁类型)
|
||||
- [3.3. 锁协议](#33-锁协议)
|
||||
- [3.4. 死锁](#34-死锁)
|
||||
- [4. 多版本并发控制](#4-多版本并发控制)
|
||||
- [4.1. 版本号](#41-版本号)
|
||||
- [4.2. Undo 日志](#42-undo-日志)
|
||||
- [4.3. 实现过程](#43-实现过程)
|
||||
- [4.4. 快照读与当前读](#44-快照读与当前读)
|
||||
- [5. 分库分表](#5-分库分表)
|
||||
- [5.1. 水平拆分](#51-水平拆分)
|
||||
- [5.2. 垂直拆分](#52-垂直拆分)
|
||||
- [5.3. Sharding 策略](#53-sharding-策略)
|
||||
- [5.4. 分库分表的问题及解决方案](#54-分库分表的问题及解决方案)
|
||||
- [5.5. 常用的分库分表中间件](#55-常用的分库分表中间件)
|
||||
- [6. SQL 优化](#6-sql-优化)
|
||||
- [6.1. 使用执行计划进行分析](#61-使用执行计划进行分析)
|
||||
- [6.2. 优化数据访问](#62-优化数据访问)
|
||||
- [6.3. 重构查询方式](#63-重构查询方式)
|
||||
- [1. 事务](#1-事务)
|
||||
- [1.1. ACID](#11-acid)
|
||||
- [1.2. 并发一致性问题](#12-并发一致性问题)
|
||||
- [1.3. 事务隔离级别](#13-事务隔离级别)
|
||||
- [2. 并发控制](#2-并发控制)
|
||||
- [2.1. 锁粒度](#21-锁粒度)
|
||||
- [2.2. 锁类型](#22-锁类型)
|
||||
- [2.3. 锁协议](#23-锁协议)
|
||||
- [2.4. 死锁](#24-死锁)
|
||||
- [3. 多版本并发控制](#3-多版本并发控制)
|
||||
- [3.1. 版本号](#31-版本号)
|
||||
- [3.2. Undo 日志](#32-undo-日志)
|
||||
- [3.3. 实现过程](#33-实现过程)
|
||||
- [3.4. 快照读与当前读](#34-快照读与当前读)
|
||||
- [4. SQL 优化](#4-sql-优化)
|
||||
- [4.1. 使用执行计划进行分析](#41-使用执行计划进行分析)
|
||||
- [4.2. 优化数据访问](#42-优化数据访问)
|
||||
- [4.3. 重构查询方式](#43-重构查询方式)
|
||||
- [5. 索引](#5-索引)
|
||||
- [5.1. 索引的优点和缺点](#51-索引的优点和缺点)
|
||||
- [5.2. 索引类型](#52-索引类型)
|
||||
- [5.3. 索引数据结构](#53-索引数据结构)
|
||||
- [5.4. 索引原则](#54-索引原则)
|
||||
- [6. 分库分表](#6-分库分表)
|
||||
- [6.1. 水平拆分](#61-水平拆分)
|
||||
- [6.2. 垂直拆分](#62-垂直拆分)
|
||||
- [6.3. Sharding 策略](#63-sharding-策略)
|
||||
- [6.4. 分库分表的问题及解决方案](#64-分库分表的问题及解决方案)
|
||||
- [6.5. 常用的分库分表中间件](#65-常用的分库分表中间件)
|
||||
- [7. 关系数据库设计理论](#7-关系数据库设计理论)
|
||||
- [7.1. 函数依赖](#71-函数依赖)
|
||||
- [7.2. 异常](#72-异常)
|
||||
|
@ -41,182 +41,14 @@
|
|||
|
||||
<!-- /TOC -->
|
||||
|
||||
## 1. 索引
|
||||
|
||||
索引能够轻易将查询性能提升几个数量级。
|
||||
|
||||
- 数据量小的表,使用全表扫描比建立索引更高效。
|
||||
- 数据量大的表,使用索引更高效。
|
||||
- 数据量特大的表,建立和维护索引的代价将会随之增长,可以使用分区技术。
|
||||
|
||||
### 1.1. 索引的优点和缺点
|
||||
|
||||
优点:
|
||||
|
||||
- 索引大大减少了服务器需要扫描的数据量。
|
||||
- 索引可以帮助服务器避免排序和临时表。
|
||||
- 索引可以将随机 I/O 变为顺序 I/O。
|
||||
|
||||
缺点:
|
||||
|
||||
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
|
||||
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立组合索引那么需要的空间就会更大。
|
||||
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
|
||||
|
||||
### 1.2. 索引类型
|
||||
|
||||
主流的关系型数据库一般都支持以下索引类型:
|
||||
|
||||
- 普通索引:最基本的索引,没有任何限制。
|
||||
- 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
|
||||
- 主键索引:一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。
|
||||
- 组合索引:多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
|
||||
|
||||
### 1.3. 索引数据结构
|
||||
|
||||
**主流数据库的索引一般使用的数据结构为:B-Tree 或 B+Tree。**
|
||||
|
||||
#### B-Tree
|
||||
|
||||
B-Tree 不同于 Binary Tree(二叉树,最多有两个子树),它是平衡搜索树。
|
||||
|
||||
一棵 M 阶的 B-Tree 满足以下条件:
|
||||
|
||||
- 每个结点至多有 M 个孩子;
|
||||
- 除根结点和叶结点外,其它每个结点至少有 M/2 个孩子;
|
||||
- 根结点至少有两个孩子(除非该树仅包含一个结点);
|
||||
- 所有叶结点在同一层,叶结点不包含任何关键字信息;
|
||||
- 有 K 个关键字的非叶结点恰好包含 K+1 个孩子;
|
||||
|
||||
对于任意结点,其内部的关键字 Key 是升序排列的。每个节点中都包含了 data。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/B-TREE.png" />
|
||||
</div>
|
||||
|
||||
对于每个结点,主要包含一个关键字数组 Key[],一个指针数组(指向儿子)Son[]。
|
||||
|
||||
在 B-Tree 内,查找的流程是:
|
||||
|
||||
1. 使用顺序查找(数组长度较短时)或折半查找方法查找 Key[]数组,若找到关键字 K,则返回该结点的地址及 K 在 Key[]中的位置;
|
||||
2. 否则,可确定 K 在某个 Key[i]和 Key[i+1]之间,则从 Son[i]所指的子结点继续查找,直到在某结点中查找成功;
|
||||
3. 或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。
|
||||
|
||||
#### B+Tree
|
||||
|
||||
B+Tree 是 B-Tree 的变种:
|
||||
|
||||
- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度)。
|
||||
- 非叶子节点不存储 data,只存储 key;叶子节点不存储指针。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/B+TREE.png" />
|
||||
</div>
|
||||
|
||||
由于并不是所有节点都具有相同的域,因此 B+Tree 中叶节点和内节点一般大小不同。这点与 B-Tree 不同,虽然 B-Tree 中不同节点存放的 key 和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中 B-Tree 往往对每个节点申请同等大小的空间。
|
||||
|
||||
**带有顺序访问指针的 B+Tree**
|
||||
|
||||
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 的基础上进行了优化,增加了顺序访问指针。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/带有顺序访问指针的B+Tree.png" />
|
||||
</div>
|
||||
|
||||
在 B+Tree 的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的 B+Tree。
|
||||
|
||||
这个优化的目的是为了提高区间访问的性能,例如上图中如果要查询 key 为从 18 到 49 的所有数据记录,当找到 18 后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。
|
||||
|
||||
#### Hash
|
||||
|
||||
Hash 索引只有精确匹配索引所有列的查询才有效。
|
||||
|
||||
对于每一行数据,对所有的索引列计算一个 hashcode。哈希索引将所有的 hashcode 存储在索引中,同时在 Hash 表中保存指向每个数据行的指针。
|
||||
|
||||
哈希索引的优点:
|
||||
|
||||
- 因为索引数据结构紧凑,所以查询速度非常快。
|
||||
|
||||
哈希索引的缺点:
|
||||
|
||||
- 哈希索引数据不是按照索引值顺序存储的,所以无法用于排序。
|
||||
- 哈希索引不支持部分索引匹配查找。如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A,无法使用该索引。
|
||||
- 哈希索引只支持等值比较查询,不支持任何范围查询,如 WHERE price > 100。
|
||||
- 哈希索引有可能出现哈希冲突,出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。
|
||||
|
||||
### 1.4. 索引原则
|
||||
|
||||
#### 独立的列
|
||||
|
||||
如果查询中的列不是独立的列,则数据库不会使用索引。
|
||||
|
||||
“独立的列” 是指索引列不能是表达式的一部分,也不能是函数的参数。
|
||||
|
||||
:x: 错误示例:
|
||||
|
||||
```sql
|
||||
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
|
||||
SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10;
|
||||
```
|
||||
|
||||
#### 前缀索引和索引选择性
|
||||
|
||||
有时候需要索引很长的字符列,这会让索引变得大且慢。
|
||||
|
||||
解决方法是:可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。
|
||||
|
||||
索引的选择性是指:不重复的索引值和数据表记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
|
||||
|
||||
对于 BLOB/TEXT/VARCHAR 这种文本类型的列,必须使用前缀索引,因为数据库往往不允许索引这些列的完整长度。
|
||||
|
||||
要选择足够长的前缀以保证较高的选择性,同时又不能太长(节约空间)。
|
||||
|
||||
#### 多列索引
|
||||
|
||||
不要为每个列创建独立的索引。
|
||||
|
||||
#### 选择合适的索引列顺序
|
||||
|
||||
经验法则:将选择性高的列或基数大的列优先排在多列索引最前列。
|
||||
|
||||
但有时,也需要考虑 WHERE 子句中的排序、分组和范围条件等因素,这些因素也会对查询性能造成较大影响。
|
||||
|
||||
#### 聚簇索引
|
||||
|
||||
聚簇索引不是一种单独的索引类型,而是一种数据存储方式。
|
||||
|
||||
聚簇表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
|
||||
|
||||
#### 覆盖索引
|
||||
|
||||
索引包含所有需要查询的字段的值。
|
||||
|
||||
具有以下优点:
|
||||
|
||||
- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
|
||||
- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
|
||||
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
|
||||
|
||||
#### 使用索引扫描来做排序
|
||||
|
||||
索引最好既满足排序,又用于查找行。这样,就可以使用索引来对结果排序。
|
||||
|
||||
#### = 和 in 可以乱序
|
||||
|
||||
比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式。
|
||||
|
||||
#### 尽量的扩展索引,不要新建索引
|
||||
|
||||
比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
|
||||
|
||||
## 2. 事务
|
||||
## 1. 事务
|
||||
|
||||
> 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/数据库事务.png"/>
|
||||
</div>
|
||||
### 2.1. ACID
|
||||
### 1.1. ACID
|
||||
|
||||
- **原子性(Automicity)**
|
||||
- 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。
|
||||
|
@ -243,7 +75,7 @@ SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10;
|
|||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/数据库ACID.png"/>
|
||||
</div>
|
||||
|
||||
### 2.2. 并发一致性问题
|
||||
### 1.2. 并发一致性问题
|
||||
|
||||
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。
|
||||
|
||||
|
@ -281,7 +113,7 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插
|
|||
|
||||
并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
|
||||
|
||||
### 2.3. 事务隔离级别
|
||||
### 1.3. 事务隔离级别
|
||||
|
||||
- **`未提交读(READ UNCOMMITTED)`** - 事务中的修改,即使没有提交,对其它事务也是可见的。
|
||||
- **`提交读(READ COMMITTED)`** - 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
|
||||
|
@ -295,11 +127,11 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插
|
|||
| 重复读 | ❌ | ❌ | ✔ |
|
||||
| 串行化 | ❌ | ❌ | ❌ |
|
||||
|
||||
## 3. 并发控制
|
||||
## 2. 并发控制
|
||||
|
||||
无论何时,只要有多个查询需要在同一时刻修改数据,就会产生并发控制的问题。
|
||||
|
||||
### 3.1. 锁粒度
|
||||
### 2.1. 锁粒度
|
||||
|
||||
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。
|
||||
|
||||
|
@ -312,7 +144,7 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插
|
|||
- **表级锁(table lock)** - 锁定整张表。用户对表进行写操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获得读锁,读锁之间不会相互阻塞。
|
||||
- **行级锁(row lock)** - 仅对指定的行记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。
|
||||
|
||||
### 3.2. 锁类型
|
||||
### 2.2. 锁类型
|
||||
|
||||
#### 读写锁
|
||||
|
||||
|
@ -358,7 +190,7 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插
|
|||
- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
|
||||
- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。
|
||||
|
||||
### 3.3. 锁协议
|
||||
### 2.3. 锁协议
|
||||
|
||||
#### 三级锁协议
|
||||
|
||||
|
@ -443,13 +275,13 @@ lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
|
|||
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)
|
||||
```
|
||||
|
||||
### 3.4. 死锁
|
||||
### 2.4. 死锁
|
||||
|
||||
**死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象**。
|
||||
|
||||
当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定一个资源时,也会产生死锁。
|
||||
|
||||
## 4. 多版本并发控制
|
||||
## 3. 多版本并发控制
|
||||
|
||||
多版本并发控制(Multi-Version Concurrency Control, MVCC)是实现隔离级别的一种具体方式。
|
||||
|
||||
|
@ -461,7 +293,7 @@ MVCC 可以视为行级锁的一个变种,但是它在很多情况下避免了
|
|||
|
||||
MVCC 的实现,是通过保存数据在某个时间的快照来实现的。
|
||||
|
||||
### 4.1. 版本号
|
||||
### 3.1. 版本号
|
||||
|
||||
- **`系统版本号`** - 是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
|
||||
- **`事务版本号`** - 事务开始时的系统版本号。
|
||||
|
@ -471,11 +303,11 @@ MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版
|
|||
- **`创建版本号`** - 指示创建一个数据行的快照时的系统版本号;
|
||||
- **`删除版本号`** - 如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
|
||||
|
||||
### 4.2. Undo 日志
|
||||
### 3.2. Undo 日志
|
||||
|
||||
MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
|
||||
|
||||
### 4.3. 实现过程
|
||||
### 3.3. 实现过程
|
||||
|
||||
以下实现过程针对可重复读隔离级别。
|
||||
|
||||
|
@ -513,7 +345,7 @@ MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把
|
|||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/mvcc_update.png"/>
|
||||
</div>
|
||||
|
||||
### 4.4. 快照读与当前读
|
||||
### 3.4. 快照读与当前读
|
||||
|
||||
#### 快照读
|
||||
|
||||
|
@ -535,13 +367,244 @@ update;
|
|||
delete;
|
||||
```
|
||||
|
||||
## 5. 分库分表
|
||||
## 4. SQL 优化
|
||||
|
||||
### 4.1. 使用执行计划进行分析
|
||||
|
||||
执行计划 Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
|
||||
|
||||
比较重要的字段有:
|
||||
|
||||
- select_type : 查询类型,有简单查询、联合查询、子查询等
|
||||
- key : 使用的索引
|
||||
- rows : 扫描的行数
|
||||
|
||||
更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
|
||||
|
||||
### 4.2. 优化数据访问
|
||||
|
||||
#### 减少请求的数据量
|
||||
|
||||
- **只返回必要的列** - 最好不要使用 `SELECT *` 语句。
|
||||
- **只返回必要的行** - 使用 `WHERE` 语句进行查询过滤,有时候也需要使用 `LIMIT` 语句来限制返回的数据。
|
||||
- **缓存重复查询的数据** - 使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
|
||||
|
||||
#### 减少服务器端扫描的行数
|
||||
|
||||
最有效的方式是使用索引来覆盖查询。
|
||||
|
||||
### 4.3. 重构查询方式
|
||||
|
||||
#### 切分大查询
|
||||
|
||||
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
|
||||
|
||||
```sql
|
||||
DELEFT 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
|
||||
```
|
||||
|
||||
#### 分解大连接查询
|
||||
|
||||
将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有:
|
||||
|
||||
- **缓存更高效**。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
|
||||
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而**减少冗余记录的查询**。
|
||||
- **减少锁竞争**;
|
||||
- **在应用层进行连接,可以更容易对数据库进行拆分**,从而更容易做到高性能和可扩展。
|
||||
- **查询本身效率也可能会有所提升**。例如下面的例子中,使用 `IN()` 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
|
||||
|
||||
```sql
|
||||
SELECT * FROM tag
|
||||
JOIN tag_post ON tag_post.tag_id=tag.id
|
||||
JOIN post ON tag_post.post_id=post.id
|
||||
WHERE tag.tag='mysql';
|
||||
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);
|
||||
```
|
||||
|
||||
## 5. 索引
|
||||
|
||||
索引能够轻易将查询性能提升几个数量级。
|
||||
|
||||
- 数据量小的表,使用全表扫描比建立索引更高效。
|
||||
- 数据量大的表,使用索引更高效。
|
||||
- 数据量特大的表,建立和维护索引的代价将会随之增长,可以使用分区技术。
|
||||
|
||||
### 5.1. 索引的优点和缺点
|
||||
|
||||
优点:
|
||||
|
||||
- 索引大大减少了服务器需要扫描的数据量。
|
||||
- 索引可以帮助服务器避免排序和临时表。
|
||||
- 索引可以将随机 I/O 变为顺序 I/O。
|
||||
|
||||
缺点:
|
||||
|
||||
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
|
||||
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立组合索引那么需要的空间就会更大。
|
||||
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
|
||||
|
||||
### 5.2. 索引类型
|
||||
|
||||
主流的关系型数据库一般都支持以下索引类型:
|
||||
|
||||
- 普通索引:最基本的索引,没有任何限制。
|
||||
- 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
|
||||
- 主键索引:一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。
|
||||
- 组合索引:多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合。
|
||||
|
||||
### 5.3. 索引数据结构
|
||||
|
||||
**主流数据库的索引一般使用的数据结构为:B-Tree 或 B+Tree。**
|
||||
|
||||
#### B-Tree
|
||||
|
||||
B-Tree 不同于 Binary Tree(二叉树,最多有两个子树),它是平衡搜索树。
|
||||
|
||||
一棵 M 阶的 B-Tree 满足以下条件:
|
||||
|
||||
- 每个结点至多有 M 个孩子;
|
||||
- 除根结点和叶结点外,其它每个结点至少有 M/2 个孩子;
|
||||
- 根结点至少有两个孩子(除非该树仅包含一个结点);
|
||||
- 所有叶结点在同一层,叶结点不包含任何关键字信息;
|
||||
- 有 K 个关键字的非叶结点恰好包含 K+1 个孩子;
|
||||
|
||||
对于任意结点,其内部的关键字 Key 是升序排列的。每个节点中都包含了 data。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/B-TREE.png" />
|
||||
</div>
|
||||
|
||||
对于每个结点,主要包含一个关键字数组 Key[],一个指针数组(指向儿子)Son[]。
|
||||
|
||||
在 B-Tree 内,查找的流程是:
|
||||
|
||||
1. 使用顺序查找(数组长度较短时)或折半查找方法查找 Key[]数组,若找到关键字 K,则返回该结点的地址及 K 在 Key[]中的位置;
|
||||
2. 否则,可确定 K 在某个 Key[i]和 Key[i+1]之间,则从 Son[i]所指的子结点继续查找,直到在某结点中查找成功;
|
||||
3. 或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。
|
||||
|
||||
#### B+Tree
|
||||
|
||||
B+Tree 是 B-Tree 的变种:
|
||||
|
||||
- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度)。
|
||||
- 非叶子节点不存储 data,只存储 key;叶子节点不存储指针。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/B+TREE.png" />
|
||||
</div>
|
||||
|
||||
由于并不是所有节点都具有相同的域,因此 B+Tree 中叶节点和内节点一般大小不同。这点与 B-Tree 不同,虽然 B-Tree 中不同节点存放的 key 和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中 B-Tree 往往对每个节点申请同等大小的空间。
|
||||
|
||||
**带有顺序访问指针的 B+Tree**
|
||||
|
||||
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 的基础上进行了优化,增加了顺序访问指针。
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/带有顺序访问指针的B+Tree.png" />
|
||||
</div>
|
||||
|
||||
在 B+Tree 的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的 B+Tree。
|
||||
|
||||
这个优化的目的是为了提高区间访问的性能,例如上图中如果要查询 key 为从 18 到 49 的所有数据记录,当找到 18 后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。
|
||||
|
||||
#### Hash
|
||||
|
||||
Hash 索引只有精确匹配索引所有列的查询才有效。
|
||||
|
||||
对于每一行数据,对所有的索引列计算一个 hashcode。哈希索引将所有的 hashcode 存储在索引中,同时在 Hash 表中保存指向每个数据行的指针。
|
||||
|
||||
哈希索引的优点:
|
||||
|
||||
- 因为索引数据结构紧凑,所以查询速度非常快。
|
||||
|
||||
哈希索引的缺点:
|
||||
|
||||
- 哈希索引数据不是按照索引值顺序存储的,所以无法用于排序。
|
||||
- 哈希索引不支持部分索引匹配查找。如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A,无法使用该索引。
|
||||
- 哈希索引只支持等值比较查询,不支持任何范围查询,如 WHERE price > 100。
|
||||
- 哈希索引有可能出现哈希冲突,出现哈希冲突时,必须遍历链表中所有的行指针,逐行比较,直到找到符合条件的行。
|
||||
|
||||
### 5.4. 索引原则
|
||||
|
||||
- **独立的列**
|
||||
|
||||
如果查询中的列不是独立的列,则数据库不会使用索引。
|
||||
|
||||
“独立的列” 是指索引列不能是表达式的一部分,也不能是函数的参数。
|
||||
|
||||
❌ 错误示例:
|
||||
|
||||
```sql
|
||||
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
|
||||
SELECT ... WHERE TO_DAYS(CURRENT_DAT) - TO_DAYS(date_col) <= 10;
|
||||
```
|
||||
|
||||
- **前缀索引和索引选择性**
|
||||
|
||||
有时候需要索引很长的字符列,这会让索引变得大且慢。
|
||||
|
||||
解决方法是:可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。
|
||||
|
||||
索引的选择性是指:不重复的索引值和数据表记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
|
||||
|
||||
对于 `BLOB`/`TEXT`/`VARCHAR` 这种文本类型的列,必须使用前缀索引,因为数据库往往不允许索引这些列的完整长度。
|
||||
|
||||
要选择足够长的前缀以保证较高的选择性,同时又不能太长(节约空间)。
|
||||
|
||||
- **多列索引**
|
||||
|
||||
不要为每个列创建独立的索引。
|
||||
|
||||
- **选择合适的索引列顺序**
|
||||
|
||||
经验法则:将选择性高的列或基数大的列优先排在多列索引最前列。
|
||||
|
||||
但有时,也需要考虑 WHERE 子句中的排序、分组和范围条件等因素,这些因素也会对查询性能造成较大影响。
|
||||
|
||||
- **聚簇索引**
|
||||
|
||||
聚簇索引不是一种单独的索引类型,而是一种数据存储方式。
|
||||
|
||||
聚簇表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
|
||||
|
||||
- **覆盖索引**
|
||||
|
||||
索引包含所有需要查询的字段的值。
|
||||
|
||||
具有以下优点:
|
||||
|
||||
- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
|
||||
- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
|
||||
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
|
||||
- **使用索引扫描来做排序**
|
||||
|
||||
索引最好既满足排序,又用于查找行。这样,就可以使用索引来对结果排序。
|
||||
|
||||
- **= 和 in 可以乱序**
|
||||
|
||||
比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式。
|
||||
|
||||
- **尽量的扩展索引,不要新建索引**
|
||||
|
||||
比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
|
||||
|
||||
## 6. 分库分表
|
||||
|
||||
分库分表的基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。
|
||||
|
||||
当然,现实中更多是这两种情况混杂在一起,这时候需要根据实际情况做出选择,也可能会综合使用垂直与水平切分,从而将原有数据库切分成类似矩阵一样可以无限扩充的数据库(server)阵列。
|
||||
|
||||
### 5.1. 水平拆分
|
||||
### 6.1. 水平拆分
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/数据库水平拆分.jpg" width="500" />
|
||||
|
@ -551,7 +614,7 @@ delete;
|
|||
|
||||
水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
|
||||
|
||||
### 5.2. 垂直拆分
|
||||
### 6.2. 垂直拆分
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/turnon/images/raw/master/images/database/RDB/数据库垂直拆分.jpg" width="500" />
|
||||
|
@ -561,13 +624,13 @@ delete;
|
|||
|
||||
如果是因为表多而数据多,这时候适合使用垂直切分,即把关系紧密(比如同一模块)的表切分出来放在一个 server 上。
|
||||
|
||||
### 5.3. Sharding 策略
|
||||
### 6.3. Sharding 策略
|
||||
|
||||
- 哈希取模:hash(key) % NUM_DB
|
||||
- 范围:可以是 ID 范围也可以是时间范围
|
||||
- 映射表:使用单独的一个数据库来存储映射关系
|
||||
|
||||
### 5.4. 分库分表的问题及解决方案
|
||||
### 6.4. 分库分表的问题及解决方案
|
||||
|
||||
#### 事务问题
|
||||
|
||||
|
@ -616,7 +679,7 @@ delete;
|
|||
- 如果是后台批处理任务要求分批获取数据,则可以加大 page size,比如每次获取 5000 条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
|
||||
- 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。
|
||||
|
||||
### 5.5. 常用的分库分表中间件
|
||||
### 6.5. 常用的分库分表中间件
|
||||
|
||||
#### 简单易用的组件:
|
||||
|
||||
|
@ -634,70 +697,6 @@ delete;
|
|||
- [OneProxy(支付宝首席架构师楼方鑫开发)](http://www.cnblogs.com/youge-OneSQL/articles/4208583.html)
|
||||
- [vitess(谷歌开发的数据库中间件)](https://github.com/youtube/vitess)
|
||||
|
||||
## 6. SQL 优化
|
||||
|
||||
### 6.1. 使用执行计划进行分析
|
||||
|
||||
执行计划 Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
|
||||
|
||||
比较重要的字段有:
|
||||
|
||||
- select_type : 查询类型,有简单查询、联合查询、子查询等
|
||||
- key : 使用的索引
|
||||
- rows : 扫描的行数
|
||||
|
||||
更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
|
||||
|
||||
### 6.2. 优化数据访问
|
||||
|
||||
#### 减少请求的数据量
|
||||
|
||||
- **只返回必要的列** - 最好不要使用 `SELECT *` 语句。
|
||||
- **只返回必要的行** - 使用 `WHERE` 语句进行查询过滤,有时候也需要使用 `LIMIT` 语句来限制返回的数据。
|
||||
- **缓存重复查询的数据** - 使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
|
||||
|
||||
#### 减少服务器端扫描的行数
|
||||
|
||||
最有效的方式是使用索引来覆盖查询。
|
||||
|
||||
### 6.3. 重构查询方式
|
||||
|
||||
#### 切分大查询
|
||||
|
||||
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
|
||||
|
||||
```sql
|
||||
DELEFT 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
|
||||
```
|
||||
|
||||
#### 分解大连接查询
|
||||
|
||||
将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有:
|
||||
|
||||
- **缓存更高效**。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
|
||||
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而**减少冗余记录的查询**。
|
||||
- **减少锁竞争**;
|
||||
- **在应用层进行连接,可以更容易对数据库进行拆分**,从而更容易做到高性能和可扩展。
|
||||
- **查询本身效率也可能会有所提升**。例如下面的例子中,使用 `IN()` 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
|
||||
|
||||
```sql
|
||||
SELECT * FROM tag
|
||||
JOIN tag_post ON tag_post.tag_id=tag.id
|
||||
JOIN post ON tag_post.post_id=post.id
|
||||
WHERE tag.tag='mysql';
|
||||
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);
|
||||
```
|
||||
|
||||
## 7. 关系数据库设计理论
|
||||
|
||||
### 7.1. 函数依赖
|
||||
|
@ -819,6 +818,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
|
|||
## 8. 参考资料
|
||||
|
||||
- [数据库系统原理](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/数据库系统原理.md)
|
||||
- [数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79)
|
||||
- [分库分表需要考虑的问题及方案](https://www.jianshu.com/p/32b3e91aa22c)
|
||||
- [数据库分库分表(sharding)系列(二) 全局主键生成策略](https://blog.csdn.net/bluishglc/article/details/7710738)
|
||||
- [一种支持自由规划无须数据迁移和修改路由代码的 Sharding 扩容方案](https://blog.csdn.net/bluishglc/article/details/7970268)
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue