ORM中的“N+1查询问题”解析

ORM中的“N+1查询问题”解析

技术背景

在使用对象关系映射(ORM)框架从关系型数据库中检索数据时,“N+1查询问题”是一个常见的性能问题。ORM 框架用于将数据库表映射到面向对象编程语言中的对象,方便开发人员进行数据库操作。然而,在某些数据检索方式下,会出现多次查询数据库的情况,从而导致性能下降。

实现步骤

示例场景

假设有两个表:CustomerOrder,每个客户可以有多个订单,存在一对多的关系。以下是使用 ORM 进行数据检索时出现 “N+1查询问题” 的示例代码:

1
2
3
4
5
customers = Customer.objects.all()

for customer in customers:
orders = customer.orders.all()
# Do something with the orders

在上述代码中,首先使用 Customer.objects.all() 检索所有客户,然后对于每个客户,使用 customer.orders.all() 检索其订单。如果有 100 个客户,就会执行 101 次查询:一次检索所有客户,100 次检索每个客户的订单。

问题产生原因

ORM 框架默认会为每个客户的订单执行单独的查询,而不是在一个查询中获取所有必要的数据。这种行为是为了避免不必要地加载所有关联数据,但在处理大数据集时会严重影响性能。

核心代码

原始 ORM 查询(存在 N+1 问题)

1
2
3
4
5
6
with Session(engine) as session:
customers = session.scalars(select(Customer))
for customer in customers:
print(f"> Customer: #{customer.customer_id}")
for order in customer.orders:
print(f"> order #{order.order_id} at {order.order_datetime}")

对应的 SQL 语句:

1
2
3
4
5
6
7
8
-- This query gets all customers:
SELECT customer.customer_id, ...
FROM customer

-- The following SQL is executed once for each customer:
SELECT "order".order_id AS order_order_id, ...
FROM "order"
WHERE "order".customer_id = %(param_1)s

优化方案 - 急切加载(Eager Loading)

1
2
3
4
5
6
7
with Session(engine) as session:
customers = session.scalars(
select(Customer).options(selectinload(Customer.orders)))
for customer in customers:
print(f"> Customer: #{customer.customer_id}")
for order in customer.orders:
print(f"> order #{order.order_id} at {order.order_datetime}")

对应的 SQL 语句:

1
2
3
4
5
6
7
SELECT customer.customer_id, ...
FROM customer

-- This loads all the orders you need in one query:
SELECT "order".order_id AS order_order_id, ...
FROM "order"
WHERE "order".customer_id IN (%(primary_keys_1)s, %(primary_keys_2)s, ...)

优化方案 - 显式连接(Explicit Join)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
with Session(engine) as session:
stmt = (
select(
Customer.customer_id,
Order.order_id,
Order.order_datetime,
)
.select_from(Customer)
.join(Customer.orders)
.order_by(Customer.customer_id)
)
results = session.execute(stmt)

current_customer_id = None
for row in results:
customer_id = row.customer_id
if current_customer_id != customer_id:
current_customer_id = customer_id
print(f"> Customer: #{current_customer_id}")
print(f"> order #{row.order_id} at {row.order_datetime}")

对应的 SQL 语句:

1
2
3
4
SELECT customer.customer_id, "order".order_id, ...
FROM customer
JOIN "order" ON customer.customer_id = "order".customer_id
ORDER BY customer.customer_id

最佳实践

  • 使用急切加载:在需要关联数据时,使用 ORM 框架提供的急切加载功能,一次性获取所有必要的数据,减少数据库往返次数。
  • 显式连接:在查询中显式使用连接操作,将多个表的数据合并到一个查询中,提高查询效率。
  • 性能测试:在开发过程中进行性能测试,及时发现和解决 “N+1查询问题”。

常见问题

如何检测 N+1 查询问题

  • 使用工具:可以使用一些工具来自动检测 N+1 查询问题,如 db-util 开源项目、jOOQ 等。
  • 代码审查:仔细审查代码,检查是否存在多次查询关联数据的情况。

为什么使用 FetchType.EAGER 也会出现 N+1 查询问题

@ManyToOne@OneToOne 关联默认使用 FetchType.EAGER 策略,但在使用 JPQL 或 Criteria API 查询时,如果忘记使用 JOIN FETCH,仍然会触发 N+1 查询问题。因为 JPQL 或 Criteria API 查询定义了显式的查询计划,Hibernate 不能自动插入 JOIN FETCH

使用 FetchType.LAZY 就一定能避免 N+1 查询问题吗

不是的。即使显式使用 FetchType.LAZY,如果在后续代码中引用了懒加载的关联数据,仍然会触发 N+1 查询问题。解决方法是在 JPQL 查询中添加 JOIN FETCH 子句。


ORM中的“N+1查询问题”解析
https://119291.xyz/posts/2025-05-15.analysis-of-n-plus-one-query-problem-in-orm/
作者
ww
发布于
2025年5月15日
许可协议