JOIN FETCH 可能比 N+1 更慢:可复现的 Doctrine 基准测试(每实体 1 行的 JSON 聚合)

发布: (2025年12月18日 GMT+8 13:54)
2 min read
原文: Dev.to

Source: Dev.to

实际问题:多个 OneToMany JOIN 导致行数爆炸

对多个 OneToMany 关联进行 JOIN 会使 SQL 结果集成倍增长(笛卡尔积)。

示例

  • 3 张图片 × 5 条评论 = 每个产品 15 行
  • 对于 2000 个产品 → 约 30,000 行 从数据库传输并处理

Doctrine 的身份映射在 PHP 中隐藏了重复项,但数据库仍然返回了乘积后的行集。这即使在查询次数很少的情况下也会严重影响性能。

链接(包 + 可复现的基准)

可复现的基准(Symfony + PostgreSQL)

我发布了一个独立的 Symfony 基准项目,包含 fixtures 和 CLI 命令,任何人都可以在本地复现结果:

https://github.com/rgalstyan/doctrine-aggregated-queries-benchmark

示例运行(limit = 2000)

注意:计时取决于你的机器/数据库/缓存状态。关键是趋势。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRODUCTS PERFORMANCE TEST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Dataset size: 2000 products

TRADITIONAL DOCTRINE (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    327.73ms
Memory:  44559.6 KB (43.52 MB)
Queries: 43
Result:  2000 Product entities

DOCTRINE JOIN FETCH (entities) (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    816.37ms
Memory:  39795.9 KB (38.86 MB)
Queries: 2
DB rows: ~30000 (Cartesian product in SQL)
Result:  2000 Product entities

SIMPLE JOINS (naive) (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    103.14ms
Memory:  11659.4 KB (11.39 MB)
Queries: 1
DB rows: 30000 (Cartesian product!)
Result:  2000 products (after deduplication)

AGGREGATED QUERIES (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    66.18ms
Memory:  13989.1 KB (13.66 MB)
Queries: 1
DB rows: 2000
Result:  2000 products (arrays)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMPARISON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

对比表

方法返回时间 (ms)内存 (KB)查询次数数据库行数产品数
传统 Doctrine实体327.7344559.643N/A2000
Doctrine JOIN fetch实体816.3739795.92300002000
简单 JOIN(朴素)数组103.1411659.41300002000
JSON 聚合数组66.1813989.1120002000
Back to Blog

相关文章

阅读更多 »