SQL调优与数据建模 面试备战

EXPLAIN分析 → 索引优化 → 查询模式 → 星型/雪花建模 → 数据仓库 → 25道真题 | 全程 Excel 类比对照

💼 想要看真实工作场景?推荐搭配实战排查

本页侧重面试考点速答。若需看真实生产问题的排查过程和修复代码,请看以下实战页面:

🔧 SQL问题排查纪实 🔧 MySQL问题驱动排查

1 EXPLAIN 执行计划详解

EXPLAIN 是 MySQL 提供的查询分析工具,不会真正执行 SQL,而是展示 MySQL 打算如何执行这条查询。就像 Excel 里按 Ctrl+~ 看公式而不是看结果——你看的是"计算计划"。

EXPLAIN SELECT * FROM orders WHERE status = 'PAID' ORDER BY create_time LIMIT 10;
-- 得到一张"执行计划表", 每一列告诉你一个维度的信息

📋 字段逐一解读

id
执行序号 — 值越大越先执行,相同id从上往下
Excel类比: 公式计算的优先级顺序
select_type
查询类型: SIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION
Excel类比: 单元格里是直接引用还是嵌套IF
table
访问哪张表, DERIVED 表示衍生临时表
Excel类比: 引用的是Sheet1还是Sheet2
type
访问类型 — 决定查询效率的核心指标!
Excel类比: VLOOKUP精确匹配 vs 翻遍整张表
possible_keys
MySQL认为可能用到的索引列表
Excel类比: 你手上有好几张索引卡片
key
实际选择的索引, NULL = 没用索引(全表扫描!)
Excel类比: 最终选了哪张索引卡来查
key_len
使用索引的字节数, 判断复合索引用了几个列
Excel类比: 索引卡用了几列信息
ref
与索引比较的列或常量: const / column_name
Excel类比: 用什么值去匹配
rows
预估扫描行数 — 越小越好!
Excel类比: 翻了多少行Excel才找到答案
Extra
额外信息: Using index / Using filesort / Using temporary 等
Excel类比: 是否需要额外排序/建临时表
Extra 字段高频值速查
Extra 值含义好坏
Using index覆盖索引, 索引中包含所有查询列, 不用回表最优
Using whereServer层在存储引擎返回后进行过滤一般
Using index condition索引下推(ICP), 在存储引擎层提前过滤
Using temporary使用了临时表 — 通常 GROUP BY / DISTINCT / UNION需优化
Using filesort无法用索引排序, 额外排序操作需优化
Using join buffer连接没有使用索引, 用了缓存块需优化
Select tables optimized away优化器直接从索引取值, 不用查表最优

🏎 Type 访问类型梯度 — 速度仪表盘

Excel 类比

system = 表里只有1行, 相当于Excel只有一个单元格
const = VLOOKUP精确匹配, 一次就找到
eq_ref = 主键/唯一索引关联, 每行精确匹配一次
ref = 普通索引查找, 可能匹配多行
range = Excel的"筛选"功能, 指定范围
index = 只扫描索引(比全表好), 像只翻索引页
ALL = 全表扫描 = 翻遍Excel每一行!!!

system const eq_ref ref range index ALL ← 越左越快 | 越右越慢 → ★ 目标区域 ⚠ 必须优化
type 值从最优到最差: 至少达到 range 级别, 避免 ALL

🔥 5个实战 EXPLAIN 案例

案例1: 全表扫描 → 加索引

-- 原始查询
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
-- 结果: type=ALL, key=NULL, rows=500000

-- 优化: 给 user_id 加索引
ALTER TABLE orders ADD INDEX idx_user_id(user_id);
-- 结果: type=ref, key=idx_user_id, rows=50
优化建议
type=ALL 是最差情况。优先检查 WHERE 条件列有没有索引。添加合适索引后通常能降到 refrange

案例2: 索引存在但不生效 (隐式转换)

-- phone 是 VARCHAR 类型, 有索引
EXPLAIN SELECT * FROM users WHERE phone = 13800138000;
-- 结果: type=ALL, key=NULL ← 字符串列用数字查, 隐式转换! MySQL对每行做CAST

-- 修复: 加引号!
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- 结果: type=ref, key=idx_phone, rows=1
常见陷阱
字符串列传入数字, 或数字列传入字符串, 都会触发隐式转换, 导致索引失效!

案例3: 复合索引只用了部分

-- 索引: idx_abc(a, b, c)
EXPLAIN SELECT * FROM t WHERE a = 1 AND c = 3;
-- 结果: type=ref, key=idx_abc, key_len=4 (只用了a列, c无法利用索引)

-- 优化: 补全中间列 或 调整索引顺序
ALTER TABLE t ADD INDEX idx_ac(a, c);
-- 或者修改查询: WHERE a=1 AND b IS NOT NULL AND c=3

