Symfony3.4中Doctrine2查询性能异常:耗时远超MySQL控制台
这种场景我帮很多Symfony/Doctrine开发者排查过,核心问题在于Doctrine执行查询的链路比直接在MySQL控制台跑SQL要复杂得多,中间多了很多额外步骤。下面是最常见的几个原因和对应的排查方向:
Doctrine默认会把查询结果转换成对应的实体对象,这个过程涉及反射、对象实例化、关联关系代理初始化(哪怕是延迟加载,也会生成代理对象),这些操作都会带来不小的性能损耗。而MySQL控制台返回的是原始的行数据,完全没有这一步的开销。
- 快速验证:把查询的
getResult()换成getArrayResult(),看看耗时是否大幅下降。如果是的话,Hydration就是主要瓶颈。 - 优化方案:
- 只查询实际需要的字段,比如用
$query->select('u.id', 'u.email')代替查询整个实体 - 调整关联关系的
fetch模式,按需使用EAGER或LAZY,避免不必要的代理对象创建 - 只读场景下,使用
@ReadOnly注解或者自定义轻量Hydrator
- 只查询实际需要的字段,比如用
Doctrine的数据库连接配置可能和你MySQL控制台用的不一样,这会导致MySQL选择不同的执行计划,进而影响耗时:
- 常见差异:字符集/排序规则不一致(比如控制台用
utf8mb4,Doctrine用utf8),会导致索引失效;sql_mode设置不同,某些模式下MySQL会放弃使用索引 - 排查方法:在Doctrine中执行
SHOW VARIABLES LIKE '%character_set%'和SHOW VARIABLES LIKE '%sql_mode%',和控制台的结果对比。如果有差异,调整config.yml中的Doctrine连接配置,比如:doctrine: dbal: connections: default: charset: utf8mb4 default_table_collate: utf8mb4_unicode_ci options: 1002: "SET sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'"
Doctrine每次执行DQL时,需要先解析DQL生成SQL,再生成执行计划。如果没开启查询缓存,这个解析过程会重复执行,复杂查询下开销很大;另外结果缓存如果没开,每次都会重新从数据库拉取数据。而控制台执行的是直接的SQL,不存在DQL解析的步骤。
- 排查方法:检查
config.yml中的缓存配置,确保开启了查询缓存和结果缓存:
同时要确保缓存池(比如APCu、Redis)已经正确配置并启用。doctrine: orm: query_cache_driver: type: pool pool: doctrine.system_cache_pool result_cache_driver: type: pool pool: doctrine.result_cache_pool
如果你的查询涉及关联实体,Doctrine可能在Hydrate实体时触发大量额外的小查询。你看到的主查询耗时只有115ms,但加上后续N次关联查询,总耗时就会飙升到1600+ms。
- 排查方法:开启Doctrine的SQL日志,查看所有执行的SQL语句。在Symfony dev环境下,可以直接看工具栏的Doctrine面板;或者配置日志文件:
monolog: handlers: doctrine_sql: type: stream path: "%kernel.logs_dir%/doctrine_sql.log" level: debug channels: [doctrine] - 解决方法:用
JOIN FETCH提前加载关联实体,比如:SELECT u, p FROM AppBundle:User u JOIN FETCH u.profile p WHERE u.status = :status
如果你的实体有@PreLoad、@PostLoad等生命周期回调方法,每次Hydrate实体时都会执行这些方法。如果回调里有复杂逻辑(比如数据转换、外部调用),累积起来的开销会非常可观。
- 排查方法:暂时注释掉所有生命周期回调,重新测试查询耗时。如果耗时明显下降,就逐个排查回调逻辑,优化或移除不必要的操作。
如果你的MySQL版本低于8.0,控制台执行查询时可能命中了MySQL自带的查询缓存,而Doctrine的连接可能禁用了这个缓存(或者缓存键不同),导致看起来耗时差异很大。
- 验证方法:在控制台执行
SELECT SQL_NO_CACHE ...再测耗时,看看是否和Doctrine的耗时接近。如果是的话,建议开启Doctrine的结果缓存来替代MySQL的查询缓存(毕竟MySQL 8.0已经移除了这个功能)。
内容的提问来源于stack exchange,提问作者Lasha Kitia

