← 返回场景决策

🐬 MySQL 场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 6 大核心场景全解析

索引决策 JOIN选型 事务隔离级别 分库分表 数据类型 慢查询

1. 索引决策 — 什么时候建 / 什么时候反而有害

💭 类比:索引 = 字典的部首检字表
有索引 = 按部首查字典,直接翻到那一页 → 快
没索引 = 从第一页翻到最后一页 → 全表扫描,慢
索引太多 = 一本字典附了10种检字表,字典变得又厚又重 → 占空间、改字时每个表都得更新

✅ 什么时候建索引

1️⃣ WHERE 条件列:SELECT * FROM user WHERE name = '张三' → name建索引

2️⃣ JOIN 关联列:SELECT * FROM a JOIN b ON a.id = b.a_id → b.a_id建索引

3️⃣ ORDER BY 排序列:SELECT * FROM user ORDER BY age → age建索引(避免filesort)

4️⃣ GROUP BY 分组列:SELECT dept, COUNT(*) FROM user GROUP BY dept → dept建索引

5️⃣ 区分度高的列:手机号/邮箱(区分度≈1)比性别(区分度≈0.5)更适合建索引

6️⃣ 覆盖索引:查询的所有列都在索引里,不需要回表

❌ 什么时候不建 / 建了反而有害

1️⃣ 区分度极低的列:性别(只有男/女)、状态(0/1)→ 索引没用了,MySQL可能还是全表扫

2️⃣ 频繁更新的列:每次UPDATE都要维护索引B+树 → 写入变慢

3️⃣ 数据量极小的表:几百行的表,全表扫比走索引还快

4️⃣ 用不到的索引:建了但从没查过 → 白占空间白拖慢写入

5️⃣ 太多单列索引:不如组合成一个联合索引(最左前缀原则)

联合索引最左前缀原则

📁 shop-db
📂 sql/
📄 index_strategy.sql
-- 建联合索引:idx_abc (a, b, c) -- ✅ 能走索引的查询: WHERE a = 1 -- 用到 a WHERE a = 1 AND b = 2 -- 用到 a, b WHERE a = 1 AND b = 2 AND c = 3 -- 用到 a, b, c WHERE a = 1 AND c = 3 -- 只用到 a(c跳过了b) -- ❌ 不能走索引的查询: WHERE b = 2 -- 没有a,最左前缀断了 WHERE b = 2 AND c = 3 -- 同上,a不在前面 WHERE c = 3 -- 同上
面试金句 最左前缀原则(Leftmost Prefix Rule)源于 B+ 树索引的有序性:联合索引 (a,b,c) 按 a→b→c 排序存储,a 确定后 b 才有序,b 确定后 c 才有序。跳过 a 直接查 b 相当于在一本按姓→名排序的电话簿里只按名查——只能逐页翻。MySQL 5.6+ 的 Index Condition Pushdown (ICP) 优化了 a=1 AND c=3 的情况:走到索引后下推 c 条件过滤,减少回表次数。

索引失效的 6 种常见场景

#场景❌ 索引失效的写法✅ 正确写法
1对索引列用函数WHERE YEAR(create_time) = 2024WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'
2隐式类型转换WHERE phone = 13800138000(varchar列用数字查)WHERE phone = '13800138000'
3LIKE 左模糊WHERE name LIKE '%张'WHERE name LIKE '张%'(右模糊可以)
4OR 连接非索引列WHERE a=1 OR b=2(b无索引)给b也加索引 / 用UNION
5不等于WHERE status != 0改用IN / 业务上避免
6IS NOT NULLWHERE col IS NOT NULLIS NULL 可以走,IS NOT NULL 优化器可能放弃
⚠️ 索引的代价
① 每个索引占独立B+树 → 磁盘空间增加
② INSERT/UPDATE/DELETE 要维护所有索引 → 写入变慢
③ 索引越多,优化器选择越慢 → 可能选错索引
④ 建议:单表索引不超过5个,联合索引优先于多个单列索引

