doctrine缓存需分清metadata_cache(实体结构)、query_cache(dql编译计划)、result_cache(查询结果),仅配query_cache无法缓存结果;联合索引须等值字段在左、范围/排序字段在右;n+1、collection过滤、querybuilder参数绑定不当均致性能下降。

Doctrine 查询缓存没生效?先确认三类缓存是否分清
Doctrine 的缓存不是“开个开关”就提速,而是必须区分 metadata_cache、query_cache 和 result_cache 三类。生产环境不配全,等于白配。
-
metadata_cache缓存实体结构(比如User类的字段映射),推荐用apcu或redis;开发环境可关,避免改注解后不刷新 -
query_cache缓存 DQL 编译后的 SQL(即“查询计划”),对固定参数的 DQL 有效;但含:status这类占位符的查询,每次参数不同都会生成新缓存项,收益有限 -
result_cache真正跳过数据库执行,缓存结果集;只适合读多写少、时效性要求低的场景(如地区列表),且必须手动调用->useResultCache(true, 3600)
常见错误:在 doctrine.yaml 里只配了 query_cache,却指望它缓存查询结果——它根本不存数据,只存“怎么查”。
WHERE + ORDER BY 字段没走索引?联合索引顺序不能错
Symfony 项目里慢查询常出在 Doctrine DQL 转 SQL 后,EXPLAIN ANALYZE 显示 type=ALL(全表扫描)。核心问题往往不是没建索引,而是联合索引字段顺序和查询条件不匹配。
- 例如 DQL:
SELECT u FROM App\Entity\User u WHERE u.status = :status AND u.createdAt > :date ORDER BY u.createdAt DESC - 对应索引必须是
INDEX idx_user_status_createdat (status, createdAt),而不是反过来——等值条件status必须放最左,范围条件createdAt放右,排序字段才能复用 - 如果还加了
JOIN,比如查用户+订单,orders表的user_id和status也要单独建索引,否则JOIN会退化成嵌套循环
别信“建了索引就万事大吉”,EXPLAIN 输出里的 key_len 值能告诉你实际用了索引的前几个字节——如果比预期小,说明索引没被完全利用。
EntityRepository 里手写 DQL,一不留神就触发 N+1
很多人以为用 Repository 写 DQL 就安全了,其实不然。典型陷阱是:在循环里反复调用一个返回单条 Entity 的方法,而该方法内部又触发了关联加载。
- 错误写法:
foreach ($users as $user) { $user->getOrders()->count(); }—— 即使getOrders()是懒加载,每调一次都发一条 SQL - 正确做法:要么在初始查询时用
->addSelect('o')->leftJoin('u.orders', 'o')预加载;要么改用原生 SQL 或 QueryBuilder 批量查出所需聚合值(如COUNT(*)) - 更隐蔽的坑:
findBy(['status' => 'active'])返回 Collection,但后续链式调用->filter(...)->map(...)会在 PHP 层过滤,本该由数据库做的筛选被搬到了内存里
Doctrine 的 Collection 不是数组,它的 filter() 不下推到 SQL,纯属 PHP 运算——数据量一大,内存和 CPU 都扛不住。
Query Builder 拼接条件时,bindParam 位置决定性能
用 QueryBuilder 动态拼条件很常见,但参数绑定时机不对,会导致 MySQL 无法复用执行计划,缓存失效。
- 错误写法:每次循环都新建
QueryBuilder实例,然后$qb->setParameter()—— 这会生成不同 SQL 文本(哪怕参数值相同),query_cache完全无效 - 正确做法:复用同一个
QueryBuilder实例,在循环外 prepare,循环内只setParameter()并execute();或者改用原生 PDO 预处理语句,显式控制prepare和execute分离 - 特别注意
IN查询:Doctrine 默认把数组展开成id IN (1,2,3,...),参数太多会超 MySQLmax_allowed_packet;应改用setParameter('ids', $ids, Connection::PARAM_STR_ARRAY)让 Doctrine 自动分批
缓存和执行计划依赖 SQL 文本一致性,动态拼字符串(如 "WHERE status = '$status'")不仅危险,还会让所有缓存形同虚设。











