N+1查询问题是一种常见的性能瓶颈,主要表现是应用中需要执行大量查询。通常这是由于数据访问模式中的代码结构不佳导致的:先执行一个查询获取记录列表(1 查询),然后针对每个记录额外执行多个单独查询(N 查询)。这最终累积为 N+1 查询问题

虽然乍看之下,多个小型查询似乎应该比一个大型复杂查询更轻量更快速,但实际上,多个查询需要与数据库进行多次交互,包括发送查询、数据库处理查询以及返回结果。

这不仅增加了延迟,还可能加重数据库的负担。而一个复杂的单一查询只需一次交互即可完成,并且往往可以通过数据库引擎优化执行效率。

因此,从整体性能角度来看,多个小查询通常比单次复杂查询效率更低。

一个 N+1 查询的示例

以下是一个示例应用场景。假设我们有两张表,分别是 items 表和 categories 表:

表结构如下:

categories 表

idname
1Produce
2Deli
3Dairy

items 表

idnamecategory_id
1Apples1
2Cheese2
3BreadNULL

示例需求:

我们希望应用程序能够列出所有商品,同时显示它们所属的分类名称。

第一种实现方式(存在 N+1 问题)

例如,我们先查询分类列表,然后针对每个分类的商品单独执行查询(为了简化代码,这里采用Python的实现方式):

import sqlite3

# 建立数据库连接
conn = sqlite3.connect("example.db")

# 查询分类
categories_query = "SELECT * FROM categories;"
categories_cursor = conn.execute(categories_query)

for category in categories_cursor.fetchall():
  category_id = category[0]
  category_name = category[1]

  # 显示分类名称
  print(f"Category: {category_name}")
   
  # 查询该分类的商品
  items_query = "SELECT id, name FROM items WHERE category_id = ? ORDER BY name;"
  items_cursor = conn.execute(items_query, (category_id,))
   
  for item in items_cursor.fetchall():
      # 显示商品 ID 和名称
      print(f" Item ID: {item[0]}, Item Name: {item[1]}")

conn.close()

这种方法逻辑简单易懂,适合小规模数据场景。然而,它的问题是对每个分类的商品都进行了单独查询。假设分类表有 N 条记录,那么总共需要执行 N+1 次查询。面对大规模数据时,查询次数会急剧增多,导致响应时间变长。

性能对比:

假设数据库中有 800 个商品和 17 个分类表记录,这种方法需要执行 1 + 17 = 18 次查询,耗时约为 1 秒。而通过结合复杂 SQL 查询优化,可以显著减少查询次数并减少时间消耗。

使用 JOIN 优化 N+1 查询

我们可以利用 SQL 的 JOIN 语句,将多个查询合并为一个复杂查询,从而避免 N+1 问题。以下是优化后的代码:

import sqlite3

# 建立数据库连接
conn = sqlite3.connect("example.db")

# 使用 JOIN 查询分类和商品
query = """
  SELECT
      c.id AS category_id,
      c.name AS category_name,
      i.id AS item_id,
      i.name AS item_name
  FROM categories c
  LEFT JOIN items i ON c.id = i.category_id
  ORDER BY c.name, i.name;
"""
cursor = conn.execute(query)

last_category_id = None

for row in cursor.fetchall():
  category_id = row[0]
  category_name = row[1]
  item_id = row[2]
  item_name = row[3]

  # 如果分类发生变化,渲染分类名称
  if category_id != last_category_id:
      print(f"Category: {category_name}")

  # 显示商品信息(如果存在)
  if item_id is not None:
      print(f" Item ID: {item_id}, Item Name: {item_name}")

  last_category_id = category_id

conn.close()

通过上述代码,我们将单次查询的结果直接按分类和商品合并,不再需要多次单独查询。响应时间从 1 秒降低到 0.16 秒。在数据量较大时,优势更加显著。

数据结构优化与复杂查询

对于更复杂的需求,比如需要展示分类及其商品数量,可以利用 SQL 的 GROUP BY 进行聚合查询:

聚合查询示例:

query = """
  SELECT
      c.id AS category_id,
      c.name AS category_name,
      COUNT(i.id) AS item_count
  FROM categories c
  LEFT JOIN items i ON c.id = i.category_id
  GROUP BY c.id, c.name
  ORDER BY c.name;
"""

复杂数据结构:分类与商品列表

如果既需要商品数量,又需要展示商品详细信息,我们可以在服务器端对查询结果进行组织,将其转换为更符合应用需求的数据结构。例如,通过构造 字典嵌套

import sqlite3

# 建立数据库连接
conn = sqlite3.connect("example.db")

# 查询分类及商品数据
query = """
  SELECT
      c.id AS category_id,
      c.name AS category_name,
      i.id AS item_id,
      i.name AS item_name
  FROM categories c
  LEFT JOIN items i ON c.id = i.category_id
  ORDER BY c.name, i.name;
"""
cursor = conn.execute(query)

categories = {}
category_items = []

last_category_id = None
last_category_name = None

for row in cursor.fetchall():
  category_id = row[0]
  category_name = row[1]
  item_id = row[2]
  item_name = row[3]

  # 在分类发生变化时,将当前分类的商品数据存储
  if last_category_id is not None and category_id != last_category_id:
      categories[last_category_name] = category_items
      category_items = []

  # 添加商品信息到当前分类
  if item_id is not None:
      category_items.append({"item_id": item_id, "item_name": item_name})

  last_category_id = category_id
  last_category_name = category_name

categories[last_category_name] = category_items

# 渲染数据
for category_name, items in categories.items():
  print(f"Category: {category_name}")
  print(f"{len(items)} items")

  for item in items:
      print(f" Item ID: {item['item_id']}, Item Name: {item['item_name']}")

conn.close()

通过上述代码,我们不仅解决了 N+1 查询问题,还生成了方便访问的嵌套数据结构,能够快速查询商品列表和商品数量。

总结

N+1 查询问题是一种典型的性能问题,在大规模数据场景中尤为突出。通过优化查询结构(如使用 JOIN 合并查询)、设计高效的数据结构以及理解查询性能的影响,我们可以显著提升应用的响应速度。

在实际项目中,避免 N+1 查询问题是性能优化的核心之一,尤其是在涉及复杂关系和大量数据库交互时。通过良好的代码设计和合理的数据库操作,我们不仅能提升性能,还能为后续功能开发打下坚实基础。



什么是N+1查询问题?插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://www.choupangxia.com/2025/09/12/slq-n1/