关注我们,设为星标,每天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_time,users表也有create_time。
当你SELECT *时,结果集中会出现两个create_time。应用层(如 MyBatis 或 Map)在映射时,很可能出现“后来居上”覆盖的情况,导致你明明想取订单时间,却取到了用户注册时间。
4. 核心影响四:安全隐患 (Data Exposure)
这是最容易被忽视的。
场景:用户表users包含password_hash,salt,mobile,id_card。
前端需求:“给我展示用户昵称”。
后端偷懒:return dao.selectUserByWait(id); // 执行了 SELECT *
结果:后端把包含密码哈希、身份证号的整个 User 对象序列化成 JSON 扔给了前端。
虽然前端页面上没展示,但黑客只要按 F12 看一下 Network 请求,所有敏感信息一览无余。这是极严重的数据泄露。
5. 什么时候可以使用 SELECT * ?
当然,SELECT *不是绝对禁忌,以下场景可以适度使用:
1.临时运维查询:你手动在 Navicat / 命令行里查问题,为了看全貌,当然用
*。2.应用层缓存预热:确实需要把整行对象存入 Redis,且表结构相对稳定。
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;}