案例4: filesort 优化

-- 索引: idx_status(status)
EXPLAIN SELECT * FROM orders WHERE status = 'PAID' ORDER BY create_time;
-- Extra: Using where; Using filesort ← 排序没走索引!

-- 优化: 创建复合索引 (status, create_time)
ALTER TABLE orders ADD INDEX idx_status_time(status, create_time);
-- Extra: Using index ← 排序也走索引了, 无需filesort

案例5: 子查询 vs JOIN

-- 慢: 子查询
EXPLAIN SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE vip = 1);
-- 可能出现: DEPENDENT SUBQUERY, 每行执行一次子查询

-- 优化: 改写为 JOIN
EXPLAIN SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id WHERE u.vip = 1;
-- 结果: type=eq_ref(ref), Using where ← 好多了

2 索引优化实战

📑 最左前缀原则

Excel 类比: 多列排序

想象 Excel 中按 省份→城市→区县 三列排序。你必须先按省份排, 同省再按城市排, 同城再按区县排。

如果你跳过省份, 直接按城市排 → 整个排序就废了, 跟没排一样!

索引(A, B, C) 就等于这个三级排序: 你必须先匹配A, 才能利用B的有序性; 先匹配A和B, 才能利用C。

索引 idx_abc(A, B, C) — 查询能否命中?
查询 WHERE 条件能否用索引?用了几列Excel 类比
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跳过城市直接按区筛选, 只能用省份 ✗
WHERE b = 2 AND c = 3不命中0跳过省份, 排序完全无效 ✗
WHERE b = 2不命中0只按城市, 省份不同没法排 ✗
WHERE a > 1 AND b = 2部分命中仅 A(范围)a是范围查询后b无法利用索引
WHERE a = 1 ORDER BY b命中(含排序)A + 排序用B筛选后B有序, 不用额外排序 ✓
核心规则

遇到范围查询(>, <, BETWEEN, LIKE)后, 后续列无法利用索引。
所以复合索引中, 等值查询列放前面, 范围查询列放后面

🔍 覆盖索引 (Covering Index)

Excel 类比

你需要"学生姓名和成绩"两项信息。如果索引卡(目录页)上已经印了姓名和成绩, 你就不用翻到正文页(回表)了——这就是覆盖索引!

在 Excel 里, 相当于 VLOOKUP 的返回列就在查找列的右边, 不需要再切到另一个Sheet去取数据。

覆盖索引原理
非覆盖索引 (需要回表)
二级索引 (name列)
name
PK
↓ 回表查询
聚簇索引 (全部列)
PK
name
age
dept
需要2次查找 → 慢
覆盖索引 (无需回表)
复合索引 (name, age)
name
age
PK
✓ 所需列都在索引中!
查询: SELECT name, age FROM t WHERE name='张三'
Extra: Using index ← 不回表!
只需1次查找 → 快
-- 覆盖索引实战
-- 需求: 查询用户的 name 和 age
SELECT name, age FROM users WHERE name = '张三';

-- 如果只有 INDEX(name) → 需要回表取 age
-- 创建覆盖索引:
ALTER TABLE users ADD INDEX idx_name_age(name, age);
-- EXPLAIN: Extra = Using index ← 索引覆盖, 不回表!

📩 索引下推 (Index Condition Pushdown, ICP)

MySQL 5.6+ 引入的优化。核心思想: 把原本在 Server 层做的索引条件过滤, 下推到存储引擎层提前做, 减少回表次数。

Excel 类比

在Excel里找"姓张且年龄>30"的人。没有ICP时, 你先把所有姓张的行都复制出来(回表), 再筛选年龄。有了ICP, 你在索引页就提前判断年龄, 只复制符合条件的行——少搬很多数据!

索引下推对比: INDEX(name, age) — 查询 WHERE name LIKE '张%' AND age > 30
无 ICP (MySQL 5.5)
1 存储引擎: 根据 name LIKE '张%' 找到所有姓张的记录
↓ 5条记录全部回表
2 回表5次, 获取完整行数据
3 Server层: 再用 age > 30 过滤, 只剩2条
4 返回2条结果, 但白白回表了3次!
有 ICP (MySQL 5.6+)
1 存储引擎: name LIKE '张%' 找到5条记录
↓ 在索引中直接判断 age > 30
2 存储引擎层: 索引中已有 age, 直接过滤, 只剩2条
↓ 只回表2次
3 回表2次, 获取完整行数据
4 回表次数从5次减少到2次! Extra: Using index condition
ICP 触发条件

⚠️ 索引失效十大场景

Excel 类比总览

