你是不是也遇到过这种情况模型写好了数据也塞进去了结果一到查询环节就卡壳。filter和get傻傻分不清Q对象不知道怎么用聚合查询更是看得一头雾水。网上教程东一篇西一篇今天翻到这个明天翻到那个最后还是在原地点打转。这篇文章就把 Django ORM 单表查询这件事讲透。从模型定义到环境准备从all()到get()从常用查询到进阶玩法再到调试方法一条线捋下来。每个知识点都配了实操代码和踩坑提醒照着做就能跑通。我写的时候尽量用大白话把抽象概念掰开揉碎让刚接触 Django 的新手也能跟上节奏有点基础但总在某些细节上卡壳的朋友也能在文章里找到答案。1. 查询前置准备模型与数据环境1.1 模型定义示范Django 单表查询说白了就是对着一个模型类进行操作。所有查询方法的演示都围绕模型展开所以我先给出一个统一使用的模型。为了让大家有代入感我用一个学生管理场景包含Student模型这也是单表查询最经典的应用场景之一。from django.db import models class Student(models.Model): GENDER_CHOICES [ (M, 男), (F, 女), (U, 未知), ] name models.CharField(姓名, max_length50) age models.IntegerField(年龄) gender models.CharField(性别, max_length1, choicesGENDER_CHOICES) grade models.CharField(年级, max_length20) score models.DecimalField(综合成绩, max_digits5, decimal_places2, nullTrue, blankTrue) is_graduated models.BooleanField(是否毕业, defaultFalse) enroll_date models.DateField(入学日期, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table student ordering [id] def __str__(self): return f{self.name} ({self.grade})这个模型里囊括了字符串、整数、布尔、日期、Decimal 等几种常用字段类型后面演示查询条件时基本都能覆盖到。db_table student指定了表名实际项目中建议显式定义不然 Django 会默认生成应用名_模型名这种格式的表名后期维护时看着容易懵。1.2 数据准备与 shell 环境模型定义好了得先准备测试数据。我个人强烈建议用 Django Shell 来跑查询语句因为它能即时打印结果非常适合学习阶段反复调试。连接数据库的操作就不多说了重点是把数据准备好。# 进入 Django shell python manage.py shell from myapp.models import Student # 批量创建测试数据 Student.objects.create(name张三, age18, genderM, grade高一, score85.50) Student.objects.create(name李四, age19, genderF, grade高一, score92.00) Student.objects.create(name王五, age17, genderM, grade高二, score78.25) Student.objects.create(name赵六, age20, genderF, grade高三, score88.00) Student.objects.create(name孙七, age18, genderU, grade高三, scoreNone) Student.objects.create(name周八, age19, genderF, grade高二, score95.50) Student.objects.create(name吴九, age18, genderM, grade高三, score60.00)数据里故意留了一个scoreNone的后面演示空值查询时就用它当典型例子。插入完成后可以先跑一句Student.objects.count()看看返回是不是 7这样可以确认数据已经进去了。2. 单表查询核心方法all / get / filter2.1 三种基础方法的用法与区别Django ORM 查询的根基就三个方法all()、get()、filter()。看似简单实则里面的门道不少初学者最容易在这上面栽跟头。先看基本用法# 查询所有记录 qs Student.objects.all() print(qs) # 输出 QuerySet包含所有数据 # 按主键查询单条记录 stu Student.objects.get(id1) print(stu.name) # 张三 # 按条件过滤返回 QuerySet freshmen Student.objects.filter(grade高一)三个方法最大的区别在于返回值类型。all()和filter()返回的是QuerySet对象这是一个可迭代的集合而get()返回的是单个模型实例。get()这个名字就透露了它的规则——只能查到一条记录查不到会抛DoesNotExist查到多条会抛MultipleObjectsReturned。但请记住一个核心原则get 查不到就报错这在实际业务中非常不友好。通常我更建议用filter().first()来替代它stu Student.objects.filter(id1).first().first()在结果为空时返回None不会抛异常这样在写视图函数时就能直接用if stu:判断省去异常处理的负担。这个习惯对开发效率提升极其明显。2.2 QuerySet 惰性机制与常见误区QuerySet有一个“惰性求值”特性就是在你真正使用结果之前Django 并不会立刻执行数据库查询。什么叫“真正使用”就是迭代、切片、打印、布尔判断等操作。# 这行不会执行数据库查询 qs Student.objects.all() # 这行才会真正执行 print(list(qs)) # 或 for stu in qs 等 # 甚至这样也算触发 if qs: print(有数据)很多新手的误区在于以为拿到QuerySet那一刻数据库就忙活起来了。实际上Django 会把QuerySet攒着直到你“消费”它。这带来的核心影响是如果你多次操作同一个QuerySet每次操作都可能触发新的数据库查询。一个小技巧在调试时想确认到底执行了几条 SQL可以加一行配置# settings.py LOGGING { version: 1, handlers: { console: { class: logging.StreamHandler, }, }, loggers: { django.db.backends: { level: DEBUG, handlers: [console], }, }, }配上之后控制台会打印出所有 SQL 语句非常直观。这也是我排查 N1 问题和性能瓶颈时几乎必开的配置。3. filter 进阶常用条件、链式调用与模糊查询3.1 链式调用的精髓filter()可以叠加使用这一点是 Django ORM 最为人称道的设计之一。多个filter()会按 AND与逻辑执行作用等同于一个 filter 里写多个参数但代码可读性会好很多# 写法一多个参数 freshmen Student.objects.filter(grade高一, genderM) # 写法二链式调用执行结果与上面完全一致 freshmen Student.objects.filter(grade高一).filter(genderM) # 写法三先查一部分再在已有条件上追加 base_qs Student.objects.filter(grade高一) males base_qs.filter(genderM)链式调用最大的价值在于“按需叠加分步构建”。比如用户在页面上按不同条件筛选时你不可能为每个组合写一遍查询代码更合理的做法是qs Student.objects.all() if grade: qs qs.filter(gradegrade) if gender: qs qs.filter(gendergender) if min_score: qs qs.filter(score__gtemin_score) # 最后统一执行 results list(qs)这种写法就叫“动态构建查询条件”在实际项目里运用得非常广泛。有人可能会疑惑多调用几次filter()会不会影响性能我的答案是只要没触发真正的数据库查询构建多少个 filter 都只是内存中的组合操作性能几乎零开销。真正花费时间的是最后一步从数据库拉取数据的过程。3.2 模糊查询与常用字段条件Django 的 ORM 支持用字段名__查询类型这样的双下划线语法表达各种查询条件。我整理了一份单表查询最常用的条件速查表查询场景写法说明精确匹配filter(name张三)完全相等模糊匹配filter(name__contains张)区分大小写的包含忽略大小写filter(name__icontainszhang)不区分大小写的包含开头匹配filter(name__startswith张)按前缀匹配结尾匹配filter(name__endswith三)按后缀匹配范围区间filter(age__range(18, 20))左右都含大于/小于filter(age__gt18)18 岁以上不含 18大于等于/小于等于filter(age__gte18)18 岁及以上空值判断filter(score__isnullTrue)查询 score 为空非空判断filter(score__isnullFalse)查询 score 不为空多值匹配filter(grade__in[高一, 高三])匹配其中一个即可实际操作中最容易记混的是contains和icontains。前者区分大小写后者不区分。如果数据库里存的是英文名用contains查Zhang和zhang结果是不同的用icontains就能避免大小写问题。对中文来说二者差异不大但英文场景下就需要格外注意。模糊查询在 MySQL 底层对应的是LIKE语句。contains对应LIKE %张%startswith对应LIKE 张%。需要注意的是在数据量巨大的表中LIKE %xxx%这种写法命中索引的可能性很低全表扫描的概率很高。开发阶段无所谓但生产环境一定要压测评估。3.3 exclude 与反向筛选与filter对应的还有一个反向筛选方法exclude()看名字就知道是“排除符合条件的记录”# 查询所有高三以外的学生 not_senior Student.objects.exclude(grade高三) # 也可以多个条件叠加 not_senior_male Student.objects.exclude(grade高三, genderM) # 链式使用 young_not_graduated Student.objects.filter(age__lt19).exclude(is_graduatedTrue)这里有一个容易出坑的点exclude多个条件之间的关系是OR或而不是 AND。即exclude(grade高三, genderM)排除的是“高三”或“男生”而不是“高三的男生”。这一点官方文档写得很清楚但很多人初次接触时下意识当成 AND结果查出来的数据和自己预想完全对不上。这个特性的逻辑在于排除多个条件时任一条件命中的记录都不能出现在结果集中所以条件之间的联系必须理解成“或”。刚接触时更容易理解的方式是先过滤出要排除的集合再从总数中取差集。虽然实现方式不同但理解角度是相通的。4. 排序、分页与聚合查询4.1 排序的多种姿势查询结果默认按模型Meta里定义的ordering排序也就是前面模型里的[id]。如果没有定义orderingDjango 会按数据库的自然顺序返回大多数情况下等于主键顺序但开发时必须明确一点不设置排序就不保证顺序。使用order_by()可以覆盖默认排序支持多个字段支持反向排序# 按成绩从低到高 sorted_by_score Student.objects.order_by(score) # 按成绩从高到低 sorted_desc Student.objects.order_by(-score) # 多字段混合排序先按年级升序同年级内按成绩降序 mixed Student.objects.order_by(grade, -score) # 条件排序和排除可以链式组合 filtered_sorted Student.objects.filter(score__isnullFalse).order_by(-score)这里有一个经验之谈order_by(字段名)会把空值排在最前面。NULL 值在数据库排序中默认排在最前如果希望空值排在最后在不同数据库里有不同写法。PostgreSQL 支持NULLS LASTMySQL 不直接支持这种细节在单表场景不一定用得上但遇到了要知道有这么回事。另外提醒一句排序字段尽量带上索引。如果一张表几十万行每次查询都在一个没有索引的字段上排序总有一天会慢到怀疑人生。Django 里给字段加索引很简单在模型定义里加一个db_indexTrue就行。4.2 分页取数与切片操作分页是 Web 开发中最常见的需求之一。获取前 N 条数据只需要用 Python 的切片语法# 获取前 3 条 top3 Student.objects.order_by(-score)[:3] # 跳过前 2 条取接下来的 3 条 next3 Student.objects.order_by(-score)[2:5] # 只取一条 latest Student.objects.order_by(-id)[:1].first() # 获取倒数若干条需要先反向排序再切片 last2 Student.objects.order_by(-id)[:2]切片操作有两个隐藏要点第一切片相当于 SQL 层面的LIMIT和OFFSET不会先加载全部数据再在内存里切。因此性能可以放心这种方式对于大表也能保持高效。第二对 QuerySet 做切片后返回的仍然是 QuerySet依旧是惰性的。但如果你做的是qs[2]这种“只拿第 3 条”的操作Django 会立即执行 SQL并且在 SQL 里体现为LIMIT 1 OFFSET 2。这个机制在开发时需要留意尤其在循环里反复用qs[i]获取数据会带来严重的性能问题每次取一条都要发一条 SQL。4.3 聚合查询count / sum / avg单表查询的进阶玩法就是聚合了。Django 提供了一套封装良好的聚合操作方法覆盖了统计场景的大多数需求from django.db.models import Count, Sum, Avg, Max, Min # 查询总数 total Student.objects.count() # 等价写法 total Student.objects.all().aggregate(Count(id)) # 查询成绩总和 total_score Student.objects.filter(score__isnullFalse).aggregate(Sum(score)) # 查询平均分 avg_score Student.objects.filter(score__isnullFalse).aggregate(Avg(score)) # 同时查多个聚合值 stats Student.objects.aggregate( totalCount(id), avg_ageAvg(age), max_scoreMax(score), min_scoreMin(score), )aggregate()和annotate()是容易混淆的一对兄弟。aggregate()返回的是一个字典对整个结果集计算汇总值annotate()则是在每个分组上计算聚合值返回的仍然是 QuerySet。这个区别在单表查询中体现为“是否按某个字段分组”from django.db.models import Count # 按年级统计人数返回 QuerySet每行是一个年级 grade_counts Student.objects.values(grade).annotate( totalCount(id) ) # 具体输出长这样 # QuerySet [{grade: 高一, total: 2}, {grade: 高二, total: 2}, {grade: 高三, total: 3}]values(grade)的功能是“按 grade 分组”annotate再给每组加上统计字段。这是对年级进行人数统计最高效的写法比在 Python 里反复循环拼接要专业得多性能也甩出好几条街。实际业务中“分组统计”是高频需求比如统计各商品销量、各部门人数、各月份订单量思路完全一样。5. 复杂条件组合Q 对象与 F 表达式5.1 Q 对象的用法与 OR 查询前面演示的所有查询都是 AND 关系。如果业务要求“成绩大于 90 或年级为高三”怎么办Django 提供了Q对象解决这个问题from django.db.models import Q # 查询成绩大于90 或 年级为高三的学生 result Student.objects.filter(Q(score__gt90) | Q(grade高三)) # 查询既不是高一也不是高二的学生 result Student.objects.exclude(Q(grade高一) | Q(grade高二)) # Q 对象可以混合普通条件 result Student.objects.filter( Q(grade高三) | Q(grade高二), age__gte18, # 这个条件与上面的 Q 条件是 AND 关系 )上表中最后一种写法是重点中的重点普通关键字参数和Q对象同时出现时普通参数作为 AND 条件与Q结合。“与”用“或”用|“非”用~。这就是一个完整的布尔表达式系统复杂筛选逻辑的根基就在这。Q对象在动态查询条件场景中同样价值巨大。比如搜索页面用户输入的关键词可能匹配姓名也可能匹配年级这时用一个Q对象就能把两个条件的“或”关系表达出来不用写一堆if-else分支去拼接字符串既优雅又不容易出错。5.2 F 表达式字段间比较F表达式解决的是“两个字段之间比较”这类问题。举个例子查询“男生的数量大于女生”或“更新所有人成绩加五分”这种场景用普通 Python 变量是做不到的因为需要在数据库层面完成字段间的比较和运算。from django.db.models import F # 场景查询入学日期年份大于 2020 的记录 # 先直观感受 F 表达式的作用域 Student.objects.create( name测试, age18, genderM, grade高一, scoreF(age) 10 # 注意这里 F 不能这么用仅作错误示例 )上面的例子是错的F不是用来创建数据时做运算的。正确的用法是在查询和更新场景中from django.db.models import F # 把所有学生的综合成绩加 5 分 Student.objects.update(scoreF(score) 5) # 查询入学日期晚于今年1月1日的学生能理解思路即可 from datetime import date target_date date(date.today().year, 1, 1) result Student.objects.filter(enroll_date__gtetarget_date) # 查询“年龄大于综合成绩”的学生纯示意分数和年龄量纲不同 result Student.objects.filter(age__gtF(score))第一个场景是F表达式最实际的用途在数据库层面直接对某个字段做更新而不是先把数据取到 Python 内存中修改再写回去。后者要循环处理遇到并发还会产生数据竞争前者发送的 SQL 是UPDATE student SET score score 5原子性强且高效。F表达式还有一个优势是避开“取数据-改数据-写数据”三步带来的时间窗口问题。在并发环境下用 Python 读出来再写回去极易出现“丢失更新”。用F直接在数据库内部完成运算可以规避这类问题。6. 查询实战数据展示与防坑指南6.1 把查询结果传到模板单表查询最终的目标是把数据展示到页面上。一个最常见的流程是视图里查数据传到模板模板里遍历。以“展示高一学生列表”为例# views.py from django.shortcuts import render from .models import Student def student_list(request): students Student.objects.filter(grade高一).order_by(-score) context { students: students, total: students.count(), } return render(request, student/list.html, context)模板里这样写table thead tr th姓名/th th年龄/th th成绩/th /tr /thead tbody {% for stu in students %} tr td{{ stu.name }}/td td{{ stu.age }}/td td{{ stu.score|default:暂无成绩 }}/td /tr {% empty %} trtd colspan3暂无数据/td/tr {% endfor %} /tbody /table这里有两个小细节值得新手关注。第一是{% empty %}标签。如果查询结果为空模板不会报错而是会执行{% empty %}中的内容。用这个标签可以让页面在没有数据时显示友好提示而不是一张空白表格。第二是|default:暂无成绩过滤器。score字段允许为None直接在页面上一显示会变成空白加上过滤后就能对空值做出友好展示。6.2 表字段遍历与动态展示模板语法里有一个实用的技巧如果需要在模板中根据某个字段名动态取值可以用{% with %}配合变量拼接其实 Django 模板不支持变量名拼接但这种需求可以通过把字段名作为字符串传入values_list或getattr来解决。模板里最常用的是直接通过属性访问# 视图里把想展示的字段列表传给模板 field_list [name, age, grade, score]模板中遍历字段名再通过obj|getattr方式取值需要自定义过滤器。不过对大多数场景来说直接在模板中写死字段名更直白也更不容易出错。单表查询的模板展示重点在于查询做好排序和过滤展示层保持简单。不要在模板里写复杂逻辑这是 Django 的哲学。6.3 常见坑位排查速查表单表查询的坑说来说去就那么几个但每一个都可能浪费你半小时甚至更久。我把最常见的陷阱和解决方法做成一张表格方便对照排查现象根本原因解决方案查询结果全表返回过滤条件无效把all()和filter()分开用了比如先查 all 之后再内存过滤在查询层面用filter()不要先取全量再 Python 过滤get()频繁抛异常数据本身有多条或零条换成.filter().first()列表页展示字段为空字段本身是None模板没做空值处理模板里加default过滤器使用contains查英文数据无结果大小写不匹配换成icontains排序结果不达预期多个字段排序时顺序写反按 SQL 排序逻辑检查order_by字段顺序更新时把所有记录都改了用了update()但没有加条件先.filter()限制范围再update查询耗时长数据量大查询条件字段没加索引在模型字段上设置db_indexTrueexclude结果与预期不符多个条件之间是 OR 逻辑用~Q(...)替代6.4 调试利器QuerySet 的 SQL 查看写查询的时候心里时刻要有一句话“这个查询最终生成的 SQL 长什么样”如果不清楚就说明你对 ORM 的理解还停留在背 API 的阶段。有一个非常实用的调试方法# 在 shell 中打印 QuerySet 对应的 SQL 语句 qs Student.objects.filter(grade高三).order_by(-score) print(qs.query) # 输出类似SELECT * FROM student WHERE grade 高三 ORDER BY score DESCqs.query属性专门用于输出 QuerySet 对应的 SQL 语句可以帮助你直观理解 Django 在数据库层面到底做了什么。另一个角度是在理解了底层 SQL 之后回到 Django 查询 API 时会非常有底气。因为你知道filter(score__gte90)最终生成的是WHERE score 90遇到数据库层面的优化问题也能独立找到思路。如果要查看带参数的完整 SQL需要开启日志。用上面提到过的django.db.backends的日志配置控制台会打印执行的全部 SQL 和对应参数。这在调试问题时几乎是终极武器。7. 补充场景与避坑总结7.1 空值处理的进阶推荐在查询结果中遇到空值None时有人喜欢在 Python 端补默认值有人喜欢在数据库端处理。如果数据量不大Python 端处理完全可行。但要养成一个习惯查询阶段就把空值过滤掉或处理掉不要等展示时再临时处理。# 查询时忽略没有成绩的 Student.objects.filter(score__isnullFalse) # 查询时把空值替换为 0 分数据库端处理 from django.db.models import Value from django.db.models.functions import Coalesce qs Student.objects.annotate( display_scoreCoalesce(score, Value(0)) )Coalesce函数能实现在 SQL 层面把NULL转成默认值。第一次用的时候你可能会觉得一个空值而已搞这么复杂干什么。但真正的好处在于这个转换在数据库查询阶段就完成了不会对应用层性能和代码复杂度产生额外负担处理十万条数据时差距就很明显。7.2 工具函数封装复用你的查询开发越深入越会发现查询逻辑的复用价值。与其在视图里反复写同一套查询不如封装成独立的工具函数或模型的静态方法class Student(models.Model): # ...字段定义... staticmethod def get_avg_score_by_grade(grade): return Student.objects.filter(gradegrade).aggregate(Avg(score))调用时只需要Student.get_avg_score_by_grade(高三)逻辑集中且便于测试。这个习惯也符合 Django 项目常见的“业务逻辑下沉到模型层”的做法视图保持轻量维护性大大提升。如果一个查询逻辑要被多处复用封装它是唯一的正解。7.3 个人操作心得单表查询是整个 Django ORM 的地基也是后续学习跨表查询、性能调优的前提。它本身入门门槛不高函数就那几个参数就是双下划线语法但真正的分水岭在于你如何组织它们去解决实际业务问题。按我自己的开发习惯总结了几条实操铁律可以给大家当个参考第一规则上只做必要查询。减少全表all()的使用频率哪怕是控制台调试也尽量带上过滤条件。第二所有多条件筛选写成链式调用不要追求“一行写完”可读性比炫技重要得多。第三涉及聚合统计时提前算好数据结构直接把查询结果按业务需求聚合好不要在 Python 里做二次内存统计。第四写完查询后打印一次qs.query确认 SQL 与业务意图一致这是对自己代码负责的表现。记住这些习惯写单表查询就能少踩很多坑。学习 ORM 就是一个不断练习、不断看生成的 SQL、不断修正自己认知的过程。把这些基本功打扎实了再去碰 select_related、prefetch_related 这些跨表优化技巧就会觉得水到渠成、顺理成章。