SELECT * 必然破坏覆盖索引,因其要求所有字段均存在于同一二级索引叶子节点中,而TEXT/BLOB等字段根本不会被任何索引存储,导致必须回表、Extra变为Using where,并引发额外随机I/O。

SELECT * 为什么必然破坏覆盖索引
MySQL 的覆盖索引生效前提是:查询所需所有字段,全部存在于同一个二级索引的叶子节点中。一旦写 SELECT *,哪怕 WHERE 条件能走 INDEX(name, age),只要表里存在 TEXT、BLOB 或任意未被该索引包含的字段(比如 profile_json),优化器就无法满足“全字段在索引中”这一硬性条件,必须回表查聚簇索引——此时 EXPLAIN 的 Extra 字段会从 Using index 变成 Using where,这是最直接的信号。
注意:TEXT/BLOB 类型字段根本不会被任何索引存储,加进联合索引定义里也会被 MySQL 静默忽略。所以不是“索引建得不够多”,而是这类字段天然与覆盖索引互斥。
回表开销不是“可能慢”,而是“一定多一次随机 IO”
辅助索引(如 INDEX(b))只存 b 和主键值;聚簇索引才存整行。用 SELECT * 查 WHERE b = ?,流程是:先查辅助索引得主键 → 再拿主键去聚簇索引捞全部字段。两次 B+ 树查找,且第二次是随机 IO。
- 若字段含
log TEXT,InnoDB 还要额外读溢出页(off-page),IO 次数可能升至三次 - buffer pool 命中率越低(线上常见 60%~80%),磁盘刷得越频繁
- 分页场景如
LIMIT 10000,20,MySQL 仍要构造并丢弃前 10000 行完整记录,CPU 和内存浪费严重
网络和客户端内存开销被严重低估
单行数据包体积由字段数 × 平均宽度决定。SELECT * 很容易把一行拉到几 KB 甚至十几 KB;而显式指定 id, nick_name, avatar_url 可压到 200B 以内。
这不只是“传得慢”:即使 DB 和应用同机部署,依然走 loopback TCP 栈,要经历序列化/反序列化、内核缓冲区拷贝;ORM 如 Hibernate 的 findAll() 会强制加载所有字段进对象属性,哪怕业务只用其中两个。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
典型风险点:
-
INSERT INTO ... SELECT *:源表加个backup_data LONGBLOB,目标表立刻报Column count doesn't match value count - 视图用
SELECT *创建:后续ALTER TABLE ADD COLUMN ... FIRST会改变物理列序,下游按位置取值(如rs.getObject(3))直接错位
哪些情况看似安全,其实更危险
“表小、字段少、就我一个服务连”不是免责理由。真正危险的是隐性耦合:
- 加了个
NOT NULL字段但没设默认值?旧 ORM 映射直接抛异常 - 字段类型从
VARCHAR(255)改成TEXT?某些 JDBC 驱动可能截断或转换失败 - 中间件(如 ShardingSphere)解析
SELECT *时依赖元数据,字段顺序错乱会导致路由错误或数据错位
最易被忽略的一点:索引设计救不了 SELECT *。哪怕你给所有字段建了联合索引,只要表里存在 TEXT 或 BLOB,这个索引就永远无法覆盖——因为它们压根不在索引结构里。










