bigywater.tech
Django · 41

使用 QuerySet.explain() 分析执行计划

ORM 生成的 SQL 慢,第一步不是猜,是看执行计划。QuerySet.explain() 一行代码就能做到。这篇在 20 万行的 PostgreSQL 表上,看索引前后计划的变化,以及 analyze 选项。

3774 → 536
给 customer 加索引前后,PostgreSQL 估算的查询成本
Seq Scan
匹配大多数行(amount > 100)时,即使有索引也是全表扫描;这里 amount 本来也没有索引
analyze=True
真的执行语句,给出实际耗时;SQLite 的 explain 输出只有一行

explain() 的基本用法

实测 · 本机执行输出
PostgreSQL(customer 没有索引):
Seq Scan on lab_order  (cost=0.00..3774.00 rows=199 width=20)
  Filter: (customer = 42)

SQLite:
2 0 216 SCAN lab_order

explain() 返回字符串。PostgreSQL 给出包含成本估算的计划树;SQLite 给的是「SCAN / SEARCH」的简要计划。

索引带来的变化

实测 · 本机执行输出
customer 上有索引之后:
Bitmap Heap Scan on lab_order  (cost=5.84..536.83 rows=199 width=20)
  Recheck Cond: (customer = 42)
  ->  Bitmap Index Scan on order_customer_idx  (cost=0.00..5.79 rows=199 width=0)
        Index Cond: (customer = 42)
amount__gt=100(amount 没有索引,匹配大多数行):
Seq Scan on lab_order  (cost=0.00..3774.00 rows=199921 width=20)
  Filter: (amount > 100)

加索引后要 ANALYZE 更新统计信息,规划器才会据此选计划。选择性差的条件(匹配大多数行)全表扫描本来就更快。

analyze 与其它选项

实测 · 本机执行输出
Bitmap Heap Scan on lab_order  (cost=5.84..536.83 rows=199 width=20) (actual time=0.038..0.159 rows=200 loops=1)
  Recheck Cond: (customer = 42)
  Heap Blocks: exact=200
  ->  Bitmap Index Scan on order_customer_idx  (cost=0.00..5.79 rows=199 width=0) (actual time=0.021..0.021 rows=200 loops=1)
        Index Cond: (customer = 42)
Planning Time: 0.048 ms
Execution Time: 0.175 ms
--- verbose + format=json 的第一层键:
['Plan']
--- buffers=True(需要 analyze):
['        Index Cond: (customer = 42)', '        Buffers: shared hit=2', 'Planning Time: 0.010 ms', 'Execution Time: 0.055 ms']

analyze=True 让数据库真正执行并统计,对比「估算 rows」与「actual rows」能发现统计信息过期。buffers、verbose、format 是 PostgreSQL 的选项(其它数据库支持的选项集不同,本文没有逐个测)。

限制

实测 · 本机执行输出
explain 只对 QuerySet;count() 不能 explain: AttributeError: 'int' object has no attribute 'explain'
对 annotate 聚合查询的 explain:
Finalize GroupAggregate  (cost=4038.75..4039.01 rows=2 width=12)

只有返回 QuerySet 的链能 explain();count()、exists() 的计划要自己写等价的 QuerySet 或直接 EXPLAIN。

分步演示:读执行计划

QuerySet.explain()
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。PostgreSQL 16.15(本机临时实例,端口 9410),20 万行,ANALYZE 之后;SQLite 只有 1000 行
    • 没有验证:MySQL 的 explain 输出与选项、大表上 analyze 的副作用(对 UPDATE/DELETE 的 explain)、复杂 JOIN 的计划阅读、pg_stat_statements
    京ICP备2026067502号-1京公网安备11010502063487号