打工四年总结的数据库知识点

国庆在家无聊,我随手翻了一下家里数据库相关的书籍,这一翻我就看上瘾了,由于大学比较熟悉的一些数据库范式我竟然都忘了,怀揣着好奇心我就看了一个小国庆。php

看的过程当中我也作了一些小笔记,可能没我以前系统文章那么有趣,可是绝对也是干货十足,适合你们去回顾或者面试突击的适合看看,也很少说先放图。html

存储引擎

InnoDB

InnoDB 是 MySQL 默认的事务型存储引擎,只要在须要它不支持的特性时,才考虑使用其余存储引擎。node

InnoDB 采用 MVCC 来支持高并发,而且实现了四个标准隔离级别(未提交读、提交读、可重复读、可串行化)。其默认级别时可重复读(REPEATABLE READ),在可重复读级别下,经过 MVCC + Next-Key Locking 防止幻读。mysql

主索引时聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,所以对主键查询有很高的性能。git

InnoDB 内部作了不少优化,包括从磁盘读取数据时采用的可预测性读,可以自动在内存中建立 hash 索引以加速读操做的自适应哈希索引,以及可以加速插入操做的插入缓冲区等。github

InnoDB 支持真正的在线热备份,MySQL 其余的存储引擎不支持在线热备份,要获取一致性视图须要中止对全部表的写入,而在读写混合的场景中,中止写入可能也意味着中止读取。面试

MyISAM

设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、能够容忍修复操做,则依然可使用它。redis

提供了大量的特性,包括压缩表、空间数据索引等。算法

不支持事务。sql

不支持行级锁,只能对整张表加锁,读取时会对须要读到的全部表加共享锁,写入时则对表加排它锁。但在表有读取操做的同时,也能够往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。

能够手工或者自动执行检查和修复操做,可是和事务恢复以及崩溃恢复不一样,可能致使一些数据丢失,并且修复操做是很是慢的。

若是指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会当即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式能够极大的提高写入性能,可是在数据库或者主机崩溃时会形成索引损坏,须要执行修复操做。

InnoDB 和 MyISAM 的比较

  • 事务:InnoDB 是事务型的,可使用 Commit 和 Rollback 语句。
  • 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
  • 外键:InnoDB 支持外键。
  • 备份:InnoDB 支持在线热备份。
  • 崩溃恢复:MyISAM 崩溃后发生损坏的几率比 InnoDB 高不少,并且恢复的速度也更慢。
  • 其它特性:MyISAM 支持压缩表和空间数据索引。

索引

B+ Tree 原理

数据结构

B Tree 指的是 Balance Tree,也就是平衡树,平衡树是一颗查找树,而且全部叶子节点位于同一层。

B+ Tree 是 B 树的一种变形,它是基于 B Tree 和叶子节点顺序访问指针进行实现,一般用于数据库和操做系统的文件系统中。

B+ 树有两种类型的节点:内部节点(也称索引节点)和叶子节点,内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存在叶子节点。

内部节点中的 key 都按照从小到大的顺序排列,对于内部节点中的一个 key,左子树中的全部 key 都小于它,右子树中的 key 都大于等于它,叶子节点的记录也是按照从小到大排列的。

每一个叶子节点都存有相邻叶子节点的指针。

操做

查找

查找以典型的方式进行,相似于二叉查找树。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来肯定这个位置。

插入

  • Perform a search to determine what bucket the new record should go into.
  • If the bucket is not full(a most b - 1 entries after the insertion,b 是节点中的元素个数,通常是页的整数倍),add tht record.
  • Otherwise,before inserting the new record

    • split the bucket.

      • original node has 「(L+1)/2」items
      • new node has 「(L+1)/2」items
    • Move 「(L+1)/2」-th key to the parent,and insert the new node to the parent.
    • Repeat until a parent is found that need not split.
  • If the root splits,treat it as if it has an empty parent ans split as outline above.

B-trees grow as the root and not at the leaves.

删除

和插入相似,只不过是自下而上的合并操做。

树的常见特性

AVL 树

平衡二叉树,通常是用平衡因子差值决定并经过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件很是严格(树高差只有1),只要插入或删除不知足上面的条件就要经过旋转来保持平衡。因为旋转是很是耗费时间的。因此 AVL 树适用于插入/删除次数比较少,但查找多的场景。

