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:
- Definir exactamente que campos necesitas de cada modelo
- Usar
includepara traer relaciones en una sola query - Aplicar
takeyskippara paginacion - 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: trueen lugar deselectespecifico. 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
includeanidado con selectividad. - Paginacion incompleta: Usar solo
takesinskippara paginas siguientes. Solucion: Implementar logica completa de paginacion. - Filtros ineficientes: Usar
ORcon muchos condiciones oLIKEsin limites. Solucion: Revisar planes de ejecucion y simplificar condiciones.
Checklist de dominio
- ¿Uso
selecten lugar deincludecuando solo necesito campos, no relaciones completas? - ¿Verifico que los campos en
orderByywhereprincipales tengan índices? - ¿Evito el problema N+1 usando
includepara traer relaciones en una sola query? - ¿Implemento paginacion con
takeyskippara limites de datos? - ¿Uso
wherepara filtrar antes de traer datos, no después en JavaScript? - ¿Reviso el plan de ejecucion de queries complejas con
prisma.$queryRawyEXPLAIN? - ¿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.
- Contexto: Tienes un endpoint
GET /usuarios/:id/pedidos-recientesque 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). - 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[]
}- 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
});
});- 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
createdAteste indexado
- Entrega: Proporciona el codigo completo del endpoint optimizado. Incluye comentarios explicando cada decision de optimizacion.
- 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.