为什么避免使用 SELECT * 以及最佳实践

为什么避免使用 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 *

  1. 快速查询存在缓存中:MySQL 结果集的缓存(注意:8.0 已废弃 Query Cache)
  2. ORM 框架的通用查询:如 Hibernate 的 refresh 操作
  3. 数据量很小的配置/字典表(行数 < 100,字段 < 10)
  4. 一次性开发调试:但不能上线

最佳实践

// 推荐:明确字段列表
@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
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容