红黑树

经过对从根节点到叶子节点路径上各个节点的颜色进行约束,确保没有一条路径会比其余路径长2倍,于是是近似平衡的。因此相对于严格要求平衡的AVL树来讲,它的旋转保持平衡次数较少。适合,查找少,插入/删除次数多的场景。(如今部分场景使用跳表来替换红黑树,可搜索“为啥 redis 使用跳表(skiplist)而不是使用 red-black?”)

B/B+ 树

多路查找树,出度高,磁盘IO低,通常用于数据库系统中。

B + 树与红黑树的比较

红黑树等平衡树也能够用来实现索引,可是文件系统及数据库系统广泛采用 B+ Tree 做为索引结构,主要有如下两个缘由:

(一)磁盘 IO 次数

B+ 树一个节点能够存储多个元素,相对于红黑树的树高更低,磁盘 IO 次数更少。

(二)磁盘预读特性

为了减小磁盘 I/O 操做,磁盘每每不是严格按需读取,而是每次都会预读。预读过程当中,磁盘进行顺序读取,顺序读取不须要进行磁盘寻道。每次会读取页的整数倍。

操做系统通常将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能彻底载入一个节点。

B + 树与 B 树的比较

B+ 树的磁盘 IO 更低

B+ 树的内部节点并无指向关键字具体信息的指针。所以其内部节点相对 B 树更小。若是把全部同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的须要查找的关键字也就越多。相对来讲IO读写次数也就下降了。

B+ 树的查询效率更加稳定

因为非叶子结点并非最终指向文件内容的结点,而只是叶子结点中关键字的索引。因此任何关键字的查找必须走一条从根结点到叶子结点的路。全部关键字查询的路径长度相同,致使每个数据的查询效率至关。

B+ 树元素遍历效率高

B 树在提升了磁盘IO性能的同时并无解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就能够实现整棵树的遍历。并且在数据库中基于范围的查询是很是频繁的,而 B 树不支持这样的操做(或者说效率过低)。

MySQL 索引

索引是在存储引擎层实现的,而不是在服务器层实现的,因此不一样存储引擎具备不一样的索引类型和实现。

B+ Tree 索引

是大多数 MySQL 存储引擎的默认索引类型。

  • 由于再也不须要进行全表扫描,只须要对树进行搜索便可,因此查找速度快不少。
  • 由于 B+ Tree 的有序性,因此除了用于查找,还能够用于排序和分组。
  • 能够指定多个列做为索引列,多个索引列共同组成键。
  • 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。若是不是按照索引列的顺序进行查找,则没法使用索引。

InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。由于没法把数据行存放在两个不一样的地方,因此一个表只能有一个聚簇索引。

辅助索引的叶子节点的 data 域记录着主键的值,所以在使用辅助索引进行查找时,须要先查找到主键值,而后再到主索引中进行查找,这个过程也被称做回表。

哈希索引

哈希索引能以 O(1) 时间进行查找,可是失去了有序性:

  • 没法用于排序与分组;
  • 只支持精确查找,没法用于部分查找和范围查找。

InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的很是频繁时,会在 B+Tree 索引之上再建立一个哈希索引,这样就让 B+Tree 索引具备哈希索引的一些优势,好比快速的哈希查找。

全文索引

MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。

查找条件使用 MATCH AGAINST,而不是普通的 WHERE。

全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。

InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。

空间数据索引

MyISAM 存储引擎支持空间数据索引(R-Tree),能够用于地理数据存储。空间数据索引会从全部维度来索引数据,能够有效地使用任意维度来进行组合查询。

必须使用 GIS 相关的函数来维护数据。

索引优化

独立的列

在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,不然没法使用索引。

例以下面的查询不能使用 actor_id 列的索引:

SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;

多列索引

在须要使用多个列做为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例以下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。

SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;

索引列的顺序

让选择性最强的索引列放在前面。

索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每一个记录都有惟一的索引与其对应。选择性越高,每一个记录的区分度越高,查询效率也越高。

例以下面显示的结果中 customer_id 的选择性比 staff_id 更高,所以最好把 customer_id 列放在多列索引的前面。

SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
               COUNT(*): 16049

前缀索引

对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。

前缀长度的选取须要根据索引选择性来肯定。

覆盖索引

索引包含全部须要查询的字段的值。

