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关系,它通过以下步骤实现:

  1. 执行主查询获取所有作者
  2. 收集作者ID列表
  3. 执行关联查询获取所有相关文章
  4. 在内存中进行数据关联映射

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

  1. 当处理M:N关系时,应该使用哪个预加载方法?

    A) select_related

    B) prefetch_related

    C) both

    D) none

    答案:B

    M:N关系需要使用prefetch_related,因为select_related仅适用于ForeignKey和OneToOne关系

  2. 以下哪种情况最适合使用prefetch_related?

    A) 查询单个对象及其关联的10条记录

    B) 列表页需要显示主对象及其关联的统计数量

    C) 需要实时更新的高频写入操作

    D) 需要关联5层以上的深度查询

    答案:B

    当需要批量处理关联数据时,prefetch_related能显著减少查询次数

常见报错解决方案

报错1:TortoiseORMError: Relation does not exist

  • 原因:模型未正确注册或字段名拼写错误
  • 解决:
    1. 检查register_tortoise的models配置
    2. 验证关联字段的related_name拼写
    3. 执行数据库迁移命令

报错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模型字段不匹配
  • 解决:
    1. 检查from_orm方法是否正确使用
    2. 验证response_model的字段定义
    3. 确保启用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

往期文章归档:

N+1查询:数据库性能的隐形杀手与终极拯救指南的更多相关文章

  1. Django查询数据库性能优化

    现在有一张记录用户信息的UserInfo数据表,表中记录了10个用户的姓名,呢称,年龄,工作等信息. models文件 from django.db import models class Job(m ...

  2. 并发性能的隐形杀手之伪共享(false sharing)

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

  3. [转载]Hibernate如何提升数据库查询的性能

    目录(?)[-] 数据库查询性能的提升也是涉及到开发中的各个阶段在开发中选用正确的查询方法无疑是最基础也最简单的 SQL语句的优化 使用正确的查询方法 使用正确的抓取策略 Hibernate的性能优化 ...

  4. (转)Db2 数据库性能优化中,十个共性问题及难点的处理经验

    (转)https://mp.weixin.qq.com/s?__biz=MjM5NTk0MTM1Mw==&mid=2650629396&idx=1&sn=3ec17927b3d ...

  5. EntityFramework之原始查询及性能优化(六)

    前言 在EF中我们可以通过Linq来操作实体类,但是有些时候我们必须通过原始sql语句或者存储过程来进行查询数据库,所以我们可以通过EF Code First来实现,但是SQL语句和存储过程无法进行映 ...

  6. SQLServer中的页如何影响数据库性能 (转)

    无论是哪一个数据库,如果要对数据库的性能进行优化,那么必须要了解数据库内部的存储结构.否则的话,很多数据库的优化工作无法展开.对于对于数据库管理员来说,虽然学习数据库的内存存储结构比较单调,但是却是我 ...

  7. 转载:SqlServer数据库性能优化详解

    本文转载自:http://blog.csdn.net/andylaudotnet/article/details/1763573 性能调节的目的是通过将网络流通.磁盘 I/O 和 CPU 时间减到最小 ...

  8. 优化MySQL数据库性能的八大方法

    本文探讨了提高MySQL 数据库性能的思路,并从8个方面给出了具体的解决方法. 1.选取最适用的字段属性 MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就 ...

  9. SQL Server数据库性能优化之索引篇【转】

    http://www.blogjava.net/allen-zhe/archive/2010/07/23/326966.html 性能优化之索引篇 近期项目需要, 做了一段时间的SQL Server性 ...

  10. SQL Server数据库性能优化之SQL语句篇【转】

    SQL Server数据库性能优化之SQL语句篇http://www.blogjava.net/allen-zhe/archive/2010/07/23/326927.html 近期项目需要, 做了一 ...

随机推荐

  1. Vue3+NestJS实现后台权限管理系统上线啦!(附源码及教程)

    最近这段时间工作不忙,想着提升一下自己的技术,沉淀沉淀.于是做了一个开源的后台权限管理系统.因为我本身是一个前端开发,所以前端和服务端都是用的 JS 语言来开发的,前端用的框架是 vue3,后端则用的 ...

  2. MYSQL数据空洞解析

    ## 背景引入 MYSQL中数据表A,在删除了一半的数据后,发现表空间的大小并没有减少,这是什么原因导致的呢? 定义 当对一定量数据执行delete操作时,MySQL将数据删除后进而导致页合并或者页删 ...

  3. android无障碍开发 企业微信 机器人

    实现 Android 无障碍开发 企业微信 机器人 作为一名新入行的开发者,你可能对如何开发一个支持企业微信的无障碍机器人感到迷茫.在这篇文章中,我将为你详细讲解实现这一功能的流程和代码示例. 流程概 ...

  4. nginx 强制https

    nginx 强制https   通常有如下两种方法强制https推荐第二种,第二种更高效1.使用nginx的rewrite方法 server { listen 80; server_name xxx. ...

  5. 在Linux系统下启动eclipse时遇到Eclipse 无法正常启动

    Eclipse: 无法打开显示: 出现此问题原因: 这通常表示 Eclipse 试图在没有合适显示环境的情况下启动,可能是在没有图形界面的环境(例如远程服务器或没有正确配置的 X11 转发)中运行. ...

  6. 一文搞懂 Redis 架构演化之路

    作者:ryetan,腾讯 CSIG 后台开发工程师 现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定.高性能地提 ...

  7. go 链表操作

    链表的特点和初始化 链表的特点 用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的) 结点 结点(node) 数据域 => 存储元素信息 指针域 => 存 ...

  8. php-fpm 启动后没有监听端口9000

    netstat -tpln未发现监听9000端口.查看/var/log/php7-fpm.log一切正常. 随后查看PHP配置文件:/usr/local/php/etc/php-fpm.conf (源 ...

  9. docker swarm CA证书到期

    1.现象 在portain平台查看日志,发现一些节点日志无法查看报错为:Error grabbing logs: rpc error: code = Unknown desc = warning: i ...

  10. Linux下对LVM逻辑卷分区大小调整 [针对xfs和ext4文件系统]

    当我们在安装系统的时候,由于没有合理分配分区空间,在后续维护过程中,发现有些分区空间不够使用,而有的分区空间却有很多剩余空间.如果这些分区在装系统的时候使用了lvm(前提是这些分区要是lvm逻辑卷分区 ...