Excel 里有"按姓名查找"的目录。如果你把姓名拆开查(只要"名"不要"姓")、拿数字去文字列查、用模糊匹配查一半, 都会让目录失效, 只好翻遍全表。

#失效场景错误示例修复方式
1 对索引列使用函数 WHERE YEAR(create_time) = 2025 WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
2 对索引列做运算 WHERE age + 1 = 30 WHERE age = 29
3 隐式类型转换 WHERE varchar_col = 123 WHERE varchar_col = '123' — 类型一致!
4 LIKE 以 % 开头 WHERE name LIKE '%张' 全文索引 / Elasticsearch / 改为 '张%'
5 OR 连接非索引列 WHERE indexed_col=1 OR no_index_col=2 给 no_index_col 也加索引, 或用 UNION
6 不等于 (!= / <>) WHERE status != 'DELETED' 改用 IN 列举有效值, 或在MySQL中这个有时能用, 看优化器
7 IS NOT NULL WHERE col IS NOT NULL IS NULL 通常可以走索引, IS NOT NULL 优化器可能放弃
8 NOT IN / NOT EXISTS WHERE id NOT IN (1,2,3) 改写为 LEFT JOIN ... WHERE IS NULL, 或用 IN 列举
9 ORDER BY 与索引方向不一致 INDEX(a ASC, b ASC)ORDER BY a ASC, b DESC MySQL 8.0+ 支持降序索引, 或统一排序方向
10 优化器认为全表更快 表小 / 数据分布不均 / 选中行超过30% FORCE INDEXUSE INDEX 提示, 但慎用!
面试高频考点

面试官最爱问: "索引什么时候会失效?" — 至少要能说出 函数、计算、隐式转换、LIKE %开头、OR 这5个。

3 查询优化模式

📄 分页优化: 深翻页问题

Excel 类比

Excel 有10万行数据, 你要"翻到第1万页, 每页10行" → 相当于先跳过10万行, 再取10行。即使你只要10行, MySQL也必须先扫描跳过的10万行!

-- 问题: 深翻页, OFFSET很大
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- MySQL会扫描100010行, 然后丢掉前100000行! 浪费!

方案1: Deferred Join (延迟关联)

-- 先用子查询+覆盖索引快速拿到10个ID, 再关联回表
SELECT o.* FROM orders o
JOIN (
  SELECT id FROM orders ORDER BY id LIMIT 100000, 10
) tmp ON o.id = tmp.id;
-- 子查询只走索引(覆盖索引), 不回表; 外层只回表10次!

方案2: 游标/书签法 (推荐)

-- 上一页最后一条记录的ID为 last_id
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- 完美利用主键索引, 不需要 OFFSET, 极快!
-- 缺点: 不支持跳页(只能上一页/下一页)

方案3: BETWEEN (有序ID)

SELECT * FROM orders WHERE id BETWEEN 100001 AND 100010;

📊 COUNT 优化

写法含义性能Excel 类比
COUNT(*)统计总行数(包含NULL)最快Excel最下方的行号
COUNT(1)等价于 COUNT(*)最快同上, 没区别
COUNT(列名)统计该列非NULL的行数较慢要逐行检查单元格是否为空
COUNT(DISTINCT 列)统计去重后的非NULL数量最慢先去重再计数
InnoDB 下 COUNT 的真相

COUNT(*)COUNT(1) 完全等价, InnoDB 会选择最小的索引来扫描(不一定扫主键)。MyISAM 有存储行数所以瞬间返回, 但 InnoDB 因为MVCC不能直接存行数。

🔄 ORDER BY 与 GROUP BY 优化

ORDER BY 优化

Excel 类比

Excel排序: 如果数据已经按"姓名"排序好了(索引), 你再要求按姓名排序 → Excel不用做任何事(Using index)。如果数据是乱序的 → Excel需要重新排一遍(filesort)。

-- Using index: ORDER BY 的列与索引顺序一致
-- INDEX(a, b)
SELECT * FROM t WHERE a = 1 ORDER BY b;  -- ✓ Using index (a已确定, b在索引中有序)
SELECT * FROM t ORDER BY a, b;           -- ✓ Using index
SELECT * FROM t ORDER BY b, a;           -- ✗ Using filesort (顺序反了)
SELECT * FROM t WHERE a > 1 ORDER BY b;  -- ✗ Using filesort (a是范围查询, b不再有序)

GROUP BY 优化

-- MySQL 默认会对 GROUP BY 的结果排序 (隐式 ORDER BY)
-- 如果不需要排序, 可以显式指定 ORDER BY NULL 避免filesort
SELECT dept, COUNT(*) FROM employees GROUP BY dept ORDER BY NULL;

-- 最好: GROUP BY 的列有索引
-- INDEX(dept) → GROUP BY dept 走索引, 无需临时表

