InnoDB | MyISAM | |
---|---|---|
事务 | 支持 | 不支持 |
外键 | 支持 | 不支持 |
行锁 | 支持 | 不支持 |
crash-safe能力 | 支持 | 不支持 |
MVCC | 支持 | 不支持 |
索引存储类型 | 聚簇索引 | 非聚簇索引 |
是否保存表行数 | 不保存 | 保存 |
哈希表以 键-值对(key - value) 存储数据, key经过哈希函数的换算, 确定其在数组中存储的位置, 但是哈希存在冲突, 可以用采用拉链法来解决哈希冲突.
但是哈希后的数据不是有序的, 如果用于区间查询, 那么就必须一个个哈希查找了, 性能非常低, 所以哈希表这种存储结构只适用于等值查询的场景.
有序数组无论是在等值查询和范围查询的场景都非常优秀, 因为有序可以使用二分法来查找, 时间复杂度是 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1NHPDFp5-1650117801138)(https://g.yuque.com/gr/latex?O(logN)]). 范围查找 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0VhPbIKO-1650117801138)(https://g.yuque.com/gr/latex?k)] 条数据, 只需要先二分查找首条数据, 之后向右遍历, 时间复杂度也就是 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gHW0pgvb-1650117801139)(https://g.yuque.com/gr/latex?O(klogN)]) .
但是有序数组为了保持有序, 若在中间插入数据时, 必须移动后面所有数据, 成本开销大.
所以, 有序数组索引只适用于静态存储引擎, 保存一些存储后就不会再去修改的数据.
BST不管是查询还是更新, 都只需要 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6MADTg5-1650117801140)(https://g.yuque.com/gr/latex?O(logN)]) 的时间复杂度. 但是BST在某种情况下, 会使得其退化成链表. 如果想让他保持平衡, 那么就可以使用AVL. 对于二叉树来说, 如果数据量十分大, 那么这个层数就会越堆越高, 而数据是存放在磁盘中, 那么意味着要访问非常多的数据块, 就非常影响性能.
B树是多路搜索树, B树每个结点都存储着数据, 解决了二叉树随数据变大而层数变高导致对磁盘IO时的性能低下问题. 但是很明显, B树还不是最理想的存储结构, 试想一下如果进行范围查询, 对于范围中的数据来说, 那么不是每次都要从根节点开始往下找么, 必然有性能的问题.
于是在基于B树的模型上, 出现了B+树, B+树只有叶子节点是存储数据的, 而其他非叶子节点均为索引, 叶子节点用链表串起来, 且保证了有序. 在范围查询就只需找到其中一个数据, 之后向后遍历即可.
TYPE | INDEX | |
---|---|---|
id | int | id(primary key) |
k | int | k |
name | varchar |
假设有如上表结构, 那么建立起的索引结构如下图
从图中看出, 根据叶子节点内容的不同, 索引类型分为主键索引和非主键索引.
当执行 SELECT * FROM t WHERE id = 500
时, 即主键查询方式, 则需要搜索ID这颗B+树; 当执行 SELECT * FROM t WHERE k = 5
时, 即普通索引查询方式, 则先在k这棵树查找到主键的值, 再从ID这棵树中查找到对应的行.
当我们执行SQL搜索数据时, 如果需要先从非主键索引中查询到主键的值, 再从主键索引中查询到对应的数据, 这个过程就被称为回表. 所以应该尽量使用主键查询.
B+树为了有序性, 需要对插入和删除数据时做出对应的维护. 当插入数据时, 如在上图中插入ID=400的数据, 那么从逻辑上来说, 需要移动后面的数据, 空出位置.
若此时R5所在数据页满了, 则需要申请一个新的数据页, 然后移动部分数据到新数据页中, 这个过程被称为页分裂. 页分裂影响了数据页的空间利用率, 而且在分裂过程中, 性能也会有所影响.
若相邻两个数据页因为删除导致利用率很低后, 那么会将这两个数据页的数据合并到一个数据页中, 这个过程被称为页合并. 即页分裂的逆过程.
如果执行了语句 SELECT id FROM t WHERE k between 3 and 5
时, 只需要查询 id 的值, 而 id 已经在 k 的索引树上, 所以不需要再回表去查询整行, 直接返回查询结果, 索引 k 已经覆盖了这条SQL查询的需求, 被称为 覆盖索引. 覆盖索引能够减少树的搜索次数, 不需要再次回表查询整行, 所以是一个常用的性能优化手段.
最左前缀原则 就是利用索引列中最左的字段优先进行匹配
TYPE | INDEX | |
---|---|---|
id | int | id(primary key) |
id_card | varchar | id_card |
name | varchar | (name, age) |
age | int | |
ismale | tinyint |
若有如上表结构, 对于INDEX(name, age)来说, 索引树结构如下, 可以看到, 索引项是按照索引定义里面出现的顺序排序的.
对于SQL语句 SELECT * FROM t WHERE name LIKE '张%'
来说, 也是能够用到INDEX(name, age)这个索引的, 只需检索到第一个姓为张的人, 之后向后遍历即可, 所以可以利用最左前缀来加速检索. 最左前缀可以是联合索引的最左N个字段, 也可以是字符串索引的最左M个字符.
其效果和单独创建一个INDEX(name)的效果是一样的, 如果你想使用INDEX(name, age)也想让name也拥有索引INDEX(name), 那么只需保留前者即可, 若通过调整索引字段的顺序, 可以少维护一个索引树, 那么这个顺序就是需要优先考虑采用的. 但如果也有SQL语句条件类似 WHERE age = 1
, 那么最好再维护一个INDEX(age)的索引.
在对字符串创建索引, 如INDEX(name)中, 若字符串非常大, 那么响应的空间使用和维护开销也非常大, 就可以使用字符串从左开始的部分字符创建索引, 减少空间和维护的成本, 但是也会降低索引的选择性. 索引的选择性指的是 : 不重复的索引值和数据表的记录总数(#T)的比值, 范围为 1/#T 到 1 之间, 索引选择性越高则查询效率越高. 对于BLOB, TEXT, VARCHAR等类型的列, 必须使用前缀索引, MySQL不允许索引这些列的完整长度.
SELECT COUNT(DISTINCT name)/COUNT(1) FROM t
SELECT COUNT(DISCTINCT LEFT(name, N)) / COUNT(1) FROM t
对于SQL语句 SELECT * FROM t WHERE name LIKE '陈%' AND age = 10
, INDEX(name, age) 情况来说
在 MySQL5.6 之前没有引入索引下推优化时, 执行流程如下图, 在定位完name字段的索引后, 需要一条条进行回表查询, 然后再判断其他字段是否满足条件.
而 MySQL5.6 引入了索引下推优化后, 可以在所有遍历过程中, 对索引中包含的字段先进行判断过滤, 然后再进行后续操作, 减少了回表次数.
InnoDB中不存在哈希索引, 但是哈希索引确实有利于快速查找, 于是InnoDB引入了"自适应哈希索引", 在某些索引值被使用的非常频繁时, InnoDB会在内存中的B+树结构之上创建一个哈希索引, 用于这些频繁使用的索引值的快速查找, 使得其存有哈希快速查找的特点.
SELECT * FROM t WHERE DATE(create_time) = 'yyyy-MM-dd'
SELECT * FROM t WHERE k - 1 = 2
, 若有INDEX(k), 则不走索引SELECT * FROM t WHERE k = 1 OR j = 2
, 若有INDEX(k), 则不走索引, 如果OR连接的时同一个字段, 则不会失效SELECT * FROM t WHERE name = '%三'
, %放字符串字段前匹配不走索引对于一个事务, 要么事务内的SQL全部执行, 要么都不执行
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;
redo log 是物理日志, 记录的是"在某个数据页做出了什么修改", 属于 InnoDB存储引擎
层面.
当有一条记录需要更新的时候, InnoDB引擎会先把记录写到 redo log 中, 并更新内存, 这时候更新就算完成. 同时, InnoDB引擎会在适当的时候, 将这个操作记录更新到磁盘里面, 往往是系统较为空闲时.
InnoDB的redo log是固定大小的, 如可以配置为一组4个文件, 每个文件1GB, 那么redo log总共就可以记录4GB的操作. 从头开始写, 写到末尾又回到开头循环写.
write pos 是当前记录的位置, 边写边后移, 写到第三号文件末尾后就回到0号文件开头.
checkpoint 是当前要擦除的位置, 也是往后推移并循环的, 擦除记录前要把记录更新到数据文件.
write pos 和 checkpoint 之间是空闲的部分, 可以用来记录新的操作, 如果 write pos 追上 checkpoint ,表示 redo log 满了, 这时不能再执行新的更新, 得停下先擦掉一些记录, 把 checkpoint 推进.
有了 redo log, InnoDB 可以保证即使数据库发生异常重启, 之前提交的记录都不会丢失, 这个能力被称为 crash-safe
binlog是逻辑日志, 记录的是SQL语句的原始逻辑, 属于 MySQL Server
层面.
binlog 主要用来保证数据的一致性, 在主从等环境下, 需要通过 binlog 来进行数据的同步.
binlog 日志有三种记录格式
update_time = now()
这种实时性强的SQL语句, 那么两次操作的时间不一样就会导致数据不一致问题.redo log 让 InnoDB 存储引擎拥有 crash-safe 能力; binlog 保证了 MySQL 集群下的数据一致性.
redo log 在事务执行过程中可以不断写入, 而 binlog 只有在提交事务时才写入, 两者写入时机不同.
假设有一个事务正在执行, 执行过程中已经写入了 redo log, 而提交完后 binlog写入时发生异常, 那么在 binlog 中可能就没有对应的更新记录, 之后从库使用 binlog 恢复时, 导致少一次更新操作. 而主库用 redo log 进行恢复, 操作则正常. 最终导致这两个库的数据不一致.
于是 InnoDB存储引擎 使用两阶段提交方案 : 将 redo log 的写入拆成了两个步骤 prepare 和 commit
若使用 redo log 恢复数据时, 发现处于 prepare 阶段, 且没有 binlog, 则会回滚该事务. 若 redo log commit 时异常, 但是存在对应 binlog, MySQL还是认为这一组操作是有效的, 并不会进行回滚.
如果需要保证事务的原子性, 就需要在异常发生时, 对已执行操作进行回滚. undo log 会保存事务未提交之前的版本数据, 在执行过程中异常时, 就可以直接利用 undo log 中的信息将数据回滚到未修改之前. 并且 undo log 中的数据可以作为数据的旧版本快照供其他并发事务进行快照读. 在 InnoDB 中也用于实现 MVCC.
对于一致性非锁定读(MVCC)的实现, 通常时加一个版本号或时间戳. 查询时, 将当前可见的版本号和对应的版本号进行比对, 若记录的版本号小于可见版本号, 则表示该记录可见.
在 InnoDB 中, 多版本控制(Multi Versioning)就是对非锁定读的实现. 若读取的行正在执行 DELETE 或 UPDATE, 这时读操作不会去等待行锁的释放, 而是读取行的一个快照, 被称为快照读
也被称为 当前读. 锁定读会对读取到的记录加锁.
select ... lock in share mode
: 对记录加 S 锁, 其它事务也可以加 S 锁, 但是加 X 锁会被阻塞select ... for update
、insert
、update
、delete
: 对记录加 X 锁当前读每次读取的都是最新数据, 两次查询中间如果有其他事务插入数据, 就会产生幻读.
MVCC是通过保存数据在某个时间点的快照来实现的. 根据事务开始的时间不同, 每个事务对同一张表, 同一时刻看到数据可能是不一样的.
MVCC实现依赖于: 隐藏字段, Read View, undo log
隐藏字段主要包含:
InnoDB 每行数据都有一个隐藏的回滚指针, 用于指向该行数据修改前的最后一个历史版本, 这个历史版本会存放在 undo log 中. 如果要执行更新操作, 会将原记录放入 undo log 中, 并通过隐藏指针指向 undo log 中的原记录. 其他事务此时需要查询时, 就是查询 undo log 中这行数据的最后一个历史版本.
但是 undo log 总不可能一直保留. 在不需要的时候它应该被删除, 这时就交由系统自动判断, 即当系统没有比这个 undo log 更早的 read-view 的时候. 所以尽量不要使用长事务, 长事务意味着系统里会存在非常古老的事务视图. 由于这些事务随时可能访问数据库中任何数据, 所以这个事务提交前, 数据库里它可能使用到的 undo log 都必须保存, 导致占用大量存储空间.
InnoDB在RR级别下通过 MVCC
和 Next-key Lock
解决幻读问题
**SELECT**
, 此时会以 **MVCC**
快照读方式读取数据.Read View
,并使用至事务提交。所以在生成 Read View
之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”InnoDB
使用 Next-key Lock
来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读根据加锁范围, MySQL里的锁大致分成 全局锁 、表锁 和 行锁
全局锁就是对整个数据库实例加锁. MySQL提供了一个加全局读锁的方法, Flush tables with read lock(FTWRL)
使整个库都处于只读状态. 一般用于全局备份.
InnoDB中的表锁十分鸡肋, 一般都是通过 MySQL
的 server 层下的 元数据锁 (Metadata Lock) 来实现当对表执行DDL语句时, 使得其他事务阻塞. InnoDB厉害之处是实现了更细粒度的行锁.
在InnoDB事务中, 行锁是需要的时候才加上的, 但并不是不需要了就立刻释放, 而是等到事务结束时才释放. 这就是 两阶段锁协议
如果事务中需要锁住多行, 要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放. 这样就最大程度减少了事务间的锁等待, 提升了并发度.
更多文章如下:
【面向校招】全力备战2023Golang实习与校招