Skip to content

MySQL 与 MariaDB ​

本篇定位

覆盖索引、事务隔离、锁、慢查询、主从复制和备份。

高频面试题 ​

Q1:为什么用 B+树做索引? ​

我的答案:

结论:MySQL InnoDB 索引用 B+树,是因为它矮胖、磁盘 IO 少、范围查询快、叶子节点有序。

第一,先对比一下为什么不用二叉树或红黑树。数据存在磁盘上,每次读一个节点就是一次 IO。二叉树太高,几千万行数据树高十几二十层,查一条数据要读十几次磁盘,扛不住。B+树是多叉平衡树,一个节点就是一个 16KB 的页,能放下上百个键,所以树非常矮——千万级数据树高也就 3~4 层,查一次主键最多 3~4 次 IO,这是核心。

第二,对比 B 树。B 树非叶子节点也存数据,导致每个节点能放的键变少,树变高;而且范围查询要中序遍历、可能再回到根。B+树把所有数据都放在叶子节点,非叶子只存键,节点塞得下更多键、树更矮;叶子节点之间用双向链表串起来,做 between、>、<、order by 这种范围扫描特别方便,找到起点顺着链表往后扫就行。

第三,对比哈希索引。哈希等值查询 O(1) 很快,但不支持范围、不支持排序、不支持最左前缀,哈希冲突还有代价。InnoDB 的自适应哈希是内部自动加的,用户建不了。

补充两个概念:聚簇索引就是主键索引,叶子节点直接存整行数据;二级索引叶子节点存的是主键值,所以查二级索引如果要拿整行,要回表——先查二级索引拿到主键,再去聚簇索引查一次。覆盖索引就是查询的列都在索引里,不用回表。


Q2:事务隔离级别及问题?MVCC 原理? ​

我的答案:

结论:SQL 标准四个隔离级别,从低到高是读未提交 RU、读已提交 RC、可重复读 RR、串行化 Serializable。MySQL InnoDB 默认是 RR。

并发问题三个:脏读是读到别的事务还没提交的修改;不可重复读是同一事务内两次读同一行,被别的事务提交更新了,值不一样(侧重单行修改);幻读是同一事务内两次范围查询,第二次多了几行(侧重插入)。

各级别解决情况:RU 啥都不解决;RC 解决脏读;RR 解决脏读和不可重复读,InnoDB 靠 Next-Key Lock 也基本解决了幻读;Serializable 全加串行锁,啥都解决但性能最差。

MVCC(多版本并发控制)是 InnoDB 在 RC/RR 下实现"读不加锁、读写不阻塞"的核心。原理:每行隐藏两个列 trx_id(最近修改它的事务 ID)和 roll_pointer(指向 undo log 里的老版本)。读的时候,根据当前事务的 Read View 判断这行哪个版本对自己可见。Read View 里记录了"生成视图时活跃的事务列表",规则是:如果行的 trx_id 比最小活跃事务还小,说明已经提交了,可见;比最大还大,是后面开的事务,不可见;在活跃列表里,也不可见。不可见就顺着 roll_pointer 往 undo log 找老版本,直到找到一个可见的。

RC 和 RR 的区别就一句话:RC 是每条 SELECT 都生成新 Read View,所以能看到别人刚提交的;RR 是事务第一次 SELECT 时生成 Read View,整个事务复用,所以整个事务内看到的快照一致。


Q3:索引失效的常见情况? ​

我的答案:

结论:索引失效本质是"优化器觉得走索引比全表扫还慢,干脆不走"。常见情况我按面试高频列。

第一,不满足最左前缀。联合索引 (a,b,c) 是按 a→b→c 排序的,如果你 where b=?、where c=?、或者跳过 a 直接查 b,索引用不上;where a=? and c=? 只能用到 a 这一列。

第二,索引列上用函数或做运算。where date(create_time)='2026-09-01'、where age+1=10、where left(name,3)='abc',列被处理过,索引树没法直接定位。改写:改成范围 create_time >= '2026-09-01' and create_time < '2026-09-02',或者建函数索引。

