Volver al inicio

Optimización de Django: de 30s a 142ms

Caso de optimización de monolito Django: eliminar N+1, agregar índices e implementar DDD redujo el tiempo de informe de 30 s a 142 ms. CPU de BD bajó un 60%, consultas — de 2800 a 3. Estructura de Use Cases aceleró desarrollo de características.

Cómo acelerar informes de Django 200 veces: caso
Advertisement 728x90

Optimización de un monolito Django: De 30 segundos a 142 ms en informes

En un proyecto Django heredado, los informes de sucursal tardaban 30 segundos en cargar debido a un escaneo secuencial completo en la tabla de pedidos con 280k registros. EXPLAIN ANALYZE reveló:

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

El ORM generaba más de 2800 consultas debido a N+1 en objetos relacionados. La lógica de negocio ubicada en controladores agravaba la carga.

Consulta original:

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;

Eliminación de N+1

N+1 causaba un crecimiento exponencial de la carga. Antes:

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

Después con prefetch:

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

El recuento de consultas bajó a 3. select_related usa JOIN para ForeignKey, prefetch_related usa consultas IN para relaciones ManyToMany e inversas.

Google AdInline article slot

Indexación de tablas

Tras N+1, el escaneo secuencial persistía. Se añadieron índices sin tiempo de inactividad:

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));

Nuevo plan:

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

El tiempo de ejecución bajó de 28 segundos a 142 ms. CONCURRENTLY evita bloqueos de tabla en producción.

Google AdInline article slot

Recomendaciones clave de indexación:

  • Compuesto en (filtro + ordenación)
  • GIN para búsqueda de texto completo
  • Siempre revisar EXPLAIN ANALYZE antes de añadir

Refactorización a DDD

La lógica de negocio en controladores creó deuda técnica. Se extrajeron Dominio y Casos de Uso independientemente del framework.

Ejemplo de Caso de Uso:

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)

La vista se volvió delgada:

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)

Los Casos de Uso se prueban con repositorios simulados sin Django y la base de datos. El tiempo de comercialización de nuevas funciones se redujo a la mitad.

Control de calidad del código

Se añadió:

  • Mypy en modo estricto:
[mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
  • Pytest con cobertura ≥87%, bloqueando puerta de calidad en GitLab CI.
  • MTTD disminuyó un 40% debido a detección temprana de errores.

Métricas de mejora

| Métrica | Antes | Después |

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

| Tiempo de informe | 30 s | 1.5 s |

| CPU de BD | 80% | 32% |

| Consultas/Página | 2800+ | 3 |

| TTM de función | X | X/2 |

| MTTD | - | -40% |

Conclusiones clave

  • EXPLAIN ANALYZE es el primer paso ante cualquier degradación de rendimiento
  • N+1 mata bajo carga, prefetch_related/select_related son obligatorios
  • Índices compuestos en (branch_id, created_at) dan una aceleración de 200x
  • DDD aísla la lógica de negocio, acelerando desarrollo y pruebas
  • Mypy + pytest + puerta CI reducen MTTD sin sobrecarga

— Editorial Team

Advertisement 728x90

Leer después