Daniel Murillo
Índice

Arquitectura · 28 de octubre de 2026 · Daniel Murillo

SQL, query builders y ORMs: Knex, Prisma, Drizzle y compañía

No compares todas las herramientas como si fueran lo mismo. Decide cuánta abstracción necesitas y cuánto control sobre SQL quieres conservar.

No compares todas las herramientas como si fueran el mismo producto. Primero decide cuánta abstracción necesitas y cuánto control sobre SQL quieres conservar.

Introducción

El acceso a datos afecta productividad, rendimiento, pruebas, migraciones y evolución del dominio.

En Node.js / TypeScript hay varias familias:

  1. Drivers y SQL directo — p. ej. pg.
  2. Query builders — Knex, Kysely.
  3. ORMs de modelos/entidades — Sequelize, TypeORM, MikroORM, Objection.
  4. Cliente tipado + esquema declarativo — Prisma.
  5. ORM tipado cercano a SQL — Drizzle.

Las categorías se solapan. El objetivo no es un ganador absoluto, sino comparar control, abstracción y costo de mantenimiento.

1. Mapa rápido

HerramientaCategoríaIdea central
pgDriverSQL directo
Knex.jsQuery builderSQL programático + migraciones
KyselyQuery builder tipadoSQL con tipado estático
SequelizeORM clásicoModelos y asociaciones
TypeORMORM de entidadesDecoradores, repositorios, QueryBuilder
MikroORMData MapperUnit of Work e Identity Map
Objection.jsORM sobre KnexModelos + acceso al builder
PrismaORM / toolkitEsquema declarativo + cliente generado
DrizzleORM / builder tipadoEsquema TS + API cercana a SQL

Compara siempre la versión vigente y el motor (PostgreSQL, MySQL, etc.).

2. La misma consulta, distintas APIs

Tabla users con id, first_name, email. En SQL usamos snake_case; en TypeScript, camelCase. Mapea explícitamente entre capas.

SQL con pg

const result = await pool.query(
  "SELECT id, first_name, email FROM users WHERE id = $1",
  [userId],
);
const user = result.rows[0];

Máximo control; más trabajo manual en mapeo, transacciones y convenciones.

Knex

const user = await knex("users")
  .select("id", "first_name", "email")
  .where({ id: userId })
  .first();

Kysely

const user = await db
  .selectFrom("users")
  .select(["id", "first_name", "email"])
  .where("id", "=", userId)
  .executeTakeFirst();

Prisma

const user = await prisma.user.findUnique({
  where: { id: userId },
  select: { id: true, firstName: true, email: true },
});

Asume @map("first_name") en el esquema para firstName.

Drizzle

const [user] = await db
  .select({
    id: users.id,
    firstName: users.firstName,
    email: users.email,
  })
  .from(users)
  .where(eq(users.id, userId))
  .limit(1);

Estos fragmentos ilustran ergonomía, no rendimiento. Inspecciona siempre el SQL generado en consultas críticas.

3. Comparación por dimensiones

CriterioSQL / pgKnex / KyselyORMs clásicosPrismaDrizzle
Control del SQLMuy altoAltoVariableMedio (+ raw)Alto
Tipado TSManualKysely fuerteSegún modelosCliente generadoFuerte
MigracionesAparteKnex incluyeSegún herramientaPrisma MigrateDrizzle Kit
Riesgo típicoBoilerplateAlinear tiposAbstracciones opacasLímites del modeloMenos “dominio” que un ORM rico

4. El problema N+1 (más importante que la marca del ORM)

1 query:  SELECT * FROM orders WHERE user_id = ?
N queries: SELECT * FROM items WHERE order_id = ?  (por cada orden)

Cualquier capa puede generarlo si cargas relaciones sin cuidado. Soluciones: joins, include/with conscientes, DataLoader, o SQL explícito. Mide con logs de consultas o EXPLAIN antes de culpar al ORM.

5. Migraciones: otra decisión

Evalúa:

  • ¿El SQL de migración se revisa en PR?
  • ¿Se aplica en CI?
  • ¿Qué pasa si falla a mitad?
  • ¿Cómo haces backfills grandes?

Patrón habitual en producción: expandir esquema → desplegar código compatible → migrar datos → contraer lo antiguo. No asumas rollback automático seguro.

6. Transacciones

No basta con que exista un método transaction. Comprueba aislamiento, propagación del contexto, rollback y que no dejas la transacción abierta esperando una API externa.

await knex.transaction(async (trx) => {
  const [order] = await trx("orders")
    .insert({ customer_id: customerId, status: "pending" })
    .returning(["id"]);

  await trx("order_items").insert(
    items.map((item) => ({
      order_id: order.id,
      product_id: item.productId,
      quantity: item.quantity,
    })),
  );
});

7. Cómo elegir

  • pg: control total o consultas especializadas.
  • Knex: builder flexible + migraciones sin ORM pesado.
  • Kysely: SQL explícito con tipado fuerte.
  • Sequelize / TypeORM / MikroORM / Objection: si el modelo de entidades y el equipo ya encajan; valida el SQL generado.
  • Prisma: esquema + cliente generado y DX fuerte.
  • Drizzle: tipado cercano a SQL con buen control.

Si ya hay producción, el costo de migración suele pesar más que la moda del ORM.

8. Checklist

  • Modelo alineado con el dominio.
  • Tipos TS alineados con el esquema.
  • Consultas críticas con plan razonable (EXPLAIN).
  • Sin N+1 accidental.
  • Transacciones donde debe haber atomicidad.
  • Migraciones versionadas y probadas.
  • Pool, timeouts y errores manejados.
  • Carga representativa medida.

Conclusión

No es Prisma vs Drizzle ni ORM vs SQL como guerra de bandos. Es abstracción, control, tipos, dominio y operación.

Primero entiende las consultas y el modelo. Después elige la herramienta que reduzca trabajo accidental sin ocultar lo que debes controlar.

En esta serie

  1. Autenticación
  2. Observabilidad
  3. SQL y ORMs (esta nota)
  4. Cómo probar una API
  5. Contenedores y Twelve-Factor

Lecturas relacionadas

Referencias