具备如下优势:

  • 索引一般远小于数据行的大小,只读取索引能大大减小数据访问量。
  • 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操做系统来缓存。所以,只访问索引能够不使用系统调用(一般比较费时)。
  • 对于 InnoDB 引擎,若辅助索引可以覆盖查询,则无需访问主索引。

索引的优势

  • 大大减小了服务器须要扫描的数据行数。
  • 帮助服务器避免进行排序和分组,以及避免建立临时表(B+Tree 索引是有序的,能够用于 ORDER BY 和 GROUP BY 操做。临时表主要是在排序和分组过程当中建立,不须要排序和分组,也就不须要建立临时表)。
  • 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一块儿)。

索引的使用条件

  • 对于很是小的表、大部分状况下简单的全表扫描比创建索引更高效;
  • 对于中到大型的表,索引就很是有效;
  • 可是对于特大型的表,创建和维护索引的代价将会随之增加。这种状况下,须要用到一种技术能够直接区分出须要查询的一组数据,而不是一条记录一条记录地匹配,例如可使用分区技术。
为何对于很是小的表,大部分状况下简单的全表扫描比创建索引更高效?

若是一个表比较小,那么显然直接遍历表比走索引要快(由于须要回表)。

注:首先,要注意这个答案隐含的条件是查询的数据不是索引的构成部分,否也不须要回表操做。其次,查询条件也不是主键,不然能够直接从聚簇索引中拿到数据。

查询性能优化

使用 explain 分析 select 查询语句

explain 用来分析 SELECT 查询语句,开发人员能够经过分析 Explain 结果来优化查询语句。

select_type

经常使用的有 SIMPLE 简单查询,UNION 联合查询,SUBQUERY 子查询等。

table

要查询的表

possible_keys

The possible indexes to choose

可选择的索引

key

The index actually chosen

实际使用的索引

rows

Estimate of rows to be examined

扫描的行数

type

索引查询类型,常常用到的索引查询类型:

**const:使用主键或者惟一索引进行查询的时候只有一行匹配
ref:使用非惟一索引
range:使用主键、单个字段的辅助索引、多个字段的辅助索引的最后一个字段进行范围查询
index:和all的区别是扫描的是索引树
all:扫描全表:**

system

触发条件:表只有一行,这是一个 const type 的特殊状况

const

触发条件:在使用主键或者惟一索引进行查询的时候只有一行匹配。

SELECT * FROM tbl_name WHERE primary_key=1;

SELECT * FROM tbl_name
  WHERE primary_key_part1=1 AND primary_key_part2=2;

eq_ref

触发条件:在进行联接查询的,使用主键或者惟一索引而且只匹配到一行记录的时候

SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;

SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
  AND ref_table.key_column_part2=1;

ref

触发条件:使用非惟一索引

SELECT * FROM ref_table WHERE key_column=expr;

SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;

SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
  AND ref_table.key_column_part2=1;

range

触发条件:只有在使用主键、单个字段的辅助索引、多个字段的辅助索引的最后一个字段进行范围查询才是 range

SELECT * FROM tbl_name
  WHERE key_column = 10;

SELECT * FROM tbl_name
  WHERE key_column BETWEEN 10 and 20;

SELECT * FROM tbl_name
  WHERE key_column IN (10,20,30);

SELECT * FROM tbl_name
  WHERE key_part1 = 10 AND key_part2 IN (10,20,30);

index

The index join type is the same as ALL, except that the index tree is scanned. This occurs two ways:

触发条件:

只扫描索引树

1)查询的字段是索引的一部分,覆盖索引。
2)使用主键进行排序

all

触发条件:全表扫描,不走索引

优化数据访问

减小请求的数据量

  • 只返回必要的列:最好不要使用 SELECT * 语句。
  • 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
  • 缓存重复查询的数据:使用缓存能够避免在数据库中进行查询,特别在要查询的数据常常被重复查询时,缓存带来的查询性能提高将会是很是明显的。

减小服务器端扫描的行数

最有效的方式是使用索引来覆盖查询。

重构查询方式

切分大查询

一个大查询若是一次性执行的话,可能一次锁住不少数据、占满整个事务日志、耗尽系统资源、阻塞不少小的但重要的查询。

DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
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

分解大链接查询

