12k
All articles

ORM vs SQL Puro: ¿Cuál Deberías Elegir?

ORM vs SQL puro: compara seguridad, velocidad y control, ve por qué N+1 duele más que la abstracción y elige la herramienta adecuada para cada consulta.

OpenReplay Team
OpenReplay Team
ORM vs SQL Puro: ¿Cuál Deberías Elegir?

Para la mayoría de las aplicaciones, la opción predeterminada más sensata es un ORM o query builder para el 90% de las operaciones CRUD rutinarias, y SQL escrito a mano para el pequeño conjunto de consultas de reporting, analítica y rutas críticas que realmente lo justifican — no es una decisión excluyente. La pregunta que realmente importa no es “¿cuál es mejor en abstracto?” sino “¿cuál es la adecuada para esta consulta, en este equipo, dados estos requisitos?” Este artículo te ofrece una regla de decisión, una tabla comparativa y el único fallo crítico — la consulta N+1 — que afecta a los equipos con mucha más frecuencia de lo que jamás lo hará la sobrecarga de abstracción. Los ejemplos están anclados en el ecosistema JS/TS (Prisma, Drizzle, Kysely, Knex), ya que es donde se toman la mayoría de estas decisiones hoy en día.

Puntos Clave

  • Utiliza un ORM o query builder por defecto para el CRUD rutinario, y recurre a SQL puro en las consultas específicas de reporting, analítica y rutas críticas que lo necesiten de forma demostrable — la mayoría de las aplicaciones en producción terminan usando ambos.
  • El verdadero coste de rendimiento de un ORM casi nunca es la abstracción en sí; es el patrón de consulta N+1, donde cargar una lista y luego sus relaciones fila por fila convierte una sola carga de página en cientos de round-trips.
  • El N+1 se soluciona cargando la relación de forma anticipada en una única consulta (el include de Prisma o un JOIN) y seleccionando únicamente las columnas que se renderizan — no abandonando el ORM.
  • Los query builders como Drizzle, Kysely y Knex son la tercera opción que el planteamiento binario pasa por alto: consultas con estructura SQL, verificadas por tipos, con control total del SQL generado y sin capa de mapeo de objetos.
  • Los ORMs parametrizan las consultas por defecto, lo que elimina el vector de inyección SQL más común; el SQL puro es tan seguro como tu disciplina con las sentencias parametrizadas.

Qué es un ORM y qué significa “SQL puro”

Un ORM (Object-Relational Mapper) es una librería que mapea tablas de base de datos a objetos en tu lenguaje de programación, de modo que consultas los datos mediante llamadas a métodos y modelos tipados en lugar de escribir cadenas SQL a mano. Genera el SQL, lo envía a la base de datos y transforma las filas de vuelta en objetos, gestionando relaciones, parametrización y mapeo de tipos en el proceso. En el mundo JS/TS, Prisma y TypeORM son los ejemplos de ORM completo. Una lectura típica en Prisma tiene este aspecto:

// Usuarios junto con sus posts, en una sola llamada
const users = await prisma.user.findMany({
  where: { posts: { some: { title: { contains: 'test' } } } },
  include: { posts: true },
});
// users[0].posts ya es un array tipado — sin necesidad de reestructuración manual

SQL puro significa escribir el texto de la consulta tú mismo y ejecutarlo a través de un driver de base de datos, para luego reestructurar las filas planas del resultado en la estructura que necesita tu aplicación. Obtienes control total sobre la consulta exacta, pero también eres responsable del mapeo. La misma lectura de “usuarios con sus posts” contra pg requiere un join y una reagrupación manual:

const { rows } = await pool.query(
  `SELECT u.id, u.name, p.id AS post_id, p.title
   FROM users u
   LEFT JOIN posts p ON p.author_id = u.id
   WHERE p.title LIKE $1`,
  ['%test%'],
);

// Filas planas -> usuarios anidados, realizado manualmente
const users = Object.values(
  rows.reduce((acc, r) => {
    acc[r.id] ??= { id: r.id, name: r.name, posts: [] };
    if (r.post_id) acc[r.id].posts.push({ id: r.post_id, title: r.title });
    return acc;
  }, {} as Record<number, { id: number; name: string; posts: any[] }>),
);

Ese reduce es el coste real del SQL puro para lecturas relacionales: la base de datos devuelve filas, no grafos de objetos, y tú escribes la lógica de join y reagrupación cada vez. El marcador de posición $1 también está haciendo un trabajo importante — véase el apartado de seguridad más adelante.