🔅 子查询改写为 JOIN & UNION 优化

-- 慢: 相关子查询 (DEPENDENT SUBQUERY, 每行执行一次)
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.vip = 1);

-- 快: 改写为 JOIN
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id WHERE u.vip = 1;

UNION vs UNION ALL

操作是否去重性能何时用
UNION是(像 DISTINCT)较慢需要去重时
UNION ALL否(直接拼接)确定无重复 / 不需要去重
最佳实践
除非确定需要去重, 否则优先用 UNION ALL。UNION 会触发临时表排序去重。

💥 生产案例: "LIMIT 100000,10 查了10秒"

生产案例

背景: 订单列表页, 第1万页查询 LIMIT 100000, 10 耗时10秒。数据库 CPU 飙升。

优化步骤
1 EXPLAIN 分析: type=ALL, rows=100万, 无索引, 全表扫描 + filesort
2 加索引: ALTER TABLE orders ADD INDEX idx_status_time(status, create_time)
3 检查效果: EXPLAIN 显示 type=ref, Extra=Using index condition, 但 rows 仍然很大
4 延迟关联: 改写为 Deferred Join, 子查询用覆盖索引取ID
5 最终SQL: SELECT o.* FROM orders o JOIN (SELECT id FROM orders WHERE status='PAID' ORDER BY create_time LIMIT 100000,10) t ON o.id=t.id
6 结果: 10秒 → 0.05秒, 提升200倍! 子查询走覆盖索引, 外层只回表10行

4 数据建模

星型模型 vs 雪花模型

Excel 类比

星型模型 = 一个大表(事实表)连接多个小下拉菜单表(维度表)。下拉菜单表直接连到大表, 没有嵌套。

雪花模型 = 下拉菜单表还有自己的下拉菜单表! 比如"产品"下拉表里还引用了"产品分类"下拉表。层层嵌套, 像雪花分叉。

星型模型 (Star Schema)
事实表
订单明细
fact_orders
维度: 时间
dim_date
维度: 产品
dim_product
维度: 客户
dim_customer
维度: 产线
dim_line
维度: 班次
dim_shift
维度表不拆分, 冗余存储, JOIN少
雪花模型 (Snowflake Schema)
事实表
订单明细
维度: 产品
维度: 客户
维度: 地区
子维度: 产品分类
子维度: 客户等级
子维度: 城市/省份
维度表继续拆分, 规范化, JOIN多
星型: 扁平化冗余, 查询快 | 雪花: 规范化, 节省空间但JOIN复杂
对比维度星型模型雪花模型
维度表结构不规范化, 适度冗余完全规范化, 无冗余
JOIN 次数少 (事实表直接连维度表)多 (维度表还要连子维度表)
查询性能较慢
存储空间较大 (冗余)较小
ETL 复杂度简单 (维度表大而全)复杂 (需维护外键关系)
适用场景OLAP分析报表 (大多数场景)存储敏感 / 层级复杂

🗂 事实表与维度表

Excel 类比: 制造业数据仓库

事实表 = Excel的"产量记录表", 每一行是一次事件(一条产量记录、一次停机), 行数疯狂增长。

维度表 = Excel的"下拉菜单选项表", 记录机台信息、产品信息、班次信息, 很少变化。

制造业数据仓库 ER 图
fact_production (事实表)
PK record_id BIGINT
FK date_id INT
FK machine_id INT
FK product_id INT
FK shift_id INT
FK line_id INT
quantity DECIMAL
defect_count INT
downtime_min INT
dim_date
PK date_id INT
full_date DATE
year INT
month INT
weekday INT
dim_machine
PK machine_id INT
machine_name VARCHAR
FK workshop_id INT
status VARCHAR
dim_product
PK product_id INT
product_name VARCHAR
category VARCHAR
spec VARCHAR
dim_shift
PK shift_id INT
shift_name VARCHAR
start_time TIME
end_time TIME
dim_line
PK line_id INT
line_name VARCHAR
FK workshop_id INT
capacity INT
事实表(黄)通过外键连接维度表(蓝), 事实表只存ID和度量值, 描述信息由维度表提供
特征事实表 Fact维度表 Dimension
内容事件/交易记录的度量值描述性属性信息
增长速度极快 (每天百万行)极慢 (几百到几万行)
列类型外键 + 数值度量主键 + 文本描述
典型大小GB ~ TB 级MB ~ GB 级
制造业示例产量记录、停机记录、质检记录机台、产品、班次、产线、车间

📅 缓慢变化维度 (SCD)

维度表的属性不是一成不变的, 比如"员工"从"A部门"调到了"B部门"。如何处理这种变化?

