Django ORM 写过滤条件,核心是用 queryset.filter() 和 exclude() 把 Python 表达式翻译成 SQL 的 WHERE 子句,字段名后缀决定比较运算符,链式调用决定条件之间是 AND 还是 OR。动手之前先确认两件事:你查的是哪个模型、字段类型是什么;条件之间是“同时满足”还是“满足其一”。这两点判断错,写得再顺手也会查错数据。
过滤条件建议按“模型字段 → 运算符后缀 → 条件组合方式 → 是否触发额外查询”的顺序写。普通等值用filter(field=value),范围、模糊、空值分别用__gte、__icontains、__isnull等后缀;多条件链式默认为 AND,OR 需要用Q对象。写完后用str(queryset.query)看生成的 SQL,再用.count()或实际返回行数比对预期,确认没有漏掉空值、类型转换和跨关联表重复行的问题。
先确认当前场景和目标
在写过滤条件前,需要先把问题说清楚,否则容易写出“能跑但结果不对”的查询。可以先确认以下几项:
- 查哪个模型、哪些字段:字段是
CharField、IntegerField、DateTimeField还是外键,直接决定能用哪些后缀。对整数列用__icontains通常不会报错但语义不对。 - 条件之间是什么关系:是“用户 ID 等于 3 且状态为已支付”,还是“状态为已支付或已退款”。前者用连续
filter()即可,后者必须用Q组合。 - 是否涉及关联表:跨外键或
ManyToMany过滤时,结果可能出现重复行,需要判断是否要加distinct()。 - 空值和默认值怎么算:数据库里 NULL 不参与普通等值比较,
filter(x=0)不会匹配 NULL,这类业务空值要单独用__isnull或__isnull=False处理。
如果这些还没确认,建议先打印模型定义或看 Model._meta.fields,比凭印象拼字段名可靠。

按这个顺序处理
第一步,用最基本的等值条件把查询跑通,再逐步加后缀。下面这段是常见骨架,字段名和值按实际模型替换:
from django.db.models import Q
from datetime import timedelta
from django.utils import timezone
# 1. 基础等值 + 范围 + 模糊
qs = Order.objects.filter(
status="paid",
amount__gte=100,
created_at__gte=timezone.now() - timedelta(days=7),
)
# 2. 排除某类记录,exclude 也是条件
qs = qs.exclude(note__isnull=True)
# 3. OR 条件用 Q,最后传给 filter
qs = Order.objects.filter(
Q(status="paid") | Q(status="refunded"),
user_id=3,
)
# 4. 跨关联表过滤
qs = Order.objects.filter(items__sku="A-001").distinct()
# 5. 看实际生成的 SQL
print(qs.query)
第二步,判断条件组合方式。链式 filter(a=1).filter(b=2) 和 filter(a=1, b=2) 在单表普通条件下结果一致,但涉及到多值关联表时,分开写会变成两次 JOIN,语义可能与预期不同。需要“同一关联对象同时满足多个条件”时,建议合并到同一个 filter() 里,或明确用 Q 构造。

第三步,注意 exclude() 和 filter() 混合时的逻辑。对可空外键使用 exclude(rel__field=value),某些版本的 Django 生成的 SQL 会把 NULL 行排除掉,通常需要显式补 Q(rel__isnull=True)。这类细节建议直接用 print(qs.query) 核对,不要只看 Python 层判断。
第四步,控制返回列和数量。只需要少数字段时用 .values() 或 .only(),但要清楚 .values() 返回的是字典而不是模型实例,后续属性访问方式要相应调整。

改完后怎么验证
验证动作建议按下面顺序做,每一步都有可观察的信号:
- 打印 SQL:执行
print(qs.query),确认 WHERE 子句里出现了预期的字段和运算符,参数占位符数量与传入值一致。 - 比对行数:用
qs.count()和手工用管理后台或另一条简单查询得到的结果对一下。如果数量明显偏大,优先怀疑跨关联表导致的重复行。 - 检查空值记录:拿一条字段为 NULL 的记录,看它是否被正确包含或排除。这是过滤条件最容易出偏差的地方。
- 检查 OR 边界:把
Q条件中仅满足一项的数据拿出来,确认它们确实出现在结果里,而不是被外层 AND 条件悄悄过滤掉。
如果查询和写入在同一业务流里,建议在测试数据库中用固定的几条样本记录跑一遍,预期信号是返回的记录集合与手工构造的预期集合完全一致,包括顺序无关、数量一致、边界记录在内。
容易忽略的边界
- 字符串大小写与排序规则:
__contains和__icontains在不同数据库上的行为不完全一致,SQLite 对大小写不敏感,部分配置下的 MySQL 或 PostgreSQL 则取决于排序规则,需要结合环境确认。 - 时区:对
DateTimeField用日期范围过滤时,USE_TZ配置会影响比较结果。通常建议传入带时区的 datetime,不要手写无时区的字符串。 - 索引与字段类型:对文本字段做模糊匹配可能不走索引,过滤条件写对了但查询变慢是另一类问题,不要把它和条件正确性混在一起判断。
- 版本差异:
exclude()对 NULL 的处理、Q对象嵌套写法在不同 Django 大版本上存在细微差别,升级后建议重跑关键查询。不确定时先回退到显式Q组合,通常更容易看懂。 - 不适用条件:需要复杂聚合或窗口函数时,单靠
filter()表达会变得难读,这时更适合用annotate配FilteredRelation或直接写原生 SQL,不要硬塞进一个过滤链里。