bigywater.tech
Flask · 86

joinedload() 与 selectinload()

什么时候用 joinedload,什么时候用 selectinload?一对多为什么必须 unique()?嵌套多层时哪个更划算?这篇在真实的 PostgreSQL 上把两者的 SQL、行数、耗时和分块行为逐项量出来。

21.7 vs 10.5 ms
300 个作者 + 3000 本书:joinedload 与 selectinload 的耗时(PG 本机,中位数)
9000 行
三层嵌套用 joinedload 时 JOIN 返回的行数(300 × 10 × 3)
500
selectinload 每块 IN 列表最多的主键数

发出的 SQL

实测 · 本机执行输出
  joinedload + limit(5):
     SELECT anon_1.id, anon_1.name, anon_1.bio, book_1.id AS id_1, book_1.title, book_1.author_id FROM (SELECT author.id AS id, author.name AS name, author.bio AS bio FROM author LIMIT %(param_1)s::INTEGER) AS anon_
  selectinload + limit(5):
     SELECT author.id, author.name, author.bio FROM author LIMIT %(param_1)s::INTEGER
     SELECT book.author_id, book.id, book.title FROM book WHERE book.author_id IN (%(primary_keys_1)s::INTEGER, %(primary_keys_2)s::INTEGER, %(primary_keys_3)s::INTEGER, %(primary_keys_4)s::INTEGER, %(primary_keys_5

上一篇:Lazy Loading 与 Eager Loading;下一篇:N+1 查询问题。

必须 unique()

实测 · 本机执行输出
   InvalidRequestError: The unique() method must be invoked on this Result, as it contains results that include joined eager loads aga

一对多的耗时

实测 · 本机执行输出
  不预加载(lazy,N+1)            47.7 ms  SQL 301 条  作者 300 本书 3000
  joinedload                21.7 ms  SQL   1 条  作者 300 本书 3000
  selectinload              10.5 ms  SQL   2 条  作者 300 本书 3000
  JOIN 的结果集行数:3000(作者的 bio 在每一行里都重复一遍),selectin 的两条查询分别返回 300 与 3000 行

数据库在本机、关了 fsync,所以只看相对关系,不看绝对值。

多层嵌套

实测 · 本机执行输出
  joinedload 链        100.3 ms  SQL 1 条
  selectinload 链       63.3 ms  SQL 8 条
  joinedload 链返回的行数 = 9000 (作者 × 书 × 评论 的乘积)

多对一

实测 · 本机执行输出
  joinedload(Book.author)       11.5 ms  SQL 1 条/次  书 1500
  selectinload(Book.author)      8.3 ms  SQL 2 条/次  书 1500

IN 列表分块

实测 · 本机执行输出
    300 个作者:2 条 SQL
    600 个作者:3 条 SQL
   1200 个作者:4 条 SQL

分步演示

joinedload 与 selectinload
本步的对象

    本文用到的工具与边界

    • 本机 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(非默认端口,本机回环)
    • 没有验证:数据库与应用跨网络时往返次数对耗时的影响(本机几乎没有网络延迟,会低估 N+1 与多条 SQL 的代价)、contains_eager、subqueryload 的耗时、大 bio 列以外的行宽、MySQL 与 SQLite 上的对比
    京ICP备2026067502号-1京公网安备11010502063487号