第三,隐式类型转换。字段是 varchar,你写 where phone=13800000000(数字),MySQL 会把每行 phone 转成数字再比,等价于在列上加函数,索引失效。字符串就老老实实地加引号。

第四,like '%xxx' 前导通配符,B+树是按前缀排序的,前 % 没法定位;like 'xxx%' 是能用的。

第五,or 两边有一边没索引,整个索引失效。

第六,is null、!=、not in、not exists 在数据量占比大时优化器会放弃索引,因为扫大半张表比回表还慢。

怎么确认?explain select ...,重点看几列:type 列,const/eq_ref/ref/range 是好的,ALL 就是全表扫;key 列实际用到的索引,NULL 就是没用上;rows 预估扫描行数;Extra 里出现 Using filesort、Using temporary 要警惕,出现 Using index 说明覆盖索引挺好。


Q4:慢 SQL 如何优化?explain 怎么看? ​

我的答案:

结论:慢 SQL 优化是个固定流程——先定位、再分析、后优化、最后验证。

第一步,定位。先开慢查询日志:slow_query_log=ON、long_query_time=1(秒)、slow_query_log_filename=/data/mysql/slow.log,然后用 mysqldumpslow -s t -t 10 /data/mysql/slow.log 或 pt-query-digest 聚合,找出频次高、平均耗时长的 Top SQL。

第二步,explain 看执行计划。重点列:

  • id:select 序号,越大越先执行;
  • select_type:SIMPLE/PRIMARY/SUBQUERY 等;
  • table:查哪张表;
  • type:访问类型,从好到差 system > const > eq_ref > ref > range > index > ALL。看到 ALL 全表扫、index 全索引扫,就要优化;
  • key:实际用的索引,NULL 说明没用上;
  • key_len:用了索引的多长,判断联合索引用了几列;
  • ref:索引跟什么比;
  • rows:预估要扫多少行,越大越糟;
  • Extra:重点。Using index 覆盖索引好;Using where 服务器层过滤;Using filesort 额外排序要优化;Using temporary 用临时表(group by/order by 常见)要优化。

第三步,优化手段:加合适的联合索引满足最左前缀;改写 SQL 避免 select *、只查需要的列;小表驱动大表;join 字段要有索引;深分页 limit 1000000,10 改成"记住上一页最大 id,where id > ? limit 10";还不行就考虑分表、加缓存、读写分离。

第四步,改完再 explain 一遍,确认 type 升到 ref/range、rows 降下来,再 show profiles 或实测耗时对比。


Q5:MySQL 主从复制原理? ​

我的答案:

结论:MySQL 主从复制分三步——主库写 binlog、从库 IO 线程拉到 relay log、从库 SQL 线程重放,默认异步复制。

具体流程:第一,主库任何增删改事务提交时,会把事务顺序写入 binlog(二进制日志,statement/row/mixed 三种格式,生产推荐 row)。第二,从库启动一个 IO 线程,连上主库,主库为它启动一个 dump 线程,把 binlog 事件推给从库。第三,从库 IO 线程收到事件后写进本地的 relay log(中继日志)。第四,从库另一个 SQL 线程(MySQL 5.7 后基于并行复制,多个 worker)实时读 relay log,重放这些事件,保持数据和主库一致。复制位点用 binlog 文件名 + position 记录,从库记住自己读到哪了,下次从断点继续。

复制方式有三种:异步复制(默认,主库提交完就返回,不等从库,性能好但主库宕机可能丢数据);半同步复制(主库至少等一个从库收到 binlog 并写进 relay log 才返回客户端,损失一点性能换一致性,MySQL 5.5+ 支持);组复制/MGR(基于 Paxos,多主强一致,成本高)。

主从延迟是老大难问题。原因:从库单线程重放追不上主库并发写入;大事务、DDL、慢查询都会卡住 SQL 线程。监控用 Seconds_Behind_Master,但这个值不太准,生产更可靠的是对比主从位点或自己记心跳表。解决办法:开启并行复制(slave_parallel_workers)、拆分大事务、避免长 DDL、必要时升级到半同步或 MGR。

主从复制的用途:读写分离(写走主、读走从)、做备份、做异地灾备。


相关链接 ​