Prisma query logging y PostgreSQL: cuándo alcanza el ORM y cuándo tenés que ir más abajo
Estaba revisando una consulta lenta en un backend TypeScript y el log de Prisma me decía que la query tardaba 12ms. El frontend seguía mostrando tiempos de respuesta de 400ms. Ahí quedé mirando los números sin entender qué estaba viendo. Tardé un rato en darme cuenta del problema: estaba leyendo el instrumento equivocado para el síntoma que tenía.
Mi tesis es esta: los query logs de Prisma son una herramienta de debugging del ORM, no de diagnóstico de base de datos. Son útiles para ver qué SQL se genera y detectar patrones como N+1. Pero si el problema está en el motor de PostgreSQL —locks, plan de ejecución, autovacuum, I/O— esos logs no te van a decir nada útil. El error más común que veo es tratar el log del ORM como si fuera instrumentación de la base.
Qué hace el logging de Prisma (y qué mide exactamente)
Según la documentación oficial de Prisma, el cliente soporta cuatro niveles de log: query, info, warn y error. El nivel query es el que más interesa para debugging.
La configuración básica:
// Inicialización del cliente con logs activados
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' }, // emitir como evento para capturar programáticamente
{ level: 'warn', emit: 'stdout' },
{ level: 'error', emit: 'stdout' },
],
})
// Escuchar el evento de query para loguear con contexto
prisma.$on('query', (e) => {
console.log('Query SQL:', e.query)
console.log('Parámetros:', e.params)
console.log('Duración reportada por Prisma:', e.duration, 'ms')
})El campo e.duration mide el tiempo que Prisma Client registra desde que envía la query hasta que recibe la respuesta del driver. No incluye tiempo de serialización del resultado en tu aplicación, ni overhead de red si usás Prisma Accelerate, ni tiempo de planificación interno de PostgreSQL que quede fuera de ese round-trip.
Esto es importante: la duración que ves en el log de Prisma no es lo mismo que el tiempo de ejecución de la query en el motor de PostgreSQL. En la mayoría de los casos prácticos son valores cercanos, pero los casos donde difieren son exactamente los que necesitás diagnosticar.
Dónde el log de queries realmente ayuda
El log de nivel query tiene valor genuino en tres escenarios concretos.
Primero: detectar N+1. Si en un loop de aplicación generás una query por cada ítem, Prisma va a loguear cada una. Podés verlo directamente en la consola durante desarrollo. El fix suele ser agregar include o usar findMany con la relación.
// Patrón N+1 — va a generar una query por cada usuario
const usuarios = await prisma.user.findMany()
for (const u of usuarios) {
const posts = await prisma.post.findMany({ where: { authorId: u.id } })
// esto genera N queries extra
}
// Versión que genera una sola query con JOIN
const usuariosConPosts = await prisma.user.findMany({
include: { posts: true }, // Prisma arma el JOIN solo
})Segundo: verificar el SQL generado. Prisma genera SQL basado en el schema y las opciones que pasás. A veces el SQL resultante no es lo que esperabas. El log te permite ver exactamente qué se manda a la base antes de buscar el problema en otro lado.
Tercero: contar queries en una operación. Si querés saber cuántas queries dispara un endpoint particular, podés instrumentar con el evento y contar. No necesitás herramienta externa para ese diagnóstico específico.
Lo que el log de Prisma no hace: no te muestra el plan de ejecución, no te avisa si una query está haciendo un sequential scan porque le falta un índice, no te reporta si hubo lock contention, no registra tiempo de I/O ni tiempo de espera en el connection pool.
Cuándo el log del ORM no alcanza y tenés que ir a PostgreSQL
Acá está la distinción que más me cuesta ver aplicada en equipos que aprenden Prisma. Hay síntomas que parecen de ORM pero en realidad son de motor.
Algunos casos típicos:
- La query reportada por Prisma es "rápida" pero el endpoint es lento → probable bottleneck fuera del tiempo de query (serialización, N+1 de red, pool exhausto)
- Queries que son rápidas en desarrollo y lentas en producción con el mismo código → probablemente diferencia en datos, índices o estadísticas del planificador
- Tiempos intermitentes altos sin patrón claro → posible lock contention o autovacuum interfiriendo
Para estos casos, PostgreSQL tiene sus propias herramientas. Las más directas:
-- Ver las queries más lentas acumuladas (requiere pg_stat_statements habilitado)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Ver el plan de ejecución de una query específica
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM "User"
WHERE email = '[email protected]';
-- Ver si hay locks activos ahora mismo
SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;pg_stat_statements es una extensión que viene incluida en la mayoría de distribuciones de PostgreSQL y que en servicios como Railway o Supabase podés habilitar con CREATE EXTENSION IF NOT EXISTS pg_stat_statements;. Te da tiempos reales acumulados por query, conteo de ejecuciones y buffers usados. Es una fuente de evidencia que el log del ORM no puede darte.
El error común: tratar el log del ORM como observabilidad completa
La receta que veo circular: "activá log: ['query'] en Prisma y ya tenés visibilidad de la base". Es parcialmente verdad y parcialmente peligrosa.
El costo oculto de esa receta es que te da confianza falsa. Si todos tus logs dicen que las queries tardan menos de 20ms y el sistema igual está lento, empezás a buscar el problema en el código de aplicación, en React, en el servidor. Y el problema real era un sequential scan sobre una tabla de 2 millones de filas que faltaba indexar.
El contraejemplo concreto: una query findMany con where sobre un campo sin índice puede reportar 8ms en Prisma durante desarrollo (tabla con 500 filas) y 900ms en producción (tabla con 800.000 filas). El log del ORM en ambos casos te muestra "8ms" y "900ms". No te dice por qué cambió. Para eso necesitás EXPLAIN ANALYZE en producción.
Checklist: qué mirar primero según el síntoma
| Síntoma | Primer instrumento | Por qué |
|---|---|---|
| Sospechás N+1 | Log de Prisma (query events) | Contás queries generadas por operación |
| SQL generado no es el esperado | Log de Prisma (query level) | Ves el SQL exacto antes de ejecutar |
| Query lenta que no mejoró con índice | EXPLAIN (ANALYZE, BUFFERS) en PG | Ves el plan real y los buffers leídos |
| Lentitud intermitente sin patrón | pg_stat_activity + pg_locks | Detectás lock waits o autovacuum |
| Queries lentas en prod, rápidas en dev | pg_stat_statements en prod | Comparás tiempos reales acumulados |
| Endpoint lento pero queries OK | APM / tracing de aplicación | El bottleneck no está en la base |
La regla que me funciona: el log de Prisma es para el qué (qué queries se generan), PostgreSQL es para el cómo (cómo las ejecuta el motor). Cuando mezclás los dos planos sin distinguirlos, el diagnóstico se complica innecesariamente.
Qué no podés concluir sin datos productivos
Quiero ser explícito sobre los límites de esta guía, porque es fácil sobre-inferir.
No podés decir que activar logs de Prisma degrada el rendimiento en producción sin medirlo en el stack específico. La documentación de Prisma no da números de overhead para el modo emit: 'event' en escenarios de alta concurrencia. Es plausible que haya un costo, pero cuánto depende del volumen y del handler que escribas.
No podés decir que pg_stat_statements es suficiente sin entender qué queries incluye. Por defecto agrega todas las queries que pasan por el planificador, pero el parámetro pg_stat_statements.track controla si también rastrea funciones y procedimientos almacenados. Revisá la configuración del servidor antes de asumir cobertura total.
No podés concluir que una query es "rápida" basándote solo en el e.duration de Prisma si no sabés si hay connection pooling de por medio (PgBouncer, Prisma Accelerate) que pueda estar absorbiendo latencia o añadiéndola.
Estos no son casos extremos. Son exactamente los escenarios donde una guía como esta puede dar confianza equivocada si se aplica mecánicamente.
FAQ: Prisma query logging y PostgreSQL
¿El log de queries de Prisma impacta el rendimiento en producción?
La documentación oficial no cuantifica el overhead. En modo emit: 'stdout' hay I/O sincrónico por cada query, lo cual en alta concurrencia puede ser relevante. En modo emit: 'event' el costo depende de qué hacés en el handler. Para producción, la práctica más prudente es activar logging solo para niveles warn y error, y usar pg_stat_statements para análisis de rendimiento real.
¿Cuál es la diferencia entre emit: 'stdout' y emit: 'event'?
Con emit: 'stdout', Prisma imprime directamente a la consola. Con emit: 'event', emite un evento al que podés suscribirte con prisma.$on('query', handler). La segunda opción te permite integrar con sistemas de logging estructurado como Pino o Winston y agregar contexto (request ID, usuario, etc.) antes de guardar.
¿Prisma tiene algo parecido a EXPLAIN ANALYZE?
No de forma nativa. Podés ejecutar EXPLAIN ANALYZE desde Prisma usando prisma.$queryRaw:
// Ejecutar EXPLAIN ANALYZE directamente desde Prisma
const plan = await prisma.$queryRaw`
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM "User" WHERE email = ${email}
`
console.log(plan)Pero para análisis serio de planes conviene usar psql directo o una herramienta como pgAdmin que formatea el output.
¿pg_stat_statements está disponible en todos los entornos de PostgreSQL?
Viene incluida en la mayoría de distribuciones de PostgreSQL, pero requiere habilitación explícita. En servicios gestionados como Railway o Supabase generalmente está disponible. Para habilitarla: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;. Verificá si el rol que usás tiene permisos suficientes.
¿Cuándo conviene usar Prisma Accelerate en lugar de conexión directa? Prisma Accelerate (docs oficiales) agrega connection pooling y caching a nivel de infraestructura. Si el problema que diagnosticás es agotamiento del connection pool (muchas conexiones simultáneas a PostgreSQL), Accelerate puede ayudar. Pero no resuelve problemas de queries lentas por planes de ejecución ineficientes. Son capas distintas.
¿Cómo logueo solo las queries que superan un umbral de tiempo?
Con el evento query podés filtrar por e.duration:
// Loguear solo queries que tardan más de 100ms
prisma.$on('query', (e) => {
if (e.duration > 100) {
console.warn(`Query lenta (${e.duration}ms):`, e.query)
}
})Es un filtro útil para desarrollo. Para producción, pg_stat_statements con mean_exec_time es más robusto porque acumula datos sin depender de que el proceso de Node esté corriendo.
Mi postura y el próximo paso concreto
Prisma logs son buenos para lo que son: visibilidad del ORM durante desarrollo, detección de N+1 y verificación del SQL generado. No son un sistema de observabilidad de base de datos.
Lo que no compro es la idea de que activar log: ['query'] en Prisma es suficiente para entender el comportamiento de PostgreSQL en producción. Es una simplificación cómoda que funciona hasta que tenés un problema real de rendimiento y no sabés por qué los números no cierran.
Mi recomendación concreta: activá el log de Prisma en nivel warn/error en producción, habilitá pg_stat_statements en PostgreSQL y revisalo antes de optimizar cualquier query. Si nunca miraste pg_stat_statements en un esquema que ya tiene tráfico real, ese es el primer paso. Lo que encontrás ahí suele ser más sorprendente que cualquier log del ORM.
Si te interesa el stack más amplio, tengo posts sobre patrones de autorización en Next.js Middleware y sobre decisiones de arquitectura de identidad que tocan PostgreSQL desde ángulos distintos.
Fuente original:
- Prisma Client logging (documentación oficial): https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging
Artículos Relacionados
Next.js App Router caching: revalidate, dynamic y no-store sin folklore
El problema con el caching en Next.js App Router no es memorizar flags. Es entender qué frescura necesita cada dato y tratarlo como un contrato explícito, no como un truco aislado que se aplica cuando algo falla.
MCP Model Context Protocol en TypeScript: diseñá tools portables entre Claude, GPT y modelos locales
El error más común al implementar MCP tools es acoplarlas al SDK del proveedor. La spec existe para evitar exactamente eso. Guía práctica de diseño desde arquitectura: el contrato de input/output que hace que una tool funcione en Claude, GPT y modelos locales sin reescribir nada.
Web Crypto API en el browser vs Node.js: las diferencias que te van a quemar
Web Crypto API parece una sola cosa hasta que intentás reutilizar el mismo código de cifrado en browser, Node.js y el edge runtime de Next.js. Las diferencias son sutiles, están documentadas y casi nadie las lee hasta que algo explota.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.