Django-Vue 列表页“又慢又乱”?一次把
N+1、排序抖动、分页重复 全部解决的实战笔记
> 场景:后端 Django(DRF)+ 前端 Vue Admin,某个“订单列表/用户列表”在数据一多就拖沓:接口 800ms 起步、翻页出现重复/漏项、筛选后导出顺序还和页面不一致。下面是我在项目里落地的一整套修复方案,无外链、可直接抄。
目标
- 稳定排序:翻页不重复、不漏项。
- 可预取:消灭 ORM N+1。
- 筛选一致:表格、导出、统计三处使用同一套查询。
- 观测可解释:哪里慢、慢多少、为什么慢,一眼看出来。
一、后端查询层:把“查询策略”抽出来
1) QuerySet 统一构造
# app/queries.py
from django.db.models import Q
from django.db.models import F
from django.db.models.functions import Coalesce
from .models import Order
def build_order_qs(params):
qs = Order.objects.select_related("user").prefetch_related("items") # 消灭 N+1
# 筛选
kw = params.get("keyword")
if kw:
qs = qs.filter(Q(user__email__icontains=kw) | Q(code__icontains=kw))
status = params.get("status")
if status:
qs = qs.filter(status=status)
# 统计字段(示例:空值置 0,便于排序)
qs = qs.annotate(amount_safe=Coalesce(F("amount"), 0))
return qs
> 经验:不要在 ViewSet 里乱拼 filter。把“可复用的查询构造器”抽到 queries.py
,前端任意页面(表格/导出/统计)都走这一个入口。
2) 稳定排序(防翻页抖动)
# app/views.py
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.pagination import CursorPagination
from .serializers import OrderSerializer
from .queries import build_order_qs
class OrderCursorPagination(CursorPagination):
page_size = 20
ordering = ("-created_at", "-id") # 关键:次级唯一键,稳定
class OrderViewSet(ReadOnlyModelViewSet):
serializer_class = OrderSerializer
pagination_class = OrderCursorPagination
def get_queryset(self):
return build_order_qs(self.request.query_params)
> 要点:
>
> * 游标分页(CursorPagination)天生防止“翻页后数据位移”;
> * 即便不用游标,也务必在 ordering
中加入唯一键(如 -id
)做次级排序。
二、序列化与只取所需字段:减少“行宽”
# app/serializers.py
from rest_framework import serializers
from .models import Order, OrderItem
class OrderItemLiteSerializer(serializers.ModelSerializer):
class Meta:
model = OrderItem
fields = ("sku", "qty")
class OrderSerializer(serializers.ModelSerializer):
user_email = serializers.EmailField(source="user.email", read_only=True)
items = OrderItemLiteSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ("id", "code", "status", "amount_safe", "created_at", "user_email", "items")</code></pre>
> 只返回表格需要的字段,详情页另做详情接口。表格页“什么都想拿”是最容易拖慢的坑。
三、统一的导出与统计:别再复制一份 SQL
1) 导出直接复用 QuerySet
# app/exports.py
import csv
from django.http import StreamingHttpResponse
from .queries import build_order_qs
def export_orders(request):
qs = build_order_qs(request.GET).only("id", "code", "status", "amount", "created_at")
def row_iter():
yield ["ID", "Code", "Status", "Amount", "Created At"]
for o in qs.iterator(chunk_size=500):
yield [o.id, o.code, o.status, o.amount or 0, o.created_at.isoformat()]
pseudo_buffer = (",".join(map(str, r)) + "\n" for r in row_iter())
resp = StreamingHttpResponse(pseudo_buffer, content_type="text/csv")
resp["Content-Disposition"] = 'attachment; filename="orders.csv"'
return resp</code></pre>
> 关键:导出与列表共用 build_order_qs
,这样筛选逻辑天然一致;大集遍历用 iterator()
,避免内存爆。
2) 统计口径也走同一入口
# app/stats.py
from django.db.models import Count, Sum
from .queries import build_order_qs
def summarize(request):
qs = build_order_qs(request.GET)
return {
"count": qs.count(),
"amount_sum": qs.aggregate(Sum("amount"))["amount__sum"] or 0,
"by_status": dict(qs.values_list("status").annotate(c=Count(1)))
}
四、前端表格:把“稳定性”当第一优先级
1) 前端强制带上排序字段
- 默认排序与后端一致(
created_at desc, id desc
);
- 切换筛选条件时,保留排序参数,避免“页面看起来排序乱了”。
2) 防止重复请求与序列错乱
- 对输入框筛选加 300ms 防抖;
- 维护一个
requestId
,返回时对比,旧响应直接丢弃,避免闪烁。
3) 空状态与骨架屏
- “无数据”“加载中”统一组件;
- 大筛选(例如日期跨度很大)直接提示“可能较慢”,给用户可预期的等待。
五、可观测性:慢在哪里要可解释
1) 慢查询日志
- PostgreSQL 开
log_min_duration_statement
(例如 >200ms);
- Django 层对
connection.queries
做采样上报;
- 重点关注:分页、聚合、distinct。
2) 应用层指标
- 接口延迟分桶:P50/P90/P99;
- 数据库命中数:每个接口 SQL 次数;
- 缓存命中率:列表页片段/数据缓存命中比。
> 没有观测,优化很容易“拍脑袋”。先让数据说话,再改。
六、常见坑与修复手册
-
分页重复/漏项
-
症状:翻页看到同一条数据或漏掉最新数据。
-
修复:稳定排序(次级唯一键)+ 游标分页;避免只用 created_at
。
-
N+1 查询
-
症状:SQL 次数随行数线性增长。
-
修复:外键用 select_related
,一/多关系用 prefetch_related
;模板/序列化器不要在循环里再查数据库。
-
筛选与导出不一致
-
症状:表格和导出数量/排序不同。
-
修复:抽 build_qs
;表格、导出、统计均走同一入口。
-
大范围筛选拖垮数据库
-
修复:限制时间跨度、分页上限;必要时做异步导出(任务队列),前端展示任务状态并提供结果下载。
-
用户体验“抖动”
-
修复:统一 loading/empty;请求防抖;响应乱序保护。
七、上线前 10 分钟核对清单
- [ ]
build_qs
已抽离,所有入口只调用它
- [ ] 列表默认排序:
-created_at,-id
(或等价稳定组合)
- [ ] 预取:
select_related/prefetch_related
针对模板/序列化器的访问路径
- [ ] 导出/统计与列表口径一致,且用流式/分批
- [ ] 前端筛选有防抖,响应有请求序列保护
- [ ] 慢查询日志开启,接口 P50/P90/P99 可见
- [ ] 大筛选提示与上限;必要时异步导出兜底
结语
把“稳定排序 + 统一查询 + 预取 + 观测
评论 0