将一个大链接查询分解成对每个表进行一次单表查询,而后在应用程序中进行关联,这样作的好处有:

  • 让缓存更高效。对于链接查询,若是其中一个表发生变化,那么整个查询缓存就没法使用。而分解后的多个查询,即便其中一个表发生变化,对其它表的查询缓存依然可使用。
  • 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减小冗余记录的查询。
  • 减小锁竞争;
  • 在应用层进行链接,能够更容易对数据库进行拆分,从而更容易作到高性能和可伸缩。
  • 查询自己效率也可能会有所提高。例以下面的例子中,使用 IN() 代替链接查询,可让 MySQL 按照 ID 顺序进行查询,这可能比随机的链接要更高效。
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);

事务

事务是指知足 ACID 特性的一组操做,能够经过 Commit 提交一个事务,也可使用 Rollback 进行回滚。

ACID

事务最基本的莫过于 ACID 四个特性了,这四个特性分别是:

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

原子性

事务被视为不可分割的最小单元,事务的全部操做要么所有成功,要么所有失败回滚。

一致性

数据库在事务执行先后都保持一致性状态,在一致性状态下,全部事务对一个数据的读取结果都是相同的。

隔离性

一个事务所作的修改在最终提交之前,对其余事务是不可见的。

持久性

一旦事务提交,则其所作的修改将会永远保存到数据库中。即便系统发生崩溃,事务执行的结果也不能丢。

ACID 之间的关系

事务的 ACID 特性概念很简单,但很差理解,主要是由于这几个特性不是一种平级关系:

  • 只有知足一致性,事务的结果才是正确的。
  • 在无并发的状况下,事务串行执行,隔离性必定可以知足。此时只要能知足原子性,就必定能知足一致性。在并发的状况下,多个事务并行执行,事务不只要知足原子性,还须要知足隔离性,才能知足一致性。
  • 事务知足持久化是为了能应对数据库崩溃的状况。

隔离级别

未提交读(READ UNCOMMITTED)

事务中的修改,即便没有提交,对其余事务也是可见的。

提交读(READ COMMITTED)

一个事务只能读取已经提交的事务所作的修改。换句话说,一个事务所作的修改在提交以前对其余事务是不可见的。

可重复读(REPEATABLE READ)

保证在同一个事务中屡次读取一样数据的结果是同样的。

可串行化(SERIALIZABLE)

强制事务串行执行。

须要加锁实现,而其它隔离级别一般不须要。

隔离级别 脏读 不可重复读 幻影读
未提交读
提交读 ×
可重复读 × ×
可串行化 × × ×

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。

锁类型

共享锁(S Lock)

容许事务读一行数据

排他锁(X Lock)

容许事务删除或者更新一行数据

意向共享锁(IS Lock)

事务想要得到一张表中某几行的共享锁

意向排他锁

事务想要得到一张表中某几行的排他锁

MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别老是读取最新的数据行,无需使用 MVCC。可串行化隔离级别须要对全部读取的行都加锁,单纯使用 MVCC 没法实现。

基础概念

版本号

  • 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号。

隐藏的列

MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:

  • 建立版本号:指示建立一个数据行的快照时的系统版本号;
  • 删除版本号:若是该快照的删除版本号大于当前事务版本号表示该快照有效,不然表示该快照已经被删除了。

Undo 日志

MVCC 使用到的快照存储在 Undo 日志中,该日志经过回滚指针把一个数据行(Record)的全部快照链接起来。

实现过程

如下实现过程针对可重复读隔离级别。

当开始一个事务时,该事务的版本号确定大于当前全部数据行快照的建立版本号,理解这一点很关键。数据行快照的建立版本号是建立数据行快照时的系统版本号,系统版本号随着建立事务而递增,所以新建立一个事务时,这个事务的系统版本号比以前的系统版本号都大,也就是比全部数据行快照的建立版本号都大。

SELECT

多个事务必须读取到同一个数据行的快照,而且这个快照是距离如今最近的一个有效快照。可是也有例外,若是有一个事务正在修改该数据行,那么它能够读取事务自己所作的修改,而不用和其它事务的读取结果一致。

把没有对一个数据行作修改的事务称为 T,T 所要读取的数据行快照的建立版本号必须小于等于 T 的版本号,由于若是大于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,所以不能去读取它。除此以外,T 所要读取的数据行快照的删除版本号必须是未定义或者大于 T 的版本号,由于若是小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不该该去读取它。

