为什么避免使用 SELECT * 以及最佳实践
SELECT * 的问题
-- 不推荐
SELECT * FROM user WHERE id = 123;
-- 推荐
SELECT id, name, email FROM user WHERE id = 123;
很多开发人员习惯 SELECT *,觉得写起来方便,但它在生产环境中会带来一系列性能问题。
问题一:索引覆盖失效
原理
MySQL 的 InnoDB 引擎在二级索引的叶子节点中只存储索引列和主键值。如果查询的字段都在索引中,MySQL 可以直接从索引获取结果,无需回表。
-- user表有联合索引:idx_name_email(name, email)
-- SELECT *:无法使用索引覆盖,需要回表
-- 执行流程:
-- 1. 走 idx_name_email 找到主键 id
-- 2. 根据 id 回主键索引取所有字段(name, email, phone, avatar, ...)
EXPLAIN SELECT * FROM user WHERE name = '张三';
-- Extra: Using index condition (只用到索引条件,还需要回表)
-- 只查索引列:可以使用索引覆盖
-- 执行流程:
-- 1. 走 idx_name_email,直接返回 name 和 email
-- 2. 不需要回表
EXPLAIN SELECT name, email FROM user WHERE name = '张三';
-- Extra: Using index (索引覆盖,无需回表)
性能差异:
– 覆盖索引查询:只访问索引页,不需要访问数据页,IO 次数少
– 回表查询:先访问索引页找到主键,再访问数据页,最多可能需要 2 次 IO
– 在千万级数据量的表中,避免回表能减少 50% 以上的 IO 开销
问题二:增加网络传输开销
-- user 表有 30 个字段,包括头像 URL、简介等长字段
SELECT * FROM user LIMIT 1000;
-- 传输数据量:1000 行 × 约 2KB = 2MB
SELECT id, name, email FROM user LIMIT 1000;
-- 传输数据量:1000 行 × 约 150B = 150KB
网络传输量的差异在大量查询时非常显著。对于同时有数千并发请求的系统,每一毫秒的网络 IO 都很宝贵。
问题三:增加数据库的 IO 和内存开销
IO 开销:
– 需要从磁盘读取更多数据页
– 即使走索引,回表 IO 也翻倍
Buffer Pool 压力:
– SELECT * 会将不需要的大字段(TEXT、BLOB等)也加载到 buffer pool
– 这些无用数据会”污染” buffer pool,淘汰掉真正需要的热点数据
-- 一个包含 TEXT 字段的表
CREATE TABLE article (
id INT PRIMARY KEY,
title VARCHAR(200),
summary VARCHAR(500),
content TEXT, -- 最多 64KB
cover_url VARCHAR(500),
created_at DATETIME
);
-- SELECT * 会将 content 也加载到内存
-- 每次查询都可能导致大页数据进入 buffer pool
-- buffer pool 中充斥着实际上用不上的 content 数据
内存浪费示例:
buffer pool 大小:8 GB
每次 SELECT * 从 article 表读取:1000 行
每行包含 content(平均 10KB)
单次查询消耗 buffer pool:10MB
如果把 content 换成大图 base64,情况更糟
问题四:无法识别依赖
在代码中,SELECT * 隐藏了数据依赖关系:
// 假设 OrderService.getOrderList() 使用 SELECT *
// 调用方只关心 order_no 和 amount
// 如果 DBA 要修改表结构(如删除一个废字段 address)
// SELECT * 的查询不受影响(列减少)
// 但应用程序如果引用了 address 字段...
// 重构时:
// - 不知道哪些地方依赖了 address 列
// - 无法做最小化数据返回的优化
// - 代码审查时看不出数据依赖
问题五:不支持 MySQL 的索引提示
-- SELECT * 无法利用"只查索引"的优化
SELECT * FROM user USE INDEX(idx_email) WHERE email = 'test@example.com';
-- MySQL Server 层需要索引的所有字段,但索引只覆盖了 id+email
-- 还是得回表
什么情况下可以使用 SELECT *
- 快速查询存在缓存中:MySQL 结果集的缓存(注意:8.0 已废弃 Query Cache)
- ORM 框架的通用查询:如 Hibernate 的 refresh 操作
- 数据量很小的配置/字典表(行数 < 100,字段 < 10)
- 一次性开发调试:但不能上线
最佳实践
// 推荐:明确字段列表
@Select("SELECT id, name, email, phone, status FROM user WHERE id = #{id}")
User findUserById(@Param("id") Long id);
// 按需创建 DTO,而不是复用 Entity
public class UserDTO { // 只包含需要返回的字段
private Long id;
private String name;
private String email;
}
-- 推荐:按需查询字段
SELECT id, order_no, amount, status, create_time
FROM order
WHERE user_id = 123;
面试要点
- SELECT * 的核心问题:打破覆盖索引、增加网络/IO/内存开销、隐藏依赖
- 能用索引覆盖就用索引覆盖——需要”覆盖索引”意识
- 面试中可以举一个具体的性能对比案例
- 知识扩展:理解 MySQL Server 层和引擎层是如何交互处理 SELECT * 的
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容