MySQL 中 SELECT * 有什么问题?如何优化?
简化版
SELECT * 会读取不必要字段,增加 IO、网络传输、内存和回表成本,还容易破坏覆盖索引;优化时应按需选择列,并让高频查询尽量被合适索引覆盖。
详细版
SELECT * 最大的问题不是语法错误,而是它让数据库、网络和应用都承担了不需要的数据成本。表字段越多、长文本字段越多、返回行数越大,问题越明显。
在 InnoDB 中,二级索引查询如果只需要索引中的字段,就可以使用覆盖索引;一旦写 SELECT *,数据库通常需要回表读取整行。比如 (user_id, status, created_at) 索引可以覆盖 user_id/status/created_at 查询,但 SELECT * 可能还要读取 remark/content/address 等列。
工程上应明确返回字段,接口层也不要把数据库实体直接全量暴露。面试回答时可以从覆盖索引、回表、网络传输、长字段、字段变更风险几个角度展开。
完整版教学
一、SELECT * 的成本从哪里来
SELECT * 表示返回表中所有列。它看起来省事,但会让查询读取很多业务并不需要的数据。
SELECT *
FROM orders
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;
如果订单表有 30 个字段,其中包含 remark TEXT、snapshot JSON、address VARCHAR(500),列表页可能只展示订单号、金额、状态和创建时间,却把整行都取出来了。
记忆钩子:
SELECT *省的是键盘,花的是数据库、网络和应用的成本。
二、它会破坏覆盖索引
覆盖索引指查询需要的列都在同一个索引里,数据库不用回表。
CREATE INDEX idx_user_status_time
ON orders(user_id, status, created_at);
SELECT user_id, status, created_at
FROM orders
WHERE user_id = 100 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
上面查询可能只扫描二级索引就返回结果。如果改成:
SELECT *
FROM orders
WHERE user_id = 100 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
二级索引里没有整行所有字段,数据库需要拿主键回聚簇索引取完整记录。返回 20 行问题不大,返回 20000 行时回表成本就会明显放大。
三、长字段会放大 IO 和网络
很多表把列表字段和详情字段放在一起。SELECT * 会把详情字段也查出来。
示例:
普通字段:id、status、amount、created_at,共 80 字节左右
详情字段:snapshot JSON,平均 8KB
一次列表返回:100 行
如果只查普通字段,数据量约 80 * 100 = 8000 字节;如果 SELECT * 带出 8KB JSON,可能接近 8KB * 100 = 800KB。网络传输、反序列化、应用内存都会增加。
| 查询方式 | 返回列 | 主要成本 |
|---|---|---|
| 按需列 | 列表所需字段 | 更小、更稳定 |
SELECT * | 所有字段 | IO、网络、回表都可能增加 |
带大字段 * | 长文本/JSON/Blob | 成本放大明显 |
四、字段变更也会影响接口稳定性
SELECT * 还会带来维护风险。表新增字段后,查询结果列顺序和列数量变化,某些老代码可能依赖列位置解析,容易出问题。
-- 风险较高:代码可能默认第 5 列是什么
SELECT * FROM users WHERE id = 1;
-- 更清晰:返回列稳定
SELECT id, name, email FROM users WHERE id = 1;
虽然现代 ORM 通常按字段名映射,但跨服务、报表脚本、导出任务中仍可能出现列位置依赖。明确列清单能让接口契约更稳定。
这类风险不是性能优化工具能完全发现的,属于 SQL 编写习惯和工程边界问题。
五、什么时候 SELECT * 可以接受
不是所有 SELECT * 都必须禁止。比如本地调试、临时排查、后台详情页根据主键查单行,成本通常可控。
SELECT *
FROM orders
WHERE id = 100;
如果按主键查 1 行,并且确实需要展示全部字段,SELECT * 的问题不大。真正需要避免的是高频接口、列表接口、批量导出、复杂连接和二级索引过滤场景中的无脑全列查询。
判断标准可以是:
是否高频?
是否返回多行?
是否包含大字段?
是否破坏覆盖索引?
是否跨服务作为稳定契约?
六、优化方法怎么落地
优化 SELECT * 的第一步是收敛返回列。
SELECT id, order_no, amount, status, created_at
FROM orders
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;
第二步是结合查询模式设计覆盖索引:
CREATE INDEX idx_orders_user_time_cover
ON orders(user_id, created_at, id, order_no, amount, status);
索引不是越宽越好,覆盖字段要围绕高频查询选择。如果为了覆盖一个低频查询把 10 个大字段塞进索引,写入成本和索引体积也会明显上升。
七、常见误区与追问
- 误区:
SELECT *只是返回字段多一点,没有性能影响。 它可能增加 IO、回表、网络和应用内存成本。 - 误区:有索引就不怕
SELECT *。 二级索引只能覆盖部分列,SELECT *往往导致回表。 - 误区:所有场景都绝对不能用
SELECT *。 调试或主键单行详情查询可以接受,但高频列表要谨慎。 - 误区:覆盖索引就是把所有字段都放进索引。 覆盖索引应服务高频查询,过宽索引会增加写入和存储成本。
- 追问:怎么判断是否回表? 看
EXPLAIN的Extra是否有Using index,并结合执行计划和查询列判断。 - 追问:大字段怎么处理? 列表查询不要读取大字段,必要时拆表或详情页按主键单独查。
八、加强记忆
SELECT * 优化题抓住四个代价:回表、IO、网络、稳定性。高频查询只拿需要的列,能覆盖就覆盖;大字段不要跟列表查询绑在一起。它不是语法禁忌,而是性能和契约意识的体现。