跳转至

00. SQL 基础与 MySQL 核心概念(面试必会版)

数据库面试里 SQL 基础是第一道门槛。很多人以为"会写 SELECT"就够了,但面试官真正关心的是:

  • 你能不能在场景里写出正确的 SQL?
  • 你知不知道一条 SQL 在 MySQL 内部是怎么跑的?
  • 你对范式、引擎、连接池这些概念有没有工程理解?

这一章建议按“先把地基打牢,再顺着高频问法展开,最后补难点和边界”的顺序阅读。


先把这一章的知识骨架搭起来

数据库基础题不能只靠 SQL 语法记忆,更关键的是理解 数据如何被组织、查询如何被执行、关系型系统为什么这样设计。表、行、列、主键、外键、索引、事务,这些不是分散术语,而是一套同时兼顾一致性、查询能力和可维护性的模型。

这一章建议从三层看:第一层是数据模型,也就是表结构、范式、主外键、约束;第二层是查询表达,也就是 SELECT、连接、聚合、排序、子查询怎样把业务问题翻译成 SQL;第三层是执行与工程实践,也就是存储引擎、连接池、执行计划、慢 SQL、锁和事务边界。

这样你在面试里写 SQL 才不会和底层理解割裂。


第一部分:先把概念和主线讲清楚

进入问答前,先把最小前置知识补齐

数据库基础题不能只停在 SQL 语法,因为真正的地基是先知道数据如何被建模、查询如何被表达、执行如何被优化。表、行、列、主键、外键、索引、事务其实共同构成了一套关系型数据组织方式。

建议先把三层分清:数据模型层看表结构和约束,查询表达层看 SELECT、连接、聚合如何描述业务问题,执行层看索引、连接池、执行计划、锁和事务如何影响性能与正确性。这样后面的 SQL 题和 MySQL 原理题才会连得上。


1. 主键、外键、索引的区别

主键(Primary Key):

  • 唯一标识表中每一行,不允许重复,不允许 NULL
  • 每个表只能有一个主键
  • 通常自动创建聚簇索引(InnoDB)

外键(Foreign Key):

  • 一个表中的字段,引用另一个表的主键
  • 用于建立表之间的关联关系
  • 会增加写操作的检查开销,高性能场景下经常在应用层做约束而非数据库层

索引(Index):

  • 用于加速查询的数据结构
  • 可以有多个,可以有 NULL 值
  • 代价是写入时需要维护索引结构

面试高分点: 主键一定是索引,但索引不一定是主键。外键在工程实践中经常被禁用(高并发写入场景),改由应用层保证一致性。


2. SQL 语句分类速查

2.1 数据查询(SELECT)

-- 基础查询
SELECT * FROM users;

-- 指定列
SELECT id, name FROM users;

-- 条件查询
SELECT * FROM users WHERE age > 25;

-- 排序
SELECT * FROM users ORDER BY age DESC;

-- 去重
SELECT DISTINCT city FROM users;

-- 分页
SELECT * FROM users LIMIT 10 OFFSET 20;

-- 别名
SELECT name AS username FROM users;

-- 模糊匹配
SELECT * FROM users WHERE name LIKE 'A%';

-- 范围查询
SELECT * FROM users WHERE age BETWEEN 20 AND 30;

-- 多条件
SELECT * FROM users WHERE age > 20 AND city = 'Beijing';

2.2 多表操作(JOIN 与子查询)

-- 内连接:只返回两表中匹配的行
SELECT u.name, o.amount
FROM users u JOIN orders o ON u.id = o.user_id;

-- 左连接:左表全保留,右表没匹配的填 NULL
SELECT u.name, o.amount
FROM users u LEFT JOIN orders o ON u.id = o.user_id;

-- 右连接:右表全保留,左表没匹配的填 NULL
SELECT u.name, o.amount
FROM users u RIGHT JOIN orders o ON u.id = o.user_id;

-- 子查询(WHERE 中)
SELECT name FROM users
WHERE id IN (SELECT user_id FROM orders);

-- 子查询(FROM 中)
SELECT t.user_id, COUNT(*)
FROM (SELECT * FROM orders WHERE amount > 100) t
GROUP BY t.user_id;

面试追问: INNER JOIN 和 LEFT JOIN 的区别?答的时候别只说"一个是交集一个保留左表",要说清楚 NULL 填充行为和对结果集大小的影响。

2.3 聚合与分组

-- 总数
SELECT COUNT(*) FROM users;

-- 求和
SELECT SUM(amount) FROM orders;

-- 平均值
SELECT AVG(age) FROM users;

-- 最大/最小值
SELECT MAX(age), MIN(age) FROM users;

-- 分组统计
SELECT city, COUNT(*) FROM users GROUP BY city;

-- 分组条件(HAVING)
SELECT city, COUNT(*) FROM users
GROUP BY city HAVING COUNT(*) > 10;

WHERE 和 HAVING 的区别: WHERE 在分组前过滤行,HAVING 在分组后过滤组。WHERE 不能用聚合函数,HAVING 可以。