SCD Type 1 / 2 / 3 对比 — 以"员工部门变更"为例
Type 1
直接覆盖
不保留历史, 新值覆盖旧值
idnamedept
1张三研发部
原"市场部"被覆盖, 历史丢失
简单 无历史
Type 2
新增行 (最常用)
每条记录加有效期, 保留完整历史
skidnamedeptactive
1100张三市场部N
2100张三研发部Y
用代理键(surrogate key)区分版本
完整历史 行数增长
Type 3
新增列
用"前一值"列记录变更, 只保留一次
idnamedeptprev_dept
1张三研发部市场部
只能记录一次变更, 再变就覆盖
空间小 仅一次历史
面试要点

Type 2 是生产环境最常用的方案。它通过新增行 + 代理键 + 有效期字段(start_date/end_date 或 is_active)来保留完整变更历史。代价是维度表行数增加, 但换来的是可以对任意时间点做准确分析。

5 数据仓库基础

⚙️ OLTP vs OLAP

Excel 类比

OLTP = 每天用的Excel出勤表, 大家轮流填, 一行一行加, 强调快速写入。

OLAP = 月底汇总的Excel透视表, 从多个表拉数据做分析, 强调复杂查询。

OLTP (联机事务处理)
特征说明
用途日常业务操作
操作INSERT / UPDATE / DELETE 为主
单次数据量少 (1~几十行)
并发量极高 (成千上万用户)
响应要求毫秒级
设计原则规范化 (3NF), 减少冗余
典型系统MySQL, PostgreSQL, Oracle
Excel类比日常填写的考勤表
OLAP (联机分析处理)
特征说明
用途数据分析、报表、决策
操作SELECT / GROUP BY / 聚合为主
单次数据量海量 (百万~亿行扫描)
并发量低 (分析人员)
响应要求秒~分钟级可接受
设计原则反规范化 (星型/雪花), 适度冗余
典型系统ClickHouse, Doris, Hive
Excel类比月底的透视表/汇总报表

🔄 ETL 与数据集市

ETL 流程

E
Extract
抽取
从MySQL/日志/API
读取原始数据
T
Transform
转换
清洗/格式化/聚合
维度关联/SCD处理
L
Load
加载
写入数据仓库
ClickHouse/Doris
ETL: 从业务系统抽取数据, 经清洗转换后加载到分析系统

数据集市 (Data Mart)

数据集市是数据仓库的子集, 面向特定部门或业务线。

Excel 类比

数据仓库 = 公司总的Excel数据中心, 所有Sheet都在。
数据集市 = 给生产部单独拷贝了"产量+停机"相关的Sheet, 只关注自己部门的数据。
从大仓库中切出一部分, 给特定用户用。

💥 生产案例: 白天跑报表影响业务查询

生产案例

背景: 制造业MES系统, MySQL主库同时承担业务写入和报表查询。白天运营人员跑报表(复杂聚合SQL), 导致业务端下单/查询超时, 用户投诉。

解决方案: OLTP / OLAP 分离
1 读写分离: MySQL主库只写, 从库承担简单查询
2 引入OLAP引擎: 部署 ClickHouse / Doris 专门跑报表
3 ETL同步: 用 Canal/Debezium 监听 MySQL binlog, 准实时同步到 ClickHouse
4 报表改道: 所有报表查询改为查询 ClickHouse, MySQL只做业务
5 效果: MySQL CPU从90%降到30%, 报表速度从30秒降到1秒, 互不干扰
核心思想: 业务系统(MySQL) 和 分析系统(ClickHouse) 物理分离, 各司其职

6 面试题精选 (25道)

覆盖 EXPLAIN、索引优化、查询模式、数据建模、OLTP vs OLAP。点击展开答案。

1 EXPLAIN 中 type 字段有哪些值? 从好到差排序。 基础

从最优到最差:

systemconsteq_refrefrangeindexALL

  • system/const: 表中只有一行或通过主键/唯一索引精确匹配一行
  • eq_ref: JOIN时通过主键/唯一索引匹配, 每行最多匹配一条
  • ref: 普通索引等值匹配, 可能多条
  • range: 索引范围扫描 (BETWEEN, >, <, IN)
  • index: 全索引扫描 (比ALL好, 但还是扫全部)
  • ALL: 全表扫描, 必须优化!

目标: 至少达到 range 级别, 最好 ref 及以上。

2 什么是覆盖索引? 为什么它快? 基础

覆盖索引: 一个索引包含了查询所需的所有列, 因此不需要回表(不需要去聚簇索引取完整行)。

为什么快: 二级索引通常比聚簇索引小得多(一个页能存更多记录), 且省去了回表的随机IO。EXPLAIN 中 Extra 会显示 Using index

