MySQL数据库面试题(完整版)
目录
- 1. 三大范式
- 2. 数据存储类型
- 2.1 varchar与char的区别
- 2.2 blob和text有什么区别?
- 2.3 DATETIME和TIMESTAMP的异同
- 2.4 IN和EXISTS区别
- 3. Select语句完整的执行顺序
- 4. MySQL事务
- 4.1 事务的基本要素ACID
- 4.2 ACID靠什么保证
- 4.3 事务的并发问题
- 4.4 MySQL事务隔离级别
- 4.5 MVCC
- 4.5.1 MVCC是什么
- 4.5.2 MVCC组成
- 4.5.3 MVCC工作原理
- 4.5.4 MVCC解决什么问题
- 5. MySQL索引
- 5.1 索引的定义、类型及使用
- 5.2 创建索引的注意点
- 5.3 索引失效的情况
- 5.4 建立索引的时机
- 5.5 MySQL索引数据结构
- 5.5.1 聚簇索引和非聚簇索引
- 5.6 B树和B+树的区别
- 5.7 为什么MySQL索引常用B+树
- 5.8 索引数据结构选择依据
- 5.9 MyISAM和InnoDB的区别
- 5.10 最左前缀原则/最左匹配原则
- 6. MySQL锁
- 6.1 乐观锁和悲观锁
- 6.2 死锁问题及排查
- 6.3 死锁解决办法
- 7. MySQL日志
- 8. MySQL高可用/高性能
- 8.1 MySQL高可用方案
- 8.2 主从同步延迟处理
- 9. SQL优化
- 9.1 慢SQL定位与排查
- 9.2 MySQL优化原则
- 10. 实际工作问题
- 10.1 百万级数据删除
- 10.2 大表添加字段
- 10.3 MySQL CPU飙升处理
- 11. 常见面试SQL
1. 三大范式
- 第一范式:数据表中的每一列(每个字段)都不可以再拆分。
- 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能依赖于主键的一部分。例如订单表里存储商品信息时,需将商品ID和订单ID作为联合主键。
- 第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键,而不依赖于其他非主键。例如订单表不能存储用户姓名、地址等用户信息。
2. 数据存储类型
2.1 varchar与char的区别
| 类型 | 长度 | 特性 | 存储方式 | 性能特点 | 空间利用 | 最大字符数 |
|---|---|---|---|---|---|---|
| char | 定长 | 插入数据长度小于设定长度时用空格填充 | - | 存取速度快,比varchar快约50% | 可能占多余空间,空间换时间 | 255 |
| varchar | 可变长 | - | 按插入数据实际长度存储 | 存取速度慢 | 不占多余空间,时间换空间 | 65532 |
2.2 blob和text有什么区别?
| 对比维度 | BLOB | TEXT |
|---|---|---|
| 用途 | 存储二进制数据(图片、音频、视频二进制编码) | 存储大字符串数据(文章内容、长描述) |
| 字符集 | 无字符集,按字节存储,无编码问题 | 有字符集,需进行字符编码处理 |
| 排序/比较依据 | 基于二进制字节值 | 根据字符集校对规则 |
2.3 DATETIME和TIMESTAMP的异同
| 对比维度 | DATETIME | TIMESTAMP |
|---|---|---|
| 表现格式 | YYYY-MM-DD HH:MM:SS | YYYY-MM-DD HH:MM:SS |
| 数据组成 | 包含日期+时间 | 包含日期+时间 |
| 微秒支持 | 支持秒后6位小数秒 | 支持秒后6位小数秒 |
| 存储范围 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | 1970-01-01 00:00:01 UTC 至 2038-01-19 03:14:07 UTC |
| 存储空间 | 8字节 | 4字节(无微秒)/5-8字节(有微秒) |
| 时区影响 | 不受时区影响,存储读取值不变 | 受时区影响,存储转UTC,读取转当前时区 |
| 默认值 | 无默认值 | 默认当前时间(CURRENT_TIMESTAMP) |
2.4 IN和EXISTS区别
| 对比项 | IN | EXISTS |
|---|---|---|
| 实现方式 | 外表和内表作hash连接 | 外表loop循环,每次循环查询内表 |
| 效率比较 | 两表相当:差别不大;子查询表小:用IN | 两表相当:差别不大;子查询表大:用EXISTS |
| NOT IN/NOT EXISTS | NOT IN全表扫描,不使用索引 | NOT EXISTS能用到索引,通常比NOT IN快 |
3. Select语句完整的执行顺序
SELECT语句完整执行顺序:FROM->JOIN->WHERE->GROUP BY->HAVING->SELECT->DISTINCT->ORDER BY->LIMIT/OFFSET
分步说明:
- FROM:确定查询数据来源表
- JOIN:多表时按条件连接表
- WHERE:筛选符合条件的行
- GROUP BY:按指定列分组
- HAVING:筛选分组后的结果
- SELECT:指定返回的列
- DISTINCT:去除重复行
- ORDER BY:对结果集排序
- LIMIT/OFFSET:限制返回行数和偏移量
4. MySQL事务
4.1 事务的基本要素ACID
- 原子性(Atomicity):事务操作要么全部成功,要么全部失败回滚
- 一致性(Consistency):事务执行前后,数据库数据保持一致状态
- 隔离性(Isolation):多个事务并发执行时,互不干扰
- 持久性(Durability):事务提交后,数据修改永久生效
4.2 ACID靠什么保证
- A(原子性):undo log日志,记录回滚信息,事务回滚时撤销已执行的SQL
- C(一致性):由其他三大特性+业务代码保证
- I(隔离性):锁机制(共享锁、排他锁)+ MVCC多版本并发控制
- D(持久性):内存+redo log,修改数据时同时记录到内存和redo log,宕机可恢复
4.3 事务的并发问题
| 问题 | 描述 | 解决方式 |
|---|---|---|
| 脏读 | 读取到另一个未提交事务修改的数据 | 1. 隔离级别设为读已提交+;2. 读数据加共享锁;3. MVCC读取已提交版本 |
| 不可重复读 | 同一事务两次读取同一数据结果不一致 | 1. 隔离级别提升到可重复读;2. MVCC保证事务内数据一致性;3. 共享锁至事务结束 |
| 幻读 | 同一条件查询,结果集因其他事务增删记录而变化 | 1. 隔离级别设为串行化;2. InnoDB可重复读级别用间隙锁;3. 应用层乐观锁 |
| 丢失更新 | 两个事务同时更新,一个覆盖另一个结果 | 1. 更新前加排他锁;2. 乐观锁(版本号/时间戳)检查后更新 |
4.4 MySQL事务隔离级别
| 隔离级别 | 含义 | 解决的并发问题 | 仍存在的问题 |
|---|---|---|---|
| 读未提交 | 可读取未提交事务的数据 | 无 | 脏读、不可重复读、幻读 |
| 读已提交 | 只能读取已提交事务的数据 | 脏读 | 不可重复读、幻读 |
| 可重复读 | 同一事务多次读取数据结果一致 | 脏读、不可重复读 | 幻读(InnoDB间隙锁缓解) |
| 串行化 | 事务依次顺序执行 | 脏读、不可重复读、幻读 | 性能差,锁竞争多 |
4.5 MVCC
4.5.1 MVCC是什么
MVCC(多版本并发控制)是数据库的版本控制机制,通过维护数据多个版本,支持不同事务并发执行,解决脏读、幻读、不可重复读问题。
4.5.2 MVCC组成
- 数据版本:新版本、旧版本
- 事务ID:每个事务唯一ID
- 回滚段:存储数据旧版本
4.5.3 MVCC工作原理
- 读未提交:MVCC无作用
- 读已提交:事务开始创建一致性视图,查询取最新已提交版本,避免脏读,但存在不可重复读/幻读
- 可重复读:事务开始创建视图且全程不变,解决脏读、不可重复读;幻读通过MVCC+间隙锁缓解
- 串行化:MVCC无作用
4.5.4 MVCC解决什么问题
- 解决脏读、不可重复读、幻读
- 读写可同时进行,提高并发量,减少锁竞争
- 读不加锁,读写不冲突,保证事务隔离性
5. MySQL索引
5.1 索引的定义、类型及使用
定义:索引是提高查询速度的数据结构
类型:
- 主键索引:唯一,非空,每个表仅一个
- 唯一索引:值唯一,可含NULL,表可多个
- 普通索引:无限制,基础索引类型
- 全文索引:用于文本数据全文搜索
- 组合索引:多列组合,遵循最左前缀原则
5.2 创建索引的注意点
- 建在查询频繁的字段上
- 索引数量适量(占用空间,更新需维护)
- 频繁更新的值不做主键/索引
- 小表不建索引
- 离散度低的字段(如性别)不建索引
5.3 索引失效的情况
- 查询条件含or、like、内置函数、运算(+/-/*/)、!=/<>=、not in、is null/is not null
- 字符串字段查询未加引号(隐式类型转换)
- 联合索引不遵循最左前缀原则
- 左右连接时关联字段编码不一致
5.4 建立索引的时机
- 表数据量大,查询慢
- 多表关联的字段
- GROUP BY/ORDER BY的字段
5.5 MySQL索引数据结构
- B树
- B+树(InnoDB/MyISAM默认)
- 哈希(Hash)(Memory引擎)
5.5.1 聚簇索引和非聚簇索引
- 聚簇索引(InnoDB):B+树叶子节点包含完整数据行
- 非聚簇索引(MyISAM):B+树叶子节点存储索引键值+主键值,查询需回表
5.6 B树和B+树的区别
- B树:平衡多路搜索树,键值分布在整棵树,非叶子节点可能找到目标
- B+树:B树变形,数据仅存于叶子节点,非叶子节点仅索引;叶子节点通过指针形成有序链表
5.7 为什么MySQL索引常用B+树
- 磁盘I/O少:非叶子节点仅存索引,节点存储更多索引,树高度低
- 范围查询优:叶子节点链表相连,便于范围查询
- 查询稳定:每次查询都到叶子节点,效率稳定
- 红黑树劣势:数据量大时树高,磁盘I/O多,且有变色旋转开销
5.8 索引数据结构选择依据
- 精确查找为主:哈希索引(存储引擎支持时)
- 范围查询/排序多:B+树索引
- 通用场景:B+树(MySQL默认)
5.9 MyISAM和InnoDB的区别
| 对比维度 | MyISAM | InnoDB |
|---|---|---|
| 事务支持 | 不支持 | 支持ACID |
| 外键支持 | 不支持 | 支持 |
| 索引存储 | 索引与数据分离,索引指向物理位置 | 聚簇索引存数据,辅助索引指向主键 |
| 锁机制 | 表级锁,并发写差 | 行级锁+表级锁,并发好 |
| 崩溃恢复 | 恢复困难,易丢数据 | 日志+崩溃恢复机制,自动恢复 |
| 存储统计 | 保存总行数,查询快 | 不精确,需实时计算 |
| 适用场景 | 读多写少,无事务/外键需求 | 写多,需数据完整性和高并发 |
5.10 最左前缀原则/最左匹配原则
InnoDB联合索引中,需匹配前一个/左边的值才能匹配下一个。
如创建组合索引(a1,a2,a3),等价于创建:
- (a1)
- (a1,a2)
- (a1,a2,a3)
6. MySQL锁
6.1 乐观锁和悲观锁
悲观锁
- 认为数据随时会被修改,事务拿到锁后,其他事务无法修改,只能等待
- 数据库行锁、表锁、读锁、写锁均为悲观锁
乐观锁
- 认为数据变动不频繁,通过版本号(version)或时间戳(timestamp)实现
- 版本号为最常用方式
6.2 死锁问题及排查
行锁争用导致的死锁
原因:事务持有锁时间过长,行锁争用激烈
解决思路:减少事务持有锁时间,移出不必要操作,缩短执行时间
示例:
-- 事务1STARTTRANSACTION;UPDATEusersSETage=30WHEREid=1;-- 对id=1加排他锁-- 事务2STARTTRANSACTION;SELECT*FROMusersWHEREid=2FORUPDATE;-- 对id=2加排他锁UPDATEusersSETage=25WHEREid=1;-- 等待事务1释放锁-- 事务1UPDATEusersSETname='John'WHEREid=2;-- 等待事务2释放锁,形成死锁事务顺序不一致导致的死锁
原因:不同事务操作表的顺序不一致
解决思路:统一事务操作表的顺序
示例:
-- 事务1STARTTRANSACTION;UPDATEtable1SETname1='A'WHEREid1=1;UPDATEtable2SETname2='X'WHEREid2=1;-- 事务2STARTTRANSACTION;UPDATEtable2SETname2='Y'WHEREid2=1;UPDATEtable1SETname1='B'WHEREid1=1;-- 顺序相反,易死锁死锁排查步骤
- 查看死锁日志:
show engine innodb status; - 找出死锁SQL
- 分析SQL加锁情况
- 模拟死锁场景
- 分析死锁日志
- 得出解决方案
6.3 死锁解决办法
- 统一操作顺序:所有事务对表的操作顺序一致(如先操作questions→answers→feedback)
- 拆分事务:大事务拆分为小事务,减少锁持有时间
- 设置超时时间:通过
innodb_lock_wait_timeout参数设置锁等待阈值,超时自动回滚
7. MySQL日志
- 错误日志(error log):记录启动、运行、关闭过程,定位MySQL问题
- 慢查询日志(slow query log):记录执行时间超过
long_query_time的SQL,用于优化 - 一般查询日志(general log):记录所有数据库请求,无论是否执行成功
- 二进制日志(bin log):记录DDL/DML语句(不含select/show),事件形式存储
- InnoDB特有日志:
- 重做日志(redo log):记录事务日志,保证持久性
- 回滚日志(undo log):记录数据回滚信息,保证原子性
8. MySQL高可用/高性能
8.1 MySQL高可用方案
读写分离
原理:将读写操作分散到不同节点
- 搭建主从集群(一主一从/一主多从)
- 主机:负责读写操作
- 从机:仅负责读操作
- 主机通过复制同步数据到从机
- 业务端:写操作发主机,读操作发从机
实现方式:
- 程序代码封装:抽象数据访问层,实现读写分离和连接管理
分库分表
| 类型 | 说明 |
|---|---|
| 垂直分库 | 按业务归属,将不同表拆分到不同库 |
| 水平分库 | 按字段策略(hash/range),将一个库数据拆分到多个库 |
| 垂直分表 | 按字段活跃性,将字段拆到主表/扩展表 |
| 水平分表 | 按字段策略(hash/range),将一个表数据拆分到多个表 |
水平分表路由方式:
- 范围路由:按有序列(整形/时间戳)分段
- Hash路由:字段Hash运算后分散
常用中间件:
- sharding-jdbc
- Mycat
分库分表问题:
- 事务问题:需分布式事务
- 跨库JOIN:业务代码中关联查询
主从复制过程
- master写入数据,更新binlog
- master创建dump线程向slave推送binlog
- slave创建IO线程接收binlog,写入relay log
- slave创建SQL线程读取relay log并执行,完成同步
- slave记录自身binlog
8.2 主从同步延迟处理
原因:从库仅一个线程读取binlog,主库大并发更新导致SQL积压
解决办法:
- 写操作后的读操作指定主库
- 读从机失败后重试主机
- 关键业务读写走主机,非关键业务读写分离
9. SQL优化
9.1 慢SQL定位与排查
发现慢SQL
- 开启慢查询日志:
重启MySQL生效# my.cnf/my.ini配置 slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 # 阈值2秒 SHOW PROCESSLIST;:查看Time列,值大的为慢SQL
分析慢SQL
EXPLAIN分析执行计划:关注type(ALL全表扫描差)、possible_keys、key- 索引检查:验证过滤/排序/连接字段是否有索引,示例:
CREATEINDEXidx_ageONusers(age); - 锁等待查看:查询
information_schema.innodb_trx/innodb_locks - SQL优化:减少复杂子查询/连接,添加过滤条件
工具辅助
- pt-query-digest分析慢查询日志:
pt-query-digest /var/log/mysql/mysql-slow.log
9.2 MySQL优化原则
- 分析表,适当建立索引
- 单表数据不超200W,适时分表;用explain分析慢查询
- FROM子句后表为基础表,选记录少的表
- 尽量缩小子查询结果
10. 实际工作问题
10.1 百万级数据删除
- 删除索引
- 删除无用数据
- 重新创建索引
10.2 大表添加字段
方案1:中间表转换
- 创建临时表,复制旧表结构
- 添加新字段
- 复制旧表数据到临时表
- 删除旧表,临时表重命名为旧表名
- 缺点:可能丢失部分数据
方案2:主从切换
- 先在从库添加字段
- 主从切换
- 其他节点添加字段
10.3 MySQL CPU飙升处理
排查步骤
top命令确认是否mysqld导致show processlist查看消耗资源的SQL- 分析SQL执行计划、索引、数据量
处理方式
- kill消耗高的线程,观察CPU是否下降
- 调整(加索引、改SQL、调内存参数)
- 重新执行SQL
11. 常见面试SQL
例1:查询每门课都大于80分的学生姓名
数据表:
| name | kecheng | fenshu |
|---|---|---|
| 张三 | 语文 | 81 |
| 张三 | 数学 | 75 |
| 李四 | 语文 | 76 |
| 李四 | 数学 | 90 |
| 王五 | 语文 | 81 |
| 王五 | 数学 | 100 |
| 王五 | 英语 | 90 |
解法1:
SELECTDISTINCTnameFROMtableWHEREnameNOTIN(SELECTDISTINCTnameFROMtableWHEREfenshu<=80);解法2:
SELECTnameFROMtableGROUPBYnameHAVINGMIN(fenshu)>80;例2:删除学生冗余信息
数据表:
| 自动编号 | 学号 | 姓名 | 课程编号 | 课程名称 | 分数 |
|---|---|---|---|---|---|
| 1 | 2005001 | 张三 | 0001 | 数学 | 69 |
| 2 | 2005002 | 李四 | 0001 | 数学 | 89 |
| 3 | 2005001 | 张三 | 0001 | 数学 | 61 |
SQL:
DELETEtablenameWHERE自动编号NOTIN(SELECTMIN(自动编号)FROMtablenameGROUPBY学号,姓名,课程编号,课程名称,分数);例3:查询球队所有比赛组合
数据表:team(name字段:a,b,c,d)
SQL:
SELECTa.name,b.nameFROMteam a,team bWHEREa.name<b.name;例4:行转列查询
原表:
| year | month | amount |
|---|---|---|
| 1991 | 1 | 1.1 |
| 1991 | 2 | 1.2 |
| 1991 | 3 | 1.3 |
| 1991 | 4 | 1.4 |
| 1992 | 1 | 2.1 |
| 1992 | 2 | 2.2 |
| 1992 | 3 | 2.3 |
| 1992 | 4 | 2.4 |
目标结果:
| year | m1 | m2 | m3 | m4 |
|---|---|---|---|---|
| 1991 | 1.1 | 1.2 | 1.3 | 1.4 |
| 1992 | 2.1 | 2.2 | 2.3 | 2.4 |
SQL:
SELECTyear,(SELECTamountFROMaaa mWHEREmonth=1ANDm.year=aaa.year)ASm1,(SELECTamountFROMaaa mWHEREmonth=2ANDm.year=aaa.year)ASm2,(SELECTamountFROMaaa mWHEREmonth=3ANDm.year=aaa.year)ASm3,(SELECTamountFROMaaa mWHEREmonth=4ANDm.year=aaa.year)ASm4FROMaaaGROUPBYyear;例5:复制表结构(源表a,新表b)
MySQL:
CREATETABLEbASSELECT*FROMaWHERE1=2;SQL Server:
SELECT*INTObFROMaWHERE1<>1;-- 1=1复制结构+数据例6:添加及格标记
原表:
| courseid | coursename | score |
|---|---|---|
| 1 | java | 70 |
| 2 | oracle | 90 |
| 3 | xml | 40 |
| 4 | jsp | 30 |
| 5 | servlet | 80 |
目标结果:
| courseid | coursename | score | mark |
|---|---|---|---|
| 1 | java | 70 | pass |
| 2 | oracle | 90 | pass |
| 3 | xml | 40 | fail |
| 4 | jsp | 30 | fail |
| 5 | servlet | 80 | pass |
SQL:
SELECTcourseid,coursename,score,IF(score>=60,"pass","fail")ASmarkFROMcourse;例7:查询购入2种以上商品的购物人
数据表:
| 购物人 | 商品名称 | 数量 |
|---|---|---|
| A | 甲 | 2 |
| B | 乙 | 4 |
| C | 丙 | 1 |
| A | 丁 | 2 |
| B | 丙 | 5 |
SQL:
SELECT*FROM购物信息WHERE购物人IN(SELECT购物人FROM购物信息GROUPBY购物人HAVINGCOUNT(*)>=2);例8:统计每日胜负次数
数据表:
| date | result |
|---|---|
| 2005-05-09 | win |
| 2005-05-09 | lose |
| 2005-05-09 | lose |
| 2005-05-09 | lose |
| 2005-05-10 | win |
| 2005-05-10 | lose |
| 2005-05-10 | lose |
目标结果:
| date | win | lose |
|---|---|---|
| 2005-05-09 | 1 | 3 |
| 2005-05-10 | 1 | 2 |
解法1:
SELECTdate,SUM(CASEWHENresult="win"THEN1ELSE0END)AS"win",SUM(CASEWHENresult="lose"THEN1ELSE0END)AS"lose"FROMinfoGROUPBYdate;解法2:
SELECTa.date,a.resultASwin,b.resultASloseFROM(SELECTdate,COUNT(result)ASresultFROMinfoWHEREresult="win"GROUPBYdate)ASaJOIN(SELECTdate,COUNT(result)ASresultFROMinfoWHEREresult="lose"GROUPBYdate)ASbONa.date=b.date;例9:联合索引生效判断
联合索引:(a,b,c)
WHERE a=1 AND b=1 AND c=1→ 生效WHERE a=1 AND c=1→ 生效(a匹配最左前缀)WHERE b=1 AND c=1→ 失效(无a)WHERE b=1 AND a=1 AND c=1→ 生效(顺序不影响)
### 总结 1. 文档完整整合了所有MySQL面试题内容,修正了原始格式错误(如表格排版、代码缩进、标点符号),补充了缺失的逻辑内容; 2. 按照目录层级结构化梳理,每个知识点独立成节,关键内容用表格/代码块/列表呈现,可读性大幅提升; 3. 保留了所有核心考点(三大范式、事务、索引、锁、优化、面试SQL等),并补充了完整的SQL示例和配置示例,可直接用于复习和面试准备。