INSERT

将当前系统版本号做为数据行快照的建立版本号。

DELETE

将当前系统版本号做为数据行快照的删除版本号。

UPDATE

将当前系统版本号做为更新前的数据行快照的删除版本号,并将当前系统版本号做为更新后的数据行快照的建立版本号。能够理解为先执行 DELETE 后执行 INSERT。

快照读与当前读

在可重复读级别中,经过MVCC机制,虽然让数据变得可重复读,但咱们读到的数据多是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就极可能出问题。

对于这种读取历史数据的方式,咱们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

快照读

MVCC 的 SELECT 操做是快照中的数据,不须要进行加锁操做。

select * from table ….;

当前读

MVCC 其它会对数据库进行修改的操做(INSERT、UPDATE、DELETE)须要进行加锁操做,从而读取最新的数据。能够看到 MVCC 并非彻底不用加锁,而只是避免了 SELECT 的加锁操做。

INSERT;
UPDATE;
DELETE;

在进行 SELECT 操做时,能够强制指定进行加锁操做。如下第一个语句须要加 S 锁,第二个须要加 X 锁。

- select * from table where ? lock in share mode;
- select * from table where ? for update;

事务的隔离级别实际上都是定义的当前读的级别,MySQL为了减小锁处理(包括等待其它锁)的时间,提高并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”的隔离性,就须要经过加锁来实现了。

锁算法

Record Lock

锁定一个记录上的索引,而不是记录自己。

若是表没有设置索引,InnoDB 会自动在主键上建立隐藏的聚簇索引,所以 Record Locks 依然可使用。

Gap Lock

锁定索引之间的间隙,可是不包含索引自己。例如当一个事务执行如下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Lock

它是 Record Locks 和 Gap Locks 的结合,不只锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含如下值:10, 11, 13, and 20,那么就须要锁定如下区间:

(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
在 InnoDB 存储引擎中,SELECT 操做的不可重复读问题经过 MVCC 获得了解决,而 UPDATE、DELETE 的不可重复读问题经过 Record Lock 解决,INSERT 的不可重复读问题是经过 Next-Key Lock(Record Lock + Gap Lock)解决的。

锁问题

脏读

脏读指的是不一样事务下,当前事务能够读取到另外事务未提交的数据。

例如:

T1 修改一个数据,T2 随后读取这个数据。若是 T1 撤销了此次修改,那么 T2 读取的数据是脏数据。

不可重复读

不可重复读指的是同一事务内屡次读取同一数据集合,读取到的数据是不同的状况。

例如:

T2 读取一个数据,T1 对该数据作了修改。若是 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不一样。

在 InnoDB 存储引擎中,SELECT 操做的不可重复读问题经过 MVCC 获得了解决,而 UPDATE、DELETE 的不可重复读问题是经过 Record Lock 解决的,INSERT 的不可重复读问题是经过 Next-Key Lock(Record Lock + Gap Lock)解决的。

Phantom Proble(幻影读)

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

Phantom Proble 是指在同一事务下,连续执行两次一样的 sql 语句可能返回不一样的结果,第二次的 sql 语句可能会返回以前不存在的行。

幻影读是一种特殊的不可重复读问题。

丢失更新

一个事务的更新操做会被另外一个事务的更新操做所覆盖。

例如:

T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

这类型问题能够经过给 SELECT 操做加上排他锁来解决,不过这可能会引入性能问题,具体使用要视业务场景而定。

分库分表数据切分

水平切分

水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。

当一个表的数据不断增多时,Sharding 是必然的选择,它能够将数据分布到集群的不一样节点上,从而缓存单个数据库的压力。

垂直切分

垂直切分是将一张表按列分红多个表,一般是按照列的关系密集程度进行切分,也能够利用垂直气氛将常常被使用的列喝不常常被使用的列切分到不一样的表中。

在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。

Sharding 策略

  • 哈希取模:hash(key)%N
  • 范围:能够是 ID 范围也能够是时间范围
  • 映射表:使用单独的一个数据库来存储映射关系

Sharding 存在的问题

事务问题

使用分布式事务来解决,好比 XA 接口

链接

能够将原来的链接分解成多个单表查询,而后在用户程序中进行链接。

惟一性

  • 使用全局惟一 ID (GUID)
  • 为每一个分片指定一个 ID 范围
  • 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)

