bigywater.tech
Django · 40

Django ORM 如何生成 SQL

.filter().exclude().select_related() 一路链下来,SQL 是什么时候、由谁写出来的?这篇拆开 QuerySet.query,看它的内部结构,再让同一个 Query 在 SQLite 与 PostgreSQL 上各编译一次,并量三个阶段各花多少时间。

Query 对象
QuerySet.query 是条件树 + 状态,不是 SQL 字符串;compiler 才生成 SQL
后端方言
同一个 Query:SQLite 用 LIKE,PostgreSQL 用 UPPER(…) LIKE UPPER(…)
65 + 84 µs
本例构建 QuerySet 与编译 SQL 的纯 Python 开销

Query 对象

实测 · 本机执行输出
type(qs.query) = django.db.models.sql.query.Query
where 节点: WhereNode  子条件: ['IntegerGreaterThan:IntegerGreaterThan(Col(lab_book, lab.Book.price), 10)', "IContains:IContains(Col(lab_book, lab.Book.title), 'x')", "WhereNode:(NOT (AND: Exact(Col(lab_author, lab.Author.name), 'bob')))"]
alias_map(参与查询的表别名): ['lab_book', 'lab_author']
order_by = ('-price',)  low_mark/high_mark = (0, 5)  select_related = {'author': {}}

每次链式调用(filter、exclude …)都是克隆一个 QuerySet 并修改它的 Query。Query 里 where 是一棵 WhereNode 树,各种 lookup 是树上的节点。

编译

实测 · 本机执行输出
[sqlite] SELECT "lab_book"."id", "lab_book"."title", "lab_book"."price", "lab_book"."author_id", "lab_author"."id", "lab_author"."name" FROM "lab_book" INNER JOIN "lab_author" ON ("lab_book"."author_id" = "lab_author"."id") WHERE ("lab_book"."price" > %s AND "lab_book"."title" LIKE %s ESCAPE '\' AND NOT ("lab_author"."name" = %s)) ORDER BY "lab_book"."price" DESC LIMIT 5
   params = (10, '%x%', 'bob')
[postgresql] SELECT "lab_book"."id", "lab_book"."title", "lab_book"."price", "lab_book"."author_id", "lab_author"."id", "lab_author"."name" FROM "lab_book" INNER JOIN "lab_author" ON ("lab_book"."author_id" = "lab_author"."id") WHERE ("lab_book"."price" > %s AND UPPER("lab_book"."title"::text) LIKE UPPER(%s) AND NOT ("lab_author"."name" = %s)) ORDER BY "lab_book"."price" DESC LIMIT 5
   params = (Int4(10), '%x%', 'bob')

SQLCompiler 遍历这棵树,调用每个节点的 as_sql,把 Col、lookup、JOIN、ORDER BY 拼成 SQL,后端(connection.ops、connection.features)决定方言细节。所以同一份 ORM 代码换数据库时,SQL 会变、性能特征也会变。

每个阶段的开销

实测 · 本机执行输出
构建 QuerySet(filter/exclude/select_related/order_by/切片):    65.3 µs
编译成 SQL(get_compiler().as_sql()):                           84.3 µs
从头到尾 list(build())(含 SQLite 执行与构造对象):         262.2 µs

构建与编译是纯 Python,没有缓存,每次调用都做一遍。对绝大多数请求这些微秒级开销不是瓶颈;热点路径上可以用 values_list / 原生 SQL / 缓存结果绕开(本文没有测这些替代方案)。

怎么看 SQL

实测 · 本机执行输出
str(qs.query)        → SELECT "lab_book"."id", "lab_book"."title", "lab_book"."price", "lab_book"."author_id", "lab_author"."id", "lab_author". ...  (参数直接拼进去,只用于阅读,不保证可直接执行)
sql_with_params()    → params = (10, '%x%', 'bob')
CaptureQueriesContext → 实际执行: SELECT "lab_book"."id", "lab_book"."title", "lab_book"."price", "lab_book"."author_id", "lab_author" ...
connection.queries 需要 DEBUG=True 或 CaptureQueriesContext

最可靠的是看真实执行的语句(CaptureQueriesContext);str(qs.query) 只适合阅读。

分步演示:从 QuerySet 到 SQL

ORM 生成 SQL
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite 与 PostgreSQL 16.15(本机临时实例,端口 9404);耗时为进程内 2000 次平均
    • 没有验证:多数据库路由(using=)下的编译差别、子查询与窗口函数的编译、query.explain 的输出、连接池对总耗时的影响
    京ICP备2026067502号-1京公网安备11010502063487号