← 返回题目列表

MySQL 中 SELECT * 有什么问题?如何优化?

高频 中等 第 12 / 28 题 更新于 2026/07/29
MySQLSELECT覆盖索引SQL 优化

简化版

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 TEXTsnapshot JSONaddress 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 * 调试或主键单行详情查询可以接受,但高频列表要谨慎。
  • 误区:覆盖索引就是把所有字段都放进索引。 覆盖索引应服务高频查询,过宽索引会增加写入和存储成本。
  • 追问:怎么判断是否回表?EXPLAINExtra 是否有 Using index,并结合执行计划和查询列判断。
  • 追问:大字段怎么处理? 列表查询不要读取大字段,必要时拆表或详情页按主键单独查。

八、加强记忆

SELECT * 优化题抓住四个代价:回表、IO、网络、稳定性。高频查询只拿需要的列,能覆盖就覆盖;大字段不要跟列表查询绑在一起。它不是语法禁忌,而是性能和契约意识的体现。