复制

主从复制

主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。

  • binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。
  • I/O 线程 :负责从主服务器上读取- 二进制日志,并写入从服务器的中继日志(Relay log)。
  • SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。

读写分离

主服务器处理写操做以及实时性要求比较高的读操做,而从服务器处理读操做。

读写分离能提升性能的缘由在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可使用 MyISAM,提高查询性能以及节约系统开销;
  • 增长冗余,提升可用性。

读写分离经常使用代理方式来实现,代理服务器接收应用层传来的读写请求,而后决定转发到哪一个服务器。

JSON

在实际业务中常常会使用到 JSON 数据类型,在查询过程当中主要有两种使用需求:

  1. 在 where 条件中有经过 json 中的某个字段去过滤返回结果的需求
  2. 查询 json 字段中的部分字段做为返回结果(减小内存占用)

JSON_CONTAINS

JSON_CONTAINS(target, candidate[, path])

若是在 json 字段 target 指定的位置 path,找到了目标值 condidate,返回 1,不然返回 0

若是只是检查在指定的路径是否存在数据,使用JSON_CONTAINS_PATH()

mysql> SET @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
mysql> SET @j2 = '1';
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.a');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.a') |
+-------------------------------+
|                             1 |
+-------------------------------+
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.b');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.b') |
+-------------------------------+
|                             0 |
+-------------------------------+

mysql> SET @j2 = '{"d": 4}';
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.a');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.a') |
+-------------------------------+
|                             0 |
+-------------------------------+
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.c');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.c') |
+-------------------------------+
|                             1 |
+-------------------------------+

JSON_CONTAINS_PATH

JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)

若是在指定的路径存在数据返回 1,不然返回 0

mysql> SET @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.a', '$.e');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.a', '$.e') |
+---------------------------------------------+
|                                           1 |
+---------------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'all', '$.a', '$.e');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@j, 'all', '$.a', '$.e') |
+---------------------------------------------+
|                                           0 |
+---------------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.c.d');
+----------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.c.d') |
+----------------------------------------+
|                                      1 |
+----------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.a.d');
+----------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.a.d') |
+----------------------------------------+
|                                      0 |
+----------------------------------------+

实际使用:

$conds = new Criteria();
        $conds->andWhere('dept_code', 'in', $deptCodes);
        if (!empty($aoiAreaId)) {
            $aoiAreaIdCond = new Criteria();
            $aoiAreaIdCond->orWhere("JSON_CONTAINS_PATH(new_aoi_area_ids,'one', '$.\"$aoiAreaId\"')", '=', 1);
            $aoiAreaIdCond->orWhere("JSON_CONTAINS_PATH(old_aoi_area_ids,'one', '$.\"$aoiAreaId\"')", '=', 1);
            $conds->andWhere($aoiAreaIdCond);
        }

column->path、column->>path

获取指定路径的值

-> vs ->>

Whereas the -> operator simply extracts a value, the ->> operator in addition unquotes the extracted result.

mysql> SELECT * FROM jemp WHERE g > 2;
+-------------------------------+------+
| c                             | g    |
+-------------------------------+------+
| {"id": "3", "name": "Barney"} |    3 |
| {"id": "4", "name": "Betty"}  |    4 |
+-------------------------------+------+
2 rows in set (0.01 sec)

mysql> SELECT c->'$.name' AS name
    ->     FROM jemp WHERE g > 2;
+----------+
| name     |
+----------+
| "Barney" |
| "Betty"  |
+----------+
2 rows in set (0.00 sec)

mysql> SELECT JSON_UNQUOTE(c->'$.name') AS name
    ->     FROM jemp WHERE g > 2;
+--------+
| name   |
+--------+
| Barney |
| Betty  |
+--------+
2 rows in set (0.00 sec)

mysql> SELECT c->>'$.name' AS name
    ->     FROM jemp WHERE g > 2;
+--------+
| name   |
+--------+
| Barney |
| Betty  |
+--------+
2 rows in set (0.00 sec)

实际使用:

$retTask = AoiAreaTaskOrm::findRows(['status', 'extra_info->>"$.new_aoi_area_infos" as new_aoi_area_infos', 'extra_info->>"$.old_aoi_area_infos" as old_aoi_area_infos'], $cond);

