面试官问:”mysql要如何优化“,你该如何满分回答?

50 阅读20分钟

开场白(展现全局思维)

"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 索引设计原则

三星索引法则:

  1. ⭐ 第一星:WHERE条件中的列
  2. ⭐⭐ 第二星:避免排序(ORDER BY的列)
  3. ⭐⭐⭐ 第三星:覆盖索引(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 = 2b列无索引时全表扫描
不等于WHERE status != 1优化器可能选择全表
IS NULLWHERE 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 字段类型选择

原则:够用就好,越小越快

场景差的选择好的选择原因
主键BIGINTINT UNSIGNED40亿够用,节省4字节
状态INTTINYINT256个值够用
金额FLOATDECIMAL(10,2)避免精度丢失
时间VARCHARDATETIME/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,全表扫描!

问题分析:

  1. LIKE '%xxx%' 前缀模糊查询无法使用索引
  2. SELECT * 查询了30多个字段,很多不需要的
  3. 运营其实只关心:订单号、用户手机号、金额、状态、时间

优化过程:

第一步:和产品沟通需求

我:"手机号必须支持模糊搜索吗?"
产品:"其实准确手机号搜索就够了,用户一般都有完整手机号"

第二步:修改查询逻辑

-- 优化后:精确查询,可以用上索引
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);

为什么会慢?

  1. IN里有1万个值,MySQL解析SQL就很慢
  2. 虽然主键索引很快,但1万次回表也需要时间
  3. 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秒

经验总结:

  1. IN查询不要超过500个值(建议200以内)
  2. 批量操作要有上限,防止运营"手抖"
  3. 故障现场先止血,再慢慢优化

案例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个索引:

  1. 主键索引(id)
  2. 8个二级索引
  3. 每个索引都要分裂页面、更新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: "索引越多越好吗?"

答: 不是。索引的代价:

  1. 占用磁盘空间
  2. 降低写入性能(INSERT/UPDATE/DELETE都要维护索引)
  3. 增加优化器选择成本

建议:

  • 单表索引不超过5个
  • 单个索引字段不超过5个
  • 定期清理不使用的索引

Q2: "分库分表后怎么处理跨库JOIN?"

答: 三种方案:

  1. 全局表:字典表在每个库都复制一份
  2. ER分片:关联表按同一个KEY分片
  3. 应用层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太大)
  • 网络延迟

解决方案:

  1. 并行复制:MySQL 5.7+支持
    slave-parallel-type = LOGICAL_CLOCK
    slave-parallel-workers = 8
    
  2. 强制读主库:金额等重要数据直接读主库
  3. 延迟监控:Seconds_Behind_Master > 5秒报警
  4. 升级硬件:SSD、万兆网卡

Q4: "什么时候用EXPLAIN,怎么看?"

答: 每次写完SQL都应该EXPLAIN一下。

重点看四个字段:

字段理想值问题值
typeconst/ref/rangeALL(全表扫描)
key有索引名NULL(未用索引)
rows越少越好成千上万
ExtraUsing indexUsing filesort/temporary
EXPLAIN SELECT * FROM orders WHERE user_id = 1000;

Q5: "InnoDB和MyISAM怎么选?"

答: 必选InnoDB(MySQL 8.0已移除MyISAM)

特性InnoDBMyISAM
事务✅ 支持❌ 不支持
行锁✅ 支持❌ 只有表锁
外键✅ 支持❌ 不支持
崩溃恢复✅ 支持❌ 容易损坏
MVCC✅ 支持❌ 不支持

唯一例外: 日志表、临时表可以考虑MyISAM(只追加写入)


十二、优化思维导图

MySQL优化
├── SQL优化(优先级最高)
│   ├── 避免全表扫描
│   ├── 优化JOIN
│   ├── 分页优化
│   └── EXPLAIN分析
├── 索引优化
│   ├── 三星索引法则
│   ├── 最左前缀原则
│   ├── 避免索引失效
│   └── 覆盖索引
├── 表结构优化
│   ├── 字段类型选择
│   ├── 垂直拆分
│   └── 水平拆分
├── 配置优化
│   ├── 缓冲池配置
│   ├── 连接池配置
│   └── 日志配置
├── 架构优化
│   ├── 读写分离
│   ├── 分库分表
│   └── 缓存层
└── 监控诊断
    ├── 慢查询日志
    ├── 性能指标
    └── 实时诊断

最后的建议

  1. 从小处着手:先优化SQL和索引,不要一上来就分库分表
  2. 量化效果:优化前后要有数据对比(响应时间、扫描行数)
  3. 持续监控:性能问题是动态的,需要长期关注
  4. 业务优先:技术优化要服务于业务目标,不能为了优化而优化

记住这句话:过早优化是万恶之源,但监控和诊断要尽早建立!


参考资料


祝你面试顺利!记住:不要背答案,要理解原理和适用场景。