VP导航 VP导航
首页 提交网址

资讯文章 资讯 · NEWS

列表页为什么越改越慢:目录站的 N+1 查询和 ORM 懒加载

2026-10-24 约 2,027 字 预计阅读 5 分钟 yiyunkj

列表页为什么越改越慢:目录站的 N+1 查询和 ORM 懒加载 列

列表页是目录站的门面,也是最容易被 N+1 拖垮的地方。一个条目卡片上通常要显示分类名、标签、缩略图地址、最近核对时间——这些数据大多存在关联的表里。

问题出在写法上。看起来很自然的一段代码:

items = Item.query.filter_by(cate_id=108).limit(50).all()   # 第 1 次
for it in items:
    print(it.category.name)      # 每行再来 1 次
    print(", ".join(t.name for t in it.tags))   # 每行再来 1 次

50 条数据的列表,加上分类和标签各一次,实际查询次数是 1 + 50 + 50 = 101 次。每条查询只要一两毫秒,开发环境十条数据完全看不出来。等线上数据量涨到几千、并发上来,连接数和往返次数一起翻番,页面就慢了。

N+1 查询的形成过程与三种修复方式对比

图片来源:网络,版权归原作者所有,侵权请联系删除

根因不是 ORM,是代码形状

ORM 的关系字段通常默认懒加载:访问 it.category 的那一刻才发一条 SELECT。代码里看不到 query,但运行时确实发了。

值得强调的是,这不是 ORM 的缺陷。手写的 SQL 只要写成「先查列表,再在循环里按 id 查关联」,问题一模一样。真正的触发条件是这个代码形状:遍历一个集合,同时在循环体里访问关系字段。

顺便说一句,这也是为什么 AI 生成的代码特别容易带 N+1——它写的是局部正确、风格地道的写法,看不到数据库那层的实际执行次数。

两条信号确认它

生产环境:看调用次数排名

PostgreSQL 的 pg_stat_statements 是最准的来源。N+1 的特征非常明显:单条按主键查询的语句,调用次数比其他查询高出一两个数量级,而平均耗时极低。

SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms,
       left(query, 60) AS query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;

注意 PostgreSQL 13 之前这一列叫 mean_time,13 起改名为 mean_exec_time。

关键判断是「查询形状 + 调用次数」的组合:SELECT * FROM category WHERE id = $1 这种单主键查询,如果调用次数随着页面上的行数同步增长,基本可以确认。单纯调用次数高不算证据,很多正常的热点查询也是高频的。

开发环境:数每个请求的查询数

打开 ORM 的 SQL 日志,加载一次页面,数一下发了多少条。Django 开 django.db.backends 的 DEBUG 日志,SQLAlchemy 用 echo=True 或者给 sqlalchemy.engine 设 INFO 级别。

如果日志里同一条语句带着不同的 id 重复出现几十上百次,那就是它了。Python 有 nplusone 这类库能在开发阶段自动检测,Rails 有 bullet,Java 侧可以看 Hibernate 的统计。

更进一步的做法是给每个请求设一个查询数上限,写进测试里。这条防线能在后续重构时机械地拦住复发的 N+1。

三种修法,对应关系不同

修法适用场景查询次数副作用
JOIN 一次性取出N:1、1:1(条目 → 分类)1 次1:N 时行会膨胀
IN 批量补取1:N、M:N(条目 → 标签)2 次需要在应用层拼装
ORM 预加载选项以上两种,让 ORM 代劳1 或 2 次选错策略等于没修

配对规则很实用:N:1 的关系用 JOIN 形状,1:N 的集合用 IN 形状。理由是 JOIN 一个集合会让结果行成倍膨胀——50 个条目每个 5 个标签,JOIN 出来是 250 行,条目字段重复 5 遍。

各框架的对应写法:

  • Django:外键用 select_related("category")(走 JOIN);多对多和反向外键用 prefetch_related("tags")(走第二条 IN 查询)。
  • SQLAlchemy:joinedload() 对应 JOIN 形状,selectinload() 对应 IN 形状。
  • Rails:includes(:category)。

还有一个场景值得单独提:GraphQL 或者结构不确定的接口,预加载不好用,这时候上 DataLoader 模式——把一个操作周期内请求的所有 id 攒起来,最后一次性批量查。

目录下典型的四个 N+1 位置

列表页卡片上的分类名和标签,这是第一位。缩略图地址如果单独存一张表,同理。

「相关条目」和「相似站点」模块,如果每条卡片都要算一次相似度或者查一次关联表,直接翻倍。

面包屑。很多实现是逐级往上查父分类,三层分类就是三次查询,而且每条都懒加载。

后台的审核列表最容易漏——运营要看的字段多,一个页面几十次关联查询很常见,只是因为并发低没人抱怨。

修完之后的下一步

N+1 清干净之后,如果还有「调用次数正常但就是慢」的问题,那就是另一个家族了:单次查询慢。这时候该看执行计划和索引,而不是继续数查询次数。

给目录站的建议是先在列表页上做一遍查询数清点,这个投入产出比通常比调索引更高——它不需要理解业务,只需要数数。

本文标签 N+1查询问题 ORM懒加载 列表页性能优化
← 上一篇 可用性该定到几个九:目录站的 SLO、错误预算和那条 下一篇 → 从首页点几下能到条目:目录站的点击深度和 URL 层

— 本文完 —

提交网址

发现好站,欢迎自荐收录

站内搜索

站点统计

  • 今日审核0
  • 已收录站点3744
  • 正常访问3737
  • 失联站点7
  • 累计访问175

网站分类

  • AI工具 0
    AI对话0 AI绘画0 AI写作0 AI编程0 AI音视频0 AI办公效率0
  • 开发编程 0
    前端开发0 后端开发0 移动开发0 开源与代码托管0 数据库0 开发文档与教程0
  • 设计素材 0
    UI设计0 图标素材0 免费图库0 字体下载0 配色工具0 设计模板0
  • 站长工具 0
    网站统计0 SEO工具0 域名与主机0 收录提交0 建站程序0 运维监控0
  • 学习教育 0
    编程教程0 语言学习0 职业技能0 公开课与慕课0 考试题库0 中小学教育0
  • 生活服务 0
    交通出行0 外卖点餐0 房产租房0 医疗健康0 政务办事0 快递物流0
  • 影音娱乐 0
    影视观看0 音乐音频0 游戏资源0 短视频直播0 小说阅读0
  • 电商购物 0
    综合商城0 比价工具0 导购返利0 二手交易0 团购优惠0
  • 综合其他 1
    实用工具0 资源合集0 小众站点0 个人博客0 论坛社区0
  • 新闻资讯 0
    科技资讯8 财经媒体1 行业动态0 社会热点0 综合门户0
  • 资讯文章 350

最新收录

  • freeCodeCamp
  • DigitalOcean
  • 途牛旅游网
  • 快手
  • 新东方
  • gzip、Brotli、zstd 该
  • 搜索结果里那段摘要怎么来的:目录站的
  • 只知道慢不知道慢在哪:目录站后端链路

浏览排行

  1. 满分作文网 6
  2. Let's Enhance 3
  3. 淘菜菜 3
  4. MasterClass 3
  5. TypeScript 中文网 3
  6. Zhengziying 3
  7. 得意生活 3
  8. 镇江网友之家 3

联系我们

bQ:12345678

电话:400-8888-888

邮箱:admin@example.com

时间:09:00-21:00

友情链接

车机屋·焊锡条·探兔AI导航·静电测试仪·大学生联盟

关于我们|广告合作|隐私条款|网站地图

原ICP备00000000号-1,@2026

由 Z-BlogPHP 驱动