🐍 Caso 02 — Python 3.12 + SQLite#
Implementacion operativa del caso 02 para demostrar N+1 anidado y una correccion medible sobre la misma base de datos.
⬅️ Caso 02 · ⚖️ Comparativa de los 7 stacks · 🐍 Perfil de Python · 🧬 Todos los perfiles
🎯 Que resuelve#
Modela un feed operacional de pedidos recientes que necesita devolver:
- datos del pedido;
- datos del cliente;
- items del pedido;
- producto y categoria de cada item.
La ruta orders-legacy hace multiples round-trips por pedido e incluso por item. La ruta orders-optimized consolida lectura base y detalles en consultas agrupadas con JOIN.
💼 Por que importa#
Este caso deja una evidencia muy clara: el problema no es "usar o no usar ORM" en abstracto, sino el patron de acceso a datos. Cuando las relaciones se cargan dentro de bucles, el costo por request crece rapido y desgasta innecesariamente la base, independientemente del lenguaje o el motor.
🔬 Analisis Tecnico de la Implementacion (Python)#
El patron N+1 en Python sobre SQLite exhibe exactamente el mismo comportamiento que sobre PostgreSQL: cada cursor.execute() dentro de un bucle es una barrera de I/O sincronica que acumula tiempo.
- Punto de Falla (
legacy): La funcionorders_legacy()carga el listado base con una sola consulta y luego entra en un buclefor order in orders. Dentro de ese bucle ejecutacursor.execute("SELECT * FROM customers WHERE id = ?", ...)y despues un segundo bucle anidadofor item in itemsdonde ejecutacursor.execute("SELECT p.*, c.name FROM products p JOIN categories c ... WHERE p.id = ?", ...)por cada item. El resultado es el patron clasico1 + N + N*M: para 20 pedidos con 3 items promedio, el servidor ejecuta mas de 80 consultas secuenciales. SQLite procesa cadaexecute()de forma sincronica bloqueando el hilo hasta terminar, sin posibilidad de pipelining. - Correccion Nativa (
optimized): La funcionorders_optimized()elimina los bucles de base de datos. Extrae todos los IDs de pedidos con[o["id"] for o in orders], construye un placeholder dinamico",".join("?" * len(order_ids))y ejecuta un unicoSELECT order_items JOIN products JOIN categories WHERE order_id IN (...). El ensamblado final ocurre en Python puro: construye undictcon{oid: [] for oid in order_ids}e itera los items una sola vez congrouped[item["order_id"]].append(item), complejidadO(N)estricta. El numero total de consultas cae a 3 independientemente de cuantos pedidos o items tenga el resultado.
🧱 Servicio#
app→ API Python 3.12 con endpoints legacy y optimized, SQLite embebida con datos semilla deterministicos.
🚀 Arranque#
docker compose -f compose.yml up -d --buildPuerto local: 832 (modo aislado, ver opciones abajo).
Como consumir (dos opciones)#
Hub Python (recomendado, 8200 en compose.python.yml): este caso queda servido en http://localhost:8200/02/... junto a los otros 11 casos.
Modo aislado (832 en este compose.yml): levanta solo este caso, util cuando la medicion necesita procesar limpio (sin otros casos compartiendo runtime).
🔎 Endpoints#
curl http://localhost:8200/02/
curl http://localhost:8200/02/health
curl "http://localhost:8200/02/orders-legacy?days=30&limit=20"
curl "http://localhost:8200/02/orders-optimized?days=30&limit=20"
curl http://localhost:8200/02/diagnostics/summary
curl http://localhost:8200/02/metrics
curl http://localhost:8200/02/metrics-prometheus
curl http://localhost:8200/02/reset-metrics🧭 Que observar#
db_queriesenorders-legacycrece como1 + N + N*M; con limit=20 y promedio 3 items/pedido, supera 80 consultas por request;db_queriesenorders-optimizedes constante (3 consultas) independientemente delimit;db_time_msen legacy supera al optimizado en proporcion directa al numero de pedidos;/diagnostics/summarymuestra densidad relacional del dataset y diferencia de queries entre rutas.
⚖️ Nota de honestidad#
No intenta reproducir un ORM especifico ni benchmarkear SQLite contra PostgreSQL. Reproduce un patron muy real: listas enriquecidas que parecen inocentes y terminan escalando mal por round-trips repetidos y relaciones cargadas dentro de bucles.