2. JOIN 选型 — 三种 JOIN 什么时候用哪种

💭 类比
INNER JOIN = 取交集:两张名单只有都有的名字才留下
LEFT JOIN = 以左为主:左边名单全留,右边没匹配的填空
RIGHT JOIN = 以右为主:右边名单全留,左边没匹配的填空

三种 JOIN 对比

JOIN类型结果✅ 什么时候用❌ 什么时候不用
INNER JOIN 两表都有的才返回 查询两表都存在的数据、不需要空值、关联关系强 需要保留某一边的所有数据
LEFT JOIN 左表全保留,右表没匹配填NULL 以左表为主、需要保留所有左表记录、右表数据可选 不需要左表的全部数据(用INNER更高效)
RIGHT JOIN 右表全保留,左表没匹配填NULL 以右表为主(少用,一般换LEFT JOIN) 习惯上优先用LEFT JOIN,RIGHT JOIN可读性差

数据演示

📁 school-db
📂 sql/
📄 join_demo.sql
-- 学生表 CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(20) ); INSERT INTO student VALUES (1,'张三'),(2,'李四'),(3,'王五'); -- 选课表 CREATE TABLE course ( id INT PRIMARY KEY, student_id INT, course_name VARCHAR(20) ); INSERT INTO course VALUES (1,1,'数学'),(2,2,'英语'),(3,99,'物理');
面试金句 Nested Loop Join 是 MySQL 执行 JOIN 的底层算法:驱动表(小表)每行去被驱动表(大表)找匹配——如果被驱动表的 JOIN 列有索引(如 student_id),走 Index Nested Loop(B+ 树快速定位),复杂度 O(N*logM);如果无索引则退化为 Block Nested Loop(双层循环全表扫),复杂度 O(N*M)。因此 JOIN 列必须建索引
— INNER JOIN vs LEFT JOIN
查询idnamecourse_name
INNER JOIN1张三数学
2李四英语
LEFT JOIN1张三数学
2李四英语
3王五NULL

💡 王五没选课,INNER JOIN 不显示,LEFT JOIN 保留并填 NULL

⚠️ JOIN 性能陷阱
① 永远在小表上做驱动表(LEFT JOIN 左边放小表)
② JOIN 列必须有索引!否则是 Nested Loop 全表扫
③ 避免超过3张表JOIN → 考虑拆查询或在应用层组装
④ LEFT JOIN + WHERE 右表条件 → 等同于 INNER JOIN(优化器可能自动转)

3. 事务隔离级别 — 四个等级实战怎么选

💭 类比
READ UNCOMMITTED = 看别人草稿纸:考场上偷看别人还没交的答案,可能是错的
READ COMMITTED = 只看交了卷的答案:Oracle默认,别人提交了才能看到
REPEATABLE READ = 你开考时拍的照片为准:MySQL默认,整个考试期间看到的数据一致
SERIALIZABLE = 一个一个考:所有人排队,完全不会冲突,但极慢

四种隔离级别对比

隔离级别脏读不可重复读幻读性能✅ 什么时候用
READ UNCOMMITTED ⚠️ 有⚠️ 有⚠️ 有 最快 几乎不用(除非完全不关心数据一致性)
READ COMMITTED ✅ 无⚠️ 有⚠️ 有 较快 Oracle默认、追求高并发+可接受不可重复读
REPEATABLE READ ✅ 无✅ 无基本无* 中等 MySQL默认、大多数场景、需要一致性读取
SERIALIZABLE ✅ 无✅ 无✅ 无 最慢 金融核心交易、数据绝对不能出错、并发量低

* MySQL InnoDB 的 REPEATABLE READ 通过 MVCC + 间隙锁基本解决幻读,但特定场景(先查后更新)仍可能出现

代码:查看和设置隔离级别

