N+1查询:数据库性能的隐形杀手与终极拯救指南
title: N+1查询:数据库性能的隐形杀手与终极拯救指南
date: 2025/05/06 00:16:30
updated: 2025/05/06 00:16:30
author: cmdragon
excerpt:
N+1查询问题是ORM中常见的性能陷阱,表现为在查询主对象时,对每个关联对象进行单独查询,导致查询次数过多。以博客系统为例,查询10位作者及其文章会产生11次查询。通过Tortoise-ORM的prefetch_related方法,可以将查询优化为2次,显著提升性能。优化后的实现方案包括使用SQL JOIN语句加载关联数据,并结合FastAPI进行实践。进阶优化技巧包括多层预加载、选择性字段加载和分页查询结合。常见报错涉及模型注册、连接关闭和字段匹配问题,需针对性解决。
categories:
- 后端开发
- FastAPI
tags:
- N+1查询问题
- Tortoise-ORM
- 异步预加载
- FastAPI
- 数据库优化
- SQL查询
- 性能分析

扫描二维码
关注或者微信搜一搜:编程智域 前端至全栈交流与成长
探索数千个预构建的 AI 应用,开启你的下一个伟大创意:https://tools.cmdragon.cn/
第一章:理解N+1查询问题本质
1.1 什么是N+1查询问题?
N+1查询是ORM使用过程中常见的性能陷阱。假设我们有一个博客系统,当查询作者列表时,如果每个作者关联了多篇文章,常规查询会先获取N个作者(1次查询),然后为每个作者单独执行文章查询(N次查询),总共产生N+1次数据库查询。
示例场景:
- 数据库包含10位作者
- 每位作者有5篇文章
- 常规查询会产生1(作者)+10(文章)=11次查询
1.2 问题复现与性能影响
使用Tortoise-ORM创建数据模型:
# models.py
from tortoise.models import Model
from tortoise import fields
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)
class Article(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=100)
content = fields.TextField()
author = fields.ForeignKeyField('models.Author', related_name='articles')
问题查询代码示例:
async def get_authors_with_articles():
authors = await Author.all()
result = []
for author in authors:
articles = await author.articles.all()
result.append({
"author": author.name,
"articles": [a.title for a in articles]
})
return result
使用EXPLAIN ANALYZE分析查询计划:
-- 主查询
EXPLAIN
ANALYZE
SELECT "id", "name"
FROM "author";
-- 单个作者的文章查询
EXPLAIN
ANALYZE
SELECT "id", "title", "content"
FROM "article"
WHERE "author_id" = 1;
第二章:prefetch_related异步预加载实战
2.1 预加载机制原理
Tortoise-ORM的prefetch_related使用SQL JOIN语句在单个查询中加载关联数据。对于1:N关系,它通过以下步骤实现:
- 执行主查询获取所有作者
- 收集作者ID列表
- 执行关联查询获取所有相关文章
- 在内存中进行数据关联映射
2.2 优化后的实现方案
完整FastAPI示例:
# main.py
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from pydantic import BaseModel
app = FastAPI()
# Pydantic模型
class ArticleOut(BaseModel):
title: str
class AuthorOut(BaseModel):
id: int
name: str
articles: list[ArticleOut]
class Config:
orm_mode = True
# 数据库配置
DB_CONFIG = {
"connections": {"default": "postgres://user:pass@localhost/blogdb"},
"apps": {
"models": {
"models": ["models"],
"default_connection": "default",
}
}
}
# 路由端点
@app.get("/authors", response_model=list[AuthorOut])
async def get_authors():
authors = await Author.all().prefetch_related("articles")
return [
AuthorOut.from_orm(author)
for author in authors
]
# 初始化ORM
register_tortoise(
app,
config=DB_CONFIG,
generate_schemas=True,
add_exception_handlers=True,
)
2.3 执行计划对比分析
优化后的SQL查询示例:
EXPLAIN
ANALYZE
SELECT a.id,
a.name,
ar.id,
ar.title,
ar.content
FROM author a
LEFT JOIN article ar ON a.id = ar.author_id;
性能对比指标:
| 指标 | 优化前 (N=10) | 优化后 |
|---|---|---|
| 查询次数 | 11 | 2 |
| 平均响应时间 (ms) | 320 | 45 |
| 网络往返次数 | 11 | 2 |
| 内存占用 (KB) | 850 | 650 |
第三章:进阶优化与最佳实践
3.1 多层预加载技巧
处理多级关联关系:
await Author.all().prefetch_related(
"articles__comments", # 文章关联的评论
"profile" # 作者个人资料
)
3.2 选择性字段加载
优化查询字段选择:
await Author.all().prefetch_related(
articles=Article.all().only("title", "created_at")
)
3.3 分页与预加载结合
分页查询优化方案:
from tortoise.functions import Count
async def get_paginated_authors(page: int, size: int):
return await Author.all().prefetch_related("articles")
.offset((page - 1) * size).limit(size)
.annotate(articles_count=Count('articles'))
课后Quiz
当处理M:N关系时,应该使用哪个预加载方法?
A) select_related
B) prefetch_related
C) both
D) none答案:B
M:N关系需要使用prefetch_related,因为select_related仅适用于ForeignKey和OneToOne关系以下哪种情况最适合使用prefetch_related?
A) 查询单个对象及其关联的10条记录
B) 列表页需要显示主对象及其关联的统计数量
C) 需要实时更新的高频写入操作
D) 需要关联5层以上的深度查询答案:B
当需要批量处理关联数据时,prefetch_related能显著减少查询次数
常见报错解决方案
报错1:TortoiseORMError: Relation does not exist
- 原因:模型未正确注册或字段名拼写错误
- 解决:
- 检查
register_tortoise的models配置 - 验证关联字段的related_name拼写
- 执行数据库迁移命令
- 检查
报错2:OperationalError: connection closed
- 原因:异步连接未正确关闭
- 解决:
# 在请求处理完成后手动关闭连接
@app.middleware("http")
async def close_connection(request, call_next):
response = await call_next(request)
await connections.close_all()
return response
报错3:ValidationError: field required (type=value_error.missing)
- 原因:Pydantic模型与ORM模型字段不匹配
- 解决:
- 检查
from_orm方法是否正确使用 - 验证response_model的字段定义
- 确保启用orm_mode配置
- 检查
环境配置与运行
安装依赖:
pip install fastapi uvicorn tortoise-orm[asyncpg] pydantic
启动服务:
uvicorn main:app --reload --port 8000
测试端点:
curl http://localhost:8000/authors
余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:N+1查询:数据库性能的隐形杀手与终极拯救指南 | cmdragon's Blog
往期文章归档:
- FastAPI与Tortoise-ORM开发的神奇之旅 | cmdragon's Blog
- DDD分层设计与异步职责划分:让你的代码不再“异步”混乱 | cmdragon's Blog
- 异步数据库事务锁:电商库存扣减的防超卖秘籍 | cmdragon's Blog
- FastAPI中的复杂查询与原子更新指南 | cmdragon's Blog
- 深入解析Tortoise-ORM关系型字段与异步查询 | cmdragon's Blog
- FastAPI与Tortoise-ORM模型配置及aerich迁移工具 | cmdragon's Blog
- 异步IO与Tortoise-ORM的数据库 | cmdragon's Blog
- FastAPI数据库连接池配置与监控 | cmdragon's Blog
- 分布式事务在点赞功能中的实现 | cmdragon's Blog
- Tortoise-ORM级联查询与预加载性能优化 | cmdragon's Blog
- 使用Tortoise-ORM和FastAPI构建评论系统 | cmdragon's Blog
- 分层架构在博客评论功能中的应用与实现 | cmdragon's Blog
- 深入解析事务基础与原子操作原理 | cmdragon's Blog
- 掌握Tortoise-ORM高级异步查询技巧 | cmdragon's Blog
- FastAPI与Tortoise-ORM实现关系型数据库关联 | cmdragon's Blog
- Tortoise-ORM与FastAPI集成:异步模型定义与实践 | cmdragon's Blog
- 异步编程与Tortoise-ORM框架 | cmdragon's Blog
- FastAPI数据库集成与事务管理 | cmdragon's Blog
- FastAPI与SQLAlchemy数据库集成 | cmdragon's Blog
- FastAPI与SQLAlchemy数据库集成与CRUD操作 | cmdragon's Blog
- FastAPI与SQLAlchemy同步数据库集成 | cmdragon's Blog
- SQLAlchemy 核心概念与同步引擎配置详解 | cmdragon's Blog
- FastAPI依赖注入性能优化策略 | cmdragon's Blog
- FastAPI安全认证中的依赖组合 | cmdragon's Blog
- FastAPI依赖注入系统及调试技巧 | cmdragon's Blog
- FastAPI依赖覆盖与测试环境模拟 | cmdragon's Blog
- FastAPI中的依赖注入与数据库事务管理 | cmdragon's Blog
- FastAPI依赖注入实践:工厂模式与实例复用的优化策略 | cmdragon's Blog
- FastAPI依赖注入:链式调用与多级参数传递 | cmdragon's Blog
- FastAPI依赖注入:从基础概念到应用 | cmdragon's Blog
- FastAPI中实现动态条件必填字段的实践 | cmdragon's Blog
- FastAPI中Pydantic异步分布式唯一性校验 | cmdragon's Blog
- 掌握FastAPI与Pydantic的跨字段验证技巧 | cmdragon's Blog
- FastAPI中的Pydantic密码验证机制与实现 | cmdragon's Blog
- XML Sitemap
N+1查询:数据库性能的隐形杀手与终极拯救指南的更多相关文章
- Django查询数据库性能优化
现在有一张记录用户信息的UserInfo数据表,表中记录了10个用户的姓名,呢称,年龄,工作等信息. models文件 from django.db import models class Job(m ...
- 并发性能的隐形杀手之伪共享(false sharing)
在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...
- [转载]Hibernate如何提升数据库查询的性能
目录(?)[-] 数据库查询性能的提升也是涉及到开发中的各个阶段在开发中选用正确的查询方法无疑是最基础也最简单的 SQL语句的优化 使用正确的查询方法 使用正确的抓取策略 Hibernate的性能优化 ...
- (转)Db2 数据库性能优化中,十个共性问题及难点的处理经验
(转)https://mp.weixin.qq.com/s?__biz=MjM5NTk0MTM1Mw==&mid=2650629396&idx=1&sn=3ec17927b3d ...
- EntityFramework之原始查询及性能优化(六)
前言 在EF中我们可以通过Linq来操作实体类,但是有些时候我们必须通过原始sql语句或者存储过程来进行查询数据库,所以我们可以通过EF Code First来实现,但是SQL语句和存储过程无法进行映 ...
- SQLServer中的页如何影响数据库性能 (转)
无论是哪一个数据库,如果要对数据库的性能进行优化,那么必须要了解数据库内部的存储结构.否则的话,很多数据库的优化工作无法展开.对于对于数据库管理员来说,虽然学习数据库的内存存储结构比较单调,但是却是我 ...
- 转载:SqlServer数据库性能优化详解
本文转载自:http://blog.csdn.net/andylaudotnet/article/details/1763573 性能调节的目的是通过将网络流通.磁盘 I/O 和 CPU 时间减到最小 ...
- 优化MySQL数据库性能的八大方法
本文探讨了提高MySQL 数据库性能的思路,并从8个方面给出了具体的解决方法. 1.选取最适用的字段属性 MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就 ...
- SQL Server数据库性能优化之索引篇【转】
http://www.blogjava.net/allen-zhe/archive/2010/07/23/326966.html 性能优化之索引篇 近期项目需要, 做了一段时间的SQL Server性 ...
- SQL Server数据库性能优化之SQL语句篇【转】
SQL Server数据库性能优化之SQL语句篇http://www.blogjava.net/allen-zhe/archive/2010/07/23/326927.html 近期项目需要, 做了一 ...
随机推荐
- Luogu P10869 LCMs 题解 [ 黄 ] [ lcm ] [ 最短路 ]
LCMs:很好的数论和构造题. 显然我们不可以直接建图跑最短路. 于是考虑分讨. 倍数关系 答案显然为 \(\max(a,b)\). 相等关系 答案显然为 \(0\). \(\gcd(a,b)> ...
- c++用正则表达式判断匹配字符串中的数字数值(包括负数,小数,整数)MFC编辑框判断数值
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/12097381.html 因为今天做那个MFC的编辑框有一些框就是要判断输入的是否是数值,一开始 ...
- Flink - [08] 状态一致性
题记部分 一.什么是状态一致性 有状态的流处理,内部每个算子任务都可以有自己的状态.对于流处理器内部来说,所谓的状态一致性,其实就是我们所说的计算结果要保证准确.一条数据也不应该丢失,也不应该重复 ...
- 【答题系统可参考】php 禁止api被跨域调用
在 PHP 中,防止 API 被跨域调用可以通过设置适当的 HTTP 响应头来实现.跨域资源共享(CORS,Cross-Origin Resource Sharing)机制允许或拒绝来自不同源的请求. ...
- My'Bug
修改时未校验工作经历是否为空
- RealityCapture重建试验
一.使用已有数据集 (一)小型物件(官网) 输入:Camera_Lubitel2_studio "Lubitel Camera" consisting of 72 images 地 ...
- React从webpack迁移到rsbuild 纪实
Why 随着团队项目规模越来越大之后,继从babel-loader迁移到esbuild之后发现打包.热重载性能随着时间迭代之后又慢慢开始成为性能瓶颈,所以决定用新的打包工具去解决这个问题.esbuil ...
- jquery简单的上传图片预览
html <div id="bcd"></div> <input type="file" id="abc"&g ...
- Linux运维面试题之:Root密码忘记如何解决
目录 6.5 Root密码忘记如何解决 6.5.1 系统自带救援模式 6.5.2 U盘.光盘救援系统 6.5 Root密码忘记如何解决 解决方案有两种:自救,别人救 解决方案 应用场景 1️⃣ 系统自 ...
- PKCS#系列规范分别规定了什么
PKCS#1:定义了RSA公钥和私钥的表示方法,以及如何进行RSA加密和签名. PKCS#2:原本是用以规范RSA加密摘要的转换方式,现已被纳入PKCS#1之中. PKCS#3:规范以Diffie-H ...