🟢 Caso 02 — Node.js 22 + SQLite embebido (node:sqlite)#
Implementacion operativa del caso 02 para estudiar N+1 y cuellos de botella relacionales con evidencia observable, manteniendo paridad funcional con la version PHP+Postgres y Python+SQLite — y ahora con SQL real bajo el contraste en Node tambien.
⬅️ Caso 02 · ⚖️ Comparativa de los 7 stacks · 🟢 Perfil de Node.js · 🧬 Todos los perfiles
🎯 Que resuelve#
Modela un endpoint de pedidos recientes que requiere enriquecer cada pedido con cliente, items, producto y categoria. Dos variantes:
orders-legacy: N+1 anidado. Por cada pedido: cliente + items; por cada item: producto + categoria. Cada acceso es unprepare().get()separado contra SQLite.orders-optimized: una lectura base con join en memoria contra customers, mas un soloprepare().all()conIN(?, ?, ?, ...)que agrupa items con producto y categoria por order_id.
💼 Por que importa#
Este caso muestra el caso clasico donde el costo escala con 1 + N + sum(items_per_order * 2) y, en Node, ese costo es doble: cada Statement.get() es file I/O sincronico sobre SQLite que bloquea el event loop mientras dura. Bajo concurrencia, event_loop_lag_ms pega a TODAS las requests en curso.
🔬 Analisis Tecnico de la Implementacion (Node.js)#
- Motor de datos:
node:sqlite(modulo built-in desde Node 22.5, sinnpm install, sin compilar bindings nativos). DB inicializada en:memory:por instancia o/tmp/case02.dbsegun env.db.prepare(sql)cachea el plan;.get(...)/.all(...)ejecutan con parametros bound.db_hitscuenta ejecuciones reales contra el motor. - Implementacion Falla (
legacy):recentOrdersLegacy()ejecuta una lectura base de orders, despuesfor (const order of baseOrders)congetCustomer.get(order.customer_id)ygetItems.all(order.id). Por cada item,getProduct.get(item.product_id). El patron de costo es real: conlimit=20y ~3.7 items promedio por order, se generan ~190prepare().get()contra SQLite. Cada uno bloquea el loop durante el roundtrip a JNI/native — el costo agregado se observa enevent_loop_lag_msy latencia p95. - Sanitizacion Algoritmica (
optimized):recentOrdersOptimized()resuelve todo con dosprepare().all(). El primero proyecta los pedidos y hace join customer en el mismo SELECT conJOIN customers ON .... El segundo construyeIN (?, ?, ?, ...)con los order_ids y trae items+product+category en una sola query con joins. La agrupacion en JS es O(N) sobre el resultset usandoMap.get(). - Primitivas idiomaticas:
Databasedenode:sqlitepara conexion,Statement(resultado dedb.prepare()) para prepared statements,Mappara indices por id,Setpara filtros de pertenencia,[].filter().sort().slice()como pipeline funcional.
🧱 Primitivas#
| Primitiva | Rol |
|---|---|
node:sqlite Database | Conexion al motor SQLite embebido (built-in Node 22.5+). |
db.prepare(sql) | Prepared statement cacheado; las ejecuciones siguientes reusan plan. |
Statement.get() / .all() | Ejecucion sincrona; ideal para mostrar el bloqueo del loop bajo N+1. |
Map<order_id, items[]> | Agrupacion O(N) tras el resultset. |
process.hrtime.bigint() | Medicion ns para db_time_ms y event_loop_lag_ms. |
🧱 Servicio#
app→ API Node.js 22 con rutas legacy y optimized. SQLite embebido inicializado al startup con el dataset semilla.
🚀 Arranque#
docker compose -f compose.yml up -d --buildPuerto local: 822 (modo aislado, ver opciones abajo).
Como consumir (dos opciones)#
Hub Node.js (recomendado, 8300 en compose.nodejs.yml): este caso queda servido en http://localhost:8300/02/... junto a los otros 11 casos.
Modo aislado (822 en este compose.yml): levanta solo este caso, util cuando la medicion necesita procesar limpio (sin otros casos compartiendo runtime).
🔎 Endpoints#
curl http://localhost:8300/02/
curl http://localhost:8300/02/health
curl "http://localhost:8300/02/orders-legacy?days=30&limit=20"
curl "http://localhost:8300/02/orders-optimized?days=30&limit=20"
curl http://localhost:8300/02/diagnostics/summary
curl http://localhost:8300/02/metrics
curl http://localhost:8300/02/metrics-prometheus
curl http://localhost:8300/02/reset-metrics🧭 Que observar#
db_queries_in_requestenorders-legacyescala conlimity con la densidad relacional (items/order); cada hit es una ejecucion real contra SQLite, no una metrica derivada;orders-optimizedse mantiene en 2 queries independientemente dellimit;delta.p95_msen/diagnostics/summarymuestra la diferencia real;event_loop_lag_msse dispara en legacy bajo concurrencia: senal Node-especifica que delata el bloqueo del loop por SQLite sincrono.
⚖️ Fidelidad#
DB real en SQLite embebido — db_hits es un contador honesto de ejecuciones contra el motor. El contraste con PHP+PostgreSQL es solo de motor (cliente/servidor vs embebido), no de fidelidad del patron: ambos miden N round-trips reales vs 1 batch. La eleccion de SQLite mantiene el container Node single-process y sin dependencias de red.