2.4 数据写入与更新

-- 插入
INSERT INTO users(name, age) VALUES('Alice', 30);

-- 批量插入
INSERT INTO users(name, age) VALUES ('Bob', 25), ('Cathy', 22);

-- 插入或更新(ON DUPLICATE KEY)
INSERT INTO users(id, name) VALUES (1, 'Tom')
ON DUPLICATE KEY UPDATE name='Tom';

-- 更新
UPDATE users SET age = 28 WHERE id = 1;

-- 删除
DELETE FROM users WHERE age < 18;

面试注意: DELETE 不加 WHERE 会删除全表数据。TRUNCATE 和 DELETE 的区别:TRUNCATE 更快(不逐行删)、不走事务日志、不触发触发器、自增 ID 重置。

2.5 索引与性能相关

-- 查看索引
SHOW INDEX FROM users;

-- 创建索引
CREATE INDEX idx_age ON users(age);

-- 删除索引
DROP INDEX idx_age ON users;

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE age > 30;

-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query_log%';

-- 强制使用某个索引
SELECT * FROM users FORCE INDEX (idx_age) WHERE age > 25;

2.6 事务与锁操作

-- 开启事务
START TRANSACTION;  -- 或 BEGIN;

-- 提交
COMMIT;

-- 回滚
ROLLBACK;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 查看当前隔离级别
SELECT @@transaction_isolation;  -- MySQL 8.0+

-- 悲观锁(SELECT ... FOR UPDATE)
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 乐观锁(版本号机制)
UPDATE users SET age = 26, version = version + 1
WHERE id = 1 AND version = 2;

3. 一条 SQL 查询语句在 MySQL 中是怎么执行的?

这是数据库面试最高频的链路题之一。

执行流程(以 MySQL 8.0 为例):

  1. 连接器 — 建立 TCP 连接,验证用户名密码,分配线程
  2. 解析器 — 词法分析(拆成关键字、标识符、操作符)→ 语法分析(生成抽象语法树 AST)
  3. 预处理器 — 检查表名/字段名是否存在,检查权限
  4. 优化器 — 基于统计信息和成本模型,选择最优执行计划(索引选择、JOIN 顺序、连接算法)
  5. 执行器 — 按执行计划调用存储引擎接口,读取数据,做过滤/排序/聚合
  6. 存储引擎(如 InnoDB)— 从 Buffer Pool 或磁盘读取数据页,返回给执行器
  7. 返回结果 — 结果集返回客户端

面试高分点:

  • MySQL 8.0 已经移除了查询缓存(Query Cache),因为在并发写入场景下维护成本高、命中率低
  • 优化器不一定选最好的计划,可以用 EXPLAIN 验证
  • 存储引擎是插件式的,InnoDB 和 MyISAM 的行为差异很大

常见追问:

Q:为什么 MySQL 8.0 移除了查询缓存? A:因为表一更新缓存就失效,并发写入场景下维护成本远高于收益。现代做法是在应用层用 Redis 等做缓存。

Q:优化器怎么决定用哪个索引? A:基于索引的选择性(区分度)、统计信息(cardinality)和查询条件。可以通过 EXPLAIN 查看 possible_keys 和 key 字段来验证。

Q:如何分析一条慢 SQL? A:① EXPLAIN 看执行计划(索引使用、扫描行数);② 开启慢查询日志捕获耗时操作;③ SHOW ENGINE INNODB STATUS 检查锁竞争。


4. 数据库三大范式

第一范式(1NF):原子性

  • 每个字段只能包含不可再分的基本值
  • 不允许一个字段存多个值(如"电话1,电话2")

第二范式(2NF):消除部分依赖

  • 满足 1NF
  • 所有非主属性完全依赖于主键,不能只依赖主键的一部分
  • 主要针对复合主键场景

第三范式(3NF):消除传递依赖

  • 满足 2NF
  • 非主属性直接依赖于主键,不通过其他非主属性间接依赖
  • 例:员工表里不应该同时存"部门 ID"和"部门名称",部门名称应该放在部门表里

工程实践: 真实项目中不会严格遵守范式。为了查询性能,适度冗余(反范式)是很常见的做法。面试里最好补一句"范式是理论指导,工程上需要根据读写比例做权衡"。


第二部分:围绕高频追问继续展开

5. JOIN 操作详解

类型 说明
INNER JOIN 只返回两表中匹配的行
LEFT JOIN 左表全保留,右表不匹配的填 NULL
RIGHT JOIN 右表全保留,左表不匹配的填 NULL
FULL OUTER JOIN 两表都全保留,不匹配的各填 NULL(MySQL 不直接支持,需用 UNION)
CROSS JOIN 笛卡尔积,每行与另一表的每行组合

高频追问:

Q:LEFT JOIN 和 INNER JOIN 什么时候结果一样? A:当左表的每一行在右表中都有匹配时,LEFT JOIN 退化为 INNER JOIN。

