MySQL学习笔记及面试题
一、索引
B+ Tree原理
1.数据结构
B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现的,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。
在 B+ Tree 中,一个节点中的key从左到右非递减列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1.
2.操作
进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。
插入删除操作会破坏平衡树的平衡性,因此在进行插入删除操作之后,需要对树进行分裂、合并、旋转等操作来维护平衡性。
3.与红黑树的比较
红黑树等平衡树可以用来实现索引,但是文件系统及数据库普遍采用 B+ Tree 作为索引结构,这是因为使用 B+ Tree 访问磁盘数据有更高的性能。
(一)B+ Tree 有更低的树高
平衡树的树高 O(h)=O(logdN),其中 d 为每个节点的出度。红黑树的出度d为2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多。
(二)磁盘访问原理
操作系统一般将内存和磁盘分割成固定大小的快,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。
如果数据不在同一个磁盘块上,那么通常需要移动制动手臂进行寻道,而制动手臂因为去物理结构导致了移动效率低下,从而增加磁盘数据读取时间。B+ Tree相对于红黑树有更低的树高,进行寻道的次数与树高成正比,在同一个磁盘块上进行访问只需要很短的磁盘旋转时间,使用 B+ Tree 更适合磁盘数据的读取。
(三)磁盘预读特性
为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的磁盘旋转时间,速度会非常快。并且可以利用预读特性,相邻的节点也能够被预先载入。
MySQL 索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具备不同的索引类型和实现。
1.B+ Tree 索引
是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
因为 B+ Tree 的有序性,所以除了用于查找,还可以用于排序和分组。
可以指定多个列作为索引列,多个索引列共同组成键。
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+ Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因此无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查询时,需要先查找主键值,然后再到主索引中进行查找。
2.哈希索引
哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
- 无法用于排序与分组;
- 只支持精确查找,无法用于部分查找和范围查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+ Tree 索引之上再创建一个哈希索引,这样就让 B+ Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
3.全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
查找条件使用MATCH AGAINST,而不是普通的 WHERE。
全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
InnoDB存储引擎在 MySQL5.6.4 版本中也开始支持全文索引。
4.空间数据索引
MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
注意:必须使用 GIS 相关的函数来维护数据。
索引优化
1.独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
例如下面的查询不能使用actor_id列的索引:
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
2.多列索引
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把actor_id和film_id设置为多列索引。
SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;
3.索引列的顺序
让选择性最强的索引列放在前面。
索引的选择性是指:不重复的索引值和记录总数的比值。最大值为1,此时每个记录都有唯一肚饿索引与其对应。选择性越高,每个记录的区分度越高,查询效率也越高。
例如下面显示的结果中customer_id的选择性比staff_id更高,因此最好把 customer_id 列放在多列索引的前面:
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
4.前缀索引
对于 BLOB 、 TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
前缀长度的选择需要根据索引选择性来确定。
5.覆盖索引
索引包含所有需要查询的字段的值,具有以下优点:
- 索引通常远小于数据行的大小,只读索引能大大减少数据访问量;
- 一些存储引擎(例如MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只能访问索引可以不使用系统调用(通常比较费时);
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则不需访问主索引。
索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+ Tree 索引是有序性的,可以使用
order by和group by操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,,也就不需要创建临时表)。 - 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。
索引的使用条件
- 对于非常小的表:大部分情况下简单的全表扫描比建立索引更高效;
- 对于中到大型的表:索引就非常有效;
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如:分区技术。
二、查询性能优化
使用 Explain 进行分析
Explain 用来分析 select 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。比较重要的字段有:
- select_type:查询类型,有简单查询、联合查询、子查询等;
- key:使用的索引;
- rows:扫描的行数
优化数据访问
1.减少请求的数据量
- 只返回必要的列:最好不要使用
select *语句; - 只返回必要的行:使用
limit语句来限制返回的数据; - 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能的提升将会是非常明显的。
2.减少服务器扫描的行数
最有效的方式是使用索引来覆盖查询。
重构查询方式
1.切分大查询
一个查询如果能一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询
2.分解大连接查询
将一个大连接查询分解成每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
- 让缓存更高效
- 减少冗余记录的查询
- 减少锁竞争
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩
- 查询本身效率也可能会有所提升。例如下面的例子中,使用
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);
三、存储引擎
InnoDB
- 是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性是,才考虑使用其它存储引擎;
- 实现了四个标准的隔离级别,默认级别是:可重复读(REPEATABLE READ)。可重复读隔离级别下,通过
多版本并发控制(MVCC) + Next-Key Locking防止幻影读; - 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升;
- 内部做了很多优化,包括:从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够快速插入操作的插入缓冲区等;
- 支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取;
- 适用于:应用中需要执行大量的 INSERT 或 UPDATE 操作
MyISAM
- 设计简单,数据以紧密格式存储,使用场景:对于只读数据,或者表比较小、可以容忍修复操作;
- 提供了大量的特性,包括:压缩表、空间数据索引等,提供高速存储和检索,以及全文搜索能力;
- 不支持事务;
- 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以用往表中插入新的记录,这称为
并发插入(CONCURRENT INSERT); - 可以手工或自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作会非常慢;
- 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引写入磁盘中,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机奔溃时会造成索引损坏,需要执行修改操作;
- 适用于:应用中需要执行大量的 SELECT 查询
比较
- 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句;
- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁;
- 外键:InnoDB 支持外键;
- 备份:InnoDB 支持在线热备份;
- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复速度也更慢;
- 其它特性:MyISAM支持压缩表和空间索引。
四、数据类型
整型
TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。- INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
浮点数
FLOAT和DOUBLE为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。- FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
字符串
- 主要有
CHAR和VARCHAR两种类型,一种是定长的,一种是变长的。 - VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
- 在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。
时间和日期
MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。
1.DATETIME
- 能够保存从 1000 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
- 它与时区无关。
- 默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
2.TIMESTAMP
- 和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。
- 它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
- MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
- 默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
- 应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
五、切分
水平切分
水平切分又称为Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
垂直切分
垂直切分是将一个表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如:将原本的电商数据库垂直切分为商品数据库、用户数据库等。
Sharding(水平切分)策略
- 哈希取模:hash(key) % N;
- 范围:可以是 ID 范围也可以是时间范围;
- 映射表:使用单独的一个数据库来存储映射关系。
Sharding存在的问题
1.事务问题
解决办法:使用分布式事务解决,比如 XA 接口。
2.连接
解决办法:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
3.ID 唯一性
解决办法:
- 使用全局唯一 ID(GUID);
- 为每个分片(Sharding)指定一个 ID 范围;
- 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)
六、数据复制
主从复制
主要涉及三个线程:binlog 线程、 I/O 线程和 SQL 线程。
- binlog 线程:负责将主服务器上的数据更改写入二进制日志(Binary log)中;
- I/O 线程:负责从主服务器上读取二进制日志,并写入从服务器的中继日志(Relay log)中;
- SQL 线程:负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。