ORM vs SQL puro: la comparación que realmente decide

La decisión se reduce a cinco criterios. Así es como se comparan los tres enfoques — ORM completo, query builder y SQL puro — en cada uno de ellos:

CriterioORM completo (Prisma, TypeORM)Query builder (Drizzle, Kysely, Knex)SQL puro
Seguridad frente a inyecciónParametrizado por defectoParametrizado por defectoSeguro solo con sentencias parametrizadas
Velocidad de desarrollo (CRUD)La más rápida — relaciones e hidratación gestionadasRápida — con estructura SQL, tú escribes los joinsLa más lenta — mapeo manual
Migraciones / esquemaDe primera clase (Prisma Migrate, TypeORM)Integrado o herramienta complementaria (Drizzle Kit, migraciones de Knex)Scripts DDL escritos a mano
Portabilidad entre BDsAlta — cambio de proveedor con pocos cambiosMedia — con conciencia de dialectoBaja — SQL específico de dialecto
Control / características específicas de BDLimitado; se necesita vía de escapeAlto — próximo a SQLTotal — cada característica, cada hint
Seguridad de tiposExtremo a extremo desde el esquemaFuerte (Kysely/Drizzle); mejor esfuerzo (Knex)Ninguna sin herramientas adicionales

En cuanto a seguridad, la verdad práctica es sencilla: los ORMs y los query builders parametrizan los valores por defecto, lo que elimina el vector de inyección clásico. El SQL puro es tan seguro como tu disciplina — la propia documentación de Prisma advierte que si usas el método inseguro con entradas de usuario, abres la posibilidad de ataques de inyección SQL, que pueden exponer tus datos a modificación o eliminación. Usa marcadores de posición ($1, ?) y nunca interpolés entradas de usuario en la cadena de consulta, y el SQL puro es seguro; omite esa disciplina y no lo será.

En cuanto a migraciones y portabilidad, los ORMs ganan en el caso rutinario. Un cambio de esquema se convierte en una migración versionada y repetible que se ejecuta de forma idéntica en todos los entornos, y cambiar de proveedor de base de datos es principalmente un cambio de configuración. El SQL puro no te ofrece ninguna de estas ventajas de forma gratuita — gestionas los scripts DDL tú mismo y tus consultas están ligadas a la sintaxis de un dialecto concreto. En cuanto a control, el SQL puro gana de forma decisiva: las funciones de ventana, las CTEs recursivas, los hints de índice específicos de la base de datos y el ajuste con EXPLAIN ANALYZE están todos disponibles sin tener que luchar contra una abstracción.

La verdad sobre el rendimiento: es el N+1, no la abstracción

La crítica más repetida a los ORMs — “generan consultas lentas” — es mayoritariamente incorrecta para el CRUD y mayoritariamente correcta para un patrón específico. Los ORMs modernos generan SQL competente para lecturas y escrituras cotidianas. El verdadero coste de rendimiento es el problema de la consulta N+1: cargas una lista de N filas y luego disparas una consulta más por fila para obtener una relación, convirtiendo una sola carga de página en N+1 round-trips a la base de datos.

Aquí está el fallo, que habitualmente se esconde dentro de un bucle de aspecto inocente:

// 1 consulta para los usuarios...
const users = await prisma.user.findMany();

