Desarrollar Endpoints con Queries Optimizadas

Video
30 min~5 min lectura

Reproductor de video

Concepto clave

Las queries optimizadas en Prisma son consultas a la base de datos que maximizan el rendimiento y minimizan el consumo de recursos. Piensa en ellas como la diferencia entre pedir un café en un bar: una query no optimizada es como pedir "un café" y esperar que el barista adivine todo (tipo, tamaño, extras), mientras que una optimizada es dar instrucciones precisas como "café americano mediano, sin azúcar". En producción, cada milisegundo cuenta y cada conexión a la base de datos tiene un costo.

La optimización se enfoca en tres pilares: selectividad (traer solo los datos necesarios), relaciones eficientes (evitar el problema N+1) y uso de índices (acelerar búsquedas). Una query mal diseñada puede funcionar en desarrollo con 100 registros, pero colapsar en producción con millones. La clave es pensar como un arquitecto de bases de datos, no solo como un desarrollador de API.

Cómo funciona en la práctica

Imagina que desarrollas una API para un e-commerce. Necesitas un endpoint que devuelva los últimos 10 pedidos con sus productos y el cliente. El enfoque naive seria: 1) Obtener los pedidos, 2) Para cada pedido, hacer una query para sus productos, 3) Para cada pedido, otra query para el cliente. Esto genera 1 + 10 + 10 = 21 queries (problema N+1).

El enfoque optimizado usa include con selectividad y paginacion:

  1. Definir exactamente que campos necesitas de cada modelo
  2. Usar include para traer relaciones en una sola query
  3. Aplicar take y skip para paginacion
  4. Ordenar por un campo indexado como createdAt

Esto reduce las 21 queries a solo 1, con un impacto dramatico en el rendimiento.

Codigo en accion

Primero, veamos el antes (query no optimizada):

// ENDPOINT NO OPTIMIZADO - PROBLEMA N+1
app.get('/pedidos/lentos', async (req, res) => {
  const pedidos = await prisma.pedido.findMany({
    take: 10,
    orderBy: { createdAt: 'desc' }
  });
  
  // PROBLEMA: queries adicionales por cada pedido
  const pedidosConDetalles = await Promise.all(
    pedidos.map(async (pedido) => {
      const productos = await prisma.producto.findMany({
        where: { pedidoId: pedido.id }
      });
      const cliente = await prisma.cliente.findUnique({
        where: { id: pedido.clienteId }
      });
      return { ...pedido, productos, cliente };
    })
  );
  
  res.json(pedidosConDetalles);
});

Ahora el despues (query optimizada):

// ENDPOINT OPTIMIZADO - 1 QUERY
app.get('/pedidos/optimizados', async (req, res) => {
  const pedidos = await prisma.pedido.findMany({
    take: 10,
    skip: 0, // Para paginacion
    orderBy: { createdAt: 'desc' }, // Campo indexado
    select: {
      id: true,
      total: true,
      createdAt: true,
      // Solo campos necesarios
      cliente: {
        select: {
          id: true,
          nombre: true,
          email: true // No traer toda la info del cliente
        }
      },
      productos: {
        select: {
          id: true,
          nombre: true,
          precio: true,
          cantidad: true
        },
        where: { activo: true } // Filtro adicional
      }
    },
    where: {
      estado: 'COMPLETADO' // Filtro principal
    }
  });
  
  res.json(pedidos);
});

Errores comunes

  • Traer todos los campos: Usar select: true en lugar de select especifico. Solucion: Listar explicitamente los campos necesarios.
  • Olvidar los índices: Ordenar o filtrar por campos no indexados. Solucion: Verificar índices en el schema y agregarlos donde sea necesario.
  • N+1 en relaciones anidadas: Incluir relaciones pero no sus sub-relaciones. Solucion: Usar include anidado con selectividad.
  • Paginacion incompleta: Usar solo take sin skip para paginas siguientes. Solucion: Implementar logica completa de paginacion.
  • Filtros ineficientes: Usar OR con muchos condiciones o LIKE sin limites. Solucion: Revisar planes de ejecucion y simplificar condiciones.