-- INDEX(name, age) 覆盖了查询
EXPLAIN SELECT name, age FROM users WHERE name = '张三';
-- Extra: Using index ← 不回表!
3 最左前缀原则是什么? 给 INDEX(a, b, c) 举例。 基础

复合索引 INDEX(a, b, c) 实际创建了 (a), (a,b), (a,b,c) 三种索引能力。查询必须从最左列开始匹配, 不能跳过中间列。

  • WHERE a=1 AND b=2 AND c=3 → 命中全部三列 ✓
  • WHERE a=1 AND b=2 → 命中 a, b ✓
  • WHERE a=1 AND c=3 → 只命中 a (c无法用, 跳过了b) △
  • WHERE b=2 AND c=3 → 完全不命中 ✗

Excel类比: 像三级排序(省→市→区), 跳过省直接按市排就没有意义。另外, 范围查询后的列也无法使用索引

4 索引什么情况下会失效? 列举5种常见场景。 高频
  1. 对索引列使用函数: WHERE YEAR(date)=2025 → 改为范围查询
  2. 对索引列做运算: WHERE age+1=30 → 改为 WHERE age=29
  3. 隐式类型转换: VARCHAR列用数字查 WHERE phone=1380000 → 加引号
  4. LIKE '%xxx': 以通配符开头 → 改为 'xxx%' 或用全文索引
  5. OR连接无索引列: WHERE a=1 OR b=2 (b无索引) → 给b也加索引或用UNION

其他还有: !=, NOT IN, IS NOT NULL, ORDER BY方向不一致, 优化器判断全表更快等。

5 COUNT(*) 和 COUNT(列名) 有什么区别? 基础
  • COUNT(*): 统计总行数, 包含NULL值, InnoDB会选择最小的索引扫描
  • COUNT(1): 与 COUNT(*) 完全等价, 没有区别
  • COUNT(列名): 统计该列非NULL的行数, 需要逐行判断, 较慢

结论: 优先用 COUNT(*), 它是标准写法且语义最清晰。

6 什么是索引下推 (ICP)? 高频

索引下推 (Index Condition Pushdown, MySQL 5.6+) 是将 WHERE可以用索引过滤但无法完全走索引的条件, 下推到存储引擎层提前执行, 减少回表次数

例子: INDEX(name, age), 查询 WHERE name LIKE '张%' AND age > 30

  • 无ICP: 先用 name LIKE '张%' 找到所有姓张的, 全部回表, 再在Server层过滤 age
  • 有ICP: 在索引中直接判断 age, 只回表满足条件的行

EXPLAIN 标志: Extra 中出现 Using index condition

7 LIMIT 100000, 10 为什么慢? 怎么优化? 必考

MySQL的 LIMIT offset, n 会先扫描 offset + n 行, 再丢掉前 offset 行。offset很大时绝大部分扫描都是浪费。

优化方案:

  1. 延迟关联 (Deferred Join): 子查询用覆盖索引取ID, 外层只回表少量行
  2. 游标法: WHERE id > last_id ORDER BY id LIMIT 10, 完美利用索引, 不需要OFFSET
  3. BETWEEN: 如果ID连续, 直接 WHERE id BETWEEN ... AND ...
-- 延迟关联
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000,10) t ON o.id=t.id;
8 EXPLAIN 中 Extra 出现 Using filesort 是什么意思? 如何消除? 高频

Using filesort 表示 MySQL 无法利用索引来完成排序, 需要在内存(或磁盘)中额外做排序操作。这是一个性能警告。

消除方法:

  • 确保 ORDER BY 的列有索引
  • 如果是 WHERE a=1 ORDER BY b, 创建复合索引 (a, b)
  • ORDER BY 的列顺序必须与索引顺序一致, 不能反
  • 如果是多列排序, 方向必须一致 (都ASC或都DESC, MySQL 8.0+支持降序索引)
9 什么是回表? 为什么要避免? 基础

回表: 通过二级索引找到主键值后, 再用主键去聚簇索引查找完整行数据的过程。涉及两次索引查找, 其中聚簇索引查找通常是随机IO

为什么避免: 回表意味着每条匹配记录都需要一次随机IO, 当匹配行数很多时(比如上万行), 性能急剧下降。

避免方式: 使用覆盖索引, 让查询所需的所有列都在二级索引中, EXPLAIN 显示 Using index

10 MySQL 为什么有时候选错索引? 怎么处理? 高频

MySQL优化器基于统计信息(索引基数 cardinality)来估算不同执行计划的成本, 选择成本最低的。但统计信息可能不准确:

  • 数据分布不均匀 (比如 status 90%都是 'ACTIVE')
  • 统计信息未及时更新
  • 对于复杂查询, 优化器的成本估算可能偏差较大