// ...luego N más, una por usuario — esta es la trampa N+1
for (const user of users) {
  user.posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

La solución no es abandonar el ORM. Cargas la relación de forma anticipada en una única consulta y seleccionas únicamente las columnas que realmente renderizas:

const users = await prisma.user.findMany({
  include: { posts: { select: { id: true, title: true } } },
});

Esto colapsa 1 + N consultas en una sola. El síntoma visible para el usuario de un N+1 sin corregir es una página de lista o panel que es ágil con datos de prueba y lenta bajo volumen de producción — exactamente el tipo de latencia en el front-end que una reproducción de sesión pone de manifiesto desde el lado del usuario, apuntándote de vuelta a la capa de consultas para contar las peticiones y añadir carga anticipada.

Inspecciona el SQL generado antes de culpar a la herramienta

Antes de concluir que un ORM es lento, observa lo que realmente emite. Prisma registra cada consulta cuando instancias el cliente con log: ['query']; Kysely expone .compile() para devolver el SQL y los parámetros; y Drizzle expone el método .toSQL() en una consulta con el mismo propósito. Luego ejecuta EXPLAIN ANALYZE sobre la salida para ver el plan que la base de datos ejecuta realmente.

const prisma = new PrismaClient({ log: ['query'] });
// cada consulta que ejecuta Prisma se imprime ahora — cuéntalas, cópialas en EXPLAIN ANALYZE

Esto convierte “el ORM es lento” de una intuición en un flujo de trabajo inspeccionable: lee el SQL, cuenta los round-trips, comprueba el plan.

Una nota sobre el motor en sí: uno de los cambios más significativos en Prisma 7 es la eliminación completa del motor de consultas basado en Rust en favor de una implementación en TypeScript. El nuevo Query Compiler se ejecuta en TypeScript y WebAssembly, lo que elimina la necesidad del paso de serialización entre lenguajes y resulta en una ejecución de consultas más rápida; y dado que el motor ya no depende de un binario nativo, ahora puedes usar Prisma en entornos que soporten JavaScript o WASM, como Cloudflare Workers, Bun y Deno. Prisma informa que la reescritura es a menudo significativamente más rápida donde más importa — en consultas grandes y complejas — mientras se mantiene a la par en las más simples. El punto relevante para esta comparación: el debate sobre el motor ha cambiado de forma en lugar de desaparecer, y sigue siendo secundario respecto al número de consultas. Para la abrumadora mayoría de páginas lentas, el N+1 y el exceso de datos recuperados son la causa, no la capa de abstracción.

Query builders: la tercera opción que el planteamiento binario pasa por alto

El enfoque “ORM vs SQL puro” oculta un sólido término medio. Los query builders como Drizzle, Kysely y Knex te permiten escribir consultas con estructura SQL, verificadas por tipos, y mantener el control total del SQL generado, sin la capa de mapeo de objetos de un ORM. Kysely es el query builder SQL con mayor seguridad de tipos para TypeScript, utilizado en producción por Deno, Maersk y Cal.com, con TypeScript moderno y sin sobrecarga en tiempo de ejecución. Es una capa de abstracción delgada sobre SQL, que se centra en la familiaridad a través de la nomenclatura y la estructura, y en la predictibilidad mediante una compilación 1:1.

const users = await db
  .selectFrom('users')
  .innerJoin('posts', 'posts.author_id', 'users.id')
  .select(['users.id', 'users.name', 'posts.title'])
  .where('posts.title', 'like', '%test%')
  .execute();

La forma refleja el SQL, pero los nombres de columnas y tablas se verifican por tipos contra tu esquema. Como señala la documentación de Kysely, un query builder te da control total sobre tu SQL mientras TypeScript detecta los errores de forma temprana, antes de que tu código siquiera se ejecute. Knex es la opción consolidada y funciona con Postgres, MySQL, SQLite y más, pero ten en cuenta sus limitaciones de tipado: según su propia guía, el soporte de TypeScript es de mejor esfuerzo, no todos los patrones de uso pueden verificarse por tipos, y la ausencia de errores de tipo no garantiza actualmente que las consultas generadas sean correctas, por lo que se recomienda escribir pruebas incluso con TypeScript. Para un nuevo proyecto en TS que quiera seguridad de tipos sin la complejidad de un ORM, Kysely o Drizzle son la opción más sólida.

Una regla de decisión por escenario

Adapta la herramienta a la carga de trabajo en lugar de elegir una para toda la base de código:

  1. CRUD simple a moderado, un equipo y un esquema en evolución → ORM. Obtienes consultas parametrizadas, migraciones versionadas, carga de relaciones y tipos extremo a extremo. Esto cubre la mayor parte del código de aplicación.
  2. Reporting, analítica, operaciones masivas o una ruta crítica medida → SQL puro, vistas o procedimientos almacenados. Cuando necesitas funciones de ventana, agregaciones complejas o planes ajustados a mano verificados con EXPLAIN ANALYZE, escribe el SQL directamente.
  3. Quieres seguridad de tipos y control sobre SQL sin un ORM → query builder. Drizzle o Kysely te ofrecen consultas tipadas con estructura SQL, salida predecible y sin comportamiento N+1 oculto.

El enfoque híbrido en el que aterrizan la mayoría de las aplicaciones en producción

En la práctica, las bases de código maduras no toman partido — utilizan un ORM para la mayor parte del trabajo y mantienen una vía de escape a SQL puro para el resto. Prisma hace esto explícito con TypedSQL: una funcionalidad de Prisma ORM que te permite escribir tus consultas SQL puras de forma completamente segura en cuanto a tipos. Escribes SQL en un archivo .sql y lo invocas a través de una función generada y tipada:

import { getUsersByAge } from './generated/prisma/sql';
const users = await prisma.$queryRawTyped(getUsersByAge(18, 30));

Es una funcionalidad en vista previa: la habilitas con previewFeatures = ["typedSql"] en el bloque del generador, y dado que está tipada estáticamente, puede no manejar ciertos escenarios como las cláusulas WHERE generadas dinámicamente — estas siguen necesitando los métodos raw sin tipado. Prisma plantea el flujo de trabajo exactamente como lo hace este artículo: una abstracción de alto nivel que sirve a la mayoría de las consultas, más una vía de escape con seguridad de tipos para cuando necesitas elaborar SQL directamente. Por eso la respuesta moderna a “ORM o SQL puro” suele ser “un ORM (o query builder) que te permita escapar a SQL puro cuando lo necesites.”

Conclusión

Elige el ORM o el query builder como tu opción predeterminada para el CRUD cotidiano, recurre al SQL puro en las consultas que lo justifiquen de forma demostrable, y trata ambos como capas en una única estrategia de acceso a datos en lugar de como bandos rivales. El siguiente paso concreto en cualquier proyecto existente: activa el registro de consultas, identifica los endpoints de lista o panel, cuenta las consultas por petición y corrige los N+1 con carga anticipada y una selección de columnas más ajustada — ese único recorrido habitualmente recupera más rendimiento del que jamás lograría cualquier reescritura de ORM a SQL puro.

Preguntas Frecuentes

¿Cuál es la diferencia entre un ORM y un query builder?

Un ORM mapea tablas de base de datos a objetos e hidrata los resultados de las consultas en modelos tipados, gestionando las relaciones y el mapeo de objetos por ti, mientras que un query builder genera SQL verificado por tipos pero devuelve filas simples sin una capa de mapeo de objetos. Prisma y TypeORM son ORMs; Drizzle, Kysely y Knex son query builders. Los query builders están más próximos al SQL, ofreciendo mayor control sobre la consulta generada con menos abstracción y sin comportamiento oculto de carga de relaciones.

¿Usar un ORM previene la inyección SQL?

Sí, para el uso normal. Los ORMs y los query builders parametrizan los valores por defecto, enviando la entrada del usuario como parámetros enlazados en lugar de interpolarla en la cadena de consulta, lo que elimina el vector de inyección clásico. La excepción son los métodos de consulta raw inseguros: la documentación de Prisma advierte que usar el método inseguro con entradas de usuario abre la posibilidad de inyección SQL. El SQL puro es tan seguro como tu disciplina con marcadores de posición como el signo de dólar-uno o el signo de interrogación.

¿Es el SQL puro más rápido que un ORM?

No en la mayoría de los casos reales. Los ORMs modernos generan SQL competente para el CRUD cotidiano, y la brecha de rendimiento habitual proviene del número de consultas, no de la abstracción. La causa dominante de las páginas lentas es el patrón N+1, donde cargar una lista y luego sus relaciones fila por fila crea cientos de round-trips. El SQL puro merece la pena para consultas de reporting, analítica y rutas críticas medidas que genuinamente necesitan planes ajustados a mano.

¿Cómo puedo ver el SQL real que genera un ORM?

Cada herramienta expone el SQL generado directamente. Prisma registra cada consulta cuando instancias el cliente con la opción de registro de consultas habilitada. Kysely expone un método compile que devuelve la cadena SQL y sus parámetros. Drizzle expone un método toSQL en una consulta con el mismo propósito. Una vez que tienes el SQL, ejecuta EXPLAIN ANALYZE en PostgreSQL sobre él para inspeccionar el plan de ejecución y confirmar si los índices se están utilizando según lo esperado.

¿Puedo escribir SQL puro dentro de un proyecto con ORM?

Sí, y es el patrón habitual en producción. La mayoría de los ORMs mantienen una vía de escape a SQL puro junto a sus consultas generadas. Prisma ofrece TypedSQL, una funcionalidad en vista previa habilitada con el flag de vista previa typedSql, que te permite escribir SQL en un archivo con extensión .sql e invocarlo a través de una función generada y completamente tipada. Al estar tipada estáticamente, no gestiona las cláusulas WHERE generadas dinámicamente, que siguen requiriendo los métodos de consulta raw sin tipado.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.