bigywater.tech
Django · 39

自定义 Lookup 与 Transform

Lookup 和 Transform 是 ORM 的扩展点:一个生成比较,一个生成函数。这篇各写一个,看它们怎么出现在 SQL 里、怎么叠加,再验证函数索引到底什么时候能用上。

叠加
score__abs__gt=498 → ABS(score) > 498,Transform 后面还能接 lookup
Index Scan vs Seq Scan
Lower(name) 索引对 annotate(Lower).filter 生效;__iexact 生成 UPPER(name),用不上
注册太宽
注册在 Field 上,CharField 也有 __abs,错误要到数据库才报

自定义 Lookup 与 Transform

实测 · 本机执行输出
score__ne=5          →  WHERE "lab_person"."score" <> 5   行数: 19999
score__abs=5         →  WHERE ABS("lab_person"."score") = 5   行数: 2
score__abs__gt=498   →  WHERE ABS("lab_person"."score") > 498   行数: 19003
score__abs__ne=5     →  WHERE ABS("lab_person"."score") <> 5   行数: 19998

Lookup 继承 Lookup 并实现 as_sql;Transform 继承 Transform,给出 function 和 output_field。装饰器 @Field.register_lookup 把它们挂到字段类上。

使用与边界

实测 · 本机执行输出
values_list("score__abs")[:3] → [500, 499, 498]
order_by("score__abs").first().score = 0
CharField 上有 abs 吗: ProgrammingError: function abs(character varying) does not exist
LINE 1: ...me", "lab_person"."score" FROM "lab_person" WHERE ABS("lab_p..

Transform 可以出现在 filter、values、order_by。想限制只给特定字段用,注册到具体的字段类(如 IntegerField)而不是 Field,并且最好在 lookup 里做类型检查(本文没有做)。

和函数索引

实测 · 本机执行输出
annotate(Lower).filter(l="name777")   → Index Scan using person_lower_name_idx on lab_person  (cost=0.29..8.31 rows=1 width=53)
filter(name__iexact="name777")       → Seq Scan on lab_person  (cost=0.00..428.00 rows=100 width=21)   SQL:  WHERE UPPER("lab_person"."name"::text) = UPPER(name777)
filter(name="Name777")               → Seq Scan on lab_person  (cost=0.00..378.00 rows=1 width=21)

数据库只有在查询里的表达式与索引定义完全一致时才会用索引:Lower(name) 索引对应 LOWER(name),而 Django 的 iexact 在 PostgreSQL 上生成的是 UPPER(name) = UPPER(…)。要让 iexact 用上索引,索引表达式要写成 Upper(本文没有验证这一点)。

注册的范围

实测 · 本机执行输出
IntegerField().get_lookup('ne') = NotEqual  CharField().get_lookup(ne) = NotEqual
IntegerField 共有 19 个 lookup,含 ne: True  get_transform(abs) = AbsoluteValue

注册是进程级别的全局操作,模型导入时执行一次。

分步演示:Lookup 与 Transform

自定义 Lookup 与 Transform
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。PostgreSQL 16.15(本机临时实例,端口 9403),2 万行,ANALYZE 之后 EXPLAIN
    • 没有验证:register_lookup 上下文管理器(局部注册)、Transform 的 bilateral、自定义 lookup 在 MySQL / SQLite 上的 SQL、索引对 iexact 在 Upper 索引下是否生效
    京ICP备2026067502号-1京公网安备11010502063487号