摘要:从一次"接口响应时间从50ms飙升到2秒"的线上故障出发,深度剖析SELECT *的5大性能陷阱。通过网络传输对比、覆盖索引失效、磁盘IO浪费、以及内存占用的真实测试数据,揭秘为什么SELECT *会导致性能下降、增加字段后程序报错、以及如何优化。配合时序图展示查询流程,给出SELECT字段的最佳实践。
💥 翻车现场
周二下午,哈吉米收到了告警通知。
告警:订单列表接口响应时间异常
- 昨天:平均50ms
- 今天:平均2100ms
- P95:5800ms
哈吉米:"卧槽,怎么突然这么慢?代码没改啊!"
查看代码:
// 订单列表接口
@GetMapping("/order/list")
public Result listOrders(Long userId) {
List<Order> orders = orderMapper.selectByUserId(userId);
return Result.ok(orders);
}
Mapper:
<select id="selectByUserId" resultType="Order">
SELECT * FROM `order` WHERE user_id = #{userId}
</select>
哈吉米:"SELECT * 有问题吗?一直都这么写啊!"
查看表结构,发现昨天运营同学加了个字段:
SHOW CREATE TABLE `order`\G
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32),
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10, 2),
status TINYINT,
create_time DATETIME,
update_time DATETIME,
-- 昨天新加的字段
remark TEXT, ← 备注(可能很长)
logistics_info JSON ← 物流信息(可能很大)
);
哈吉米:"原来是新加了两个大字段,SELECT * 把它们都查出来了!"
下午,南北绿豆和阿西噶阿西来了。
南北绿豆:"SELECT * 有5个性能陷阱,我逐个给你讲。"
🕳️ 陷阱1:网络传输浪费
原理:查询不需要的列,浪费网络带宽。
问题场景
-- 订单列表只需要显示这些字段
前端需要:id, order_no, amount, status, create_time
-- 但SELECT * 查了所有字段
实际查询:id, order_no, user_id, product_id, amount, status,
create_time, update_time, remark(TEXT), logistics_info(JSON)
数据量对比:
需要的数据:
id(8) + order_no(32) + amount(10) + status(1) + create_time(8) = 59字节
SELECT * 的数据:
59字节 + user_id(8) + product_id(8) + update_time(8)
+ remark(平均2KB) + logistics_info(平均1KB) = 约3KB
单行数据:3KB / 59B = 51倍
100行数据:
- 需要:59B × 100 = 5.9KB
- SELECT *:3KB × 100 = 300KB
浪费:300KB / 5.9KB = 51倍网络传输
网络传输时序图
sequenceDiagram
participant Client as 客户端
participant Server as 应用服务器
participant MySQL
Note over Client,MySQL: SELECT * 的流程
Client->>Server: 1. 请求订单列表
Server->>MySQL: 2. SELECT * (查所有字段)
MySQL->>MySQL: 3. 读取300KB数据
MySQL->>Server: 4. 传输300KB (耗时50ms)
Server->>Server: 5. 序列化JSON
Server->>Client: 6. 传输300KB (耗时100ms)
Note over Client: 总耗时: 150ms+
Note over Client,MySQL: 只查需要字段的流程
Client->>Server: 1. 请求订单列表
Server->>MySQL: 2. SELECT id,order_no,... (只查5个字段)
MySQL->>MySQL: 3. 读取5.9KB数据
MySQL->>Server: 4. 传输5.9KB (耗时1ms)
Server->>Server: 5. 序列化JSON
Server->>Client: 6. 传输5.9KB (耗时2ms)
Note over Client: 总耗时: 3ms+
性能对比:
| 方式 | 传输数据量 | 网络耗时 | 提升 |
|---|---|---|---|
| SELECT * | 300KB | 150ms | - |
| 只查需要的字段 | 5.9KB | 3ms | 50倍 |
南北绿豆:"看到了吗?网络传输浪费了50倍!"
🕳️ 陷阱2:无法使用覆盖索引
原理:SELECT * 需要所有列,无法用覆盖索引,必须回表。
问题场景
-- 创建联合索引
CREATE INDEX idx_user_status ON `order`(user_id, status);
-- 查询1:SELECT *(需要回表)
SELECT * FROM `order` WHERE user_id = 10086 AND status = 1;
EXPLAIN:
type: ref
key: idx_user_status
Extra: Using index condition ← 需要回表
-- 查询2:只查索引列(覆盖索引)
SELECT user_id, status FROM `order` WHERE user_id = 10086 AND status = 1;
EXPLAIN:
type: ref
key: idx_user_status
Extra: Using index ← 覆盖索引,不回表
性能对比
-- 假设user_id=10086有100条订单
SELECT *:
1. 在idx_user_status索引找到100个主键
2. 回表100次,读取完整数据
总IO:3次(索引查询) + 100次(回表) = 103次
SELECT user_id, status:
1. 在idx_user_status索引找到100行
2. 索引中就有user_id和status,直接返回
总IO:3次(索引查询)
性能提升:103 / 3 = 34倍
测试数据:
| 查询方式 | 执行时间 | IO次数 |
|---|---|---|
| SELECT * | 45ms | 103次 |
| SELECT user_id, status | 1.3ms | 3次 |
阿西噶阿西:"看到了吗?覆盖索引能减少97%的IO!"
🕳️ 陷阱3:增加字段后程序可能出错
原理:SELECT * 返回的字段顺序可能变化。
问题场景
// Java代码(不好的写法)
String sql = "SELECT * FROM user WHERE id = ?";
Object[] row = jdbcTemplate.queryForArray(sql, userId);
String username = (String) row[1]; // 假设username是第2列
String phone = (String) row[2]; // 假设phone是第3列
如果DBA调整了字段顺序:
-- 原表结构
CREATE TABLE user (
id BIGINT,
username VARCHAR(50),
phone VARCHAR(20),
...
);
-- DBA调整后(在username前加了字段)
ALTER TABLE user ADD COLUMN email VARCHAR(100) AFTER id;
-- 新表结构
CREATE TABLE user (
id BIGINT,
email VARCHAR(100), ← 新字段
username VARCHAR(50), ← 变成第3列了
phone VARCHAR(20), ← 变成第4列了
...
);
结果:
String username = (String) row[1]; // 现在row[1]是email,不是username!
String phone = (String) row[2]; // 现在row[2]是username,不是phone!
// 程序出错 ❌
正确写法:
// ✅ 明确指定字段
String sql = "SELECT id, username, phone FROM user WHERE id = ?";
Object[] row = jdbcTemplate.queryForArray(sql, userId);
String username = (String) row[1]; // 永远是username
String phone = (String) row[2]; // 永远是phone
🕳️ 陷阱4:内存占用高
原理:查询大字段(TEXT、BLOB),占用大量内存。
问题场景
// 查询1000个订单
List<Order> orders = orderMapper.selectList(
new QueryWrapper<Order>().eq("status", 1).last("LIMIT 1000")
);
// SELECT * 包含了remark(TEXT)和logistics_info(JSON)
内存占用:
每个Order对象:
- 基础字段:200字节
- remark(平均2KB)
- logistics_info(平均1KB)
总计:约3KB
1000个Order:3KB × 1000 = 3MB
如果只查需要的字段:
每个Order对象:200字节
1000个Order:200B × 1000 = 200KB
内存占用:3MB / 200KB = 15倍
问题:
- ❌ 堆内存占用高
- ❌ GC频繁(对象多、体积大)
- ❌ 接口响应慢(序列化JSON慢)
🕳️ 陷阱5:磁盘IO浪费
原理:读取不需要的列,浪费磁盘IO。
问题场景
-- 表结构
CREATE TABLE article (
id BIGINT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(50),
content TEXT, ← 大字段(平均10KB)
create_time DATETIME,
INDEX idx_author(author)
);
-- 查询作者的文章列表(只需要标题)
SELECT * FROM article WHERE author = 'alice';
IO对比:
SELECT *:
1. 在idx_author索引找到10个主键
2. 回表10次,每次读取10KB(包含content)
总IO:10 × 10KB = 100KB
SELECT id, title, create_time:
1. 在idx_author索引找到10个主键
2. 回表10次,每次只读需要的列
总IO:10 × 200B = 2KB
IO浪费:100KB / 2KB = 50倍
阿西噶阿西:"大字段(TEXT、BLOB、JSON)用SELECT * 特别浪费IO!"
🎯 正确写法和最佳实践
实践1:明确指定需要的字段
// ❌ 错误
@Select("SELECT * FROM `order` WHERE user_id = #{userId}")
List<Order> selectByUserId(Long userId);
// ✅ 正确
@Select("SELECT id, order_no, amount, status, create_time " +
"FROM `order` WHERE user_id = #{userId}")
List<OrderVO> selectByUserId(Long userId);
实践2:用DTO/VO对象
// 订单列表VO(只包含需要的字段)
@Data
public class OrderListVO {
private Long id;
private String orderNo;
private BigDecimal amount;
private Integer status;
private Date createTime;
// 不包含remark和logistics_info
}
// Mapper
@Select("SELECT id, order_no, amount, status, create_time " +
"FROM `order` WHERE user_id = #{userId}")
List<OrderListVO> listOrders(Long userId);
实践3:利用覆盖索引
-- 创建覆盖索引
CREATE INDEX idx_user_cover ON `order`(user_id, order_no, amount, status, create_time);
-- 查询(覆盖索引,不回表)
SELECT user_id, order_no, amount, status, create_time
FROM `order`
WHERE user_id = 10086;
EXPLAIN:
Extra: Using index ← 覆盖索引
实践4:分离大字段
-- 方案1:拆表(主表+扩展表)
-- 主表(查询频繁)
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32),
user_id BIGINT,
amount DECIMAL(10, 2),
status TINYINT,
create_time DATETIME
);
-- 扩展表(查询少)
CREATE TABLE order_ext (
order_id BIGINT PRIMARY KEY,
remark TEXT,
logistics_info JSON,
FOREIGN KEY (order_id) REFERENCES `order`(id)
);
-- 列表查询(只查主表)
SELECT * FROM `order` WHERE user_id = 10086; -- 这里的SELECT * 可以接受
-- 详情查询(JOIN扩展表)
SELECT o.*, e.remark, e.logistics_info
FROM `order` o
LEFT JOIN order_ext e ON o.id = e.order_id
WHERE o.id = 100001;
实践5:接口分层
// 列表接口(只查必要字段)
@GetMapping("/order/list")
public Result listOrders(Long userId) {
List<OrderListVO> orders = orderMapper.listOrders(userId);
return Result.ok(orders);
}
// 详情接口(查所有字段)
@GetMapping("/order/detail/{id}")
public Result getOrderDetail(@PathVariable Long id) {
OrderDetailVO order = orderMapper.getOrderDetail(id);
return Result.ok(order);
}
📊 性能测试对比
测试环境:
- 100万订单数据
- 每条订单remark平均2KB,logistics_info平均1KB
测试1:查询100条订单
-- SELECT *
SELECT * FROM `order` WHERE user_id = 10086 LIMIT 100;
-- 只查需要的字段
SELECT id, order_no, amount, status, create_time
FROM `order` WHERE user_id = 10086 LIMIT 100;
结果对比:
| 方式 | 执行时间 | 数据传输量 | 提升 |
|---|---|---|---|
| SELECT * | 125ms | 300KB | - |
| 指定字段 | 8ms | 6KB | 15倍 |
测试2:覆盖索引的影响
-- 创建索引
CREATE INDEX idx_user_status ON `order`(user_id, status, order_no, amount);
-- SELECT *(需要回表)
SELECT * FROM `order` WHERE user_id = 10086;
-- 执行时间:45ms
-- Extra: Using index condition
-- 只查索引列(覆盖索引)
SELECT user_id, status, order_no, amount FROM `order` WHERE user_id = 10086;
-- 执行时间:1.2ms
-- Extra: Using index
-- 性能提升:37倍
测试3:网络传输的影响
测试条件:
- 服务器到数据库:千兆网络(理论带宽125MB/s)
- 查询1000条订单
SELECT *:
传输数据量:3KB × 1000 = 3MB
网络传输时间:3MB / 125MB/s = 24ms
MySQL执行时间:15ms
总时间:39ms
只查需要字段:
传输数据量:60B × 1000 = 60KB
网络传输时间:60KB / 125MB/s = 0.5ms
MySQL执行时间:15ms
总时间:15.5ms
性能提升:2.5倍
阿西噶阿西:"网络传输占比越大,SELECT * 的性能损失越严重!"
🎓 面试标准答案
题目:为什么不推荐SELECT *?
答案:
5个性能陷阱:
-
网络传输浪费
- 查询不需要的列,浪费带宽
- 大字段(TEXT、JSON)影响尤其大
- 性能下降10-50倍
-
无法使用覆盖索引
- SELECT * 需要所有列,必须回表
- 覆盖索引能减少90%的IO
-
增加字段后程序出错
- 字段顺序变化,数组下标取值出错
- 维护性差
-
内存占用高
- 查询大字段,对象体积大
- GC频繁,接口响应慢
-
磁盘IO浪费
- 读取不需要的列,浪费IO
- 大字段影响尤其大
最佳实践:
- 明确指定需要的字段
- 利用覆盖索引
- 大字段拆表
- 列表和详情接口分离
🎉 结束语
晚上8点,哈吉米把所有SELECT * 都改成了明确字段。
哈吉米:"接口响应时间从2秒降到50ms了!"
南北绿豆:"对,SELECT * 看起来方便,但隐藏了很多性能问题。"
阿西噶阿西:"特别是加了大字段后,SELECT * 的性能会暴跌。"
哈吉米:"以后我再也不偷懒用SELECT * 了!"
南北绿豆:"对,明确字段不仅性能好,代码可读性也更好。"
记忆口诀:
SELECT星号虽方便,五个陷阱要小心
网络传输浪费多,覆盖索引用不上
增加字段程序错,内存占用GC忙
磁盘IO读无用,明确字段性能强
希望这篇文章能帮你理解为什么不推荐SELECT *!下次写SQL,记得明确指定字段!💪