Q:如果 JOIN 的两个表都很大,性能怎么优化? A:① 确保 JOIN 列有索引;② 用小表驱动大表;③ 考虑是否可以先过滤再 JOIN;④ 检查执行计划中的 JOIN 算法(Nested Loop / Hash Join / Sort Merge)。


6. InnoDB 和 MyISAM 的区别

维度 InnoDB MyISAM
事务支持 支持 ACID 事务 不支持
锁粒度 行级锁 表级锁
外键 支持 不支持
崩溃恢复 支持(基于 redo log) 不支持
全文索引 MySQL 5.6+ 支持 支持
存储方式 聚簇索引(数据和主键索引一起存) 堆表(数据和索引分离)
COUNT(*) 需要遍历(有 MVCC 版本) 有预存的行数,很快

面试高分点: MySQL 5.5 以后默认引擎是 InnoDB。选择 MyISAM 的场景已经很少,除非是只读、不需要事务的历史数据表。


7. 分片(Sharding)和分区(Partitioning)

分片:

  • 数据水平切分到多个独立的数据库实例/服务器
  • 每个分片有独立的存储和计算资源
  • 适用于大规模分布式系统
  • 复杂度高:需要处理路由、跨分片查询、分布式事务

分区:

  • 单个表的数据按规则划分到同一个数据库实例内的多个部分
  • 分区之间共享数据库资源
  • 适用于管理和查询大表
  • 复杂度相对低

核心区别: 分片是跨机器的水平扩展,分区是单机内的数据管理优化。


8. 数据库连接池

是什么: 预先创建并管理一组数据库连接,应用需要时从池中获取,用完归还,而不是每次都创建/销毁。

为什么要用:

  1. 创建连接开销大(TCP 握手 + 认证 + 分配资源)
  2. 连接池复用已有连接,减少延迟
  3. 限制最大连接数,保护数据库不被打爆
  4. 提高系统稳定性

常见追问:

Q:连接池满了怎么办? A:取决于配置——排队等待(设超时)、直接拒绝、或动态扩容。工程上通常设一个合理的最大连接数 + 等待超时。


9. 面试高频场景 SQL

场景 思路
查询每个用户的最后一笔订单 GROUP BY user_id + MAX(order_time) 或子查询 + JOIN
查询重复数据 GROUP BY + HAVING COUNT(*) > 1
查询某字段为空 WHERE phone IS NULL(注意不能用 = NULL)
查询某天注册的用户 WHERE DATE(register_time) = '2024-01-01'(注意:对列用函数会导致索引失效)
分页优化 用 WHERE id > ? LIMIT 10 代替 OFFSET(深度分页时 OFFSET 性能很差)

面试陷阱: WHERE DATE(register_time) = '...' 会让索引失效,因为对索引列用了函数。更好的写法是 WHERE register_time >= '2024-01-01' AND register_time < '2024-01-02'。


第三部分:把难点、边界和代价吃透

10. DELETE、TRUNCATE、DROP 的区别

DELETE TRUNCATE DROP
操作对象 行 表数据 整张表
可以加 WHERE 可以 不可以 不适用
事务日志 逐行记录 不逐行记录 不适用
触发器 触发 不触发 不适用
自增 ID 不重置 重置 不适用
速度 慢(逐行) 快 最快
可回滚 可以(在事务内) 不可以 不可以

11. MySQL 和 Redis 的区别

维度 MySQL Redis
类型 关系型数据库 键值对/内存数据库
存储 基于磁盘 基于内存
数据结构 表/行/列 String/Hash/List/Set/ZSet 等
事务 完整 ACID 有限事务支持
持久化 redo log + binlog AOF + RDB
适用场景 复杂查询、事务、大量结构化数据 缓存、高速读写、计数器、排行榜

12. 数据库备份与恢复策略

备份类型:

  • 全备份: 复制整个数据库,恢复简单但耗时耗空间
  • 增量备份: 只备份自上次备份后变化的数据,节省空间但恢复需要链式回放
  • 差异备份: 备份自上次全备份后的变化,恢复比增量简单

恢复策略:

  • 完全恢复: 全备份 + 所有增量/差异备份
  • 时间点恢复(PITR): 利用 binlog 恢复到指定时间点
  • 部分恢复: 只恢复受损的表或分区

工程要点: 备份不等于可恢复。一定要定期做恢复演练,验证备份的完整性。


13. 表优化常见手段

  1. 合理建索引 — 在高频查询列上建索引,但不要建太多(影响写入)
  2. **避免 SELECT *** — 只查需要的列,减少 IO 和网络传输
  3. 选择合适的数据类型 — TINYINT 代替 INT、VARCHAR 按需分配长度
  4. 时间类型用 TIMESTAMP — 比 DATETIME 省空间
  5. 尽量少用 NULL — NULL 值增加索引和查询优化的复杂度
  6. 大表考虑分表 — 水平拆分或垂直拆分
  7. 读写分离 — 主库写、从库读

关联章节