资讯详情

Django工程化实践指南:从项目结构到性能优化的进阶之路

📅 2026/10/14 2:12:53 | 华诺云谱 👁 阅读
Django工程化实践指南:从项目结构到性能优化的进阶之路
简介《Django专业网站构建指南》是一本面向中高级Web开发者的生产级实践书聚焦如何借助Django搭建可稳定部署的完整网站解决开发环境与线上环境脱节的问题。内容顺着真实项目从零展开先用Docker容器搭建开发环境、配置PostgreSQL数据库再逐步实现自定义用户模型、邮箱验证登录、页面应用、静态资源管理与自动化测试最后讲解环境变量、安全与性能优化等部署要点覆盖生产环境中易被忽略的关键细节。压缩包内为1个PDF电子书大小25.89MB排版清晰书签完整适合离线阅读、按章节查阅与快速检索已有61人浏览学习。通过本书可以系统补齐容器化、数据库连接、用户认证与测试加固的方法论掌握从原型到上线的完整路线书中还给出大量可操作的配置示例与项目案例帮助理解Django项目从零搭建、测试驱动开发直到部署上线的每一步。对正在承担真实项目或准备进阶的开发者可按照书中流程边读边练构建出一个具备真实功能的Django网站是一份扎实的参考。1. Django专业网站构建指南从能跑的Demo到能扛事的工程大多数Django开发者都会在某个节点遇到同一种困惑教程跟完了博客写出来了管理后台也能用了但真要把一个网站推到生产环境、接受真实用户流量时心里完全没底。这不是Django的问题而是教程和工程实践之间本来就隔着一条河——文档教你框架的每一个API但没人告诉你项目结构怎么摆才不烂、配置怎么管才不慌、数据模型怎么设计才扛得住迭代、查询怎么写才不会在数据量上来之后一夜翻车。这篇笔记就是来填这条河的。我按自己给多个项目做架构梳理和性能整改时的习惯把“专业”二字拆成六块工程布局与配置管理、数据模型设计、数据库能力利用、常见坑排查、以及最后一公里的性能手段。适合已经写过一两个Django项目、想把代码从“能跑”推进到“能维护、能上线、能扛流量”的开发者。2. 工程布局与配置管理专业和业余的第一个分水岭2.1 为什么默认的startproject结构撑不起一个专业项目Django官方文档教你的目录结构是这样的一个project目录里面放着settings.py、urls.py然后每个app单独建一个包。这套结构在Demo阶段完全够用但项目一复杂就暴露问题——所有app平铺在根目录下配置全堆在一个settings.py里模板和静态文件散落在各个app中。我见过太多项目走到这一步想找某个业务逻辑不知道去哪个app翻改个数据库连接要小心翼翼怕影响本地开发环境静态文件收集时各种覆盖错乱。我一般会在项目第一天就重构目录。常见做法是采用“apps/”收纳所有业务app同时拆出独立的“commons/”或“common/”包来放跨app的公共逻辑。这样做的直接收益是当你需要判断某个功能属于哪个模块时看路径就够了不需要阅读代码逻辑。更重要的是这为后续引入微服务拆分、celery任务独立部署留了退路——app之间本来就该通过接口通信而不是互相import内部实现。另外不要把模板和静态资源全塞在app内部。Django的app目录结构在小型项目里没问题但一旦开始用前端构建工具webpack、vite或者多主题换肤资源文件就应当集中管理。我的习惯是建一个“assets/”目录存放原始前端资源再用collectstatic的输出目录做生产静态文件。开发时用Django自带的static serving生产时交给Nginx这个边界从一开始就要划清楚。2.2 用django-environ拆分配置一份代码跑遍所有环境专业项目必须解决一个问题同一份代码要在本地开发环境、测试环境、预发布环境、生产环境跑出不同行为。Django原生的settings.py只有一个文件新手最常见的做法是复制出settings_dev.py、settings_prod.py再手动改import——这方案能用但非常容易漏改。我改用django-environ后这个问题基本绝迹了。django-environ的核心思路是配置文件里不写死任何环境相关的值全部从环境变量或.env文件读取。数据库连接、Redis地址、密钥、调试开关、日志级别全部走配置项。代码仓库里只保留一个.env.example模板文件真实环境变量在部署平台上配置或由运维注入。这样换环境只是换一组环境变量的值不需要改任何Python代码。# settings.py 核心段落 import environ env environ.Env( # 设置默认值避免环境变量缺失时直接崩 DEBUG(bool, False), ALLOWED_HOSTS(list, []), ) environ.Env.read_env() # 读取项目根目录的 .env 文件 SECRET_KEY env(DJANGO_SECRET_KEY) DEBUG env(DEBUG) ALLOWED_HOSTS env(ALLOWED_HOSTS) DATABASES { default: { ENGINE: env(DB_ENGINE, defaultdjango.db.backends.postgresql), NAME: env(DB_NAME), USER: env(DB_USER), PASSWORD: env(DB_PASSWORD), HOST: env(DB_HOST, default127.0.0.1), PORT: env(DB_PORT, default5432), } } CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: env(REDIS_URL, defaultredis://127.0.0.1:6379/0), } }这段配置里有个容易被忽略的细节environ.Env()构造时传入的默认值参数格式决定了环境变量如何被解析。DEBUG用bool类型那么环境变量里写“False”会被正确解析成False而不是truthy字符串ALLOWED_HOSTS用list类型意味着环境变量里可以用空格或逗号分隔多个域名。这两个默认值的设置技巧能让配置代码大幅简化不用到处做类型转换。2.3 多环境配置的组织方式与常见做法django-environ已经让配置切换足够优雅但大型项目还会面临更细的问题测试环境要跑migration校验预发布环境需要开启debug_toolbar生产环境必须关掉swagger文档。我的处理方式是把settings拆成“base 环境覆盖”三层结构settings/base.py放所有公共配置settings/dev.py和settings/prod.py分别覆盖差异项启动时通过DJANGO_SETTINGS_MODULE环境变量选择入口。# 项目根目录 .env 文件示例 DJANGO_SETTINGS_MODULEconfig.settings.prod DJANGO_SECRET_KEYyour-secret-key-here DEBUGFalse ALLOWED_HOSTSexample.com,www.example.com DB_NAMEmyapp_prod DB_USERmyapp_user DB_PASSWORDstrong-password-here DB_HOST10.0.0.5 DB_PORT5432 REDIS_URLredis://10.0.0.6:6379/0强调一点SECRET_KEY生产环境必须单独生成绝不能沿用Django默认的或开发环境的值。很多安全事故的起因就是部署时忘了改这一个配置项。我习惯在部署脚本里加一步校验判断SECRET_KEY是否等于开发环境的值是则直接终止启动流程。除了django-environ还有一个方向是django-split-settings它的做法是把配置按功能模块拆成多个文件数据库一个文件、缓存一个文件、第三方app一个文件再用依赖解析组合起来。两种方案各有侧重django-environ更侧重“值从哪来”split-settings更侧重“配置怎么组织”我个人的选择是environ为主配置逻辑的拆分通过Python模块自身的import机制完成少一个依赖少一份维护成本。3. 数据模型与ORM数据库设计决定项目的天花板3.1 模型设计的五个核心判断数据模型是一张网每个模型都是一个节点关系就是边。模型设计得不好后期每次迭代都在补洞。我在做模型评审时会按顺序过五个问题回答不满意就返工。第一有没有软删除需求。业务数据通常不允许物理删除因为要留痕、要恢复。Django默认的.delete()是物理删专业方案是自己加一个is_active或deleted_at字段并在查询时默认过滤。第二Status字段用什么类型。不要用字符串存中文状态不要用Python int存0/1/2然后靠注释记含义Django的TextChoices就是为这个设计的。第三Decimal vs Float。金融相关字段一律DecimalFieldFloatField的精度误差在累计运算时完全不可控。第四字段的db_index和unique_together有没有提前规划。第五外键关系是正向多还是反向多这决定了谁该持有ForeignKey。class Order(models.Model): class Status(models.TextChoices): PENDING pending, 待支付 PAID paid, 已支付 SHIPPED shipped, 已发货 COMPLETED completed, 已完成 CANCELLED cancelled, 已取消 order_no models.CharField(max_length32, uniqueTrue, db_indexTrue) user models.ForeignKey(user.User, on_deletemodels.PROTECT, related_nameorders) amount models.DecimalField(max_digits10, decimal_places2) status models.CharField(max_length16, choicesStatus.choices, defaultStatus.PENDING) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue) deleted_at models.DateTimeField(nullTrue, blankTrue, db_indexTrue) class Meta: ordering [-created_at]这个模型里有两个容易踩坑的点。第一on_deletemodels.PROTECT而不是CASCADE——订单关联用户如果允许CASCADE哪天误删用户会把他的所有订单也带走这在财务系统里是不能接受的。第二ordering写在Meta里会作为默认排序条件不要小看它如果一个表的数据量级上了百万这个排序字段必须有索引否则所有查询都会慢。created_at加db_index就是配合这个排序用的。3.2 一次写对迁移迁移的三个好习惯Django的迁移系统是把双刃剑。它让数据库结构变更变得可追踪但用不好会产生大量无意义的迁移文件甚至会在生产环境上执行出意外行为。我见过有人在合并代码时把自己的一个字段修改拆成了四五条迁移记录这倒还能忍最离谱的是在迁移里混入数据操作导致回滚时数据不一致。我的习惯有三个。第一makemigrations只负责结构数据迁移单独写migrations.RunPython或migrations.RunSQL代码块。第二任何迁移提交前必然在干净数据库上执行migrate --plan验证一遍确认不会误跑无关操作。第三合并分支时发现迁移冲突用makemigrations --merge解决但合并后要自查一遍两条迁移路径是否都对同一字段做过修改。# 查看将要执行的迁移操作不真正执行 python manage.py migrate --plan # 合并分叉的迁移文件 python manage.py makemigrations --mergemigrate --plan是Django 3.2之后实用的排查工具它会把每个待执行迁移的依赖关系和SQL操作提前打出来方便在正式执行前发现问题。使用--merge时Django会生成一个MergeMigration文件它本身不包含任何数据操作只用于把两条迁移历史合并成一条线保证迁移图的完整性。3.3 Select_related与Prefetch_related的正确打开方式ORM查询性能是Django项目踩坑最多的地方没有之一。查N条记录却触发了N1次数据库查询的问题每个Django项目都会遇到。解决方法说起来很简单能用select_related用select_related能上prefetch_related上prefetch_related。但很多人不知道这两个方法背后的机制是有本质区别的。select_related用于单值关系——ForeignKey和OneToOneField它通过SQL层的JOIN把关联数据一次性查出来。prefetch_related用于多值关系——ManyToManyField和反向ForeignKey它先查主表再查关联表然后在Python内存里做组合。它们没有优劣之分只有适用场景之分。用错了性能反而更差——比如多值关系用select_relatedJOIN出来的结果集是主表记录的M倍行数爆炸内存飙升。# 反例循环内查询产生N1问题 orders Order.objects.filter(statusOrder.Status.PAID) for order in orders: print(order.user.nickname) # 每行都发起一次user查询 # 正例select_related一次join出user orders Order.objects.filter(statusOrder.Status.PAID).select_related(user) for order in orders: print(order.user.nickname) # 无额外查询 # prefetch_related适合反向外键和多对多 users User.objects.prefetch_related(orders) for user in users: print(user.orders.count()) # 两次查询搞定再说一个极易被忽略的性能细节prefetch_related查询出来的数据在Python内存中做关联时是按主键匹配的。如果主表查询结果集很大维护这个映射也有成本。对于超大批量的场景比如导出报表有时直接用values_list配合原生SQL聚合更省内存。3.4 事务与锁并发场景下防翻车的底线Django的atomic()装饰器是事务控制的入门级方案但真正到高并发场景光有事务不够。典型的问题是并发扣减库存两个请求同时读到库存剩余1件各自执行扣减操作最终库存变成负数。加个transaction.atomic()只能保证要么都执行要么都不执行不能解决并发读写的竞态条件。这时候要用select_for_update()它在数据库层面给被查询的行加锁直到当前事务提交或回滚才释放。这个锁是行级锁不是表级锁所以并发事务还能读其他行不影响整体吞吐。但要注意select_for_update必须配合atomic使用否则没有事务包裹锁会立即释放等于没锁。另外不要对不需要锁的查询用select_for_update因为它会引入等待时间一旦加锁的顺序不一致极端情况下会形成死锁。from django.db import transaction from django.db.models import F with transaction.atomic(): product Product.objects.select_for_update().get(pkproduct_id) if product.stock quantity: raise ValueError(库存不足) # 关键用F()表达式做原子更新避免check-then-act Product.objects.filter(pkproduct_id).update( stockF(stock) - quantity, soldF(sold) quantity )这段代码里还有一层进阶技巧在内层检查库存后实际扣减用的是F()表达式而不是先查询再赋值。F()表达式直接把加减操作下推到SQL层数据库引擎自己处理并发即使两个事务同时执行到update数据库的行锁也会保证它们串行更新不会产生超卖。F()表达式的另一个好处是它天然免疫读改写竞态——你不先读到旧值再写新值而是告诉数据库“把这个字段减掉quantity”这个语义本身就是原子的。4. 从ORM到PostgreSQL让数据库为Django补课4.1 为什么专业项目普遍选择PostgreSQLDjango对数据库的适配层做得非常干净理论上SQLite、MySQL、PostgreSQL都能跑但专业项目的最新选择几乎一致地偏向PostgreSQL。这不是因为“PG更牛”这种玄学而是Django对PostgreSQL的特性支持比其他数据库更完整。PostgreSQL在Django里有独立的后端模块django.contrib.postgres提供了一批开箱即用的字段类型数组ArrayField、JSONJSONField、区间RangeField、全文检索SearchVector/SearchQuery、以及实用的数据库函数。MySQL在JSON字段上的支持深度、全文检索的体验都不如PG直接。如果你的项目有地理信息需求搭配PostGISDjango的GeoDjango在空间查询上的能力是另一个量级——而GeoDjango官方对PostGIS的支持也是最完善的。4.2 JSONField与ArrayField避开关系模型的过度设计我见过不少团队在关系型数据库里硬建模把明明可以用单个字段表达的数据拆成一张关联表。比如用户的手环运动数据每天的步数、心率、睡眠时长为了“规范化”拆成一张运动记录表每行一个指标。这种设计在查询单日汇总时要写一堆聚合在按时间范围拉取时要join好几张表纯粹是给自己挖坑。更合理的设计是用JSONField存一天的完整数据包配合PostgreSQL的jsonb类型做高效查询。from django.contrib.postgres.fields import JSONField # 注意Django 3.1 已支持 models.JSONField推荐直接用内置字段 class DailyHealthRecord(models.Model): user models.ForeignKey(user.User, on_deletemodels.CASCADE) record_date models.DateField(db_indexTrue) data models.JSONField() # {steps: 8321, heart_rate_avg: 72, sleep_minutes: 420} class Meta: indexes [ models.Index(fields[user, record_date]), ]这个设计的直接优势是写入简单、读取完整、不需要为了拼凑一条完整记录而join多张表。缺点也有JSON字段内部的结构数据库本身不感知没法做字段级别的约束校验也不利于字段值的索引优化。所以我的判断标准是——如果某个JSON字段里的属性会被独立作为查询条件那就应该拆成独立列如果它只是被整体存取、最多做几个粗粒度的过滤JSONField是更好的选择。4.3 全文检索用SearchVector替代LIKE查询LIKE %关键词%这类模糊查询在数据量小的时候无感数据量到了几十万条就明显变慢因为数据库无法用普通索引加速这个查询除非你建trigram索引。PostgreSQL的全文检索能力在Django中可以通过SearchVector直接使用它底层不是LIKE匹配而是对文档做分词、词干提取再对索引做快速匹配。中英文搜索场景里英文的体验最好中文需要分词插件但在Django的调用方式上是一致的。from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank # 给文章title和content建搜索向量并查询 articles Article.objects.annotate( documentSearchVector(title, content) ).filter( documentSearchQuery(Django) ).order_by(-rank)更进阶的用法是给搜索相关度排个序用SearchRank对匹配结果做排序。但要注意SearchVector在小数据量下也可以直接用却在百万行以上时必须配合GIN索引才有效。Django没有在migration里自动创建GIN索引需要手动执行CreateExtension和GinIndex操作——这个细节是很多团队上线后搜索变慢的元凶。# 在迁移中为全文搜索创建GIN索引 from django.contrib.postgres.indexes import GinIndex from django.db import migrations class Migration(migrations.Migration): dependencies [] operations [ migrations.AddIndex( model_namearticle, indexGinIndex(fields[content], namearticle_content_gin), ), ]GIN索引是PostgreSQL为全文检索设计的倒排索引它的特点是查询极快、写入稍慢对搜索场景来说是值得的。创建索引的迁移文件也可以直接重写ORM里的Meta.indexes让Django在makemigrations时自动生成但显式写出来更清晰也方便审核。4.4 表分区与冷热数据分离数据量上来之后的最后一招数据量级跨过千万行之后即使索引建得再合理写性能和部分查询性能也会开始下滑。这时候最有效的方案不是换数据库而是做表分区。PostgreSQL支持RANGE分区、LIST分区、HASH分区Django从3.x开始提供了数据库侧的partitioning支持但配置起来还是有一些手工活。最常见的分区场景是按时间分区日志表、订单表、流水表按月份做RANGE分区。这样查询某一时间段的数据时数据库能自动跳过无关分区只扫描目标分区。更实用的是删除过期数据的操作可以直接drop整个分区而不是执行DELETE FROM ... WHERE ...这个操作的代价是天壤之别。不过我要泼一盆冷水Django的ORM对表分区的支持还远非无缝。分区的核心约束是——分区键必须包含在主键或唯一索引中这会导致Django默认的自增id主键不再适用因为你没法在分区表中定义一个全局唯一的主键。常见做法是改用复合主键分区键id或引入UUID主键这会对现有代码产生连锁影响。所以我的建议是不要为了“未来可能用到”而提前分区只有当你能明确说出“这个表会在什么时候超过千万行哪个查询会因为分区而变快”时才动手。分区是一种有代价的优化手段不是一种默认的最佳实践。5. 生产环境避坑五条能让网站半夜翻车的常见问题5.1 DEBUG忘了关错误信息全裸奔现象生产环境的页面一旦报错浏览器里直接显示完整的堆栈信息其中包含文件路径、环境变量片段、甚至数据库连接信息。原因部署时settings.py里的DEBUGTrue没改或者改在了一个没被加载的配置文件里。解决强制在启动脚本里加一层保护——当DJANGO_SETTINGS_MODULE指向生产配置时启动校验如果发现DEBUGTrue就直接拒绝启动。这是我给所有项目定的启动红线没有例外的余地。# settings/prod.py 文件头部 import sys if DEBUG: sys.exit(生产环境禁止开启DEBUG!)这段代码放在prod.py的开头在DEBUG变量被读取后立即判断并强制退出。它比在部署文档里写“记得关DEBUG”要可靠一万倍因为人总是会忘的机器不会。同样的手段也可以用来校验SECRET_KEY是否与开发环境一致。5.2 静态文件Collectstatic的覆盖错乱现象生产环境的页面样式一会对一会不对刷新几次后变得更乱部分图片直接404。原因开发时Django直接从一个app的static目录读文件但生产环境通过collectstatic把所有app的静态文件收集到一个目录。如果有同名文件后收集的会覆盖先收集的顺序不可控就会错乱。解决第一给静态文件路径带上app前缀从根源上避免同名冲突第二部署脚本里先删掉旧的收集目录再重新collectstatic保证每次上线都是干净构建。这个问题的排查方法很简单在服务器上查看收集目录里的文件列表对照一下哪个文件被谁覆盖了就能定位到是哪个app里的同名文件。最保险的预防手段是在每个app的static目录下再建一层以app名为名的子目录让所有文件的路径都带上命名空间。5.3 时区混乱存储UTC展示时再转换现象用户在前一天晚上创建的订单后台管理页面看时间变成了第二天凌晨和日志里的时间对不上。原因Django默认USE_TZTrue会把时间以UTC格式存储但部署时服务器系统时区没设置对或者前端渲染的时候没有做时区转换。解决遵循一个原则——数据库一律存UTC时间所有展示逻辑使用Django的模板过滤器和前端库做本地化转换。另外服务器系统时区统一设置为UTC避免系统层面的时间命令和Python进程的时间不一致。排查时先确认TIME_ZONE和USE_TZ两个配置项再看数据库里存的是不是带时区信息的timestamptz类型最后检查前端渲染用的模板标签有没有加本地化过滤器。5.4 ALLOWED_HOSTS漏配置健康检查疯狂报500现象服务部署完成后健康检查接口返回500但本地跑一切正常。原因ALLOWED_HOSTS是Django内置的Host头校验机制。当请求的Host不在白名单内时Django直接拒绝访问。很多容器平台的健康检查请求Host字段是IP地址或内部服务名不在默认白名单里面就会报错。解决把健康检查的Host加到ALLOWED_HOSTS里或者直接在配置中放行内部网段。更规范的做法是给健康检查单独写一个视图并绕过Host校验中间件。这个现象极为迷惑因为它不是代码问题、不是服务没起来而是框架层面的安全策略在工作——不了解ALLOWED_HOSTS机制的人会在这个问题上浪费大半天。5.5 迁移在生产上执行失败的连锁反应现象生产环境执行migrate时报错“column does not exist”但本地环境迁移一切正常。原因本地环境的数据库经过多次迁移已经到达最新结构但生产环境因为某些原因停在了一个旧版本这次迁移依赖的某个列根本不存在。解决第一迁移前先执行migrate --plan看依赖链第二在生产环境执行前先备份数据库第三如果已经执行到一半失败Django的迁移记录里已经有了一条“已执行”的假记录需要手动删除migration记录并且回滚对应的DDL变更。血的教训是永远不要在默认的migrate命令里加上--fake参数它会标记迁移已执行但不会改动数据库结构这个坑会把后续所有的迁移都带歪。6. 性能优化三板斧Cache、索引与查询瘦身6.1 用Django Cache框架给热点数据加速Django自带的Cache框架是启动性能优化最直接、最低风险的手段。它的抽象层做得很好同一个cache.set和cache.get调用后端可以是内存、文件、数据库也可以是Redis。我一般建议项目直接上Redis作为缓存的默认后端理由很简单Redis不止能做Cache还能同时承担session存储、celery broker和限流计数器的角色一套基础设施多种用途运维成本反而更低。from django.core.cache import cache # 缓存某个热点数据的查询结果3分钟有效 def get_hot_products(): products cache.get(hot_products) if products is not None: return products products list(Product.objects.filter(is_hotTrue)[:10]) cache.set(hot_products, products, 180) return products这段代码里的cache.get/set是最基础的用法但在正式项目里我几乎不这么写——因为每个地方都手动写一遍“缓存有没有、没有就查、查到就放”的逻辑太啰嗦了。Django提供了更优雅的cache_page装饰器和django.views.decorators.cache的API直接作用在视图层一个装饰器就能给整个页面加HTTP缓存。更进阶的方案是使用django-cacheops之类的第三方库给ORM查询自动做缓存。但注意ORM级缓存有个隐患数据更新之后缓存可能还停留着旧值必须配置好失效机制。用cacheops时我遇到过数据改了好几分钟页面不更新的情况排查后发现是Cacheops默认的失效策略没有覆盖到通过原生SQL改数据的场景。6.2 索引的最后一课索引不是越多越好很多人在慢查询出现后第一反应是加索引加完发现变快了于是继续加直到某一天发现写入速度变得不可接受。索引的本质是拿写入开销换查询速度每个索引都会拖慢INSERT和UPDATE。专业做法是先慢查询日志找出真正慢的SQL再针对SQL的WHERE条件和ORDER BY设计索引。最基础的原则是等值查询的字段建普通索引范围查询的字段建索引需要考虑最左前缀原则频繁更新的字段减少索引低区分度的字段比如性别建索引几乎没有意义。我在优化一个订单查询接口时遇到过很有意思的情况WHERE条件有五个字段dba给其中四个都建了索引但查询还是慢。用explain查看执行计划后发现数据库没有选择组合索引而是只在其中一个区分度最高的字段上用索引然后回表过滤其他索引完全浪费了。解决方式是改为建立一个组合索引user_id, status, created_at让三个字段的过滤条件可以同时在索引内完成。这一个调整让接口的查询时间从800ms降到了30ms。6.3 查询瘦身values_list、annotate与批量操作性能优化的最后一步往往不是加缓存也不是加索引而是把没必要的查询瘦身。很多人习惯从数据库查出来Model对象再取其中一两个字段其实用values_list或values能避免Model实例化的开销。数据量大时两者差距非常明显。# 反例查了完整行建了Model对象只取一个字段 user_ids [u.id for u in User.objects.filter(is_activeTrue)] # 正例直接只查id列不实例化Model user_ids list(User.objects.filter(is_activeTrue).values_list(id, flatTrue))values_list和values的选择也有讲究values_list返回的是元组列表配合flatTrue变成单值列表values返回的是字典列表。前者适合做数据传递后者适合做数据处理。除了查询瘦身另一个常用的手段是bulk_create和bulk_update——批量写入。循环里单条插入每次都走一次完整ORM和事务5000条数据可能要几十秒换成bulk_create同样的数据几秒钟就完成。这两个API在导入功能、数据迁移脚本和测试数据准备场景里使用频率极高但我见过不少项目代码里还在用for循环加save()的老旧写法。当初我第一次做百万级数据表查询优化时也是先加缓存、再调索引、再瘦身查询三轮折腾下来才勉强摸到门道。后来形成一个习惯每个新接口上线前先看一遍explain计划再顺手算一下N1的查询次数。这些动作早就不是“进阶技巧”而是专业项目的写作基本功了。希望这篇笔记能帮你把Django项目从“能跑”推向“能扛”少走点弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