精选面试题数据库系列
Posted 工作LIFE
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精选面试题数据库系列相关的知识,希望对你有一定的参考价值。
一、mysql 聚集索引、非聚集索引
聚集索引:
给表上了主键,那么表在内存上的由整齐排列的结构转变成了树状结构,也就是「平衡树」结构,换句话说,就是整个表就变成了一个索引。没错, 再说一遍, 整个表变成了一个索引,也就是所谓的「聚集索引」。这就是为什么一个表只能有一个主键, 一个表只能有一个「聚集索引」,因为主键的作用就是把「表」的数据格式转换成「索引(平衡树)」的格式放置。
非聚集索引:
非聚集索引和聚集索引一样, 同样是采用平衡树作为索引的数据结构。索引树结构中各节点的值来自于表中的索引字段, 假如给user表的name字段加上索引 , 那么索引就是由name字段中的值构成,在数据改变时, DBMS需要一直维护索引结构的正确性。如果给表中多个字段加上索引 , 那么就会出现多个独立的索引结构,每个索引(非聚集索引)互相之间不存在关联。
区别在于,通过聚集索引可以查到需要查找的数据, 而通过非聚集索引可以查到记录对应的主键值 , 再使用主键的值通过聚集索引查找到需要的数据。
总结:
非聚集索引就是一般常用的索引,索引树的根节点是表的主键;聚集索引就是主键组成的树,根节点是数据库真实数据的位置。许多数据库的文档会告诉读者:聚集索引按照顺序物理的存储数据到磁盘。但是试想下,如果聚集索引必须按照特定顺序存放物理记录,则维护成本显得非常高。所以,聚集索引的磁盘存储并不是物理上连续的,而是逻辑上连续。
二、mysql 索引为什么会选择B+树存储实现
概念:
mysql底层使用的是B+树,mysql索引是放在磁盘上面的,因此每次读取索引时通过IO从磁盘读取。
1、hash索引:无规则、不能排序
2、二叉树:解决hash索引不能排序问题,但是当数据有序时会出现线性排列,树的深度会变得很深,会消耗大量IO。
3、平衡二叉树:解决二叉树数据有序时出现的线性插入树太深问题,树的深度会明显降低,极大提高性能,但是当数据量很大时,一般mysql中一张表达到3-5百万条数据是很普遍的,因此平衡二叉树的深度还是非常大,mysql读取时还是会消耗大量IO,不仅如此,计算从磁盘读取数据时以页(4KB)为单位的,及每次读取4096byte。平衡二叉树每个节点只保存了一个关键字(如int即4byte),浪费了4092byte,极大的浪费了读取空间。
4、B-树:解决平衡二叉树树深度的问题,解决了平衡二叉树读取消耗大量内存空间的问题。因为B-树每个节点可以存放多个关键字,最大限度的利用了从磁盘读取的内存空间,单节点存放多个关键字同时也大大减少了树的深度。极大的提高了mysql的查询性能。但是B-树还是有缺点,B-树对有范围查找的查询(如age>20)时采用的还是中序排序法,因此也需要多遍历,并且查询性能不稳定,比如查询(select * from table where id = 222 和 select * from table where id = 223)时在查询效率(耗时)上可能会存在一定的差别,因为B-树还是将关键字,这里为id,存放在根节点和叶节点的,如果运气好,可能id=222这个关键字就在第一个节点,消耗一次IO就找到了,而id=223可能在叶节点,需要消耗3次IO才能找到。因此B-树对同一条sql语句的查询性能可能会有很大影响(确实感觉有点扯,但是事实是这样)。
B+树是B-树的变种(PLUS版)多路绝对平衡查找数,他拥有B-树的优势。
B+树扫库、表能力更强(因为B+树只在叶子节点保存数据了,因此每次IO读取的数据会更多。)
B+树的磁盘读写能力更强(因为B+树只在叶子节点保存数据了,因此每次IO读取的数据会更多。)
B+树的排序能力更强。(因为叶子节点添加了左边最大的指向右边最小的,有天然的排序。)
三、mysql 执行计划
1.执行计划的概念:
我们都知道mysql对于一条sql在执行过程中会对它进行优化,而对于查询语句的来说最好的优化方案就是使用索引。而执行计划就是显示mysql执行sql时的详细执行情况。其中包含了是否使用索引,使用了哪些索引…
2.执行计划的语法:
常规执行计划语法
explain select * from user;
扩展执行计划的语法
explain 的extended 扩展能够在原本explain的基础上额外的提供一些查询优化的信息,这些信息可以通过mysql的show warnings命令得到。
explain extended select * from user;
分区表的执行计划语法
explain partitions select * from user;
3.执行计划包含的信息:
不同版本的Mysql和不同的存储引擎执行计划不完全相同,但基本信息都差不多。mysql执行计划主要包含以下信息:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
1 |
SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 1 | NULL |
id:查询的顺序
由一组数字组成,表示一个查询中各个子查询的执行顺序。
id | id不同情况 | 执行顺序 |
1 | id相同 | 执行顺序由上至下 |
2 | id不同 | 值越大优先级越高,越先被执行 |
3 | id为null | 表示一个结果集,不需要使用它查询,常出现在包含union等查询语句中 |
select_type:查询类型
id | select_type | description | 举例 |
1 | SIMPLE | 不包含任何子查询或union等查询 | EXPLAIN SELECT * FROM user WHERE id=1; |
2 | PRIMARY | 包含子查询最外层查询就显示为 PRIMARY |
EXPLAIN SELECT * FROM user WHERE id=(SELECT user_id FROM address WHERE id=1); |
3 | SUBQUERY | 在select 或 where 字句中包含的查询 |
EXPLAIN SELECT * FROM user WHERE id=(SELECT user_id FROM address WHERE id=1); |
4 |
DERIVED | from 字句中包含的查询(衍生查询) |
EXPLAIN SELECT * FROM (SELECT * FROM user WHERE id>1) t; |
5 |
UNION | 出现在union 后的查询语句中(当前执行计划的中间记录 就是 UNION RESULT)) |
EXPLAIN SELECT * FROM user WHERE id=1 UNION SELECT * FROM user WHERE id=2; |
6 |
UNION RESULT | 从UNION中获取结果集当前执行计划的最后一条记录 就是 UNION RESULT) |
EXPLAIN SELECT * FROM user WHERE id=1 UNION SELECT * FROM user WHERE id=2; |
table:查询涉及到的表
如果查询使用了别名,那么这里显示的是别名,如果不涉及对数据表的操作,那么这显示为null,如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。如果是尖括号括起来的<union M,N>,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。
type:访问类型
id | type | 含义 | 举例 |
1 | ALL | 全表扫描(没有使用到索引) | EXPLAIN SELECT * FROM user WHERE PASSWORD=‘1’; |
2 |
index | 遍历索引(只查询索引列) | EXPLAIN SELECT id FROM user ; |
3 |
range | 索引范围查找(在索引列添加范围查询) | EXPLAIN SELECT id FROM user where id>1; |
4 |
index_subquery | 在子查询中使用 ref | 暂时没有合适例子 |
5 | unique_subquery | 在子查询中使用 eq_ref | 暂时没有合适例子 |
6 |
ref_or_null | 对Null进行索引的优化的 ref | 暂时没有合适例子 |
7 | fulltext | 使用全文索引 | 暂时没有合适例子 |
8 |
ref | 使用非唯一索引查找数据 | EXPLAIN SELECT id FROM user WHERE username=‘p1’; |
9 |
eq_ref | 在join查询中使用 PRIMARY KEY or UNIQUE NOT NULL索引关联 (这里需要把address的user_id设置为unique类型的索引) | EXPLAIN SELECT * FROM user u left JOIN address a on u.id=a.user_id WHERE a.id>1; |
10 |
const | 使用主键或者唯一索引,且匹配的结果只有一条记录 | EXPLAIN SELECT * FROM user WHERE id=1; |
11 | system const | 连接类型的特例,查询的表为系统表 |
possible_keys:可能使用的索引
注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 NULL时就要考虑当前的SQL是否需要优化了。
key:实际使用的索引
显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。
TIPS:查询中若使用了覆盖索引(覆盖索引:索引的数据覆盖了需要查询的所有数据),则该索引仅出现在key列表中。select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。
key_length:索引长度
char()、varchar()索引长度的计算公式:
(Character Set:utf8mb4=4,utf8=3,gbk=2,latin1=1) * 列长度 + 1(允许null) + 2(变长列)
ref:连接匹配条件
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值,如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func。
rows:估算的结果集数量
返回估算的结果集数目,注意这并不是一个准确值。
extra:额外信息
extra的信息非常丰富,常见的有如下4种类型:
id | extra | 含义 |
1 | Using index | 使用覆盖索引 |
2 | Using where | 使用了用where子句来过滤结果集 |
3 | Using filesort | 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化 |
4 |
Using temporary | 使用了临时表 |
四、mysql 优化
尽量少 join
MySQL 的优势在于简单,但这在某些方面其实也是其劣势。MySQL 优化器效率高,但是由于其统计信息的量有限,优化器工作过程出现偏差的可能性也就更多。对于复杂的多表 Join,一方面由于其优化器受限,再者在 Join 这方面所下的功夫还不够,所以性能表现离 Oracle 等关系型数据库前辈还是有一定距离。但如果是简单的单表查询,这一差距就会极小甚至在有些场景下要优于这些数据库前辈。
尽量少排序
排序操作会消耗较多的 CPU 资源,所以减少排序可以在缓存命中率高等 IO 能力足够的场景下会较大影响 SQL 的响应时间。对于MySQL来说,减少排序有多种办法,比如:上面误区中提到的通过利用索引来排序的方式进行优化、减少参与排序的记录条数。非必要不对数据进行排序…
尽量避免 select *
很多人看到这一点后觉得比较难理解,上面不是在误区中刚刚说 select 子句中字段的多少并不会影响到读取的数据吗?
是的,大多数时候并不会影响到 IO 量,但是当我们还存在 order by 操作的时候,select 子句中的字段多少会在很大程度上影响到我们的排序效率,这一点可以通过我之前一篇介绍 MySQL ORDER BY 的实现分析的文章中有较为详细的介绍。此外,上面误区中不是也说了,只是大多数时候是不会影响到 IO 量,当我们的查询结果仅仅只需要在索引中就能找到的时候,还是会极大减少 IO 量的。
尽量用 join 代替子查询
虽然 Join 性能并不佳,但是和 MySQL 的子查询比起来还是有非常大的性能优势。MySQL 的子查询执行计划一直存在较大的问题,虽然这个问题已经存在多年,但是到目前已经发布的所有稳定版本中都普遍存在,一直没有太大改善。虽然官方也在很早就承认这一问题,并且承诺尽快解决,但是至少到目前为止我们还没有看到哪一个版本较好的解决了这一问题。
尽量少 or
当 where 子句中存在多个条件以“或”并存的时候,MySQL 的优化器并没有很好的解决其执行计划优化问题,再加上 MySQL 特有的 SQL 与 Storage 分层架构方式,造成了其性能比较低下,很多时候使用 union all 或者是union(必要的时候)的方式来代替“or”会得到更好的效果。
尽量用 union all 代替 union
union 和 union all 的差异主要是前者需要将两个(或者多个)结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的 CPU 运算,加大资源消耗及延迟。所以当我们可以确认不可能出现重复结果集或者不在乎重复结果集的时候,尽量使用 union all 而不是 union。
尽量早过滤
这一优化策略其实最常见于索引的优化设计中(将过滤性更好的字段放得更靠前)。在 SQL 编写中同样可以使用这一原则来优化一些 Join 的 SQL。比如我们在多个表进行分页数据查询的时候,我们最好是能够在一个表上先过滤好数据分好页,然后再用分好页的结果集与另外的表 Join,这样可以尽可能多的减少不必要的 IO 操作,大大节省 IO 操作所消耗的时间。
避免类型转换
这里所说的“类型转换”是指 where 子句中出现 column 字段的类型和传入的参数类型不一致的时候发生的类型转换:①认为在column_name 上通过转换函数进行转换;②直接导致 MySQL(实际上其他数据库也会有同样的问题)无法使用索引,如果非要转换,应该在传入的参数上进行转换;③由数据库自己进行转换;⑤如果我们传入的数据类型和字段类型不一致,同时我们又没有做任何类型转换处理,MySQL 可能会自己对我们的数据进行类型转换操作,也可能不进行处理而交由存储引擎去处理,这样一来,就会出现索引无法使用的情况而造成执行计划问题。
优先优化高并发的 SQL,而不是执行频率低某些“大”SQL
对于破坏性来说,高并发的 SQL 总是会比低频率的来得大,因为高并发的 SQL 一旦出现问题,甚至不会给我们任何喘息的机会就会将系统压跨。而对于一些虽然需要消耗大量 IO 而且响应很慢的 SQL,由于频率低,即使遇到,最多就是让整个系统响应慢一点,但至少可能撑一会儿,让我们有缓冲的机会。
从全局出发优化,而不是片面调整
SQL 优化不能是单独针对某一个进行,而应充分考虑系统中所有的 SQL,尤其是在通过调整索引优化 SQL 的执行计划的时候,千万不能顾此失彼,因小失大。
尽可能对每一条运行在数据库中的SQL进行 explain
优化 SQL,需要做到心中有数,知道 SQL 的执行计划才能判断是否有优化余地,才能判断是否存在执行计划问题。在对数据库中运行的 SQL 进行了一段时间的优化之后,很明显的问题 SQL 可能已经很少了,大多都需要去发掘,这时候就需要进行大量的 explain 操作收集执行计划,并判断是否需要进行优化。
四、mysql 四种隔离级别和MVCC
MySQL四种隔离级别如下:
未提交读(READ UNCOMMITTED)
这就是上面所说的例外情况了,这个隔离级别下,其他事务可以看到本事务没有提交的部分修改.因此会造成脏读的问题(读取到了其他事务未提交的部分,而之后该事务进行了回滚).这个级别的性能没有足够大的优势,但是又有很多的问题,因此很少使用.
已提交读(READ COMMITTED)
其他事务只能读取到本事务已经提交的部分.这个隔离级别有 不可重复读的问题,在同一个事务内的两次读取,拿到的结果竟然不一样,因为另外一个事务对数据进行了修改.
REPEATABLE READ(可重复读)
可重复读隔离级别解决了上面不可重复读的问题(看名字也知道),但是仍然有一个新问题,就是 幻读,当你读取id> 10 的数据行时,对涉及到的所有行加上了读锁,此时例外一个事务新插入了一条id=11的数据,因为是新插入的,所以不会触发上面的锁的排斥,那么进行本事务进行下一次的查询时会发现有一条id=11的数据,而上次的查询操作并没有获取到,再进行插入就会有主键冲突的问题.
这个隔离级别也是Innodb存储引擎默认的隔离级别.
SERIALIZABLE(可串行化)
这是最高的隔离级别,可以解决上面提到的索引问题,因为他强制将所以的操作串行执行,这会导致并发性能极速下降,因此也不是很常用.Mysql中实施的是自动提交,也就是说默认一个语句未一个事务,当然你可以通过设置AUTOCOMMIT变量来关闭自动提交,也可以通过begin来显式的开启一个事务.
MVCC:
MVCC, Multiversion Concurrency Control多版本并发控制。MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作, 因此服务器的开销更低(减少了锁的生产和分配)。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
总结:
在实现上,MySQL通过三个列实现对版本的控制,即6字节的事务ID(DB_TRX_ID)字段,7字节的回滚指针(DB_ROLL_PTR)字段 ,6字节的DB_ROW_ID字段。
而实际上InnoDB并非完全意义上的MVCC,因为没有实现多版本并存。关键在于并存两字,即在事务执行对数据操作时同时存在多个版本。而无论如何MySQL在事务执行的时候是串行化的,即使这两个事务几乎同时发生。其实这正是对数据安全性的一个保障。
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容。
MySQL的事务隔离级别定义都是针对读操作,并且读操作指的是“当前读”;MySQL的默认隔离级别RR使用Gap-Lock来解决幻读,Record-Lock解决脏读和可重复读;因此RR级别是通过Next-Key Lock(Gap-Lock + Record-Lock)实现的;MVCC机制是MySQL为实现一致性非锁定读,提高部分读写效率而引入的机制。
五、mysql 乐观锁和悲观锁
数据库管理系统中并发控制的任务是确保在多个事务同时存取数据库中同一数据不破坏事务的隔离性和统一性以及数据库的统一性,乐观锁和悲观锁式并发控制主要采用的技术手段。
1.悲观锁:
在关系数据库管理系统中,悲观并发控制(悲观锁,PCC)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作的每行数据应用了锁,那只有当这个事务锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要应用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本环境。
悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此在整个暑假处理过程中,将数据处于锁定状态。悲观锁的实现,一般依靠数据库提供的锁机制(推荐教程:MySQL教程)。
数据库中,悲观锁的流程如下:
在对任何记录进行修改之前,先尝试为该记录加上排他锁
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或抛出异常
如果成功加锁,则就可以对记录做修改,事务完成后就会解锁
其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常
MySQL InnoDB中使用悲观锁:
要使用悲观锁,必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是当你执行一个更新操作后,MySQL会立即将结果进行提交
//开始事务
begin;/begin work;/start transaction;(三者选一个)
select status from t_goods where id=1 for update;
//根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//修改商品status为2
update t_goods set status=2;
// 提交事务
commit;/commit work;
以上查询语句中,使用了select...for update方式,通过开启排他锁的方式实现了悲观锁。则相应的记录被锁定,其他事务必须等本次事务提交之后才能够执行。
我们使用select ... for update会把数据给锁定,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
特点:
为数据处理的安全提供了保证。效率上,由于处理加锁的机制会让数据库产生额外开销,增加产生死锁机会。在只读型事务中由于不会产生冲突,也没必要使用锁,这样会增加系统负载,降低并行性。
2.乐观锁:
乐观并发控制也是一种并发控制的方法。
假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据,在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没其他事务修改该数据,如果有则回滚正在提交的事务。
乐观锁相对悲观锁而言,是假设数据不会发生冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误信息,让用户决定如何做。
乐观锁实现一般使用记录版本号,为数据增加一个版本标识,当更新数据的时候对版本标识进行更新。
实现:
使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新版本号
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
特点:
乐观并发控制相信事务之间的数据竞争概率是较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁
以上是关于精选面试题数据库系列的主要内容,如果未能解决你的问题,请参考以下文章
面试系列一:精选大数据面试真题10道(混合型)-附答案详细解析