判断 MySQL 查询是否命中查询缓存,最可靠的依据就是 Qcache_hits 是否持续增长;只有字节级完全一致的 SQL 语句才可能命中缓存;一旦包含函数、变量、注释等内容通常就不会进入缓存;另外在 MySQL 8.0 及以上版本中,查询缓存功能已经被官方移除。

直接看 Qcache_hits 是否增长
MySQL 查询缓存命中后既不会报错,也通常不会单独记录日志,因此判断是否命中缓存的唯一可靠方法,就是观察 Qcache_hits 这个状态值是否随着查询次数增加。执行一次 SELECT 之后,立即运行:
SHOW GLOBAL STATUS LIKE 'Qcache_hits';
对比执行前后的数值变化——该值只会增加不会减少,只有当增量与你实际执行的 SELECT 次数相匹配时,才能说明这次查询真正命中了缓存。
- 如果
Qcache_hits没有变化,但Qcache_inserts增加了:表示该查询结果被写入缓存了,但后续查询没有命中,通常是因为 SQL 文本存在细微差异 - 如果
Qcache_not_cached增加了:说明该查询被系统直接跳过缓存,比如包含NOW()、RAND()、@var等内容 - 如果
Qcache_lowmem_prunes快速上升:说明缓存条目刚写入就被内存压力挤出,并不是没有命中,而是缓存空间留不住数据
SQL文本必须字节级完全一致
MySQL 查询缓存不会对 SQL 语句做语义解析,而是直接对原始 SQL 字符串进行哈希计算。也就是说,即使只是多了一个空格、换了一行,或者 SQL 大小写不同,也会被视为一条新的查询语句。
- ✅ 命中:
SELECT id FROM users WHERE id = 1;→ 再执行完全相同的一模一样语句 - ❌ 不命中:
select id from users where id=1;(小写且无空格)、SELECT id FROM users WHERE id = 1 -- comment;(添加注释)、SELECT id, name FROM users...(字段数量或顺序变化) - ORM 场景尤其需要注意:例如 Django 的
.filter().order_by()和.order_by().filter()生成出来的 SQL 字符串并不一致,因此对应的缓存键天然不同
确认缓存没被自动绕过
很多人排查 MySQL 缓存命中率时会忽略一个关键点:有些查询根本不会进入查询缓存流程。也就是说,不是“没有命中缓存”,而是这类 SQL 从一开始就没有参与缓存判断。
- 包含以下任一成分的
SELECT通常都不会被缓存:NOW()、CURRENT_DATE()、RAND()、USER()、CONNECTION_ID()、LAST_INSERT_ID() - 涉及临时表、用户变量(
@var := 1)、子查询(尤其是相关子查询)、FOR UPDATE或LOCK IN SHARE MODE的语句,也会被自动绕过缓存 - 如果结果集超过
query_cache_limit(默认 1MB),或者单条返回结果过长,同样会被静默跳过,不写缓存也不报错 - 当
query_cache_type = DEMAND时,还必须显式加上SQL_CACHE提示,否则默认一律不进入查询缓存
MySQL 8.0+ 用户请停止检查 Qcache
在 MySQL 8.0 及以上版本中,查询缓存模块已经被彻底移除,因此 SHOW VARIABLES LIKE 'query_cache%' 和 SHOW STATUS LIKE 'Qcache%' 这两条命令都会返回空结果。很多人看到“Qcache 命中率低”就以为是缓存配置有问题,实际上这往往是误判,真正更值得关注的是 innodb_buffer_pool_read_requests 与 innodb_buffer_pool_reads 的比值。
更常见的复杂情况是:不少人仍在使用旧版监控脚本持续轮询 Qcache_hits,结果数值始终为 0,于是误以为 MySQL 查询缓存配置异常,但真实原因其实是数据库版本已经不再支持该功能。最稳妥的做法是先执行 SELECT VERSION();,确认 MySQL 版本后,再决定应该检查哪一组性能指标。