📁 shop-db
📂 sql/
📄 isolation_level.sql
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 结果:REPEATABLE-READ -- 设置隔离级别(会话级) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置隔离级别(全局) SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
面试金句 SET SESSION 仅影响当前连接,SET GLOBAL 影响所有新连接(不影响已有连接)。MySQL InnoDB 默认 REPEATABLE READ,通过 MVCC(多版本并发控制)+ Undo Log 版本链实现:事务开启时拍一个 ReadView 快照,此后所有 SELECT 都读快照版本。间隙锁(Gap Lock)防止幻读,但也是死锁高发区——降级为 READ COMMITTED 可减少死锁(代价是放弃可重复读)。
⚠️ 什么时候需要降级为 READ COMMITTED?
① 死锁频繁 → RC 没有间隙锁,死锁更少
② 需要实时看到其他事务已提交的数据(如库存实时显示)
③ 但要注意:RC 下每次 SELECT 都生成新 ReadView → 可能不可重复读

4. 分库分表 — 什么时候该做

💭 类比
单库单表 = 所有数据一个表,数据量小时够用
读写分离 = 主库写+从库读:主库处理写入,从库分担查询压力
分表 = 同库拆表:一张大表按规则拆成多张结构相同的表
分库 = 不同库服务不同业务域,物理隔离

决策树

数据量多大?
<500万行 优化索引/SQL就够了
500万~5000万 读写分离(主从)
>5000万 分库分表

决策表

场景✅ 用什么❌ 不要过早做
慢查询但数据量不大优化索引/SQL/加缓存分库分表(杀鸡用牛刀)
读多写少读写分离 + 缓存分库分表
单表数据>5000万分表(按时间/ID范围/Hash)继续单表
单库连接数不够分库(不同业务拆不同库)继续单库
需要跨库事务避免分库/用分布式事务随便分库
⚠️ 分库分表的代价
① 跨库JOIN → 要在应用层组装数据
② 分布式事务 → 从本地事务变成2PC/TCC,复杂度暴增
③ 全局唯一ID → 不能用自增,要雪花算法
④ 运维复杂 → 数据迁移、扩容、备份都是噩梦
能不分就不分!先优化索引、SQL、缓存、读写分离

5. 数据类型选型 — 怎么选最省空间

💭 类比
选数据类型 = 选箱子装东西:小东西用大箱子浪费空间,大东西用小箱子装不下
TINYINT = 邮票盒(1字节)、INT = 鞋盒(4字节)、BIGINT = 行李箱(8字节)

整数类型

类型字节无符号范围✅ 什么时候用
TINYINT10~255状态(0/1/2)、类型枚举
SMALLINT20~65535年龄、数量小范围
INT40~42亿主键、大多数整数场景
BIGINT80~1844亿亿雪花ID、超大数量

字符串类型

类型✅ 什么时候用❌ 什么时候不用
CHAR(n)固定长度:手机号(11)、MD5(32)、性别(1)长度变化大的字符串
VARCHAR(n)变长字符串:姓名、地址、标题超长文本(用TEXT)
TEXT长文本:文章内容、JSON需要默认值(TEXT不支持)

时间类型

类型字节范围✅ 什么时候用
DATETIME81000~9999年业务时间(创建时间/更新时间),不受时区影响
TIMESTAMP41970~2038年系统时间(自动更新),省空间,受时区影响
BIGINT8不限存毫秒时间戳,跨语言通用
⚠️ 常见坑
① TIMESTAMP 2038年溢出!如果你的业务可能存2038年以后的时间,用DATETIME
② VARCHAR(255) 不是越大越好!MySQL 内存分配和索引前缀都受影响
③ 金额用 DECIMAL(10,2)!不要用 FLOAT/DOUBLE(精度丢失)
④ IP地址用 INT UNSIGNED + INET_ATON() 存储!不要用VARCHAR存IP
📁 shop-db
📂 sql/
📄 data_type_best_practice.sql
-- ✅ 金额:DECIMAL 精确存储 CREATE TABLE `order` ( id BIGINT PRIMARY KEY, amount DECIMAL(10,2) NOT NULL COMMENT '金额,精确到分' ); -- ❌ 金额用 FLOAT 会丢失精度 SELECT 0.1 + 0.2; -- 结果:0.30000000000000004 💥 SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)); -- 结果:0.30 ✅ -- ✅ IP地址存为INT INSERT INTO login_log (ip) VALUES (INET_ATON('192.168.1.1')); SELECT INET_NTOA(ip) FROM login_log; -- 192.168.1.1
面试金句 DECIMAL(10,2) 底层将数字转为二进制字符串存储,精确到指定小数位,不产生 IEEE 754 浮点误差(0.1+0.2≠0.3)。金融场景必须用 DECIMAL。INET_ATON/INET_NTOA 将 IPv4 转为 4 字节 INT UNSIGNED 存储,比 VARCHAR(15) 节省 11 字节,且支持高效范围查询(如 BETWEEN INET_ATON('192.168.0.0') AND ...)。

