写在前面
大家好,欢迎来到MySQL全面教学系列的最后一篇------第15天。经过了前面14天的系统学习,我们已经从零开始,逐步掌握了MySQL的方方面面:
- 基础篇:数据类型、SQL语句、字符集
- 进阶篇:索引原理、事务机制、锁机制
- 实战篇:视图触发器、性能优化、备份恢复
今天,我们将系统梳理所有面试高频考点,帮助你在面试中从容应对。这篇文章是汇总性质,内容较长,建议收藏后反复阅读。
让我们一起完成这最后的学习之旅!

目录
-
- 写在前面
- 一、基础篇考点
-
- [1.1 数据类型选择](#1.1 数据类型选择)
- [1.2 SQL语句考点](#1.2 SQL语句考点)
- [1.3 字符集考点](#1.3 字符集考点)
- 二、查询篇考点
-
- [2.1 JOIN类型](#2.1 JOIN类型)
- [2.2 子查询优化](#2.2 子查询优化)
- [2.3 分页优化](#2.3 分页优化)
- 三、索引篇考点
-
- [3.1 B+树原理](#3.1 B+树原理)
- [3.2 最左前缀原则](#3.2 最左前缀原则)
- [3.3 索引失效场景](#3.3 索引失效场景)
- [3.4 覆盖索引](#3.4 覆盖索引)
- 四、事务篇考点
-
- [4.1 ACID特性](#4.1 ACID特性)
- [4.2 隔离级别](#4.2 隔离级别)
- [4.3 MVCC机制](#4.3 MVCC机制)
- [4.4 幻读解决](#4.4 幻读解决)
- 五、锁篇考点
-
- [5.1 锁的分类](#5.1 锁的分类)
- [5.2 死锁](#5.2 死锁)
- [5.3 乐观锁和悲观锁](#5.3 乐观锁和悲观锁)
- 六、优化篇考点
-
- [6.1 慢查询分析](#6.1 慢查询分析)
- [6.2 EXPLAIN详解](#6.2 EXPLAIN详解)
- [6.3 大表优化](#6.3 大表优化)
- 七、架构篇考点
-
- [7.1 主从复制](#7.1 主从复制)
- [7.2 读写分离](#7.2 读写分离)
- [7.3 分库分表](#7.3 分库分表)
- 八、高频SQL手写题
-
- [8.1 TopN问题](#8.1 TopN问题)
- [8.2 连续登录问题](#8.2 连续登录问题)
- [8.3 行列转换](#8.3 行列转换)
- [8.4 中位数计算](#8.4 中位数计算)
- [8.5 树形结构查询](#8.5 树形结构查询)
- 九、学习路线总结
-
- [9.1 知识体系图谱](#9.1 知识体系图谱)
- [9.2 学习建议](#9.2 学习建议)
- [9.3 推荐书籍](#9.3 推荐书籍)
- 十、参考资料
- 互动话题
一、基础篇考点
1.1 数据类型选择
面试题:CHAR和VARCHAR的区别?
答案:
| 对比项 | CHAR | VARCHAR |
|---|---|---|
| 存储方式 | 固定长度 | 变长 |
| 空间占用 | 可能浪费空间 | 节省空间 |
| 性能 | 略快 | 略慢(需要计算长度) |
| 适用场景 | 固定长度(MD5、手机号) | 变长数据(姓名、地址) |
| 最大长度 | 255字符 | 65535字节 |
sql
-- CHAR示例:手机号(固定11位)
phone CHAR(11)
-- VARCHAR示例:用户名(变长)
username VARCHAR(50)
面试题:DECIMAL和FLOAT/DOUBLE的区别?
答案:
- DECIMAL:精确小数,定点数存储,适合金额计算
- FLOAT/DOUBLE:浮点数,近似值存储,有精度损失
sql
-- 金额必须用DECIMAL
amount DECIMAL(19,4) -- 总共19位,小数4位
-- 不要用FLOAT存金额
-- 0.1 + 0.2 != 0.3 (浮点数精度问题)
面试题:DATETIME和TIMESTAMP的区别?
答案:
| 对比项 | DATETIME | TIMESTAMP |
|---|---|---|
| 存储范围 | 1000-9999年 | 1970-2038年(MySQL 8.0已扩展) |
| 存储空间 | 8字节 | 4字节 |
| 时区处理 | 存储时不变 | 自动转换时区 |
| 默认值 | 可设置 | 可设置CURRENT_TIMESTAMP |
| 自动更新 | 不支持 | 支持ON UPDATE |
1.2 SQL语句考点
面试题:WHERE和HAVING的区别?
答案:
- WHERE:对原始数据过滤,在分组前执行,不能使用聚合函数
- HAVING:对分组结果过滤,在分组后执行,可以使用聚合函数
sql
-- WHERE:先过滤再分组
SELECT department_id, AVG(salary)
FROM employees
WHERE salary > 5000 -- 先过滤salary>5000的记录
GROUP BY department_id;
-- HAVING:先分组再过滤
SELECT department_id, AVG(salary) as avg_sal
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 8000; -- 过滤平均工资>8000的部门
面试题:IN和EXISTS的区别?
答案:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 子表小,外表大 | IN | 子查询先执行,结果集小 |
| 子表大,外表小 | EXISTS | 外表驱动,有索引时效率高 |
| 需要判断NULL | EXISTS | IN对NULL处理有坑 |
sql
-- IN:适合子查询结果集小
SELECT * FROM employees
WHERE department_id IN (SELECT id FROM departments WHERE location = 'Beijing');
-- EXISTS:适合外表小,且关联列有索引
SELECT * FROM employees e
WHERE EXISTS (SELECT 1 FROM departments d WHERE d.id = e.department_id AND d.location = 'Beijing');
1.3 字符集考点
面试题:utf8和utf8mb4的区别?
答案:
- utf8:MySQL的"utf8"实际上是utf8mb3,只支持3字节UTF-8字符(最大编码0xFFFF)
- utf8mb4:支持4字节UTF-8字符,包括emoji、生僻汉字
sql
-- 推荐:使用utf8mb4
CREATE TABLE users (
name VARCHAR(100)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 排序规则选择:
-- utf8mb4_general_ci:速度快,精度略低
-- utf8mb4_unicode_ci:精度高,支持更多语言特性
-- utf8mb4_bin:二进制比较,区分大小写
二、查询篇考点
2.1 JOIN类型
面试题:各种JOIN的区别?
答案:
sql
-- 创建示例表
CREATE TABLE A (id INT, name VARCHAR(10));
CREATE TABLE B (id INT, a_id INT, value VARCHAR(10));
INSERT INTO A VALUES (1, 'A1'), (2, 'A2'), (3, 'A3');
INSERT INTO B VALUES (1, 1, 'B1'), (2, 1, 'B2'), (3, 4, 'B3');
| JOIN类型 | 结果 | 说明 |
|---|---|---|
| INNER JOIN | A1-B1, A1-B2 | 只返回匹配的行 |
| LEFT JOIN | A1-B1, A1-B2, A2-NULL, A3-NULL | 返回左表所有行 |
| RIGHT JOIN | A1-B1, A1-B2, NULL-B3 | 返回右表所有行 |
| FULL JOIN | MySQL不支持,用UNION模拟 | 返回两边所有行 |
| CROSS JOIN | 笛卡尔积 | 所有组合 |
sql
-- INNER JOIN
SELECT * FROM A INNER JOIN B ON A.id = B.a_id;
-- 结果:(1,A1,1,1,B1), (1,A1,2,1,B2)
-- LEFT JOIN
SELECT * FROM A LEFT JOIN B ON A.id = B.a_id;
-- 结果:(1,A1,1,1,B1), (1,A1,2,1,B2), (2,A2,NULL,NULL,NULL), (3,A3,NULL,NULL,NULL)
-- MySQL实现FULL JOIN
SELECT * FROM A LEFT JOIN B ON A.id = B.a_id
UNION
SELECT * FROM A RIGHT JOIN B ON A.id = B.a_id;
2.2 子查询优化
面试题:如何优化子查询?
答案:
sql
-- 不推荐:相关子查询(每行都执行子查询)
SELECT * FROM employees e
WHERE salary > (SELECT AVG(salary) FROM employees WHERE department_id = e.department_id);
-- 推荐:派生表(子查询只执行一次)
SELECT e.*
FROM employees e
INNER JOIN (
SELECT department_id, AVG(salary) as avg_sal
FROM employees
GROUP BY department_id
) d ON e.department_id = d.department_id
WHERE e.salary > d.avg_sal;
-- 推荐:MySQL 5.7+ 使用WITH(CTE)
WITH dept_avg AS (
SELECT department_id, AVG(salary) as avg_sal
FROM employees
GROUP BY department_id
)
SELECT e.*
FROM employees e
INNER JOIN dept_avg d ON e.department_id = d.department_id
WHERE e.salary > d.avg_sal;
2.3 分页优化
面试题:大表分页如何优化?
答案:
sql
-- 不推荐:深分页性能差
SELECT * FROM orders
ORDER BY create_time DESC
LIMIT 1000000, 10;
-- 需要扫描1000010行,丢弃前1000000行
-- 方案1:覆盖索引+子查询
SELECT * FROM orders o
INNER JOIN (
SELECT order_id FROM orders
ORDER BY create_time DESC
LIMIT 1000000, 10
) tmp ON o.order_id = tmp.order_id;
-- 方案2:书签/游标分页(推荐)
-- 上一页最后一条的create_time = '2024-01-15 10:00:00'
SELECT * FROM orders
WHERE create_time < '2024-01-15 10:00:00'
ORDER BY create_time DESC
LIMIT 10;
-- 方案3:限制分页深度
-- 业务上只允许翻到100页以内
三、索引篇考点
3.1 B+树原理
面试题:MySQL索引为什么使用B+树?
答案:
B+树特点:
- 非叶子节点不存数据:只存键值和指针,一个节点可以存更多键值,树更矮
- 叶子节点存数据:并且叶子节点之间有指针相连,便于范围查询
- 所有叶子节点在同一层:查询效率稳定
为什么不用其他数据结构:
| 数据结构 | 为什么不使用 |
|---|---|
| 哈希表 | 不支持范围查询和排序 |
| 二叉树 | 数据量大时树太高,IO次数多 |
| B树 | 非叶子节点也存数据,同样数据量树更高 |
| 跳表 | 范围查询效率不如B+树 |
B+树结构示意:
[10 | 20 | 30]
/ | | \
[1|5|9] [11|15|19] [21|25|29] [31|35|39]
/ | \ / | \ / | \ / | \
1 5 9 11 15 19 21 25 29 31 35 39
| | | | | | | | | | | |
数据 数据 ...
3.2 最左前缀原则
面试题:什么是最左前缀原则?
答案:
复合索引(a, b, c)的生效情况:
sql
-- 索引生效
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3 -- a生效,c不生效
WHERE a = 1 AND b > 2 AND c = 3 -- a,b生效,c不生效(b是范围查询)
-- 索引不生效
WHERE b = 2
WHERE b = 2 AND c = 3
WHERE c = 3
原理:B+树按照索引列的顺序排序,必须先匹配最左边的列。
3.3 索引失效场景
面试题:哪些情况会导致索引失效?
答案:
sql
-- 1. 对索引列使用函数
WHERE YEAR(create_time) = 2023 -- 失效
WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01' -- 有效
-- 2. 隐式类型转换
WHERE phone = 13800138000 -- 失效(字符串字段用数字查)
WHERE phone = '13800138000' -- 有效
-- 3. LIKE以通配符开头
WHERE name LIKE '%张%' -- 失效
WHERE name LIKE '张%' -- 有效(使用索引)
-- 4. OR条件使用不当
WHERE id = 1 OR age = 20 -- 可能失效(除非都有索引且优化器选择使用)
-- 5. 不等于、NOT IN
WHERE status != 1 -- 可能失效(取决于数据分布)
WHERE status NOT IN (1, 2) -- 可能失效
-- 6. IS NULL(取决于数据分布和数据量)
WHERE name IS NULL -- 可能失效
-- 7. 复合索引不满足最左前缀
-- 索引(a, b, c)
WHERE b = 2 AND c = 3 -- 失效
-- 8. 范围查询后失效
-- 索引(a, b, c)
WHERE a = 1 AND b > 2 AND c = 3 -- c不生效
3.4 覆盖索引
面试题:什么是覆盖索引?
答案:
覆盖索引:查询的所有列都在索引中,无需回表查询数据行。
sql
-- 创建复合索引
CREATE INDEX idx_dept_name ON employees(department_id, first_name, last_name);
-- 覆盖索引查询(Extra: Using index)
SELECT first_name, last_name
FROM employees
WHERE department_id = 10;
-- 只需要读索引,不需要回表
-- 非覆盖索引查询(需要回表)
SELECT first_name, last_name, email -- email不在索引中
FROM employees
WHERE department_id = 10;
-- 先查索引找到主键,再回表查email
四、事务篇考点
4.1 ACID特性
面试题:事务的ACID特性是什么?
答案:
| 特性 | 说明 | MySQL实现 |
|---|---|---|
| Atomicity(原子性) | 事务要么全成功,要么全失败 | undo log |
| Consistency(一致性) | 事务前后数据完整性约束不被破坏 | 约束检查+其他特性保证 |
| Isolation(隔离性) | 事务之间相互隔离 | 锁 + MVCC |
| Durability(持久性) | 事务提交后数据永久保存 | redo log |
4.2 隔离级别
面试题:四种隔离级别及问题?
答案:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不会 | 可能 | 可能 |
| REPEATABLE READ(默认) | 不会 | 不会 | 可能(InnoDB不会) |
| SERIALIZABLE | 不会 | 不会 | 不会 |
sql
-- 查看隔离级别
SELECT @@transaction_isolation; -- MySQL 8.0
SELECT @@tx_isolation; -- MySQL 5.7
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
4.3 MVCC机制
面试题:什么是MVCC?
答案:
MVCC(Multi-Version Concurrency Control):多版本并发控制,通过保存数据的历史版本,实现读写不阻塞。
实现原理:
sql
-- 每行记录隐藏字段
DB_TRX_ID:最后修改事务ID
DB_ROLL_PTR:回滚指针,指向undo log
DB_ROW_ID:行ID(无主键时自动生成)
-- Read View(读视图)
在RC和RR级别下,SELECT时生成Read View,判断数据可见性
版本链:
当前行数据 <- undo log <- undo log <- undo log
(最新) (上个版本) (更早版本) (最初版本)
判断规则:
- DB_TRX_ID < min_trx_id:已提交,可见
- DB_TRX_ID >= max_trx_id:将来事务,不可见
- min_trx_id <= DB_TRX_ID < max_trx_id:
- 在m_ids列表中:未提交,不可见
- 不在m_ids列表中:已提交,可见
4.4 幻读解决
面试题:InnoDB如何解决幻读?
答案:
幻读:同一事务两次查询,第二次查到了第一次没有的行。
解决方案:
sql
-- 方案1:快照读(普通SELECT)
-- MVCC保证可重复读,但可能幻读
SELECT * FROM orders WHERE amount > 100;
-- 方案2:当前读(加锁读)
-- 使用间隙锁(Gap Lock)解决幻读
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
SELECT * FROM orders WHERE amount > 100 LOCK IN SHARE MODE;
-- 间隙锁锁定范围,阻止其他事务插入
注意:InnoDB在RR级别下,当前读使用间隙锁解决幻读;快照读使用MVCC,但严格来说RR的快照读也可能出现幻读(特殊场景)。
五、锁篇考点
5.1 锁的分类
面试题:MySQL有哪些锁?
答案:
按粒度分:
| 锁类型 | 粒度 | 开销 | 并发度 | 适用引擎 |
|---|---|---|---|---|
| 行锁 | 行 | 大 | 高 | InnoDB |
| 表锁 | 表 | 小 | 低 | MyISAM、InnoDB |
| 页锁 | 页 | 中 | 中 | BDB(已淘汰) |
按功能分:
| 锁类型 | 说明 |
|---|---|
| 共享锁(S锁) | 读锁,多个事务可同时持有 |
| 排他锁(X锁) | 写锁,只能一个事务持有 |
| 意向共享锁(IS) | 表级锁,表示事务将获取行级S锁 |
| 意向排他锁(IX) | 表级锁,表示事务将获取行级X锁 |
InnoDB行锁算法:
| 锁算法 | 说明 |
|---|---|
| Record Lock | 锁定单个记录 |
| Gap Lock | 锁定范围,不包括记录本身(防幻读) |
| Next-Key Lock | Record Lock + Gap Lock,锁定记录及范围 |
5.2 死锁
面试题:什么是死锁?如何解决?
答案:
死锁:两个或多个事务相互等待对方释放锁,形成循环等待。
sql
-- 死锁示例
-- 事务A:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待id=2
-- 事务B:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2; -- 锁id=2
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 等待id=1
-- 死锁!
解决方案:
sql
-- 1. 查看死锁日志
SHOW ENGINE INNODB STATUS; -- 查看LATEST DETECTED DEADLOCK
-- 2. 设置死锁检测超时
SET innodb_lock_wait_timeout = 50; -- 默认50秒
-- 3. 预防死锁
-- a) 按固定顺序访问资源
-- b) 尽量缩短事务长度
-- c) 使用较低的隔离级别
-- d) 使用乐观锁代替悲观锁
-- 乐观锁示例
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1;
-- 如果version变了,更新失败,业务层重试
5.3 乐观锁和悲观锁
面试题:乐观锁和悲观锁的区别?
答案:
| 对比项 | 乐观锁 | 悲观锁 |
|---|---|---|
| 思想 | 认为不会冲突,提交时检查 | 认为会冲突,先加锁 |
| 实现 | 版本号、CAS | SELECT FOR UPDATE |
| 适用场景 | 读多写少 | 写多读少 |
| 性能 | 高并发时可能重试 | 锁等待开销 |
| 复杂度 | 业务层实现 | 数据库支持 |
sql
-- 悲观锁
BEGIN;
SELECT * FROM inventory WHERE product_id = 100 FOR UPDATE;
-- 执行业务逻辑
UPDATE inventory SET count = count - 1 WHERE product_id = 100;
COMMIT;
-- 乐观锁
-- 表结构增加version字段
UPDATE inventory
SET count = count - 1, version = version + 1
WHERE product_id = 100 AND version = #{version};
-- 如果更新行数为0,说明数据被修改,业务层重试
六、优化篇考点
6.1 慢查询分析
面试题:如何定位慢SQL?
答案:
sql
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 2. 查看正在执行的慢查询
SHOW PROCESSLIST;
-- 3. 使用performance_schema
SELECT * FROM performance_schema.events_statements_history_long
WHERE TIMER_WAIT > 1 * 10^12 -- 超过1秒
ORDER BY TIMER_WAIT DESC;
-- 4. 使用EXPLAIN分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- 关注:
-- type:访问类型,ALL表示全表扫描
-- key:使用的索引
-- rows:扫描行数
-- Extra:额外信息,Using filesort、Using temporary要警惕
6.2 EXPLAIN详解
面试题:EXPLAIN的字段含义?
答案:
| 字段 | 说明 | 注意点 |
|---|---|---|
| id | 执行顺序 | id相同从上到下,id不同从大到小 |
| select_type | 查询类型 | SIMPLE、PRIMARY、SUBQUERY、DERIVED等 |
| table | 访问的表 | 可能是别名或派生表 |
| type | 访问类型 | system>const>eq_ref>ref>range>index>ALL |
| possible_keys | 可能使用的索引 | - |
| key | 实际使用的索引 | NULL表示没用索引 |
| key_len | 索引长度 | 越短越好 |
| ref | 与索引比较的列 | - |
| rows | 估算扫描行数 | 越小越好 |
| filtered | 过滤比例 | 百分比 |
| Extra | 额外信息 | Using index、Using where、Using filesort等 |
type从优到劣:
system > const > eq_ref > ref > fulltext > ref_or_null >
index_merge > unique_subquery > index_subquery >
range > index > ALL
-- 至少要达到range,最好达到ref
6.3 大表优化
面试题:大表如何优化?
答案:
sql
-- 1. 优化SQL和索引(首要)
-- 添加合适的索引
CREATE INDEX idx_user_time ON orders(user_id, create_time);
-- 2. 垂直拆分
-- 将大字段拆分到扩展表
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
bio TEXT,
avatar_url VARCHAR(500)
);
-- 3. 水平拆分/分区
-- 按时间分区
CREATE TABLE orders_2024 PARTITION BY RANGE (MONTH(create_time)) (
PARTITION p1 VALUES LESS THAN (2),
PARTITION p2 VALUES LESS THAN (3),
...
);
-- 4. 读写分离
-- 主库写,从库读
-- 5. 使用缓存
-- 热点数据放入Redis
-- 6. 归档历史数据
-- 将冷数据移到历史表或数据仓库
七、架构篇考点
7.1 主从复制
面试题:主从复制的原理?
答案:
主从复制流程:
1. Master写入数据,记录binlog(二进制日志)
2. Slave的IO线程连接Master,请求binlog
3. Master的Dump线程发送binlog给Slave
4. Slave的IO线程将binlog写入relay log(中继日志)
5. Slave的SQL线程读取relay log,重放SQL
Master Slave
| |
| 1. 写数据,记binlog |
|-------------------------->|
| |
| 2. IO线程请求binlog |
|<--------------------------|
| |
| 3. Dump线程发送binlog |
|-------------------------->|
| |
| | 4. 写入relay log
| | 5. SQL线程重放
复制模式:
- 异步复制:Master不等待Slave确认,性能最好,可能丢数据
- 半同步复制:Master等待至少一个Slave确认,平衡方案
- 组复制:多数派确认,强一致性
7.2 读写分离
面试题:如何实现读写分离?
答案:
方案1:应用层实现
java
// Spring Boot动态数据源
@Configuration
public class DataSourceConfig {
@Bean
public DataSource routingDataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
@Service
public class OrderService {
@Transactional // 默认走主库
public void createOrder(Order order) { ... }
@Transactional(readOnly = true) // 走从库
public Order getOrder(Long id) { ... }
}
方案2:中间件实现
- MyCat
- ShardingSphere
- Atlas
方案3:DNS/VIP
- 写域名指向Master
- 读域名指向Slave集群
7.3 分库分表
面试题:什么时候需要分库分表?
答案:
分库时机:
- 单库数据量超过500GB
- 单库QPS超过10000
- 需要隔离不同业务数据
分表时机:
- 单表数据量超过1000万
- 单表大小超过10GB
- 索引无法完全放入内存
分片策略:
sql
-- 1. 取模分片(适合均匀分布)
-- user_id % 4 决定分到哪个库/表
-- 2. 范围分片(适合时间序列)
-- 按时间分表:orders_202401, orders_202402
-- 3. 哈希分片(适合避免热点)
-- 对user_id做哈希后取模
-- 4. 标签分片(适合按业务)
-- 按地区:user_north, user_south
分库分表问题:
| 问题 | 解决方案 |
|---|---|
| 跨库JOIN | 避免JOIN,数据冗余或应用层组装 |
| 跨库事务 | 分布式事务(Seata)、最终一致性 |
| 全局ID | 雪花算法、号段模式 |
| 分页排序 | 归并排序、限制深度 |
| 数据迁移 | 双写、增量同步 |
八、高频SQL手写题
8.1 TopN问题
题目:查询每个部门工资最高的前3名员工
sql
-- 方案1:窗口函数(MySQL 8.0+)
SELECT *
FROM (
SELECT
*,
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) as rank_num
FROM employees
) t
WHERE rank_num <= 3;
-- 方案2:关联子查询(兼容旧版本)
SELECT e.*
FROM employees e
WHERE (
SELECT COUNT(DISTINCT e2.salary)
FROM employees e2
WHERE e2.department_id = e.department_id
AND e2.salary > e.salary
) < 3;
8.2 连续登录问题
题目:查询连续登录3天以上的用户
sql
-- 表结构:user_login(user_id, login_date)
-- 解题思路:
-- 1. 去重登录日期
-- 2. 按用户分组,日期排序
-- 3. 计算日期与排序的差值(连续日期差值相同)
-- 4. 分组统计连续天数
WITH login_with_rank AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) as grp
FROM (SELECT DISTINCT user_id, login_date FROM user_login) t
)
SELECT user_id, COUNT(*) as continuous_days
FROM login_with_rank
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;
8.3 行列转换
题目:将行转列,统计每个用户各月份的订单金额
sql
-- 源数据
-- user_id | month | amount
-- 1 | 1 | 100
-- 1 | 2 | 200
-- 2 | 1 | 150
-- 目标结果
-- user_id | Jan | Feb | Mar | ...
-- 1 | 100 | 200 | 0 | ...
-- 2 | 150 | 0 | 0 | ...
-- 行转列
SELECT
user_id,
SUM(CASE WHEN month = 1 THEN amount ELSE 0 END) as Jan,
SUM(CASE WHEN month = 2 THEN amount ELSE 0 END) as Feb,
SUM(CASE WHEN month = 3 THEN amount ELSE 0 END) as Mar,
-- ...
FROM orders
GROUP BY user_id;
-- 列转行(反向操作)
-- 使用UNION ALL
8.4 中位数计算
题目:查询每个部门工资的中位数
sql
-- MySQL 8.0+ 窗口函数
WITH ranked AS (
SELECT
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary) as rn,
COUNT(*) OVER (PARTITION BY department_id) as cnt
FROM employees
)
SELECT department_id, AVG(salary) as median_salary
FROM ranked
WHERE rn IN (FLOOR((cnt + 1) / 2), CEIL((cnt + 1) / 2))
GROUP BY department_id;
8.5 树形结构查询
题目:查询部门层级结构(递归CTE)
sql
-- 表结构:departments(id, name, parent_id)
-- MySQL 8.0+ 递归CTE
WITH RECURSIVE dept_tree AS (
-- 锚点:顶级部门
SELECT id, name, parent_id, 0 as level, CAST(name AS CHAR(255)) as path
FROM departments
WHERE parent_id IS NULL
UNION ALL
-- 递归:子部门
SELECT d.id, d.name, d.parent_id, dt.level + 1, CONCAT(dt.path, ' > ', d.name)
FROM departments d
INNER JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT * FROM dept_tree;
九、学习路线总结
9.1 知识体系图谱
MySQL知识体系
│
├── 基础篇
│ ├── 数据类型(INT/VARCHAR/DECIMAL/DATETIME)
│ ├── SQL语句(CRUD、JOIN、子查询)
│ ├── 字符集(utf8mb4)
│ └── 存储引擎(InnoDB vs MyISAM)
│
├── 进阶篇
│ ├── 索引(B+树、最左前缀、覆盖索引)
│ ├── 事务(ACID、隔离级别、MVCC)
│ ├── 锁(行锁、表锁、死锁、乐观/悲观锁)
│ └── 日志(binlog、redo log、undo log)
│
├── 实战篇
│ ├── 视图与触发器
│ ├── 性能优化(慢查询、EXPLAIN、SQL优化)
│ ├── 备份恢复(mysqldump、xtrabackup)
│ └── 主从复制与读写分离
│
└── 架构篇
├── 分库分表
├── 高可用架构
└── 监控与运维
9.2 学习建议
| 阶段 | 目标 | 时间 | 重点 |
|---|---|---|---|
| 入门 | 掌握基础SQL | 1-2周 | CRUD、简单查询 |
| 进阶 | 理解原理 | 2-4周 | 索引、事务、锁 |
| 实战 | 解决问题 | 4-8周 | 优化、备份、架构 |
| 精通 | 源码级理解 | 持续 | 内核、源码 |
9.3 推荐书籍
| 书籍 | 难度 | 适合阶段 |
|---|---|---|
| 《MySQL必知必会》 | 入门 | 初学者 |
| 《高性能MySQL(第4版)》 | 进阶 | 有一定基础 |
| 《MySQL技术内幕:InnoDB存储引擎》 | 高级 | 深入原理 |
| 《MySQL运维内参》 | 高级 | DBA方向 |
十、参考资料
互动话题
系列完结,感谢陪伴!
- 这15天的学习,你最大的收获是什么?
- 还有哪些MySQL知识点想深入了解?
- 你在面试中遇到过哪些印象深刻的MySQL问题?
- 对这个系列有什么建议或期待?
欢迎在评论区留言,我会持续关注和回复。如果觉得这个系列对你有帮助,别忘了点赞收藏,分享给更多需要的朋友!
后续计划:
- 根据大家反馈,可能会推出进阶系列(源码分析、内核原理)
- 或者推出其他技术栈的系列教程
敬请期待!
本文是MySQL全面教学系列第15篇,也是最后一篇。15天的陪伴,感谢有你!
系列完结,撒花!