select * 会拖慢查询,因强制回表读取整行(含大字段),引发临时表、文件排序、缓存命中率下降等问题;在子查询无别名、union、etl、多表join等场景下更会导致报错或数据错乱。

为什么SELECT *会让查询变慢
它不是语法错误,而是执行路径上的“隐形减速带”。MySQL拿到 SELECT * 后,无法判断你真正需要哪些字段,只能默认走最保守的路线:先用索引定位行,再回到聚簇索引把整行数据都读出来(即“回表”)。哪怕 WHERE 条件只用到了 id 和 status,只要 SELECT 里是 *,引擎就不得不把 avatar_url、bio、raw_log 这类大字段也一并捞出来。
常见错误现象包括:Using temporary; Using filesort 频繁出现、Buffer Pool 命中率断崖式下跌、网卡流量突然飙升、应用层反序列化耗时暴涨。这些都不是偶然,而是 SELECT * 在 quietly 拖后腿。
哪些场景下SELECT *会直接失效或报错
它在某些语法结构里根本没法用,不是慢,是“不合法”或“不可控”:
- 子查询无别名时,MySQL 8.0+ 会直接拒绝执行:
SELECT * FROM (SELECT * FROM user) t→ 报错Every derived table must have its own alias -
UNION查询要求两侧字段数、类型、顺序严格一致;一旦某张表新增字段,SELECT * FROM a UNION SELECT * FROM b就中断 - ETL 工具或下游同步任务依赖固定 schema,
SELECT *导致目标表字段错位、DATA_TRUNCATED或导入失败 - ORM 映射时字段名冲突:比如
SELECT u.*, o.* FROM user u JOIN order o ON u.id = o.user_id,两个id字段会被后一个覆盖,业务静默取错值
如何安全替换SELECT *为显式字段列表
关键不是“少选”,而是“按需选 + 索引对齐”。替换动作本身简单,但背后要验证三件事:
- 确认业务逻辑真正用到的字段(翻代码、看 DTO、查日志),例如用户列表页其实只用
id、name、email、status - 检查这些字段是否能被现有索引覆盖;如果 WHERE 条件是
WHERE status = 1,而你只查id和name,那INDEX(status, id, name)就能让整个查询免回表 - 注意 NULL 值和字段顺序:应用层解析时,字段顺序错一位、某个字段突然允许 NULL,都可能让 ORM 反序列化失败
- 对含大字段的表,务必排除
TEXT、BLOB、超长VARCHAR——它们单行 IO 成本可能比其余所有字段加起来还高
示例对比:
-- 反例(全字段 + 无索引支撑) SELECT * FROM orders WHERE user_id = 10001 LIMIT 20; <p>-- 正例(仅必要字段 + 覆盖索引可用) SELECT order_id, amount, status, created_at FROM orders WHERE user_id = 10001 LIMIT 20;</p>
JOIN 查询中SELECT *的风险放大器效应
单表 SELECT * 已经危险,多表 JOIN 下它是“风险乘数”。不只是字段膨胀,更关键的是语义模糊:
- 两个表都有
id、created_at、updated_by,SELECT *返回的列顺序取决于 MySQL 内部实现,不同版本可能不同 - 隐式类型转换:比如
user.id是BIGINT,order.user_id是INT,JOIN 时 MySQL 自动转,可能绕过索引 - 即使加了表别名,如
SELECT u.*, o.order_no,一旦user表新增avatar_url TEXT,这个字段就会被无差别拉进结果集,前端渲染或导出时直接 OOM
正确做法是彻底放弃 *,明确写出每个字段并带上别名:SELECT u.id AS user_id, u.name, o.order_no, o.amount。这不是啰嗦,是把字段契约从“数据库当前状态”变成“代码可验证的接口定义”。
最容易被忽略的一点:字段列表不是写完就完事。当表结构变更(尤其是新增大字段或调整字段顺序)时,所有用到该表的 SELECT 语句都得重新核对——因为显式字段列表的脆弱性,恰恰是它强制你保持关注的代价。