6. 慢查询排查 — 五步法

1.开启慢日志 2.EXPLAIN分析 3.看type/key 4.优化 5.验证

Step 1: 开启慢查询日志

📁 shop-db
📂 sql/
📄 slow_query_log.sql
-- 查看慢日志状态 SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; -- 开启慢日志(超过1秒记录) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;
面试金句 慢查询日志(Slow Query Log)是 MySQL 性能诊断的第一道防线:long_query_time 阈值建议生产环境设为 0.5~1s。SET GLOBAL 对新连接生效,已有连接需重连。线上通常配合 pt-query-digest 工具聚合分析慢日志,找出 TOP N 高频慢 SQL。⚠️ 开启慢日志有约 1~3% 的性能开销,但远小于不开导致的生产事故。

Step 2: EXPLAIN 的 type 字段(从好到差)

type含义性能✅ 达标?
system/const单行匹配⭐⭐⭐⭐⭐✅ 最优
eq_ref唯一索引关联⭐⭐⭐⭐⭐✅ 最优
ref非唯一索引查找⭐⭐⭐⭐✅ 良好
range索引范围扫描⭐⭐⭐✅ 可接受
index全索引扫描⭐⭐⚠️ 需优化
ALL全表扫描❌ 必须优化

Step 3: 看重点字段

📁 shop-db
📂 sql/
📄 explain_analysis.sql
EXPLAIN SELECT * FROM user WHERE name = '张三'; -- 重点看: -- type: ALL = 全表扫描 ❌ / ref = 走了索引 ✅ -- key: NULL = 没走索引 ❌ / idx_name = 走了索引 ✅ -- rows: 扫描行数,越小越好 -- Extra: Using filesort = 额外排序 ❌ / Using index = 覆盖索引 ✅
面试金句 EXPLAIN 输出的四列是慢 SQL 优化的核心依据:type(访问类型)从 ALL→index→range→ref→eq_ref→const 性能递增;key 显示实际使用的索引(NULL = 全表扫);rows 是优化器预估扫描行数;ExtraUsing index 表示覆盖索引(索引包含了查询所有列,不回表),是最优状态。Using filesort 和 Using temporary 必须优化。

Step 4: 常见优化手段

问题优化方式示例
type=ALL 全表扫描加索引WHERE name=? → 给name加索引
Extra=Using filesortORDER BY列加索引/联合索引ORDER BY age → 给age加索引
Extra=Using temporaryGROUP BY列加索引GROUP BY dept → 给dept加索引
扫描行数过大缩小查询范围/用覆盖索引SELECT id,name 不用 SELECT *
索引失效避免函数/隐式转换/左模糊见第1节索引失效6种场景
✅ 慢查询排查口诀
① 先开慢日志抓SQL
② EXPLAIN 看type → ALL必须优化
③ 看key → NULL说明没走索引
④ 看rows → 扫描行数是不是远大于返回行数
⑤ 看Extra → Using filesort/Using temporary 要优化
全栈资料库 · 场景决策 · MySQL篇