☰
Django工程化实践指南:从项目结构到性能优化的进阶之路
2026/10/14 2:12:37 网站建设 项目流程

简介:《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", default="django.db.backends.postgresql"), "NAME": env("DB_NAME"), "USER": env("DB_USER"), "PASSWORD": env("DB_PASSWORD"), "HOST": env("DB_HOST", default="127.0.0.1"), "PORT": env("DB_PORT", default="5432"), } } CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": env("REDIS_URL", default="redis://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_MODULE=config.settings.prod DJANGO_SECRET_KEY=your-secret-key-here DEBUG=False ALLOWED_HOSTS=example.com,www.example.com DB_NAME=myapp_prod DB_USER=myapp_user DB_PASSWORD=strong-password-here DB_HOST=10.0.0.5 DB_PORT=5432 REDIS_URL=redis://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。金融相关字段一律DecimalField,FloatField的精度误差在累计运算时完全不可控。第四,字段的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_length=32, unique=True, db_index=True) user = models.ForeignKey('user.User', on_delete=models.PROTECT, related_name='orders') amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField(auto_now_add=True, db_index=True) deleted_at = models.DateTimeField(null=True, blank=True, db_index=True) class Meta: ordering = ['-created_at']

这个模型里有两个容易踩坑的点。第一,on_delete=models.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 --merge

migrate --plan是Django 3.2之后实用的排查工具,它会把每个待执行迁移的依赖关系和SQL操作提前打出来,方便在正式执行前发现问题。使用--merge时,Django会生成一个MergeMigration文件,它本身不包含任何数据操作,只用于把两条迁移历史合并成一条线,保证迁移图的完整性。

3.3 Select_related与Prefetch_related的正确打开方式

ORM查询性能是Django项目踩坑最多的地方,没有之一。查N条记录却触发了N+1次数据库查询的问题,每个Django项目都会遇到。解决方法说起来很简单:能用select_related用select_related,能上prefetch_related上prefetch_related。但很多人不知道这两个方法背后的机制是有本质区别的。

select_related用于单值关系——ForeignKey和OneToOneField,它通过SQL层的JOIN把关联数据一次性查出来。prefetch_related用于多值关系——ManyToManyField和反向ForeignKey,它先查主表,再查关联表,然后在Python内存里做组合。它们没有优劣之分,只有适用场景之分。用错了,性能反而更差——比如多值关系用select_related,JOIN出来的结果集是主表记录的M倍,行数爆炸,内存飙升。

# 反例:循环内查询,产生N+1问题 orders = Order.objects.filter(status=Order.Status.PAID) for order in orders: print(order.user.nickname) # 每行都发起一次user查询 # 正例:select_related一次join出user orders = Order.objects.filter(status=Order.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(pk=product_id) if product.stock < quantity: raise ValueError("库存不足") # 关键:用F()表达式做原子更新,避免check-then-act Product.objects.filter(pk=product_id).update( stock=F('stock') - quantity, sold=F('sold') + quantity )

这段代码里还有一层进阶技巧:在内层检查库存后,实际扣减用的是F()表达式而不是先查询再赋值。F()表达式直接把加减操作下推到SQL层,数据库引擎自己处理并发,即使两个事务同时执行到update,数据库的行锁也会保证它们串行更新,不会产生超卖。F()表达式的另一个好处是它天然免疫读改写竞态——你不先读到旧值再写新值,而是告诉数据库“把这个字段减掉quantity”,这个语义本身就是原子的。

4. 从ORM到PostgreSQL:让数据库为Django补课

4.1 为什么专业项目普遍选择PostgreSQL

Django对数据库的适配层做得非常干净,理论上SQLite、MySQL、PostgreSQL都能跑,但专业项目的最新选择几乎一致地偏向PostgreSQL。这不是因为“PG更牛”这种玄学,而是Django对PostgreSQL的特性支持比其他数据库更完整。

PostgreSQL在Django里有独立的后端模块django.contrib.postgres,提供了一批开箱即用的字段类型:数组(ArrayField)、JSON(JSONField)、区间(RangeField)、全文检索(SearchVector/SearchQuery)、以及实用的数据库函数。MySQL在JSON字段上的支持深度、全文检索的体验都不如PG直接。如果你的项目有地理信息需求,搭配PostGIS,Django的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_delete=models.CASCADE) record_date = models.DateField(db_index=True) 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( document=SearchVector('title', 'content') ).filter( document=SearchQuery('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_name='article', index=GinIndex(fields=['content'], name='article_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里的DEBUG=True没改,或者改在了一个没被加载的配置文件里。解决:强制在启动脚本里加一层保护——当DJANGO_SETTINGS_MODULE指向生产配置时,启动校验如果发现DEBUG=True就直接拒绝启动。这是我给所有项目定的启动红线,没有例外的余地。

# 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_TZ=True会把时间以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_hot=True)[: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_active=True)] # 正例:直接只查id列,不实例化Model user_ids = list(User.objects.filter(is_active=True).values_list('id', flat=True))

values_list和values的选择也有讲究:values_list返回的是元组列表(配合flat=True变成单值列表),values返回的是字典列表。前者适合做数据传递,后者适合做数据处理。除了查询瘦身,另一个常用的手段是bulk_create和bulk_update——批量写入。循环里单条插入每次都走一次完整ORM和事务,5000条数据可能要几十秒;换成bulk_create,同样的数据几秒钟就完成。这两个API在导入功能、数据迁移脚本和测试数据准备场景里使用频率极高,但我见过不少项目代码里还在用for循环加save()的老旧写法。

当初我第一次做百万级数据表查询优化时,也是先加缓存、再调索引、再瘦身查询,三轮折腾下来才勉强摸到门道。后来形成一个习惯:每个新接口上线前,先看一遍explain计划,再顺手算一下N+1的查询次数。这些动作早就不是“进阶技巧”,而是专业项目的写作基本功了。希望这篇笔记能帮你把Django项目从“能跑”推向“能扛”,少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询