关系数据库设计理论

函数依赖

记 A->B 表示 A 函数决定 B,也能够说 B 函数依赖于 A。

若是 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它全部属性而且是最小的,那么该集合就称为键码。

对于 A->B,若是能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,不然就是彻底函数依赖。

对于 A->B,B->C,则 A->C 是一个传递函数依赖

异常

如下的学生课程关系的函数依赖为 {Sno, Cname} -> {Sname, Sdept, Mname, Grade},键码为 {Sno, Cname}。也就是说,肯定学生和课程以后,就能肯定其它信息。

Sno Sname Sdept Mname Cname Grade
1 学生-1 学院-1 院长-1 课程-1 90
2 学生-2 学院-2 院长-2 课程-2 80
2 学生-2 学院-2 院长-2 课程-1 100
3 学生-3 学院-2 院长-2 课程-2 95

不符合范式的关系,会产生不少异常,主要有如下四种异常:

  • 冗余数据:例如 学生-2 出现了两次。
  • 修改异常:修改了一个记录中的信息,可是另外一个记录中相同的信息却没有被修改。
  • 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 课程-1 须要删除第一行和第三行,那么 学生-1 的信息就会丢失。
  • 插入异常:例如想要插入一个学生的信息,若是这个学生还没选课,那么就没法插入。

范式

范式理论是为了解决以上提到四种异常。

高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。

第一范式 (1NF)

属性不可分。

第二范式 (2NF)

每一个非主属性彻底函数依赖于键码。

能够经过分解来知足。

分解前

Sno Sname Sdept Mname Cname Grade
1 学生-1 学院-1 院长-1 课程-1 90
2 学生-2 学院-2 院长-2 课程-2 80
2 学生-2 学院-2 院长-2 课程-1 100
3 学生-3 学院-2 院长-2 课程-2 95

以上学生课程关系中,{Sno, Cname} 为键码,有以下函数依赖:

  • Sno -> Sname, Sdept
  • Sdept -> Mname
  • Sno, Cname-> Grade

Grade 彻底函数依赖于键码,它没有任何冗余数据,每一个学生的每门课都有特定的成绩。

Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现屡次,形成大量冗余数据。

分解后

关系-1

Sno Sname Sdept Mname
1 学生-1 学院-1 院长-1
2 学生-2 学院-2 院长-2
3 学生-3 学院-2 院长-2

有如下函数依赖:

  • Sno -> Sname, Sdept
  • Sdept -> Mname

关系-2

Sno Cname Grade
1 课程-1 90
2 课程-2 80
2 课程-1 100
3 课程-2 95

有如下函数依赖:

  • Sno, Cname -> Grade

第三范式 (3NF)

非主属性不传递函数依赖于键码。

上面的 关系-1 中存在如下传递函数依赖:

  • Sno -> Sdept -> Mname

能够进行如下分解:

关系-11

Sno Sname Sdept
1 学生-1 学院-1
2 学生-2 学院-2
3 学生-3 学院-2

关系-12

Sdept Mname
学院-1 院长-1
学院-2 院长-2

ER 图

Entity-Relationship,有三个组成部分:实体、属性、联系。

用来进行关系型数据库系统的概念设计。

实体的三种联系

包含一对一,一对多,多对多三种。

  • 若是 A 到 B 是一对多关系,那么画个带箭头的线段指向 B;
  • 若是是一对一,画两个带箭头的线段;
  • 若是是多对多,画两个不带箭头的线段。

下图的 Course 和 Student 是一对多的关系。

表示出现屡次的关系

一个实体在联系出现几回,就要用几条线链接。

下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,所以须要用两条线来表示这种关系。

联系的多向性

虽然老师能够开设多门课,而且能够教授多名学生,可是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。

表示子类

用一个三角形和两条线来链接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。

参考资料

总结

这都是些基础知识,我没想到再次回顾大半我都已忘却了,也庆幸有这样的假期可以从新拾起来。

说实话作自媒体后我充电的时间少了不少,也少了不少时间研究技术栈深度,国庆假期我也思考反思了好久,后面准备继续压缩本身业余时间,好比看手机看B站的时间压缩一下,仍是得按时充电,目前做息还算规律早睡早起都作到了,咱们一块儿加油哟。

我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞收藏评论,咱们下期见!

相关文章
相关标签/搜索