findall() 慢是因为默认全表扫描并全量 hydrate,每行都创建完整实体、触发生命周期事件、启用变更跟踪;应按需改用 querybuilder、findby() 或分页查询。

findAll() 慢不是方法本身的问题,而是它默认不做任何优化,直接全表扫描 + 全量 hydrate —— 你查多少行,Doctrine 就加载多少行实体,每行都走完整生命周期(包括关联代理初始化、变更跟踪、事件触发等)。
findAll() 为什么会触发全量实体加载
Doctrine 的 findAll() 底层调用的是 SELECT * FROM table,然后把每一行结果映射成完整实体对象。哪怕你只想要 ID 和 name,它也会:
- 加载所有字段(包括大文本、JSON、二进制字段)
- 为每个实体创建代理对象(即使没访问关联)
- 启用 UnitOfWork 跟踪(准备后续 flush)
- 触发 preLoad/postLoad 事件(如果注册了)
- 如果你的表有 10 万行,
findAll()就会实例化 10 万个 Entity 对象,内存占用和 GC 压力陡增 - 哪怕实体只有 3 个字段,Doctrine 仍会为每个对象分配完整内存结构 + 元数据引用
- 若实体含
@ORM\ManyToOne关系,默认 LAZY,但 Collection 初始化时仍会挂载代理——数量一大,PHP 对象图就失控
替代方案:什么时候该用 find() / findBy() / QueryBuilder
绝大多数场景下,findAll() 都不该出现在生产代码里。真实需求通常更具体:
-
只需要部分字段? 用
QueryBuilder显式select('u.id', 'u.name'),返回关联数组或 DTO,跳过实体构建 -
要分页? 别先
findAll()再array_slice()—— 用Query::setFirstResult()+setMaxResults()让数据库做 limit/offset -
要按条件过滤?
findBy(['status' => 'active'])比findAll()+ PHP 循环快得多,它生成 WHERE SQL 并复用缓存键 -
要关联预加载?
findAll()完全不支持 JOIN,必须用QueryBuilder或自定义 Repository 方法显式leftJoin()
性能对比:findAll() vs 手动 QueryBuilder
假设 User 表有 5000 行,含 profile 关联(LAZY):
-
$users = $repo->findAll();→ 5000 次实体构造 + 5000 个代理对象 + 内存峰值 >200MB -
$users = $qb->select('u.id', 'u.email')->from(User::class, 'u')->getQuery()->getScalarResult();→ 返回纯数组,内存
注意:getScalarResult() 和 getSingleScalarResult() 是绕过实体化的关键出口;getResult() 若传 HYDRATE_ARRAY 也能降负载,但不如 scalar 直接。
容易被忽略的陷阱:dev 环境掩盖问题
Symfony dev 模式下,debug 开启、缓存关闭、Profiler 注入大量钩子,findAll() 的慢会被进一步放大,但上线后可能更糟——因为生产环境没 Profiler 拦着,OOM 杀手可能直接干掉进程。
真正要警惕的不是“为什么慢”,而是“为什么还在用”。只要业务逻辑不强制要求操作完整实体对象,就别碰 findAll()。