Checklist de dominio

  1. ¿Uso select en lugar de include cuando solo necesito campos, no relaciones completas?
  2. ¿Verifico que los campos en orderBy y where principales tengan índices?
  3. ¿Evito el problema N+1 usando include para traer relaciones en una sola query?
  4. ¿Implemento paginacion con take y skip para limites de datos?
  5. ¿Uso where para filtrar antes de traer datos, no después en JavaScript?
  6. ¿Reviso el plan de ejecucion de queries complejas con prisma.$queryRaw y EXPLAIN?
  7. ¿Considero usar transacciones para operaciones multiples que deben ser atomicas?

Optimizar endpoint de usuarios con pedidos recientes

En este ejercicio, optimizaras un endpoint existente que tiene problemas de rendimiento. Trabajaras con un schema de Prisma para una aplicacion de pedidos.

  1. Contexto: Tienes un endpoint GET /usuarios/:id/pedidos-recientes que devuelve los ultimos 5 pedidos de un usuario, con los productos de cada pedido. Actualmente hace 1 + 5 + (5 * N) queries (donde N es productos por pedido).
  2. Schema relevante:
model Usuario {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  nombre    String
  pedidos   Pedido[]
}

model Pedido {
  id         Int       @id @default(autoincrement())
  createdAt  DateTime  @default(now())
  usuarioId  Int
  usuario    Usuario   @relation(fields: [usuarioId], references: [id])
  productos  ProductoPedido[]
}

model ProductoPedido {
  id        Int     @id @default(autoincrement())
  pedidoId  Int
  productoId Int
  cantidad  Int
  pedido    Pedido   @relation(fields: [pedidoId], references: [id])
  producto  Producto @relation(fields: [productoId], references: [id])
}

model Producto {
  id     Int     @id @default(autoincrement())
  nombre String
  precio Float
  productoPedidos ProductoPedido[]
}
  1. Codigo actual no optimizado:
app.get('/usuarios/:id/pedidos-recientes', async (req, res) => {
  const usuarioId = parseInt(req.params.id);
  
  // 1. Obtener usuario
  const usuario = await prisma.usuario.findUnique({
    where: { id: usuarioId }
  });
  
  // 2. Obtener ultimos 5 pedidos
  const pedidos = await prisma.pedido.findMany({
    where: { usuarioId: usuarioId },
    take: 5,
    orderBy: { createdAt: 'desc' }
  });
  
  // 3. Para cada pedido, obtener productos (PROBLEMA N+1)
  const pedidosConProductos = await Promise.all(
    pedidos.map(async (pedido) => {
      const productosPedido = await prisma.productoPedido.findMany({
        where: { pedidoId: pedido.id },
        include: { producto: true }
      });
      return {
        ...pedido,
        productos: productosPedido.map(pp => ({
          ...pp.producto,
          cantidad: pp.cantidad
        }))
      };
    })
  );
  
  res.json({
    usuario: { id: usuario.id, nombre: usuario.nombre },
    pedidos: pedidosConProductos
  });
});
  1. Tu tarea: Refactoriza este endpoint para que use maximo 2 queries en total. Aplica:
    • Selectividad: Trae solo los campos necesarios
    • Include anidado: Para relaciones usuario-pedidos-productos
    • Paginacion: Manten take: 5
    • Filtros: Asegurate de que createdAt este indexado
  2. Entrega: Proporciona el codigo completo del endpoint optimizado. Incluye comentarios explicando cada decision de optimizacion.
Pistas
  • Recuerda que puedes usar include anidado: usuario → pedidos → productosPedido → producto
  • Considera usar select dentro de include para limitar los campos de producto
  • Piensa si realmente necesitas todos los campos de ProductoPedido o solo algunos

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.