select *一查就慢是因为innodb对超768字节的text/blob启用溢出页存储,主记录仅存20字节指针,导致每次查询需多次随机i/o读取溢出页,并污染buffer pool、引发oom或缓存命中率骤降。

为什么SELECT *一查就慢
因为InnoDB对超过768字节的TEXT或BLOB字段启用溢出页存储,主记录只存20字节指针。执行SELECT *时,MySQL必须先读主页,再跳转读一个或多个溢出页——哪怕只查1行,也可能触发数次随机I/O。SSD上延迟不明显,但Buffer Pool会被大量无效数据污染,SHOW ENGINE INNODB STATUS里Buffer pool hit rate会骤降。
常见错误现象:
-
EXPLAIN显示type=ALL且rows很大,但Extra没写Using index - 慢查询日志中
Query_time动辄秒级,Rows_examined远大于Rows_sent - ORM自动映射全字段时,客户端内存被撑爆(尤其JSON/HTML类文本)
绕开大字段的三种硬招
目标不是“加速读大字段”,而是让执行计划完全不触碰溢出页。以下写法在MySQL 5.7+和8.0均生效,无需改应用代码:
- 永远不用
SELECT *,显式列出非大字段:SELECT id, title, created_at FROM article - 对需要条件过滤但不读内容的场景,加虚拟列哈希:
ALTER TABLE article ADD COLUMN content_md5 CHAR(32) AS (MD5(content)) STORED,再建索引CREATE INDEX idx_content_md5 ON article(content_md5) - 用
SELECT ... INTO DUMPFILE或应用层流式读取超大BLOB,避免一次性载入内存;必要时调大max_allowed_packet(如SET SESSION max_allowed_packet = 268435456)
前缀索引怎么设才有效
前缀索引只对WHERE content LIKE 'xxx%'有效,对LIKE '%xxx%'或SELECT content毫无帮助。关键不是“设多长”,而是让前缀具备足够区分度。
实操步骤:
- 别按“字符数”设长度——UTF8MB4下
content(50)最多存12个汉字(每个4字节),要用LENGTH()而非CHAR_LENGTH()统计 - 先看分布:
SELECT LEFT(content, 200) AS p, COUNT(*) FROM articles GROUP BY p ORDER BY COUNT(*) DESC LIMIT 10 - 再算选择性:
SELECT COUNT(DISTINCT LEFT(content, 80)) / COUNT(*) AS sel_80 FROM articles,对比sel_50、sel_100,找提升率明显放缓的拐点 - 最终长度建议比拐点多留10~20字节缓冲,比如
sel_80=0.92、sel_100=0.94,就用content(120)
ORDER BY或GROUP BY TEXT为什么会OOM
MySQL的MEMORY引擎根本不支持TEXT/BLOB类型。只要语句里出现ORDER BY content或GROUP BY SUBSTRING(content, 1, 50),优化器就会放弃内存临时表,强制创建磁盘临时表——哪怕你把tmp_table_size设到2G也一样落盘。
真正可行的解法是把排序/分组依据物化成独立列:
-
ALTER TABLE article ADD content_updated_at DATETIME NOT NULL DEFAULT '1970-01-01',应用层同步更新,然后ORDER BY content_updated_at -
ALTER TABLE article ADD content_hash CHAR(32) AS (MD5(content)) STORED,建索引后用于等值过滤 - 绝对避免
ORDER BY UPPER(content)或GROUP BY JSON_EXTRACT(content, '$.title')——这类表达式会让优化器彻底放弃内存路径
最易被忽略的是:字段类型为TEXT不等于一定用了溢出页,得结合ROW_FORMAT(DYNAMIC或COMPACT)和真实长度(SELECT LENGTH(content) FROM article WHERE id = 123)交叉验证。盲目加索引或压缩,反而可能放大IO压力。











