PDD进二面了,开冲了!
前段时间阿里换帅的消息甚嚣尘上,也激起了大家对阿里、京东、PDD这三家电商巨头的讨论。
不知不觉间我对PDD也从最开始的看不上不想用,到现在的离不开。
消费降级后用了6年的京东会员都没续期了,捂紧钱袋子。讲真的:多多买菜今天买明天到的用户体验也挺香的,还有拼多多7天无理由的极速退款。
早在去年12月就有阿里市值被拼多多反超的新闻。
话题扯远了,咱们抓紧回到干货面经:
今天分享一位战友拼多多一面的面经, 主要问到了MySQL和项目相关的问题,目前已经拿到了二面通知。
正好趁着这个机会和大家分享一下常见数据库面试题。
面试题详解
索引下推和索引覆盖
什么是覆盖索引
覆盖索引是指在查询语句中,所需的数据都可以从索引中获取,而不需要再去聚集索引中查找。这样就避免了回表操作,从而提高了查询性能。
覆盖索引的优缺点
优点:
(1)避免了二次查找:使用覆盖索引可以直接从索引中返回所有需要的列,避免了再次到表中进行查找的开销,提高了查询效率。
(2)减少了 I/O 操作:覆盖索引通常可以使用索引下推技术,直接在索引中进行过滤,从而减少了要读取的行数,降低了 I/O 操作的开销。
缺点:
(1)对索引的要求较高:使用覆盖索引必须创建一个包含所有需要返回的列的联合索引,而联合索引的效率和使用场景都有一定限制,否则可能会导致索引扫描的代价比较大。
(2)占用更多的空间:覆盖索引包含了所有需要返回的列,因此会占用更多的存储空间,而且在修改表数据时需要更新索引,也会带来额外的开销。
什么是索引下推
索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率。
索引下推优化的原理
我们先简单了解一下MySQL大概的架构:
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
我们来具体看一下,在没有使用ICP的情况下,MySQL的查询:
存储引擎读取索引记录; 根据索引中的主键值,定位并读取完整的行记录; 存储引擎把记录交给Server层去检测该记录是否满足WHERE条件。使用ICP的情况下,查询过程: 存储引擎读取索引记录(不是完整的行记录); 判断WHERE条件部分能否用索引中的列来做检查,条件不满足,则处理下一行索引记录; 条件满足,使用索引中的主键去定位并读取完整的行记录(就是所谓的回表); 存储引擎把记录交给Server层,Server层检测该记录是否满足WHERE条件的其余部分。
join原理实现
Nested-Loop Join 算法,需要区分驱动表和被驱动表,先访问驱动表,筛选出结果集,然后将这个结果集作为循环的基础,访问被驱动表过滤出需要的数据。
算法这两种表分为驱动表和被驱动表,使用嵌套循环。驱动表在外循环,被驱动表在内循环。
不同 Nested-Loop Join ,讨论其实是对内循环的优化。
为了更专注于 Nested-Loop Join 的讨论,我们这里的 join 操作都不带 where 子句对结果集进行过滤。所以默认驱动表的结果集就是整张表的数据。
SNLJ SNLJ,Simple Nested-Loop Join,简单嵌套循环。这是最简单的方案,性能也一般。对内循环没优化。
假设 A 是驱动表,B 是被驱动表。
MySQL-SNLJ
这里会扫描 A 表,将记录一行一行地取出来进行匹配。其实就是用 A 的结果集做为外循环,读取每一行都会触发一个内循环(扫描 B 表)。对 B 表的数据进行比较,加入结果集。
最后根据 join 类型合并驱动表和被驱动表的结果集。看是 left join、right join 还是 inner join,效果在上面有描述过。
伪代码如下:
For each row a in A do
For each row b in B do
If a and b satify the join condition
Then output the tuple
假设 A 表有 N 行,B 表有 M 行。SNL 的开销如下:
A 表扫描 1 次。 B 表扫描 N 次。 一共有 N 个内循环,每个内循环要 M 次,一共有内循环 N * M 次。有就是比较了 N * M 次。 总共读取记录数:N + N * M 。 回表读取记录数:0。
INLJ INLJ,Index Nested-Loop Join,索引嵌套循环。
整个算法过程和 SNL 一致,最大的区别在于,用来进行 join 的字段已经在被驱动表中建立了索引。
我们假设 A 是驱动表,B 是被驱动表。
使用聚簇索引:
MySQL-INTJ-2
没有使用聚簇索引,需要增加回表操作:
MySQL-INTJ-1
A 的行数为 N,所以内循环个数没变也是 N,因为还是要对 N 行 A 数据进行比较。但是内循环次数被优化了。
之前的 SNLJ 算法,因为没有索引,每个内循环要扫码一次 B 表。有了索引后,不需要再全表扫描 B 表,而是进行 B 表的索引查询。最终查询和比较的次数大大降低。
伪代码如下:
For each row a in A do
lookup b in B index
if found b == a
Then ouput the tuple
假设 A 表 N 行,B 表 M 行,索引 B+ 树的高度为 IndexHeight。
A 表扫描 1 次。 B 表扫描 0 次。因为使用了索引查询。 一共有 N 个内循环,每个内循环遍历次数为索引树的高度,为 IndexHeight 次,一共有内循环 N * IndexHeight 次。也就是比较了 N * IndexHeight 次。 总共读取的记录数:N + M(match)。M(match) 为索引返回的记录数。 回表次数:主键的聚簇索引为,非聚簇索引为 M(match),即每个记录还需要进行一次回表。
这里有个回表问题需要关注一下。
如果要查询的字段为 B 表的主键,使用了主键的聚簇索引,可以直接拿到记录。
如果要查询的字段不是 B 表的主键,使用的不是主键的聚簇索引,而是辅助索引,还需要进行一次回表查主键的聚簇索引。这里的性能会很有很大的下降。
BNLJ BNLJ,Block Nested-Loop Join,块嵌套循环。
如果 join 的字段有索引,MySQL 会使用 INL 算法。如果没有的话,MySQL 会如何处理?
因为不存在索引了,所以被驱动表需要进行扫描。这里 MySQL 并不会简单粗暴的应用 SNL 算法,而是加入了 buffer 缓冲区,降低了内循环的个数,也就是被驱动表的扫描次数。
这个 buffer 被称为 join buffer,顾名思义,就是用来缓存 join 需要的字段。MySQL 默认 buffer 大小 256K,如果有 n 个 join 操作,会生成 n-1 个 join buffer。
假设这里 A 为驱动表,B 为被驱动表。
在外层循环扫描 A 中的所有记录。扫描的时候,会把需要进行 join 用到的列都缓存到 buffer 中。buffer 中的数据有一个特点,里面的记录不需要一条一条地取出来和 B 表进行比较,而是整个 buffer 和 B 表进行批量比较。
如果我们把 buffer 的空间开得很大,可以容纳下 A 表的所有记录,那么 B 表也只需要访问访问一次。
很显然应用了 buffer ,实际上是加入了一个中间过程,优化内循环发生的次数。
伪代码如下:
For each tuple a In A do
store used column as p from A in join buffer
for each tuple b In B do
If p and b satisfy the join condition
Then ouput the tuple
假设 A 表 N 个记录,B 表 M 个记录。开销如下:
A 表扫描 1 次。 B 表扫描的次数和 buffer 的大小有关,N * (used_column_size)/join_buffer_size + 1。其中 used_column_size 为 join 字段总大小,join_buffer_size 为 buffer 的大小。 虽然加了 buffer,但实际上 A 表的每个记录和 B 表的每个记录都进行了比较,有 N * M 次比较。 总共读取的记录数:设置 B 表扫码次数为 H,则这里记录数 = M * H。 回表次数:0。不走索引,不存在回表。
索引设计讲究/原则
选择唯一性索引
唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。
为经常需要排序、分组和联合操作的字段建立索引
经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。
为常作为查询条件的字段建立索引
如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此, 为这样的字段建立索引,可以提高整个表的查询速度。
限制索引的数目
索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。
尽量使用数据量少的索引
如果索引的值很长,那么查询的速度会受到影响。例如,对一个CHAR(100)类型的字段进行全文 检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。
尽量使用前缀来索引
如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索 会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。
删除不再使用或者很少使用的索引
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理 员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
小表不应建立索引
聚簇索引和非聚簇索引
聚簇索引
聚簇索引(Clustered Index)一般指的是主键索引(如果存在主键索引的话),聚簇索引也被称之为聚集索引。
聚簇索引在 InnoDB 中是使用 B+ 树实现的,比如我们创建一张 student 表,它的构建 SQL 如下:
drop table if exists student;
create table student(
id int primary key,
name varchar(16),
class_id int not null,
index (class_id)
)engine=InnoDB;
-- 添加测试数据
insert into student(id,name,class_id) values(1,'张三',100),
(2,'李四',200),(3,'王五',300);
以上 student 表中有一个聚簇索引(也就是主键索引)id,和一个非聚簇索引 class_id。
聚簇索引 id 对应的 B+ 树如下图所示:
在聚簇索引的叶子节点直接存储用户信息的内存地址,我们使用内存地址可以直接找到相应的行数据。
非聚簇索引
非聚簇索引在 InnoDB 引擎中,也叫二级索引,以上面 student 表为例,在 student 中非聚簇索引 class_id 对应 B+ 树如下图所示:
从上图我们可以看出,在非聚簇索引的叶子节点上存储的并不是真正的行数据,而是主键 ID,所以当我们使用非聚簇索引进行查询时,首先会得到一个主键 ID,然后再使用主键 ID 去聚簇索引上找到真正的行数据,我们把这个过程称之为回表查询。
总结
在 MySQL 的 InnoDB 引擎中,每个索引都会对应一颗 B+ 树,而聚簇索引和非聚簇索引最大的区别在于叶子节点存储的数据不同,聚簇索引叶子节点存储的是行数据,因此通过聚簇索引可以直接找到真正的行数据;而非聚簇索引叶子节点存储的是主键信息,所以使用非聚簇索引还需要回表查询,因此我们可以得出聚簇索引和非聚簇索引的区别主要有以下几个:
聚簇索引叶子节点存储的是行数据;而非聚簇索引叶子节点存储的是聚簇索引(通常是主键 ID)。 聚簇索引查询效率更高,而非聚簇索引需要进行回表查询,因此性能不如聚簇索引。 聚簇索引一般为主键索引,而主键一个表中只能有一个,因此聚簇索引一个表中也只能有一个,而非聚簇索引则没有数量上的限制。
InnoDB为什么建议用自增整数作为主键
在InnoDB中,主键被用作索引来加速查询操作。使用自增id作为主键可以确保新插入的记录在主键索引中是有序的,这样在查询时可以更快地定位到目标记录,提高了查询的效率。
此外,使用自增id作为主键还可以减少主键的冲突概率。如果使用其他类型的主键,比如UUID或者字符串,由于其分布更为均匀,可能会导致更多的主键冲突。
MySQL内存磁盘同步机制
虽然说 MySQL 的数据是存储在磁盘里的,但是也不能每次都从磁盘里面读取数据,这样性能是极差的。
要想提升查询性能,加个缓存就行了嘛。所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。
为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。
数据页在内存中是完整的数据吗?
在数据库系统中,数据页在内存中通常是完整的数据。数据页是数据库系统中用于存储数据的基本单位,通常是一个固定大小的数据块,例如在 MySQL 中通常是 16KB。当数据库系统从磁盘读取数据时,会将数据按照数据页的大小加载到内存中进行操作。
一旦数据页被加载到内存中,通常情况下是完整的数据。这意味着数据页中包含了该数据块的所有信息,包括行记录、索引数据等。数据库系统会在内存中维护数据页的完整性,以确保数据的一致性和正确性。
分库分表
什么是MySQL分库分表?
MySQL分库分表是一种数据库分割和划分数据表的技术。它将原本存储在单个数据库中的数据分散到多个数据库中,将原本单个表中的数据分散到多个数据表中。通过这种方式,可以提高数据库的查询性能和扩展性,同时降低数据库的负载。
为什么需要MySQL分库分表?
当数据库中存储的数据量庞大,且并发访问量高时,单个数据库可能无法满足快速查询和高并发的需求。此时,通过分库分表可以将数据分散存储,提高查询效率和负载均衡,进而提高系统的性能。
MySQL分库分表的实现方法和步骤
水平分库
水平分库是指将原本存储在一个数据库中的数据分散到多个独立的数据库中。实现水平分库的方法一般有两种:
按数据范围分库:根据数据的某个字段的取值范围,将数据分散到不同的数据库中。比如,根据用户ID的奇偶性将数据分散到两个数据库中。按哈希算法分库:根据数据的哈希值,将数据均匀地分散到多个数据库中。这种分库方式可以保证数据在各个库中的分布相对均衡。
垂直分库
垂直分库是指将原本存储在一个数据库中的数据根据表的关系分散到多个数据库中。实现垂直分库的方法一般有两种:
按功能分库:将不同功能的数据分散到不同的数据库中。比如,将用户信息和订单信息分别存储在不同的数据库中。按表的关系分库:将数据库中的表按照其关系划分到不同的数据库中。比如,将用户表和订单表分别存储在不同的数据库中。
水平分表
水平分表是指将原本存在单个数据表中的数据分散存储到多个数据表中。实现水平分表的方法一般有两种:
按数据范围分表:根据数据的某个字段的取值范围,将数据分散到不同的数据表中。比如,根据用户ID的奇偶性将数据分散到两个数据表中。按哈希算法分表:根据数据的哈希值,将数据均匀地分散到多个数据表中。这种分表方式可以保证数据在各个表中的分布相对均匀。
垂直分表
垂直分表是指将原本存在单个数据库表中的字段根据字段的不同特性分散到多个数据表中。实现垂直分表的方法一般有两种:
按类型分表:将不同类型的字段分散到不同的数据表中。比如,将用户的基本信息和扩展信息分别存储在不同的数据表中。按字段关系分表:将字段之间有关联的数据分散存储到不同的数据表中。比如,将用户的基本信息和订单信息分别存储在不同的数据表中。
主从架构
主从架构有什么用?通过搭建MySQL主从集群,可以缓解MySQL的数据存储以及访问的压力。
数据安全
给主服务增加一个数据备份。基于这个目的,可以搭建主从架构,或者也可以基于主从架构搭建互主的架构。
读写分离
对于大部分的JAVA业务系统来说,都是读多写少的,读请求远远高于写请求。这时,当主服务的访问压力过大时,可以将数据读请求转为由从服务来分担,主服务只负责数据写入的请求,这样大大缓解数据库的访问压力。
要理解,MySQL的主从架构只是实现读写分离的一个基础。实现读写分离还是需要一些中间件来支持,比如ShardingSphere。
故障转移-高可用
当MySQL主服务宕机后,可以由一台从服务切换成为主服务,继续提供数据读写功能。
对于高可用架构,主从数据的同步也只是实现故障转移的一个前提条件,要实现MySQL主从切换,还需要依靠一些其他的中间件来实现。比如MMM、MHA、MGR。
在一般项目中,如果数据库的访问压力没有那么大,那读写分离不一定是必须要做的,但是,主从架构和高可用架构则是必须要搭建的。
MySQL什么时候会死锁
MySQL都有什么锁?
MySQL有三种锁的级别:页级、表级、行级。
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
什么情况下会造成死锁
所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等竺的进程称为死锁进程.
表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB.
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
点击下方文章,看看他们是怎么找到好工作的!
还有最新鲜的腾讯面经,不要错过哦!