返回首页

Django 优化:从 30s 到 142ms

Django 单体优化案例:消除 N+1、添加索引并实现 DDD,将报表时间从 30 s 减少到 142 ms。DB CPU 下降 60%,查询 — 从 2800 到 3。Use Cases 结构加速了功能开发。

如何将 Django 报表加速 200 倍:案例
Advertisement 728x90

Django单体应用优化:从30秒到142毫秒的报告加载

在一个遗留的Django项目中,由于对包含28万条记录的订单表进行全表顺序扫描,分支报告加载耗时30秒。EXPLAIN ANALYZE显示:

Seq Scan on orders (cost=0.00..18420.00 rows=2841 width=847)
Filter: (branch_id = 42)
Rows Removed by Filter: 284100
Execution Time: 28340 ms

ORM因相关对象的N+1问题生成了2800多个查询。业务逻辑放在控制器中进一步加剧了负载。

原始查询:

Google AdInline article slot
SELECT * FROM orders
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id
WHERE orders.branch_id = 42;

消除N+1问题

N+1导致负载呈指数级增长。优化前:

orders = Order.objects.filter(branch_id=branch_id)
for order in orders:
    items = order.order_items.all()  # N+1
    for item in items:
        product = item.product  # N+1

使用预取优化后:

orders = Order.objects.filter(
    branch_id=branch_id
).select_related('customer').prefetch_related('order_items__product')

查询次数降至3次。select_related用于外键的JOIN,prefetch_related用于多对多和反向关系的IN查询。

Google AdInline article slot

为表添加索引

解决N+1后,顺序扫描问题依然存在。我们无停机添加了索引:

CREATE INDEX CONCURRENTLY idx_orders_branch_created ON orders(branch_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_products_search ON products USING GIN(to_tsvector('english', name));

新的执行计划:

Index Scan using idx_orders_branch_created on orders
Index Cond: (branch_id = 42)
Execution Time: 142 ms

执行时间从28秒降至142毫秒。CONCURRENTLY可防止生产环境中的表锁定。

Google AdInline article slot

关键索引建议:

  • 在(过滤条件 + 排序字段)上创建复合索引
  • 使用GIN索引进行全文搜索
  • 添加索引前务必检查EXPLAIN ANALYZE

重构为领域驱动设计(DDD)

控制器中的业务逻辑造成了技术债务。我们独立于框架提取了领域和用例。

用例示例:

class GetBranchReportUseCase:
    def __init__(self, repo: OrderRepository):
        self._repo = repo

    def execute(self, branch_id, period) -> BranchReport:
        orders = self._repo.get_by_branch_and_period(branch_id, period)
        return BranchReport.from_orders(orders)

视图变得简洁:

class BranchReportView(APIView):
    def get(self, request, branch_id):
        use_case = GetBranchReportUseCase(DjangoOrderRepository())
        report = use_case.execute(branch_id, DateRange.from_request(request))
        return Response(BranchReportSerializer(report).data)

用例可通过模拟仓库进行测试,无需依赖Django和数据库。新功能的上市时间缩短了一半。

代码质量控制

新增:

  • Mypy严格模式
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
  • Pytest覆盖率≥87%,在GitLab CI中设置质量门禁。
  • 由于早期错误检测,平均故障检测时间(MTTD)降低了40%。

改进指标

| 指标 | 优化前 | 优化后 |

|--------|--------|-------|

| 报告加载时间 | 30秒 | 1.5秒 |

| 数据库CPU使用率 | 80% | 32% |

| 每页查询次数 | 2800+ | 3 |

| 功能上市时间 | X | X/2 |

| 平均故障检测时间 | - | -40% |

关键要点

  • EXPLAIN ANALYZE是排查性能问题的第一步
  • N+1在高负载下是性能杀手,prefetch_related/select_related必不可少
  • 在(branch_id, created_at)上创建复合索引可带来200倍加速
  • DDD隔离业务逻辑,加速开发和测试
  • Mypy + pytest + CI门禁可降低MTTD,且无额外开销

— Editorial Team

Advertisement 728x90

继续阅读