bigywater.tech
Flask · 87

N+1 查询问题

N+1 到底有多慢?怎么发现它藏在哪一行、哪个模板里?数据库不在本机时会放大多少?这篇用真实的 PostgreSQL 和一个加了延迟的 TCP 代理,把这些量出来。

101 → 2
同一个页面的 SQL 条数:逐个懒加载与 selectinload
21 倍
单向 5 ms 延迟下 /bad 与 /good 的耗时比
location
RECORD_QUERIES 记录的发出 SQL 的代码位置

数条数

实测 · 本机执行输出
  /bad        → X-Query-Count = 101
  /good       → X-Query-Count = 2
  /tpl-bad    → X-Query-Count = 101
  /tpl-good   → X-Query-Count = 2
  /books-bad  → X-Query-Count = 101
  (/tpl-bad 的 N+1 藏在模板里:视图函数里没有任何循环,是模板里 a.books|length 触发的)

上一篇:joinedload() 与 selectinload();下一篇:SQLAlchemy Identity Map。

找到发 SQL 的那一行

实测 · 本机执行输出
   ['SELECT author.id, author.name  FROM auth', 'f87.py:71 · where']
   ['SELECT book.id, book.title, book.author_', 'f87.py:71 · where']
   ['SELECT book.id, book.title, book.author_', 'f87.py:71 · where']

用 raiseload 提前暴露

实测 · 本机执行输出
   InvalidRequestError: 'Author.books' is not available due to lazy='raise'

网络延迟放大 N+1

实测 · 本机执行输出
  单向延迟       /bad(1+N 条)      /good(2 条)     倍数
     0 ms           23.1 ms         3.0 ms     7.7x
     1 ms          408.6 ms        16.3 ms    25.1x
     5 ms         1481.8 ms        70.8 ms    20.9x

延迟代理是本篇自己用 asyncio 写的,只是模拟「数据库不在本机」,不代表某个真实网络的数字。

用测试守住

实测 · 本机执行输出
  /good  查询数 2,阈值 3 → 通过
  /bad   查询数 101,阈值 3 → 失败(N+1)

分步演示

N+1 查询问题
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)与一个自写的延迟 TCP 代理
    • 没有验证:真实跨机房网络下的数字、连接池耗尽时 N+1 的并发表现、ORM 之外(如 GraphQL 解析器)里的 N+1、dataloader 这类批处理方案
    京ICP备2026067502号-1京公网安备11010502063487号