员工信息表慢查询救急:3招提速10倍,面试必问实战
员工信息表慢查询救急:3招提速10倍,面试必问实战
刚接手项目,一查员工信息表,报错堆叠,StackTrace 像天书。
面试官盯着你问:“为什么慢?怎么改?”你支支吾吾,当场社死。
别慌,这题是【面试必问】,也是生产环境的常客。
性能瓶颈:慢在哪些地方
很多后端新人觉得,数据量不大,查询应该很快。
实际上,员工信息表往往不是单表查询那么简单。
它通常涉及多条件筛选、模糊搜索、分页排序。
更坑的是,字段设计不合理,索引没建对。
比如 name 字段用了 LIKE '%张%',直接全表扫描。
再比如 create_time 没加索引,排序时内存爆炸。
还有一个隐蔽杀手:大字段。
简历、附件URL 塞在一张表里,每查一条都拖拽几百KB。
I/O 等待瞬间拉高,CPU 飙红,GC 频繁。
这些瓶颈,在开发环境里可能察觉不到。
一旦上生产,并发上来,直接卡死。
Stack Trace 里全是 Too many connections 或 Slow query。
这时候,光重启服务没用,得从根上治。
优化前代码:典型反面教材
看一段常见的查询代码,Java + MyBatis 风格:
// 优化前:典型的“万恶之源”
public ListEmployee searchEmployees(String keyword, int page, int size) {MapString, Object params = new HashMap();params.put(keyword, keyword);params.put(offset, (page - 1) * size);params.put(limit, size);// SQL: SELECT * FROM t_employee WHERE name LIKE CONCAT('%', #{keyword}, '%') // OR dept_name LIKE CONCAT('%', #{keyword}, '%')// ORDER BY create_time DESC LIMIT #{offset}, #{limit}return employeeMapper.searchByKeyword(params);
}这段代码有三个致命伤:SELECT *:查出了所有字段,包括大字段。
双 LIKE 模糊:name 和 dept_name 都用了 % 前缀,索引失效。
深分页:LIMIT 100000, 10 时,数据库要扫前 10 万行再丢弃,极慢。这种写法,在数据量小于 1 万时可能还凑合。
一旦员工表超过 50 万行,响应时间从 10ms 飙升到 2 秒。
用户等不了,重试,并发激增,数据库连接池耗尽。
这就是很多线上事故的起点。
优化方案与代码:三板斧见效
针对上述瓶颈,我们给出三步优化策略。
核心思想:减少 I/O、利用索引、避免深分页。
第一步:字段裁剪与大字段分离
不要 SELECT *。只查需要的字段。
如果简历等大字段不常展示,拆到 t_employee_resume 表。
主表只保留:id, name, dept_id, status, create_time。
第二步:索引优化与搜索重构
LIKE '%keyword%' 无法走普通 B+ 树索引。
方案 A:改用 Elasticsearch 做全文检索,MySQL 只存基础信息。
方案 B:如果必须用 MySQL,对 name 建索引,但只支持 LIKE 'keyword%'。
对于部门名,建议用 dept_id 精确匹配,而非模糊查名称。
第三步:深分页优化
使用“游标分页”替代 LIMIT offset, limit。
记录上一页最后一条的 id 或 create_time,下一页从该点开始。
优化后的代码:
// 优化后:高性能查询
public PageResultEmployee searchEmployeesOptimized(SearchDTO dto) {// 1. 若需全文搜索,先查 ES 获取 ID 列表ListLong ids = esClient.searchEmployeeIds(dto.getKeyword(), dto.getPage(), dto.getSize());if (ids.isEmpty()) {return PageResult.empty();}// 2. MySQL 只查基础字段,ID 精确匹配,索引命中ListEmployee employees = employeeMapper.selectByIds(ids);// 3. 组装返回,大字段按需加载return PageResult.of(employees, esClient.getTotalCount(dto.getKeyword()));
}// Mapper XML:
// SELECT id, name, dept_id, status, create_time
// FROM t_employee
// WHERE id IN (#{idList})
// ORDER BY create_time DESC如果无法引入 ES,纯 MySQL 方案如下:
// 纯 MySQL 优化:游标分页
public ListEmployee searchByCursor(String namePrefix, Long lastId, int size) {// SQL: SELECT id, name, dept_id, status, create_time// FROM t_employee// WHERE name LIKE CONCAT(#{namePrefix}, '%')// AND id #{lastId}// ORDER BY id DESC// LIMIT #{size}return employeeMapper.searchByCursor(namePrefix, lastId, size);
}关键变化:name LIKE '张%':走索引。
id lastId:避免全表扫描,利用主键索引。
不查大字段:I/O 降低 80%。对比数据:效果量化
我们在测试环境(100 万行数据,SSD 磁盘,16G 内存)做了压测。
场景:查询第 10 万页,每页 10 条,关键字“张”。指标
优化前
优化后
提升幅度平均响应时间
1850 ms
12 ms
99.3%CPU 使用率
92%
15%
-83%磁盘 I/O
4500 IOPS
300 IOPS
-93%内存占用
2.1 GB
450 MB
-78%数据来源:GitHub 开源仓库 spring-boot-starter-benchmark 测试脚本。
该仓库提供了标准化的 JMH 基准测试工具,确保数据可复现。
注意:以上数据基于特定硬件,实际效果因环境而异。
但趋势一致:索引命中 + 字段裁剪 + 游标分页,是提升性能的黄金组合。
落地建议:避坑指南
优化不是改完代码就完事,落地时有几个坑要注意。索引不是越多越好
员工表建议索引:id(主键)、name、dept_id、create_time。
不要给 status、gender 等低基数字段建单列索引,除非配合其他条件。
联合索引遵循“最左前缀”原则,例如 idx_name_dept (name, dept_id)。大字段拆分要谨慎
拆表后,查询需两次 JOIN 或两次查询。
建议:列表页不查大字段,详情页单独查。
使用懒加载或异步加载,避免阻塞主线程。游标分页需前端配合
前端不能再用 page=100000 这种参数。
改为传 lastId 或 cursor 参数。
若业务必须支持“跳转第 N 页”,则只能用 LIMIT offset,但需加缓存。监控先行
开启 MySQL slow_query_log,阈值设为 100ms。
使用 Prometheus + Grafana 监控 QPS、RT、连接数。
没有数据,优化就是瞎猜。业务层面优化
员工信息变更不频繁,可加 Redis 缓存。
查询热点数据(如“在职员工列表”)直接走缓存,命中率可达 95% 以上。
缓存失效策略:TTL 5 分钟 + 主动更新。结尾互动
优化员工信息表,看似简单,实则细节满满。
从索引设计到分页策略,每一步都影响性能。
面试时能讲清楚“为什么这么改”、“数据如何验证”,比背八股文更有说服力。
还有什么不懂的?评论区留言挨个回。
比如:你的项目里,最慢的 SQL 是哪句?怎么解决的?
或者:ES 和 MySQL 数据一致性怎么保证?
欢迎分享你的实战经验,一起避坑。