处理方式:

  1. ANALYZE TABLE 更新统计信息
  2. FORCE INDEX(索引名) 强制使用指定索引(慎用)
  3. USE INDEX(索引名) 建议使用(优化器仍可覆盖)
  4. 调整 SQL 写法让优化器更容易做正确选择
11 UNION 和 UNION ALL 的区别? 什么时候用哪个? 基础
  • UNION: 合并结果集并去重, 相当于加了 DISTINCT, 需要临时表排序
  • UNION ALL: 合并结果集不去重, 直接拼接, 性能更好

最佳实践: 除非确定需要去重, 否则优先用 UNION ALL。UNION 的去重操作消耗大量资源。

12 子查询和 JOIN 哪个快? 什么情况下需要改写? 高频

在 MySQL 中, JOIN 通常比子查询快, 尤其是:

  • DEPENDENT SUBQUERY: 相关子查询会对外层每一行都执行一次子查询, 极慢 → 改写为JOIN
  • IN 子查询: MySQL 5.6+ 优化为 semi-join, 但有时仍不如直接 JOIN
-- 慢: 相关子查询
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip=1);
-- 快: JOIN
SELECT o.* FROM orders o JOIN users u ON o.user_id=u.id WHERE u.vip=1;
13 星型模型和雪花模型的区别? 各自适用场景? 高频

星型模型: 事实表居中, 维度表直接连接, 维度表不规范化 (有冗余)。JOIN少, 查询快。

雪花模型: 维度表继续拆分成子维度表, 完全规范化。节省空间, 但JOIN多, 查询复杂。

Excel类比: 星型=大表直连下拉菜单表; 雪花=下拉菜单表还有自己的下拉菜单。

选择建议: 大多数OLAP场景选星型模型, 查询性能优先。存储特别敏感且维度层级复杂时考虑雪花模型。

14 什么是事实表? 什么是维度表? 各自特点? 基础

事实表 (Fact Table):

  • 存储业务事件/交易的度量数据 (订单、产量、停机记录)
  • 包含外键(关联维度表) + 数值度量(金额、数量、时长)
  • 增长极快, 通常百万到数十亿行

维度表 (Dimension Table):

  • 存储描述性属性 (产品信息、员工信息、时间信息)
  • 包含主键 + 文本描述字段
  • 增长缓慢, 通常几百到几万行

Excel类比: 事实表=每天不断追加的记录行; 维度表=固定不变的下拉选项列表。

15 SCD (缓慢变化维度) Type 2 是怎么实现的? 高频

SCD Type 2 通过新增行来保留维度属性的完整变更历史。每条记录需要:

  • 代理键 (Surrogate Key): 自增主键, 唯一标识每个版本
  • 自然键 (Natural Key): 原始业务ID (如员工编号)
  • 有效期: start_date / end_dateis_current 标记

当属性变更时, 将旧记录的 end_date 设为当前日期, is_current 设为 N, 然后插入新记录。事实表通过代理键关联, 确保历史事实关联的是当时有效的维度值。

16 OLTP 和 OLAP 的区别? 基础
维度OLTPOLAP
全称联机事务处理联机分析处理
操作类型INSERT/UPDATE/DELETESELECT/GROUP BY/聚合
单次数据量少(几行)海量(百万~亿行)
并发量极高
设计原则规范化(3NF)反规范化(星型/雪花)
典型系统MySQL, PostgreSQLClickHouse, Doris, Hive
17 ORDER BY 如何利用索引避免 filesort? 高频

当 ORDER BY 的列顺序与索引列顺序完全一致时, MySQL 可以利用索引的有序性直接返回排序结果, 不需要额外排序。

  • INDEX(a, b) + ORDER BY a, bUsing index
  • INDEX(a, b) + WHERE a=1 ORDER BY bUsing index ✓ (a确定后b有序)
  • INDEX(a, b) + ORDER BY b, aUsing filesort ✗ (顺序反了)
  • INDEX(a, b) + WHERE a > 1 ORDER BY bUsing filesort ✗ (a是范围, b不再有序)
18 什么是聚簇索引和非聚簇索引? 基础

聚簇索引 (Clustered Index):

  • InnoDB中主键索引就是聚簇索引, 叶子节点存储完整行数据
  • 一张表只有一个聚簇索引
  • 数据物理上按主键顺序存储

非聚簇索引 (Secondary Index / 二级索引):

  • 叶子节点存储主键值而非完整行
  • 查询非索引列时需要回表 (用主键去聚簇索引取数据)
  • 可以有多个二级索引

Excel类比: 聚簇索引=Excel按行号排列的完整数据表; 二级索引=按姓名排序的索引卡, 卡上只有姓名和行号。

