Day 04 · 让数据活起来:QuerySet 与 Django ORM 实战

发布时间:2026/9/8 16:34:59
Day 04 · 让数据活起来:QuerySet 与 Django ORM 实战 摘要数据写进数据库只是第一步怎么高效取出来才是真功夫。本篇带你掌握 Django ORM 的核心操作——增删改查、过滤排序、聚合统计搞清楚关联查询中select_related和prefetch_related的区别拆解 N1 问题这个最常见的性能杀手最后初体验 Django 5.x 的异步 ORM。所有示例基于 Day03 的商城 Model。数据写进去了然后呢上篇我们把 Model 建好、跑通了migrate数据库表一张张建出来了。但你很快会发现一个尴尬的事实数据写进去容易取出来才是真功夫。总不能每次都打开 MySQL 命令行手写 SQL 吧你的小程序需要「商品列表页」要按分类筛选、按价格排序、分页显示——这些逻辑写在哪怎么高效实现更现实的问题怎么查出「价格低于 100 且库存大于 0」的商品怎么算每个分类下有多少件商品一个订单关联着用户、商品明细怎么一次性全取出来页面加载越来越慢是不是数据库查询在拖后腿传说中的 N1 问题到底是什么怎么排查别急今天一次讲清楚。从增删改查到关联查询性能优化再到 Django 5.x 异步 ORM一步步来。ORM 是什么一句话讲透ORM Object-Relational Mapping对象关系映射。翻译成人话让你用 Python 代码操作数据库而不用写 SQL。举个例子你想查所有上架的商品# ❌ 原生 SQLcursor.execute(SELECT * FROM shop_goods WHERE is_on_sale 1)# ✅ Django ORMGoods.objects.filter(is_on_saleTrue)两种写法等价但 ORM 的好处很明显不用记 SQL 语法——会写 Python 就行数据库无关——换 MySQL 到 PostgreSQL代码不用改安全——自动处理 SQL 注入不用手动转义可链式调用——.filter().order_by().values()像搭积木核心概念Manager 和 QuerySet动手之前先搞清楚两个概念Manager就是Goods.objects这个对象Model 和数据库之间的桥梁。每个 Model 默认都有个叫objects的 Manager。QuerySet调用 Manager 的方法filter()、all()返回的就是 QuerySet代表一组对象的集合。关键点QuerySet 是惰性的lazy。你写Goods.objects.filter(is_on_saleTrue)的时候数据库还没被查询。只有真正遍历结果、取某条数据、或调用list()时SQL 才执行。这个特性很重要——你可以链式调好多次 filterDjango 最终合并成一条 SQL# 看起来是 3 次调用实际只执行 1 条 SQLqsGoods.objects.filter(is_on_saleTrue)qsqs.filter(price__lt1000)qsqs.order_by(-sales)print(list(qs))# 到这里才真正查数据库理解惰性求值你就理解了 ORM 的底层逻辑。动手从 CRUD 到性能优化下面的示例都基于 Day03 的 ModelCategory、Goods、User、Order、OrderItem。没建好 Model 的先回去补不然跑不起来。增创建数据创建单条用create()catCategory.objects.create(name手机数码,sort_order1)goodsGoods.objects.create(nameiPhone 16 Pro,categorycat,price7999.00,stock100)批量创建用bulk_create()一条 SQL 搞定goods_list[Goods(nameiPhone 16 Pro,category_id1,price7999,stock100),Goods(nameMacBook Air M4,category_id1,price9999,stock50),Goods(nameAirPods Pro 3,category_id1,price1899,stock200),]Goods.objects.bulk_create(goods_list)# 一次插入 3 条还有个实用的——get_or_create()有就取没有就建导入数据防重复特别好用cat,is_createdCategory.objects.get_or_create(name手机数码,defaults{sort_order:1})# is_created True 表示新建了False 表示已存在删删除数据# 删除单条Goods.objects.filter(id1).delete()# 按条件批量删除返回 (删除数量, {表名: 数量})Goods.objects.filter(is_on_saleFalse,stock0).delete()⚠️ 千万别手滑调Goods.objects.all().delete()这会把全表清空。改更新数据单条——先查再改goodsGoods.objects.get(id1)goods.price6999.00goods.save()批量——用update()一条 SQLGoods.objects.filter(category__name手机数码).update(is_on_saleTrue)字段自增/自减——用F()表达式不用先把数据查出来fromdjango.db.modelsimportF# 库存减 1销量加 1数据库层面直接运算Goods.objects.filter(id1).update(stockF(stock)-1,salesF(sales)1)为什么强调F()因为它是数据库层面的原子操作不会出现并发问题。如果先get()再save()高并发下两个请求可能同时读到同一个值导致库存扣错。查QuerySet 的十八般武艺这是最核心、也最常用的部分。基础过滤Goods.objects.filter(price__lt100)# 价格低于 100Goods.objects.filter(price__range(100,1000))# 100 到 1000 之间Goods.objects.filter(name__icontainsiPhone)# 名称包含不区分大小写Goods.objects.filter(stock__gt0,is_on_saleTrue)# 库存0 且已上架OR 查询——需要Q对象fromdjango.db.modelsimportQ# 名称包含 iPhone 或 MacBookGoods.objects.filter(Q(name__icontainsiPhone)|Q(name__icontainsMacBook))# 组合(iPhone 或 MacBook) 且 价格低于 10000Goods.objects.filter(Q(name__icontainsiPhone)|Q(name__icontainsMacBook),price__lt10000)排序Goods.objects.order_by(price)# 升序Goods.objects.order_by(-price)# 降序Goods.objects.order_by(-sales,price)# 先销量降序再价格升序聚合统计fromdjango.db.modelsimportAvg,Sum,Count,Max,Min statsGoods.objects.aggregate(avg_priceAvg(price),total_stockSum(stock),max_priceMax(price),total_countCount(id),)# {avg_price: 6632.33, total_stock: 350, max_price: 9999.00, ...}分组统计——annotate的妙用categoriesCategory.objects.annotate(goods_countCount(goods),avg_priceAvg(goods__price),)forcatincategories:print(f{cat.name}:{cat.goods_count}件, 均价 ¥{cat.avg_price:.2f})aggregate和annotate的区别一句话记住方法作用返回aggregate对整个 QuerySet 聚合一个字典annotate对每个对象附加聚合值QuerySet多了新字段aggregate 是总结annotate 是加备注。只要特定字段values() 和 values_list()很多时候不需要完整 Model 对象只要几个字段。用values()或values_list()省内存又方便序列化成 JSON# values() 返回字典goods_dataGoods.objects.filter(is_on_saleTrue).values(id,name,price)# QuerySet [{id: 1, name: iPhone, price: 7999}, ...]# values_list() 返回元组namesGoods.objects.values_list(name,flatTrue)# QuerySet [iPhone, MacBook, AirPods, ...]一个容易忽略的性能优化用values()指定字段时Django 只 SELECT 这些字段不会把整表都取出来。表字段很多比如有大文本字段时效果明显。实用技巧速查first_goodsGoods.objects.get(id1)# 取第一条不存在报错 DoesNotExistfirst_goodsGoods.objects.filter(id999).first()# 不存在返回 None不报错hasGoods.objects.filter(price__gt10000).exists()# 判断是否存在只查一条高效countGoods.objects.filter(is_on_saleTrue).count()# 计数SELECT COUNT(*)不取全部数据unique_categoriesGoods.objects.values(category).distinct()# 去重# 链式过滤逐步缩小范围最终才执行 SQLqsGoods.objects.filter(is_on_saleTrue)qsqs.filter(price__range(100,5000))qsqs.filter(stock__gt0)qsqs.order_by(-sales)[:10]关联查询最容易踩坑的地方商城的数据都是有关系的订单属于某个用户订单里有多件商品商品属于某个分类。Django 查关联数据有两种方式搞混了就容易出性能问题。select_relatedJOIN 一把梭适用于ForeignKey和OneToOneField多对一、一对一。原理用 SQL 的 JOIN一条语句把主表和关联表的数据都取出来。# ❌ 不用1 10 11 条 SQLordersOrder.objects.all()[:10]fororderinorders:print(f{order.user.username}: ¥{order.total_amount})# ✅ 用只 1 条 SQLJOINordersOrder.objects.select_related(user).all()[:10]fororderinorders:print(f{order.user.username}: ¥{order.total_amount})prefetch_related分步加载适用于反向 ForeignKey和ManyToManyField一对多、多对多。原理先查主表再用WHERE id IN (...)查关联表最后用 Python 在内存拼起来。# ❌ 不用1 N 条 SQLN 分类数量categoriesCategory.objects.all()forcatincategories:print(f{cat.name}:{cat.goods.count()}件商品)# ✅ 用只 2 条 SQLcategoriesCategory.objects.prefetch_related(goods).all()forcatincategories:print(f{cat.name}:{cat.goods.count()}件商品)进阶Prefetch 对象要对预加载的数据做过滤时用Prefetch对象fromdjango.db.modelsimportPrefetch categoriesCategory.objects.prefetch_related(Prefetch(goods,querysetGoods.objects.filter(is_on_saleTrue,price__lt500),to_attrcheap_goods# 自定义属性名)).all()forcatincategories:print(f{cat.name}:{len(cat.cheap_goods)}件特价商品)N1 问题最常见的性能杀手如果今天只记一个章节记这个。因为N1 问题是 Django 新手最容易踩的性能坑而且排查起来特别隐蔽。什么是 N1看这段代码ordersOrder.objects.all()[:100]fororderinorders:print(f{order.user.username}: ¥{order.total_amount})表面看只有 1 条SELECT * FROM order。但循环里每次访问order.userDjango 都会自动发一条SELECT * FROM user WHERE id ?。所以实际执行了1查订单 100查用户101 条 SQL。这就是 N11 条主查询 N 条关联查询。怎么发现方法 1django-debug-toolbar——每个请求显示执行了多少条 SQL看到页面动辄几百条大概率就是 N1。方法 2手动计数fromdjango.dbimportconnection start_querieslen(connection.queries)# 你的查询逻辑...total_querieslen(connection.queries)-start_queriesprint(f本次请求执行了{total_queries}条 SQL)方法 3看日志——在settings.py配置 SQL 日志django.db.backendslogger 设为 DEBUG开发时打开看到满屏 SQL 就是警铃大作。怎么解决对症下药关联类型解决方案ForeignKey / OneToOne多对一select_related(字段名)反向 ForeignKey / ManyToMany一对多/多对多prefetch_related(字段名)# 查最近 100 个订单 用户名 商品明细ordersOrder.objects.select_related(user).prefetch_related(items__goods).all()[:100]效果对比用真实数据量感受下差距假设数据库有 1000 个订单写法SQL 条数耗时方案 AN1301 条约 2.1s方案 B优化后3 条约 0.04s301 条 SQL → 3 条2.1 秒 → 0.04 秒快了 50 倍。数据量越大差距越恐怖。1000 条订单时N1 可能要跑十几秒优化后还是零点几秒。Django 5.x 异步 ORM 初体验Django 从 3.1 开始引入异步视图到 5.x 异步 ORM 已经很成熟。新增了aaggregate()、aget()、acount()等以a开头的异步方法你可以在async def视图里直接await数据库操作。同步 vs 异步写法对比同步视图defgoods_list(request):goodsGoods.objects.filter(is_on_saleTrue).order_by(-sales)[:20]data[{id:g.id,name:g.name,price:str(g.price)}forgingoods]returnJsonResponse({code:0,message:success,data:data})同样的逻辑改成异步asyncdefasync_goods_list(request):goodsGoods.objects.filter(is_on_saleTrue).order_by(-sales)[:20]data[]asyncforgingoods:# 异步遍历用 async for 替代普通 fordata.append({id:g.id,name:g.name,price:str(g.price),})returnJsonResponse({code:0,message:success,data:data})关键区别视图函数用async def遍历 QuerySet 用async for聚合操作用aaggregate()并加await什么时候用 async什么时候用 sync场景建议接口要同时调多个外部 API爬虫、推送✅ asyncWebSocket 实时通信✅ async高并发、长连接场景✅ async简单的 CRUD 接口sync 就够了有大量 CPU 计算图片处理等syncasync 没用用了不支持 async 的第三方库sync实际建议大部分 Django 项目用同步就完全够了。只有当接口响应慢200ms且瓶颈在 I/O 等待时才值得迁移到 async。如果你的项目只有部分接口需要异步完全可以混用——同步视图和异步视图共存Django 自动处理不需要把整个项目都改成异步。异步最大的价值一个请求要同时等数据库、外部 API、缓存等多个 I/O 时async 让这些等待时间重叠而不是串行。比如一个接口既要查数据库又要调 AI 生成推荐async 可以让两个 I/O 同时进行。对于不支持 async 的操作用sync_to_async包装fromasgiref.syncimportsync_to_asyncsync_to_asyncdefget_order_detail(order_id):returnOrder.objects.select_related(user).get(pkorder_id)# 在异步视图中调用asyncdefasync_order_detail(request,order_id):orderawaitget_order_detail(order_id)returnJsonResponse({order_no:order.order_no})让 AI 帮你写复杂查询ORM 语法看着简单遇到复杂查询常要翻文档。这时候 AI 是好帮手直接把需求描述给它秒出 ORM 代码。场景 1复杂聚合——“统计过去 30 天每天的订单数和总金额只保留总金额大于 5000 的日期”。AI 会生成TruncDateannotatevalues组合的查询自己翻文档写要折腾半小时。场景 2子查询——“找出每件商品销售额超过该分类平均销售额的商品”。涉及SubqueryOuterRefAI 生成后你再验证修改效率高得多。场景 3性能诊断——把一段慢查询代码贴给 AI让它分析性能问题。AI 通常能精准定位 N1给出select_related/prefetch_related方案。⚠️ 注意AI 生成的 ORM 代码一定要在 Django shell 里跑一遍验证。特别是annotateSubquery这种复杂嵌套AI 偶尔会搞混字段名和关联名。记得用connection.queries看生成的 SQL 是不是预期的。光看不算会试试这两道练习 1订单统计用filteraggregateannotate组合写出本月订单总数和总销售额、每个状态待支付/已支付/已发货/已完成/已取消的订单数、销量最高的 5 件商品。练习 2N1 排查这段代码有 N1 问题用select_related和prefetch_related优化对比优化前后的 SQL 条数goodsGoods.objects.all()[:50]forgingoods:print(f{g.name}- 分类:{g.category.name})itemsOrderItem.objects.filter(goodsg)foriteminitems:print(f 被购买{item.quantity}次)相关文档Django ORM 官方文档 · QuerySet API 参考 · Django 异步支持 · django-debug-toolbar。配套示例见day04_orm_examples.py。本系列为梅雅达编程笔记原创首发于 CSDN。有问题评论区见

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询