开场白(展现全局思维)
"MySQL优化是一个系统性工程,我会从架构设计、SQL语句、索引、表结构、配置参数、硬件资源六个维度来回答。根据实际情况,优化的优先级应该是:先优化SQL和索引(成本低、效果好),再优化表结构和配置,最后考虑架构升级和硬件扩容。"
一、SQL语句优化(最优先、成本最低)
1.1 避免全表扫描
反例:
-- 在WHERE子句中使用函数
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- 隐式类型转换
SELECT * FROM users WHERE phone = 13800138000; -- phone是varchar类型
正例:
-- 避免在索引列上使用函数
SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
-- 显式类型转换
SELECT * FROM users WHERE phone = '13800138000';
1.2 优化JOIN查询
原则:
- 小表驱动大表(用小表做驱动表)
- 确保JOIN字段有索引
- 避免JOIN太多表(建议不超过3个)
-- 优化前:大表驱动小表
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.status = 1; -- users表很小
-- 优化后:小表驱动大表
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 1;
1.3 分页查询优化
深分页问题:
-- 问题:需要扫描10000条再返回10条
SELECT * FROM orders ORDER BY id LIMIT 10000, 10;
-- 优化方案1:使用子查询
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 10000, 1)
LIMIT 10;
-- 优化方案2:记录上次最大ID(最优)
SELECT * FROM orders
WHERE id > 10000
ORDER BY id LIMIT 10;
1.4 避免SELECT *
-- 问题:返回不需要的字段,增加IO和网络开销
SELECT * FROM users WHERE id = 1;
-- 优化:只查询需要的字段,可能走覆盖索引
SELECT id, name, email FROM users WHERE id = 1;
1.5 使用EXPLAIN分析
关键指标:
EXPLAIN SELECT * FROM orders WHERE user_id = 1000;
| 关键字段 | 说明 | 理想值 |
|---|---|---|
| type | 访问类型 | const > eq_ref > ref > range > index > ALL |
| possible_keys | 可能用到的索引 | 有索引可用 |
| key | 实际使用的索引 | 用上索引 |
| rows | 扫描行数 | 越少越好 |
| Extra | 额外信息 | Using index最好,避免Using filesort |
二、索引优化(核心中的核心)
2.1 索引设计原则
三星索引法则:
- ⭐ 第一星:WHERE条件中的列
- ⭐⭐ 第二星:避免排序(ORDER BY的列)
- ⭐⭐⭐ 第三星:覆盖索引(SELECT的列)
-- 查询:
SELECT name, age FROM users
WHERE status = 1 AND city = 'Beijing'
ORDER BY create_time DESC;
-- 最优索引:
CREATE INDEX idx_status_city_time_name_age
ON users(status, city, create_time, name, age);
2.2 最左前缀原则
-- 索引:KEY idx_abc (a, b, c)
-- ✅ 可以使用索引
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
-- ❌ 无法使用索引
WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3
2.3 索引失效场景
| 场景 | 示例 | 原因 |
|---|---|---|
| 使用函数 | WHERE DATE(time) = '2024-01-01' | 破坏索引有序性 |
| 运算符 | WHERE id + 1 = 10 | 破坏索引有序性 |
| 类型转换 | WHERE phone = 138001380 | 隐式转换 |
| 前缀模糊 | WHERE name LIKE '%张' | 无法定位起始位置 |
| OR条件 | WHERE a = 1 OR b = 2 | b列无索引时全表扫描 |
| 不等于 | WHERE status != 1 | 优化器可能选择全表 |
| IS NULL | WHERE email IS NULL | 取决于数据分布 |
2.4 索引监控与维护
-- 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes;
-- 查看冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 重建索引(碎片整理)
ALTER TABLE orders ENGINE=InnoDB;
OPTIMIZE TABLE orders;
三、表结构优化
3.1 字段类型选择
原则:够用就好,越小越快
| 场景 | 差的选择 | 好的选择 | 原因 |
|---|---|---|---|
| 主键 | BIGINT | INT UNSIGNED | 40亿够用,节省4字节 |
| 状态 | INT | TINYINT | 256个值够用 |
| 金额 | FLOAT | DECIMAL(10,2) | 避免精度丢失 |
| 时间 | VARCHAR | DATETIME/TIMESTAMP | 便于比较和索引 |
| IP地址 | VARCHAR(15) | INT UNSIGNED | 节省空间,便于范围查询 |
-- 优化前
CREATE TABLE users (
id BIGINT,
status INT,
balance FLOAT,
ip VARCHAR(15)
);
-- 优化后
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT,
status TINYINT UNSIGNED DEFAULT 0,
balance DECIMAL(10,2) DEFAULT 0.00,
ip INT UNSIGNED,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 垂直拆分
大字段分离:
-- 优化前:content字段影响其他字段查询
CREATE TABLE articles (
id INT,
title VARCHAR(200),
content TEXT, -- 大字段
author VARCHAR(50),
create_time DATETIME
);
-- 优化后:拆分为两张表
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
author VARCHAR(50),
create_time DATETIME,
INDEX idx_create_time (create_time)
);
CREATE TABLE article_content (
article_id INT PRIMARY KEY,
content TEXT,
FOREIGN KEY (article_id) REFERENCES articles(id)
);
3.3 水平拆分
分表策略:
-- 按日期分表
orders_202401
orders_202402
orders_202403
-- 按ID范围分表(取模)
users_0 -- id % 10 = 0
users_1 -- id % 10 = 1
...
users_9 -- id % 10 = 9
四、配置参数优化
4.1 连接池配置
# my.cnf
[mysqld]
# 最大连接数
max_connections = 1000
# 连接超时时间
wait_timeout = 28800
interactive_timeout = 28800
# 线程缓存
thread_cache_size = 100
4.2 缓冲池配置(重要)
# InnoDB缓冲池大小(建议:物理内存的60-80%)
innodb_buffer_pool_size = 8G
# 缓冲池实例数(提高并发)
innodb_buffer_pool_instances = 8
# 每个表独立表空间
innodb_file_per_table = 1
监控缓冲池命中率:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_%';
-- 计算命中率(应该 > 99%)
命中率 = (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100
4.3 日志配置
# 重做日志大小(影响写入性能)
innodb_log_file_size = 512M
innodb_log_buffer_size = 16M
# 刷盘策略(性能与安全的平衡)
innodb_flush_log_at_trx_commit = 2 # 0最快,1最安全,2折中
# 二进制日志(主从复制)
sync_binlog = 1 # 1最安全,100可以提升性能
4.4 查询缓存(MySQL 8.0已移除)
# MySQL 5.7及以下
query_cache_type = 1
query_cache_size = 256M
# 注意:对于写多读少的场景,建议关闭查询缓存
五、架构层面优化
5.1 读写分离
写请求
↓
┌──────────────┐
│ 主库(Master) │
└──────┬───────┘
│ binlog复制
┌───────┴────────┐
↓ ↓
┌────────┐ ┌────────┐
│从库1 │ │从库2 │
└────────┘ └────────┘
↑ ↑
└────────┬───────┘
读请求(负载均衡)
实现方式:
- Spring Boot + ShardingSphere
- MyCAT中间件
- Atlas(360开源)
- MySQL Router
5.2 分库分表
垂直拆分(按业务模块):
单库 → 拆分
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ ALL DB │ → │ 用户库 │ │ 订单库 │ │ 商品库 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
水平拆分(按数据量):
分库分表路由规则:
- Hash取模:user_id % 4 → db_0, db_1, db_2, db_3
- Range范围:按时间、ID区间分表
- 一致性Hash:便于扩容
工具选择:
- ShardingSphere-JDBC(推荐,轻量级)
- MyCat(中间件方案)
- TDDL(淘宝)
5.3 缓存层
多级缓存架构:
请求 → 本地缓存(Caffeine) → Redis → MySQL
(毫秒级) (毫秒级) (秒级)
缓存策略:
// Cache-Aside Pattern(旁路缓存)
public User getUser(Long id) {
// 1. 查询缓存
User user = redis.get("user:" + id);
if (user != null) {
return user;
}
// 2. 查询数据库
user = userMapper.selectById(id);
// 3. 写入缓存
redis.setex("user:" + id, 3600, user);
return user;
}
六、硬件与操作系统优化
6.1 硬件选择
| 组件 | 建议 | 原因 |
|---|---|---|
| CPU | 多核心(16核+) | 提升并发处理能力 |
| 内存 | 越大越好(64GB+) | 缓冲池可以更大 |
| 磁盘 | SSD(必须) | IOPS提升10-100倍 |
| 网络 | 万兆网卡 | 减少主从延迟 |
6.2 操作系统优化
# 1. 文件系统选择(推荐XFS)
mkfs.xfs /dev/sdb
# 2. 禁用SWAP(避免内存交换)
swapoff -a
# 3. 调整文件打开数
ulimit -n 65535
# 4. 修改内核参数
cat >> /etc/sysctl.conf << EOF
# TCP连接队列
net.core.somaxconn = 32768
# TIME_WAIT快速回收
net.ipv4.tcp_tw_reuse = 1
EOF
sysctl -p
七、监控与诊断
7.1 慢查询日志
# 开启慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2 # 超过2秒记录
# 记录未使用索引的查询
log_queries_not_using_indexes = 1
分析工具:
# mysqldumpslow
mysqldumpslow -s t -t 10 slow.log # 最慢的10条
# pt-query-digest(推荐)
pt-query-digest slow.log > slow_report.txt
7.2 性能监控指标
关键指标:
-- QPS/TPS
SHOW GLOBAL STATUS LIKE 'Questions';
SHOW GLOBAL STATUS LIKE 'Com_commit';
-- 连接数
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
-- 缓冲池命中率
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_%';
-- 锁等待
SHOW ENGINE INNODB STATUS;
SELECT * FROM information_schema.INNODB_LOCKS;
监控工具:
- Prometheus + Grafana + mysqld_exporter
- Percona Monitoring and Management (PMM)
- 云服务商自带监控(阿里云RDS、腾讯云CDB)
7.3 实时诊断
-- 查看当前执行的SQL
SHOW PROCESSLIST;
-- 查看锁等待
SELECT * FROM performance_schema.data_locks;
-- 查看事务
SELECT * FROM information_schema.INNODB_TRX;
-- 杀死慢查询
KILL 123456;
八、实战案例(加分项)
案例1:电商系统订单列表慢查询(真实遇到)
背景: 运营同事反馈后台订单列表页面经常超时,特别是按手机号搜索时。订单表600万数据,每天新增2-3万单。
问题SQL:
-- 运营要按手机号搜索订单
SELECT * FROM orders
WHERE phone LIKE '%138001380%'
ORDER BY create_time DESC
LIMIT 20;
-- EXPLAIN结果:type=ALL, rows=6000000,全表扫描!
问题分析:
LIKE '%xxx%'前缀模糊查询无法使用索引SELECT *查询了30多个字段,很多不需要的- 运营其实只关心:订单号、用户手机号、金额、状态、时间
优化过程:
第一步:和产品沟通需求
我:"手机号必须支持模糊搜索吗?"
产品:"其实准确手机号搜索就够了,用户一般都有完整手机号"
第二步:修改查询逻辑
-- 优化后:精确查询,可以用上索引
SELECT order_no, phone, amount, status, create_time
FROM orders
WHERE phone = '13800138000'
ORDER BY create_time DESC
LIMIT 20;
-- 添加索引
CREATE INDEX idx_phone_time ON orders(phone, create_time);
第三步:如果真的需要模糊搜索呢?
// 方案:用ES做搜索,MySQL只做精确查询
// 1. 订单数据实时同步到ES
// 2. 模糊搜索走ES,得到订单ID列表
List<Long> orderIds = elasticsearchService.searchByPhone("138");
// 3. 根据ID列表去MySQL查详情
List<Order> orders = orderMapper.selectByIds(orderIds);
效果对比:
- 优化前:平均8秒,经常超时
- 优化后:平均80ms,从未超时
- 额外收获:运营反馈"终于不卡了"
经验总结:
技术优化要先和产品确认需求,很多"必须"的功能其实可以换个实现方式。
案例2:财务对账页面凌晨跑批超时(踩坑经历)
背景: 每天凌晨3点跑对账任务,统计前一天各个状态的订单数量和金额。最近任务从5分钟涨到40分钟,DBA收到告警。
问题SQL:
-- 财务对账统计
SELECT
DATE(create_time) AS date,
status,
COUNT(*) AS cnt,
SUM(amount) AS total_amount
FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'
GROUP BY DATE(create_time), status;
-- EXPLAIN: type=range, rows=50000, Extra=Using temporary; Using filesort
踩坑点: 我一开始想当然地加了索引:
-- ❌ 这个索引没用上!
CREATE INDEX idx_time_status ON orders(create_time, status);
-- 原因:GROUP BY用了DATE(create_time)函数,破坏了索引
正确的优化方案:
方案1:避免在索引列上用函数
-- 添加冗余字段(空间换时间)
ALTER TABLE orders ADD COLUMN order_date DATE;
UPDATE orders SET order_date = DATE(create_time);
-- 新增订单时自动填充
INSERT INTO orders(order_no, amount, create_time, order_date)
VALUES('202401010001', 100, NOW(), CURDATE());
-- 优化后的查询
SELECT
order_date,
status,
COUNT(*) AS cnt,
SUM(amount) AS total_amount
FROM orders
WHERE order_date = '2024-01-01'
GROUP BY order_date, status;
-- 添加索引
CREATE INDEX idx_date_status ON orders(order_date, status);
方案2:统计表(更彻底的优化)
-- 创建每日统计表
CREATE TABLE order_daily_stats (
stat_date DATE NOT NULL,
status TINYINT NOT NULL,
order_count INT DEFAULT 0,
total_amount DECIMAL(15,2) DEFAULT 0,
PRIMARY KEY (stat_date, status)
) ENGINE=InnoDB;
-- 每天凌晨定时任务更新统计表
INSERT INTO order_daily_stats
SELECT order_date, status, COUNT(*), SUM(amount)
FROM orders
WHERE order_date = CURDATE() - INTERVAL 1 DAY
GROUP BY order_date, status
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
total_amount = VALUES(total_amount);
-- 查询直接读统计表
SELECT * FROM order_daily_stats
WHERE stat_date = '2024-01-01';
效果对比:
- 优化前:40分钟
- 方案1(加冗余字段):2分钟
- 方案2(统计表):200毫秒
经验总结:
聚合查询能预计算就预计算,不要每次都实时统计。报表类需求90%都可以用统计表。
案例3:用户中心接口雪崩(线上故障)
故障回顾: 某天下午3点,用户中心接口突然大量超时,QPS从2000降到200,CPU飙到98%。
现场排查:
# 1. 登录数据库服务器,查看慢查询
mysql> SHOW PROCESSLIST;
# 发现100多个查询都卡在同一个SQL
SELECT * FROM users WHERE id IN (1,2,3,4,5...9998,9999,10000);
# Time: 45秒,State: Sending data
问题原因:
运营后台有个"批量发券"功能,运营同事一次性选了1万个用户。开发当时图方便,直接用了IN查询。
原问题代码:
// ❌ 错误示范
public List<User> batchGetUsers(List<Long> userIds) {
// userIds有1万个ID
return userMapper.selectList(
new QueryWrapper<User>().in("id", userIds)
);
}
// 生成的SQL
SELECT * FROM users WHERE id IN (1,2,3...9999,10000);
为什么会慢?
IN里有1万个值,MySQL解析SQL就很慢- 虽然主键索引很快,但1万次回表也需要时间
SELECT *查询了50多个字段(包括头像URL等大字段)
临时止血(5分钟内完成):
-- 先杀掉慢查询
SELECT GROUP_CONCAT(id) FROM information_schema.processlist
WHERE time > 30 AND info LIKE '%WHERE id IN%';
-- 批量KILL
KILL 12345; KILL 12346; ...
紧急修复代码(30分钟上线):
// ✅ 正确做法:分批查询
public List<User> batchGetUsers(List<Long> userIds) {
// 限制单次最多200个
if (userIds.size() > 200) {
List<User> result = new ArrayList<>();
// 分批查询
for (List<Long> batch : Lists.partition(userIds, 200)) {
result.addAll(userMapper.selectList(
new QueryWrapper<User>()
.in("id", batch)
.select("id", "nickname", "phone") // 只查需要的字段
));
}
return result;
}
return userMapper.selectList(
new QueryWrapper<User>().in("id", userIds)
);
}
后续优化(第二天):
// 更进一步:改成JOIN,避免多次查询
// 1. 先把ID列表写入临时表
CREATE TEMPORARY TABLE temp_user_ids (
user_id BIGINT PRIMARY KEY
);
// 2. 批量插入(JDBC批量)
INSERT INTO temp_user_ids VALUES (1),(2),(3)...(10000);
// 3. JOIN查询
SELECT u.id, u.nickname, u.phone
FROM temp_user_ids t
INNER JOIN users u ON t.user_id = u.id;
// 4. 清理临时表
DROP TEMPORARY TABLE temp_user_ids;
效果对比:
- 优化前:1万个ID,45秒
- 临时方案:分50批,每批200个,共10秒
- 最终方案:临时表+JOIN,1.5秒
经验总结:
IN查询不要超过500个值(建议200以内)- 批量操作要有上限,防止运营"手抖"
- 故障现场先止血,再慢慢优化
案例4:分页查询越翻越慢(产品体验问题)
背景: 商品列表支持按销量排序,测试反馈"翻到第100页就卡死"。
问题SQL:
-- 第1页:很快
SELECT * FROM products ORDER BY sales DESC LIMIT 0, 20;
-- 第100页:5秒
SELECT * FROM products ORDER BY sales DESC LIMIT 2000, 20;
-- 第500页:超时
SELECT * FROM products ORDER BY sales DESC LIMIT 10000, 20;
为什么会越来越慢?
LIMIT 10000, 20的执行过程:
1. 按sales排序,找到前10020条数据(全扫描或索引扫描)
2. 丢弃前10000条
3. 返回20条
相当于做了10000次无用功!
尝试过的优化(踩坑):
方案1:子查询(效果一般)
-- 先查ID,再查详情
SELECT * FROM products
WHERE id >= (
SELECT id FROM products
ORDER BY sales DESC
LIMIT 10000, 1
)
ORDER BY sales DESC
LIMIT 20;
-- 问题:子查询还是要扫10000行,只是少了回表
方案2:记录上次位置(最有效)
-- 第1页:正常查询
SELECT id, name, sales FROM products
ORDER BY sales DESC, id DESC -- 注意:要加id保证唯一性
LIMIT 20;
-- 返回:最后一条 sales=9999, id=12345
-- 第2页:基于上次位置继续查
SELECT id, name, sales FROM products
WHERE sales <= 9999 AND (sales < 9999 OR id < 12345)
ORDER BY sales DESC, id DESC
LIMIT 20;
前端改造:
// 之前:传统分页
<pagination
current={page}
onChange={page => loadData(page)}
/>
// 优化后:流式加载(类似微博、抖音)
<InfiniteScroll
loadMore={() => loadMore(lastSales, lastId)}
hasMore={hasMore}
>
{list.map(item => <ProductCard {...item} />)}
</InfiniteScroll>
和产品的沟通:
我:"翻到第100页的用户少吗?"
产品:"查了埋点数据,99%用户只看前3页"
我:"那我们可以限制最多翻50页,超过50页提示'请使用搜索功能'"
产品:"可以!"
最终方案:
// 限制最大页数
if (page > 50) {
throw new BusinessException("最多支持查看前50页,请使用搜索功能");
}
// 前3页走普通查询(最常用)
if (page <= 3) {
return productMapper.selectPage(page, 20);
}
// 4-50页走子查询优化
return productMapper.selectPageOptimized(page, 20);
效果对比:
- 优化前:第100页超时
- 优化后:限制最大50页,第50页500ms
- 附加价值:搜索功能的使用率从2%提升到15%
经验总结:
深分页问题,技术优化+产品优化结合效果最好。有时候改产品比改技术更简单。
案例5:加索引导致插入变慢(意外的副作用)
背景: 优化了用户表的查询性能,加了几个索引。第二天收到报警:订单创建接口P99从50ms变成300ms。
问题排查:
-- 查看订单表的索引
SHOW INDEX FROM orders;
-- 发现有8个索引!
KEY idx_user_id (user_id)
KEY idx_status (status)
KEY idx_create_time (create_time)
KEY idx_user_status (user_id, status)
KEY idx_user_time (user_id, create_time)
KEY idx_status_time (status, create_time)
KEY idx_phone (phone)
KEY idx_order_no (order_no)
慢查询日志:
-- 插入变慢了
INSERT INTO orders (user_id, order_no, amount, status, phone, create_time)
VALUES (1001, 'O202401010001', 99.00, 1, '13800138000', NOW());
-- 执行时间从5ms → 30ms
原因分析: 每次插入都要维护8个索引:
- 主键索引(id)
- 8个二级索引
- 每个索引都要分裂页面、更新B+树
优化步骤:
1. 分析哪些索引真的有用
-- 查看索引使用情况(MySQL 5.6+)
SELECT
object_schema,
object_name,
index_name,
rows_selected,
rows_inserted,
rows_updated,
rows_deleted
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_db'
AND object_name = 'orders'
ORDER BY rows_selected;
-- 发现:idx_status 半年从未使用!
2. 删除冗余索引
-- idx_user_status已经包含了user_id,删除idx_user_id
DROP INDEX idx_user_id ON orders;
-- idx_status从未使用
DROP INDEX idx_status ON orders;
-- idx_user_time和idx_user_status功能重叠,删除一个
DROP INDEX idx_user_time ON orders;
3. 定期维护索引
-- 写一个脚本,每周检查一次
SELECT
TABLE_NAME,
INDEX_NAME,
CARDINALITY,
NULLABLE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_db'
AND INDEX_NAME != 'PRIMARY'
ORDER BY TABLE_NAME, INDEX_NAME;
效果对比:
- 8个索引:插入30ms
- 5个索引:插入12ms
- 副作用:查询性能没有下降(因为删的都是冗余索引)
经验总结:
索引不是越多越好!每个索引都有维护成本。定期检查并删除不使用的索引。
九、优化检查清单(总结)
✅ SQL层面
- 避免SELECT *
- 避免在WHERE中使用函数
- 避免隐式类型转换
- 小表驱动大表
- 深分页优化
- 使用EXPLAIN分析
✅ 索引层面
- 高频查询字段有索引
- 联合索引遵循最左前缀
- 避免冗余索引
- 定期检查未使用索引
- 索引覆盖SELECT字段
✅ 表设计层面
- 字段类型合理(够用就好)
- 大字段分离
- 单表数据量控制(<2000万)
- 冷热数据分离
✅ 配置层面
- innodb_buffer_pool_size合理
- max_connections足够
- 慢查询日志开启
- 主从延迟监控
✅ 架构层面
- 读写分离
- 分库分表
- 缓存层(Redis)
- 监控告警
十、面试回答模板(推荐话术)
第一步:表明态度
"MySQL优化是一个系统工程,我会从成本和收益角度分优先级来考虑。"
第二步:分层阐述
"首先是SQL和索引优化,这是成本最低、效果最好的。比如我之前优化过一个订单查询,通过添加联合索引,从5秒优化到50毫秒。"
第三步:举实际案例
"其次是表结构优化,比如大字段分离、冷热数据分离。我们项目中把文章内容从主表拆出去,主表查询速度提升了3倍。"
第四步:提升到架构
"如果数据量继续增长,我会考虑读写分离、分库分表。我们现在用ShardingSphere做的分表,单表从5000万降到500万,查询速度稳定在100毫秒以内。"
第五步:强调监控
"最后,优化不是一劳永逸的,需要持续监控。我们用Prometheus监控慢查询、连接数、缓冲池命中率,发现问题及时优化。"
十一、常见追问及答案
Q1: "索引越多越好吗?"
答: 不是。索引的代价:
- 占用磁盘空间
- 降低写入性能(INSERT/UPDATE/DELETE都要维护索引)
- 增加优化器选择成本
建议:
- 单表索引不超过5个
- 单个索引字段不超过5个
- 定期清理不使用的索引
Q2: "分库分表后怎么处理跨库JOIN?"
答: 三种方案:
- 全局表:字典表在每个库都复制一份
- ER分片:关联表按同一个KEY分片
- 应用层JOIN:分别查询再内存聚合(推荐)
// 应用层JOIN示例
List<Order> orders = orderService.getByUserId(userId);
List<Long> productIds = orders.stream()
.map(Order::getProductId)
.collect(Collectors.toList());
List<Product> products = productService.getByIds(productIds);
// 内存中组装数据
Q3: "主从延迟怎么解决?"
答: 延迟原因:
- 主库TPS高,从库回放慢
- 大事务(BINLOG太大)
- 网络延迟
解决方案:
- 并行复制:MySQL 5.7+支持
slave-parallel-type = LOGICAL_CLOCK slave-parallel-workers = 8 - 强制读主库:金额等重要数据直接读主库
- 延迟监控:Seconds_Behind_Master > 5秒报警
- 升级硬件:SSD、万兆网卡
Q4: "什么时候用EXPLAIN,怎么看?"
答: 每次写完SQL都应该EXPLAIN一下。
重点看四个字段:
| 字段 | 理想值 | 问题值 |
|---|---|---|
| type | const/ref/range | ALL(全表扫描) |
| key | 有索引名 | NULL(未用索引) |
| rows | 越少越好 | 成千上万 |
| Extra | Using index | Using filesort/temporary |
EXPLAIN SELECT * FROM orders WHERE user_id = 1000;
Q5: "InnoDB和MyISAM怎么选?"
答: 必选InnoDB(MySQL 8.0已移除MyISAM)
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务 | ✅ 支持 | ❌ 不支持 |
| 行锁 | ✅ 支持 | ❌ 只有表锁 |
| 外键 | ✅ 支持 | ❌ 不支持 |
| 崩溃恢复 | ✅ 支持 | ❌ 容易损坏 |
| MVCC | ✅ 支持 | ❌ 不支持 |
唯一例外: 日志表、临时表可以考虑MyISAM(只追加写入)
十二、优化思维导图
MySQL优化
├── SQL优化(优先级最高)
│ ├── 避免全表扫描
│ ├── 优化JOIN
│ ├── 分页优化
│ └── EXPLAIN分析
├── 索引优化
│ ├── 三星索引法则
│ ├── 最左前缀原则
│ ├── 避免索引失效
│ └── 覆盖索引
├── 表结构优化
│ ├── 字段类型选择
│ ├── 垂直拆分
│ └── 水平拆分
├── 配置优化
│ ├── 缓冲池配置
│ ├── 连接池配置
│ └── 日志配置
├── 架构优化
│ ├── 读写分离
│ ├── 分库分表
│ └── 缓存层
└── 监控诊断
├── 慢查询日志
├── 性能指标
└── 实时诊断
最后的建议
- 从小处着手:先优化SQL和索引,不要一上来就分库分表
- 量化效果:优化前后要有数据对比(响应时间、扫描行数)
- 持续监控:性能问题是动态的,需要长期关注
- 业务优先:技术优化要服务于业务目标,不能为了优化而优化
记住这句话:过早优化是万恶之源,但监控和诊断要尽早建立!
参考资料
- 《高性能MySQL》(第三版)
- MySQL官方文档:dev.mysql.com/doc/
- 《MySQL实战45讲》- 极客时间
- Percona工具集:www.percona.com/software
祝你面试顺利!记住:不要背答案,要理解原理和适用场景。