19 EXPLAIN 中 rows 很大但查询很快, 可能是什么原因? 进阶

rows 是优化器的估算值, 基于统计信息, 可能不准确:

  • 实际数据分布可能比统计信息更集中 (选择性更好)
  • 使用了 LIMIT, 可能在扫描很多行之前就满足了
  • 统计信息过期, ANALYZE TABLE 后可能更新
  • 覆盖索引场景下虽然 rows 大但全是内存操作, 无随机IO
20 GROUP BY 为什么会出现 Using temporary? 如何优化? 高频

Using temporary 表示 MySQL 创建了临时表来处理 GROUP BY。通常发生在:

  • GROUP BY 的列没有索引
  • GROUP BY 的列与索引顺序不一致
  • GROUP BY 和 ORDER BY 的列不同

优化方式:

  1. 给 GROUP BY 的列添加索引
  2. 如果不需要排序, 加 ORDER BY NULL 避免隐式排序
  3. 调大 tmp_table_sizemax_heap_table_size 让临时表在内存中
  4. 考虑使用松散索引扫描 (Loose Index Scan)
21 如何设计一个订单系统的事实表和维度表? 进阶

事实表 fact_orders:

  • order_id (代理键), date_id (FK→dim_date), customer_id (FK→dim_customer), product_id (FK→dim_product)
  • 度量: quantity, amount, discount_amount

维度表:

  • dim_date: date_id, full_date, year, quarter, month, weekday
  • dim_customer: customer_id, name, city, level, industry
  • dim_product: product_id, name, category, brand, unit_price

设计原则: 事实表只存ID和度量, 描述信息由维度表提供; 维度表做适度冗余(星型模型)减少JOIN。

22 白天跑报表导致业务系统慢, 怎么解决? 必考

核心问题是 OLTP 和 OLAP 共享同一个数据库实例, 复杂查询占用了大量资源。

  1. 读写分离: MySQL主库写, 从库读, 减轻主库压力
  2. OLTP/OLAP分离: 引入 ClickHouse/Doris 等OLAP引擎专门跑报表
  3. 数据同步: 通过 Canal/Debezium 监听 binlog 准实时同步到OLAP系统
  4. 报表改道: 所有报表查询指向 OLAP 系统
  5. 报表缓存: 高频报表结果缓存到 Redis, 定时刷新

本质: 业务系统和分析系统物理分离, 各自使用合适的数据库引擎。

23 如何判断一个查询需不需要加索引? 加什么索引? 高频

判断步骤:

  1. EXPLAIN 查看 type 是否为 ALL 或 index
  2. 检查 key 是否为 NULL
  3. 看 rows 是否远大于预期
  4. 检查 Extra 是否有 Using filesort / Using temporary

加索引原则:

  1. WHERE 条件中的等值列 → 放在索引前面
  2. ORDER BY / GROUP BY 的列 → 接在 WHERE 条件后面
  3. 范围查询 (> , BETWEEN, LIKE) → 放在索引最后
  4. SELECT 的列 → 考虑用覆盖索引避免回表
  5. 优先创建复合索引而非多个单列索引 (遵循最左前缀)
24 什么是 ETL? 数据仓库中为什么需要 ETL? 基础

ETL = Extract(抽取) + Transform(转换) + Load(加载)

  • Extract: 从MySQL、日志文件、API等数据源读取原始数据
  • Transform: 数据清洗(去重、格式化、空值处理)、维度关联、SCD处理、业务计算
  • Load: 将处理好的数据写入数据仓库(ClickHouse/Doris/Hive)

为什么需要: 业务系统(OLTP)的数据格式和结构不适合直接做分析。ETL将数据从"事务友好"转换为"分析友好"的形态。

25 一个生产SQL从10秒优化到50毫秒, 你的优化步骤是什么? 必考

系统化优化步骤:

  1. EXPLAIN 分析: 查看 type, key, rows, Extra, 定位瓶颈
  2. 检查表结构: 表有多少行? 哪些列有索引? 数据分布如何?
  3. 加索引: 根据WHERE/ORDER BY/GROUP BY创建合适的复合索引
  4. 改写SQL: 子查询→JOIN, UNION→UNION ALL, 深翻页→延迟关联
  5. 利用覆盖索引: 减少回表次数
  6. 检查索引是否生效: 隐式转换? 函数操作? 类型不匹配?
  7. 业务层面: 真的需要查这么多数据? 能否缓存? 能否预计算?
  8. 架构层面: 数据量是否到了需要分库分表/引入OLAP的程度?

核心思路: 先定位(慢查询日志+EXPLAIN) → 再优化(索引+SQL改写) → 最后考虑架构升级。

SQL调优与数据建模 — 面试备战库 | 全程 Excel 类比对照 | 25道真题