读写分离
概念:主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离能提高性能的主要原因在于: - 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
- 从服务器可以使用MyISAM,提升查询性能以及节约系统开销;
- 增加冗余,提高可用性。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
谈谈select语句执行过程?
MySQL 查询的大致语法结构如下:
(5)SELECT DISTINCT <select_list>
(1)FROM <left_table> <join_type> JOIN <right_table> ON <on_predicate>
(2)WHERE <where_predicate>
(3)GROUP BY <group_by_specification>
(4)HAVING <having_predicate>
(6)ORDER BY <order_by_list>
(7)LIMIT n, m
查询处理的顺序如下:
- from
- on
- join
- where
- group by:对结果集进行分组,如统计用户可以访问多少个菜单(
select role_id, count(menu_id) from sys_role_menu group by role_id;) - having:having 子句用于筛选查询结果,如查询总成绩>1000的学生,我们不能用where来筛选超过1000的学生,因为表中不存在这样一条记录
having sun(nums) > 1000 - select
- distinct:去重
- order by:对结果集进行排序
- limit
这些步骤执行时,每个步骤都会产生一个虚拟表,该虚拟表被用作下一个步骤的输入。这些虚拟表对调用者(客户端应用程序或者外部查询)不可用。只是最后一步生成的表才会返回给调用者。如果没有在查询中指定某一子句,将跳过相应的步骤。
为什么使用数据索引能提高效率?
- 数据索引的存储是有序的;
- 在有序情况下,通过索引查询一个数据是无需遍历索引记录的;
- 极端情况下,数据索引的查询效率为二分发查询效率,趋近于log2N
什么情况下应不建或少建索引?
- 表记录太少;
- 经常插入、修改、删除的表;
- 数据重复且分布均匀的表字段,假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度;
- 经常和主字段一块查询但主字段索引值比较多的表字段。
谈谈 MVCC?
首先我们要了解LBCC(Lock-Based Concurrency Control)——基于锁的并发控制,而MVCC (Multi-Version Concurrency Control)并发版本控制 是在 LBCC 上的改进,主要是在读操作上提高了并发量。
InnoDB存储引擎,实现的是MVCC
MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,现阶段几乎所有的RDBMS,都支持了MVCC。
快照读、当前读和 MVCC
1.快照读
快照读是基于 MVCC 和 undo log 来实现的,适用于简单 select 语句。
- 读已提交:一个事务内操作一条数据,可以查询到另一个已提交事务操作同一条数据的最新值。(
Oracle 默认隔离级别)所以当一个事务内有多个 sql 查询时,会生成多个 readView,每条 sql 都能查询到最新 readView 的值; - 可重复读:每个事务只关注自己事务开始查询到的数据值,无论事务查询同一条数据多少次,该数据改了多少次,都只能查询到事务开始之前的数据值。(
MySQL 默认隔离级别)所以当一个事务内有多个 sql 查询时,读取到的 readView 都是同一个,那么查询某条数据的值也是一样的。
而所谓 MVCC 并发版本控制,是靠 readView(事务视图)来实现的,readView 是针对同一条数据生成的视图。多个 readView 组成 undo log(回滚日志)。
2.当前读
当前读是基于 临键锁(行锁 + 间歇锁) 来实现的,适用于insert、update、delete、select … for update、select … lock in share mode语句,以及加锁了的 select 语句。
更新数据时,都是先读后写,而这个读,就是当前读;读取数据时,读取该条数据的已经提交的最新事务生成的 readView。假设现在事务A有2个 sql 语句,事务开始时生成 readView(id = n):
- 如果第一个 sql 操作一条数据时读当前的 readView(id = n),此时开始一个事务B生成 readView(id = n+1),并且对该条数据做了操作(非简单select操作),此时事务A的第二个 sql 语句当前读该数据,就会读取到最新的 readView(id = n+1);
- 假设事务A的第二个 sql 语句操作数据时,事务B还未提交其非简单的 select 操作,那么这条数据就会被事务B写锁锁住,所以事务A就会阻塞,等待事务B释放锁。
char、varchar、text的区别
- char 长度固定,每条数据占用等长字节空间;适合用在身份证号码、手机号码等;
- varchar 可变长度,可以设置最大长度;适合用在长度可变的属性;
- text 不设置长度,当不知道属性的最大长度时,适用text;
查询速度:char最快 → varchar其次 → text最慢
- *char(n)**:中的 n 表示字符数,最大字符数是255;如果是 utf8 编码,那么 char 类型占2553个字节(utf8 下一个字符占用1~3个字节)
- **varchar(n)**:中的 n 表示字符数,最大空间是65535个字节,存放字符数量跟字符集有关系;(实际范围是65532或65533, 因为内容头部会占用1或2个字节保存该字符串的长度;如果字段default null,整条记录还需要1个字节保存默认值 null)
- MySQL5.0.3 以前版本:n表示字节数;
- MySQL5.0.3 之后版本:n表示字符数;
- text:跟varchar基本相同, 理论上最多保存65535个字符, 实际上text占用内存空间最大也是65535个字节; 考虑到字符编码方式, 一个字符占用多个字节, text并不能存放那么多字符; 跟varchar的区别是text需要2个字节空间记录字段的总字节数
- 注意:由于 varchar 查询速度更快,所以能用 varchar 的时候就不要用 text!
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!