关注我们,设为星标,每天7:30不见不散,每日java干货分享

场景还原:
周五下午,线上系统突然报警,接口响应时间从 20ms 飙升到 2s。
DBA 排查发现,数据库网卡流量被打满,且 Buffer Pool 命中率急剧下降。

原因: 有个新发布的这个功能,查询用户详情时用了 SELECT *。原本表里只有几个字段,前天为了存用户头像,新加了一个 avatar_base64 的大字段(TEXT)。
结果,每次查询都把这个巨大的字符串拖出来,不仅撑爆了内存,还堵死了网络。


1. 核心影响一:扼杀“覆盖索引” (The Covering Index Killer)

这是 SELECT * 最致命的性能影响,却鲜有人知。

原理:
假设你有一个用户表 users,建立了一个联合索引 idx_name_age (name, age)

  • • 场景: 只需要查用户的年龄。

写法 A (恶习):

SELECT*FROM users WHERE name ='Alice';

过程: MySQL 先去 idx_name_age 索引树找到 'Alice',拿到主键 ID。然后,它必须拿着 ID 去“聚簇索引”(主数据文件)里把整行数据读出来(称为 “回表”),因为你需要 *(包括 address, email 等不在索引里的字段)。

写法 B (最佳):

SELECT age FROM users WHERE name ='Alice';

过程: MySQL 去 idx_name_age 索引树找到 'Alice',发现你只要 age,而 age 就在这个索引树上。直接返回! 不需要回表,不需要读主数据文件。

结论:SELECT * 强制数据库进行“回表”操作,导致大量的随机磁盘 I/O,让原本可以毫秒级完成的“覆盖索引”查询失效。


2. 核心影响二:带宽与内存的无底洞

场景:
你的表里包含 TEXT (文章内容), BLOB (图片), 或者超长的 VARCHAR

  • • DB 内存 (Buffer Pool): 数据库会将读取的数据页缓存到内存。如果你总是 SELECT * 读出大字段,这些冷数据会迅速挤掉热点数据,导致缓存命中率暴跌。

  • • 网络带宽: 假设一行数据 1KB,并发 1000 QPS,就是 1MB/s。如果一行数据因为大字段变成了 10KB,流量瞬间变成 10MB/s,网卡直接报警。

  • • 应用层开销: Java/Go 程序接收到这些无用的大数据后,需要进行反序列化(Unmarshalling),消耗大量的 CPU 和 GC 资源。

实战教训:
很多系统变慢,不是因为 CPU 算不过来,而是因为**“对象太大了”**,GC 停顿(Stop-the-World)太频繁。


3. 核心影响三:架构耦合与代码脆弱性

SELECT * 会让你的代码与数据库 Schema 强耦合

**场景一:INSERT INTO ... SELECT ***

-- 这里的 * 非常危险
INSERT INTO table_archive SELECT*FROM table_current;

如果某天你在 table_current 加了一个字段,但忘了给 table_archive 加。这条 SQL 会直接报错,导致归档任务失败。

场景二:字段顺序依赖
有些老旧的代码(或 CSV 导出逻辑)依赖列的顺序。
String name = resultSet.getString(2); // 假设第二列是 name
如果你在表中第一列前面加了个新字段,SELECT * 返回的列顺序变了,代码逻辑全乱,张三变成了“1001”。

场景三:多表 JOIN 的命名冲突

SELECT*FROM orders 
JOIN users ON orders.user_id = users.id;

orders 表有 create_timeusers 表也有 create_time
当你 SELECT * 时,结果集中会出现两个 create_time。应用层(如 MyBatis 或 Map)在映射时,很可能出现“后来居上”覆盖的情况,导致你明明想取订单时间,却取到了用户注册时间。


4. 核心影响四:安全隐患 (Data Exposure)

这是最容易被忽视的。

场景: 用户表 users 包含 password_hashsaltmobileid_card

前端需求: “给我展示用户昵称”。
后端偷懒:return dao.selectUserByWait(id); // 执行了 SELECT *
结果: 后端把包含密码哈希、身份证号的整个 User 对象序列化成 JSON 扔给了前端。

虽然前端页面上没展示,但黑客只要按 F12 看一下 Network 请求,所有敏感信息一览无余。这是极严重的数据泄露。


5. 什么时候可以使用 SELECT * ?

当然,SELECT * 不是绝对禁忌,以下场景可以适度使用:

  1. 1. 临时运维查询: 你手动在 Navicat / 命令行里查问题,为了看全貌,当然用 *

  2. 2. 应用层缓存预热: 确实需要把整行对象存入 Redis,且表结构相对稳定。

  3. 3. COUNT(*): 这是一个特例。在 MySQL 中 COUNT(*) 被专门优化过(忽略 NULL),通常比 COUNT(id) 还要快或持平,这里的 * 是可以用的


6. 总结

不要为了省去打几个字段名的功夫,而给系统埋下无数的地雷。

  • • 性能上: 它破坏覆盖索引,增加 I/O 负担。

  • • 架构上: 它增加耦合,降低代码健壮性。

  • • 安全上: 它泄露隐私数据。

口诀: 字段按需取,索引能覆盖;大列要避开,流量省一半。

推荐阅读  点击标题可跳转

50个Java代码示例:全面掌握Lambda表达式与Stream API

16 个 Java 代码“痛点”大改造:“一般写法” VS “高级写法”终极对决,看完代码质量飙升!

为什么高级 Java 开发工程师喜爱用策略模式

精选Java代码片段:覆盖10个常见编程场景的更优写法

提升Java代码可靠性:5个异常处理最佳实践

为什么大佬的代码中几乎看不到 if-else,因为他们都用这个...

还在 Service 里疯狂注入其他 Service?你早就该用 Spring 的事件机制了

看完本文有收获?请转发分享给更多人

关注「java干货」加星标,提升java技能

❤️给个「推荐 」,是最大的支持❤️

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