{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql","title":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","description":"Sintaxis SQL-like que es realmente type-safe, cero overhead en runtime, soporte para edge runtime, y una comparación honesta con Prisma y Kysely.","language":"es","datePublished":"2026-03-11","dateModified":null,"contentFormat":"text/markdown","content":"Backend\n\n# Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma\n\n2026-03-116 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/drizzle-orm-type-safe-sql-postgresql>)\n\n## La Idea Central: SQL como Ciudadano de Primera Clase en TypeScript\n\nLa filosofía de [Drizzle](<https://orm.drizzle.team/>) es simple: SQL ya es un buen lenguaje de queries. No abstraerlo, hacerlo type-safe. A continuación se presenta cómo se ve en la práctica:\n\n```typescript\nimport { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name').notNull(),\n role: text('role', { enum: ['admin', 'user', 'viewer'] }).notNull().default('user'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n lastLoginAt: timestamp('last_login_at'),\n});\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n content: text('content').notNull(),\n published: boolean('published').notNull().default(false),\n authorId: integer('author_id').notNull().references(() => users.id),\n viewCount: integer('view_count').notNull().default(0),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n});\n\nexport const comments = pgTable('comments', {\n id: serial('id').primaryKey(),\n body: text('body').notNull(),\n postId: integer('post_id').notNull().references(() => posts.id),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n posts: many(posts),\n comments: many(comments),\n}));\n\nexport const postsRelations = relations(posts, ({ one, many }) => ({\n author: one(users, {\n fields: [posts.authorId],\n references: [users.id],\n }),\n comments: many(comments),\n}));\n```\n\nAhora observar qué pasa cuando hacés queries. Cada campo, cada join, cada condición está completamente type-checked:\n\n```typescript\nimport { drizzle } from 'drizzle-orm/neon-http';\nimport { neon } from '@neondatabase/serverless';\nimport { eq, and, gt, desc, sql, count } from 'drizzle-orm';\nimport * as schema from './schema';\n\nconst queryClient = neon(process.env.DATABASE_URL!);\nconst db = drizzle(queryClient, { schema });\n\n// Select simple, el tipo de retorno se infiere exactamente\nconst activeUsers = await db\n .select({\n id: schema.users.id,\n email: schema.users.email,\n name: schema.users.name,\n })\n .from(schema.users)\n .where(eq(schema.users.role, 'admin'))\n .orderBy(desc(schema.users.createdAt))\n .limit(10);\n\n// TypeScript sabe: activeUsers es Array<{ id: number; email: string; name: string }>\n// Intentá acceder a activeUsers[0].role, error de compilación! No fue seleccionado.\n```\n\nEsta es la diferencia clave con Prisma. En Prisma, si utilizars ` select `, teners type safety. Pero queries complejas con escape hatches de raw SQL pierden información de tipos. En Drizzle, incluso fragmentos de SQL raw pueden llevar información de tipos:\n\n```typescript\n// Query compleja con subquery, agregación y raw SQL type-safe\nconst topAuthors = await db\n .select({\n authorId: schema.posts.authorId,\n authorName: schema.users.name,\n postCount: count(schema.posts.id).as('post_count'),\n totalViews: sql<number>`sum(${schema.posts.viewCount})`.as('total_views'),\n avgViews: sql<number>`avg(${schema.posts.viewCount})::integer`.as('avg_views'),\n })\n .from(schema.posts)\n .innerJoin(schema.users, eq(schema.posts.authorId, schema.users.id))\n .where(eq(schema.posts.published, true))\n .groupBy(schema.posts.authorId, schema.users.name)\n .having(gt(count(schema.posts.id), 5))\n .orderBy(desc(sql`sum(${schema.posts.viewCount})`))\n .limit(20);\n```\n\nEse template literal tag ` sql<number> ` es genial. Escribirs SQL real pero anotás el tipo de retorno, y Drizzle lo propaga por toda la inferencia de tipos de la query.\n\n## Estrategias de Migración: Donde Drizzle Kit Brilla\n\nDrizzle Kit es la herramienta CLI compañera para manejo de schemas. Tiene dos modos:\n\n### ` drizzle-kit push `, El Modo Desarrollo\n\n```bash\n# Lee tus archivos de schema, compara contra la base real,\n# genera y aplica ALTER statements directamente\nnpx drizzle-kit push\n\n# Output:\n# [✓] Leyendo archivos de schema...\n# [✓] Conectando a la base...\n# [i] Cambios detectados:\n# ALTER TABLE \"posts\" ADD COLUMN \"slug\" text;\n# CREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");\n# [✓] 2 cambios aplicados\n```\n\nEsto es para iteración rápida. Cambiás tu archivo TypeScript de schema, ejecutars ` push `, y tu base coincide con. Sin archivos de migración, sin versionado.\n\n### ` drizzle-kit generate ` + ` migrate `, El Modo Producción\n\n```bash\nnpx drizzle-kit generate\n# Crea: drizzle/0003_add_post_slug.sql\n```\n\nEl archivo de migración generado:\n\n```sql\n-- drizzle/0003_add_post_slug.sql\nALTER TABLE \"posts\" ADD COLUMN \"slug\" text;--> statement-breakpoint\nCREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");\n```\n\nEs posible editar estos archivos, agregar migraciones de datos, wrappear en transacciones:\n\n```sql\n-- drizzle/0003_add_post_slug.sql (editado)\nBEGIN;\n\nALTER TABLE \"posts\" ADD COLUMN \"slug\" text;--> statement-breakpoint\n\n-- Migración de datos: generar slugs desde los títulos\nUPDATE \"posts\" SET \"slug\" = lower(\n regexp_replace(\n regexp_replace(\"title\", '[^a-zA-Z0-9\\s-]', '', 'g'),\n '\\s+', '-', 'g'\n )\n);--> statement-breakpoint\n\nALTER TABLE \"posts\" ALTER COLUMN \"slug\" SET NOT NULL;--> statement-breakpoint\nCREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");--> statement-breakpoint\n\nCOMMIT;\n```\n\n## Prepared Statements y la Historia del Edge Runtime\n\nEste es el punto donde la arquitectura de Drizzle rinde dividendos. Como no hay binario de query engine, Drizzle corre en cualquier lugar donde corra JavaScript:\n\n```typescript\n// Cloudflare Workers, funciona directo\nimport { drizzle } from 'drizzle-orm/neon-http';\nimport { neon } from '@neondatabase/serverless';\n\nexport default {\n async fetch(request: Request, env: Env): Promise<Response> {\n const sql = neon(env.DATABASE_URL);\n const db = drizzle(sql);\n\n // Prepared statement, compilado una vez, ejecutado muchas\n const getUser = db\n .select()\n .from(schema.users)\n .where(eq(schema.users.id, sql.placeholder('userId')))\n .prepare('get_user_by_id');\n\n const user = await getUser.execute({ userId: 42 });\n\n return Response.json(user);\n }\n};\n```\n\n## La Comparación Honesta con Prisma\n\n[Prisma](<https://www.prisma.io/>) es una buena herramienta con años de validación en producción. Pero este es el punto donde los dos genuinamente difieren:\n\n### Tiempo de Cold Start\n\nEsto es medible y significativo para serverless:\n\n```\nCold start de Prisma (AWS Lambda, Node.js 20):\n - Carga binario query engine: ~180ms\n - Parseo de schema: ~45ms\n - Primera query: ~80ms\n Total: ~305ms\n\nCold start de Drizzle (mismo entorno):\n - Import de módulo: ~12ms\n - Primera query: ~75ms\n Total: ~87ms\n```\n\nEse es ~220ms de diferencia son el query engine de Prisma cargando. En Cloudflare Workers con presupuestos de cold start más estrictos, es todavía más doloroso.\n\n### Generación de Queries\n\nPrisma genera su propio plan de query y lo traduce a SQL. A veces produce queries subóptimas:\n\n```typescript\n// Prisma genera: múltiples queries separadas (potencial 1+N)\n// SELECT * FROM \"posts\" WHERE \"published\" = true\n// SELECT * FROM \"users\" WHERE \"id\" IN (...)\n// SELECT * FROM \"comments\" WHERE \"post_id\" IN (...)\n// SELECT * FROM \"users\" WHERE \"id\" IN (...)\n\n// Drizzle genera: lateral joins donde sea posible\n// SELECT ... FROM \"posts\"\n// LEFT JOIN LATERAL (\n// SELECT ... FROM \"comments\"\n// LEFT JOIN \"users\" ON ...\n// WHERE \"comments\".\"post_id\" = \"posts\".\"id\"\n// ORDER BY \"created_at\" DESC LIMIT 5\n// ) ON true\n// LEFT JOIN \"users\" ON \"users\".\"id\" = \"posts\".\"author_id\"\n// WHERE \"posts\".\"published\" = true\n```\n\nEl enfoque de lateral join es una sola query que PostgreSQL puede optimizar holísticamente.\n\n## Transacciones y Manejo de Errores\n\n```typescript\nconst newPost = await db.transaction(async (tx) => {\n const [author] = await tx\n .select()\n .from(schema.users)\n .where(eq(schema.users.id, authorId))\n .for('update'); // SELECT FOR UPDATE, lock a nivel fila\n\n if (!author) throw new Error('Autor no encontrado');\n\n const [post] = await tx\n .insert(schema.posts)\n .values({\n title,\n content,\n authorId: author.id,\n published: false,\n })\n .returning();\n\n await tx\n .update(schema.users)\n .set({ lastLoginAt: new Date() })\n .where(eq(schema.users.id, authorId));\n\n return post;\n});\n// La transacción auto-commitea en éxito, auto-rollbackea en throw\n```\n\n## Limitaciones Notables\n\nDespués de uso extendido en producción con Drizzle, las limitaciones notables son:\n\n1. **El query builder relacional (` db.query.* `) es menos flexible que el builder SQL-like.** Para queries complejas, siempre bajo al API ` select/from/where `. El ` findMany ` con ` with ` está genial para lecturas anidadas simples pero se rompe para cualquier cosa con joins custom o CTEs.\n2. **Los mensajes de error del diff de schema pueden ser crípticos.** Cuando ` drizzle-kit push ` detecta un cambio de tipo de columna, el error a veces no te dice qué tabla o columna cambió.\n3. **No tiene connection pooling built-in.** Se necesita traer el tuyo (` postgres.js ` pool, driver serverless de [Neon](<https://neon.com/>), etc.).\n\nPero estos son cortes de papel comparados con los beneficios fundamentales: cero dependencias de binarios, type safety genuino a nivel SQL, y la libertad de desplegar en cualquier lugar donde corra JavaScript. Drizzle representa la dirección que el espacio de ORMs necesitaba tomar.\n\n## Preguntas frecuentes\n\n### ¿Qué hace diferente a Drizzle ORM de Prisma?\n\nDrizzle genera SQL a nivel TypeScript con cero overhead en runtime, las queries compilan a strings SQL planos. Prisma usa un query engine binario basado en Rust que agrega ~10-15MB a tu deployment e introduce su propia capa de planificación de queries. La API de Drizzle espeja la sintaxis SQL, mientras que Prisma es más abstraída.\n\n### ¿Puede Drizzle ORM correr en edge runtimes como Cloudflare Workers?\n\nSí. Drizzle no tiene dependencias de binarios nativos, haciéndolo compatible con Cloudflare Workers, Vercel Edge Functions, Deno Deploy y cualquier otro edge runtime.\n\n### ¿Cómo funcionan las migraciones de Drizzle?\n\nDrizzle Kit genera archivos de migración SQL diffeando tu schema TypeScript contra el estado actual de la base o snapshots de migraciones anteriores. Obteners archivos .sql planos que es posible revisar, editar y correr con cualquier herramienta de migración.\n\n## Tips\n\n1. **Tip:** Utilizar ` drizzle-kit push ` para prototipar rápido y ` drizzle-kit generate ` + ` migrate ` para producción. Es común sobre-diseñar pipelines de migración custom antes de darse cuenta de que ` push ` hace exactamente lo necesario para entornos dev, diffea tu schema contra la base y aplica cambios directo.\n2. **Tip:** Definir siempre el schema en un directorio separado ` schema/ ` con un archivo por tabla. Cuando el schema está en un solo ` schema.ts ` y se llega a 40+ tablas, el archivo se vuelve inmanejable y la introspección de drizzle-kit se arrastra.\n3. **Tip:** Envolver queries complejas en CTEs ` db.$with() ` en vez de subqueries, el soporte de CTEs de Drizzle preserva la inferencia de tipos a lo largo de toda la cadena, y el query planner de PostgreSQL maneja los CTEs como barreras de optimización (que es frecuentemente lo que se desea).\n4. **Tip:** Para Neon serverless, configurar ` prepare: false ` en la config de Drizzle. El connection pooler de Neon (pgbouncer en modo transacción) no soporta prepared statements entre conexiones, y se va a tener errores crípticos ` prepared statement does not exist ` bajo carga.\n\n- drizzle\n- orm\n- postgresql\n- type-safe-sql\n- prisma\n- kysely\n- edge-runtime\n- migraciones\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#title"},{"level":2,"text":"La Idea Central: SQL como Ciudadano de Primera Clase en TypeScript","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#la-idea-central-sql-como-ciudadano-de-primera-clase-en-typescript"},{"level":2,"text":"Estrategias de Migración: Donde Drizzle Kit Brilla","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#estrategias-de-migracin-donde-drizzle-kit-brilla"},{"level":3,"text":"drizzle-kit push, El Modo Desarrollo","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#drizzle-kit-push-el-modo-desarrollo"},{"level":3,"text":"drizzle-kit generate + migrate, El Modo Producción","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#drizzle-kit-generate-migrate-el-modo-produccin"},{"level":2,"text":"Prepared Statements y la Historia del Edge Runtime","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#prepared-statements-y-la-historia-del-edge-runtime"},{"level":2,"text":"La Comparación Honesta con Prisma","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#la-comparacin-honesta-con-prisma"},{"level":3,"text":"Tiempo de Cold Start","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#tiempo-de-cold-start"},{"level":3,"text":"Generación de Queries","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#generacin-de-queries"},{"level":2,"text":"Transacciones y Manejo de Errores","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#transacciones-y-manejo-de-errores"},{"level":2,"text":"Limitaciones Notables","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#limitaciones-notables"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#questions"},{"level":3,"text":"¿Qué hace diferente a Drizzle ORM de Prisma?","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-1"},{"level":3,"text":"¿Puede Drizzle ORM correr en edge runtimes como Cloudflare Workers?","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-2"},{"level":3,"text":"¿Cómo funcionan las migraciones de Drizzle?","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#article-tips"}],"sections":[{"heading":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","level":1,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql","content":"Backend"},{"heading":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","level":1,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#title","content":"2026-03-116 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/drizzle-orm-type-safe-sql-postgresql>)"},{"heading":"La Idea Central: SQL como Ciudadano de Primera Clase en TypeScript","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#la-idea-central-sql-como-ciudadano-de-primera-clase-en-typescript","content":"La filosofía de [Drizzle](<https://orm.drizzle.team/>) es simple: SQL ya es un buen lenguaje de queries. No abstraerlo, hacerlo type-safe. A continuación se presenta cómo se ve en la práctica:\n\n```typescript\nimport { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name').notNull(),\n role: text('role', { enum: ['admin', 'user', 'viewer'] }).notNull().default('user'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n lastLoginAt: timestamp('last_login_at'),\n});\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n content: text('content').notNull(),\n published: boolean('published').notNull().default(false),\n authorId: integer('author_id').notNull().references(() => users.id),\n viewCount: integer('view_count').notNull().default(0),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n});\n\nexport const comments = pgTable('comments', {\n id: serial('id').primaryKey(),\n body: text('body').notNull(),\n postId: integer('post_id').notNull().references(() => posts.id),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n posts: many(posts),\n comments: many(comments),\n}));\n\nexport const postsRelations = relations(posts, ({ one, many }) => ({\n author: one(users, {\n fields: [posts.authorId],\n references: [users.id],\n }),\n comments: many(comments),\n}));\n```\n\nAhora observar qué pasa cuando hacés queries. Cada campo, cada join, cada condición está completamente type-checked:\n\n```typescript\nimport { drizzle } from 'drizzle-orm/neon-http';\nimport { neon } from '@neondatabase/serverless';\nimport { eq, and, gt, desc, sql, count } from 'drizzle-orm';\nimport * as schema from './schema';\n\nconst queryClient = neon(process.env.DATABASE_URL!);\nconst db = drizzle(queryClient, { schema });\n\n// Select simple, el tipo de retorno se infiere exactamente\nconst activeUsers = await db\n .select({\n id: schema.users.id,\n email: schema.users.email,\n name: schema.users.name,\n })\n .from(schema.users)\n .where(eq(schema.users.role, 'admin'))\n .orderBy(desc(schema.users.createdAt))\n .limit(10);\n\n// TypeScript sabe: activeUsers es Array<{ id: number; email: string; name: string }>\n// Intentá acceder a activeUsers[0].role, error de compilación! No fue seleccionado.\n```\n\nEsta es la diferencia clave con Prisma. En Prisma, si utilizars ` select `, teners type safety. Pero queries complejas con escape hatches de raw SQL pierden información de tipos. En Drizzle, incluso fragmentos de SQL raw pueden llevar información de tipos:\n\n```typescript\n// Query compleja con subquery, agregación y raw SQL type-safe\nconst topAuthors = await db\n .select({\n authorId: schema.posts.authorId,\n authorName: schema.users.name,\n postCount: count(schema.posts.id).as('post_count'),\n totalViews: sql<number>`sum(${schema.posts.viewCount})`.as('total_views'),\n avgViews: sql<number>`avg(${schema.posts.viewCount})::integer`.as('avg_views'),\n })\n .from(schema.posts)\n .innerJoin(schema.users, eq(schema.posts.authorId, schema.users.id))\n .where(eq(schema.posts.published, true))\n .groupBy(schema.posts.authorId, schema.users.name)\n .having(gt(count(schema.posts.id), 5))\n .orderBy(desc(sql`sum(${schema.posts.viewCount})`))\n .limit(20);\n```\n\nEse template literal tag ` sql<number> ` es genial. Escribirs SQL real pero anotás el tipo de retorno, y Drizzle lo propaga por toda la inferencia de tipos de la query."},{"heading":"Estrategias de Migración: Donde Drizzle Kit Brilla","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#estrategias-de-migracin-donde-drizzle-kit-brilla","content":"Drizzle Kit es la herramienta CLI compañera para manejo de schemas. Tiene dos modos:"},{"heading":"drizzle-kit push, El Modo Desarrollo","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#drizzle-kit-push-el-modo-desarrollo","content":"```bash\n# Lee tus archivos de schema, compara contra la base real,\n# genera y aplica ALTER statements directamente\nnpx drizzle-kit push\n\n# Output:\n# [✓] Leyendo archivos de schema...\n# [✓] Conectando a la base...\n# [i] Cambios detectados:\n# ALTER TABLE \"posts\" ADD COLUMN \"slug\" text;\n# CREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");\n# [✓] 2 cambios aplicados\n```\n\nEsto es para iteración rápida. Cambiás tu archivo TypeScript de schema, ejecutars ` push `, y tu base coincide con. Sin archivos de migración, sin versionado."},{"heading":"drizzle-kit generate + migrate, El Modo Producción","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#drizzle-kit-generate-migrate-el-modo-produccin","content":"```bash\nnpx drizzle-kit generate\n# Crea: drizzle/0003_add_post_slug.sql\n```\n\nEl archivo de migración generado:\n\n```sql\n-- drizzle/0003_add_post_slug.sql\nALTER TABLE \"posts\" ADD COLUMN \"slug\" text;--> statement-breakpoint\nCREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");\n```\n\nEs posible editar estos archivos, agregar migraciones de datos, wrappear en transacciones:\n\n```sql\n-- drizzle/0003_add_post_slug.sql (editado)\nBEGIN;\n\nALTER TABLE \"posts\" ADD COLUMN \"slug\" text;--> statement-breakpoint\n\n-- Migración de datos: generar slugs desde los títulos\nUPDATE \"posts\" SET \"slug\" = lower(\n regexp_replace(\n regexp_replace(\"title\", '[^a-zA-Z0-9\\s-]', '', 'g'),\n '\\s+', '-', 'g'\n )\n);--> statement-breakpoint\n\nALTER TABLE \"posts\" ALTER COLUMN \"slug\" SET NOT NULL;--> statement-breakpoint\nCREATE UNIQUE INDEX \"posts_slug_idx\" ON \"posts\" (\"slug\");--> statement-breakpoint\n\nCOMMIT;\n```"},{"heading":"Prepared Statements y la Historia del Edge Runtime","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#prepared-statements-y-la-historia-del-edge-runtime","content":"Este es el punto donde la arquitectura de Drizzle rinde dividendos. Como no hay binario de query engine, Drizzle corre en cualquier lugar donde corra JavaScript:\n\n```typescript\n// Cloudflare Workers, funciona directo\nimport { drizzle } from 'drizzle-orm/neon-http';\nimport { neon } from '@neondatabase/serverless';\n\nexport default {\n async fetch(request: Request, env: Env): Promise<Response> {\n const sql = neon(env.DATABASE_URL);\n const db = drizzle(sql);\n\n // Prepared statement, compilado una vez, ejecutado muchas\n const getUser = db\n .select()\n .from(schema.users)\n .where(eq(schema.users.id, sql.placeholder('userId')))\n .prepare('get_user_by_id');\n\n const user = await getUser.execute({ userId: 42 });\n\n return Response.json(user);\n }\n};\n```"},{"heading":"La Comparación Honesta con Prisma","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#la-comparacin-honesta-con-prisma","content":"[Prisma](<https://www.prisma.io/>) es una buena herramienta con años de validación en producción. Pero este es el punto donde los dos genuinamente difieren:"},{"heading":"Tiempo de Cold Start","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#tiempo-de-cold-start","content":"Esto es medible y significativo para serverless:\n\n```\nCold start de Prisma (AWS Lambda, Node.js 20):\n - Carga binario query engine: ~180ms\n - Parseo de schema: ~45ms\n - Primera query: ~80ms\n Total: ~305ms\n\nCold start de Drizzle (mismo entorno):\n - Import de módulo: ~12ms\n - Primera query: ~75ms\n Total: ~87ms\n```\n\nEse es ~220ms de diferencia son el query engine de Prisma cargando. En Cloudflare Workers con presupuestos de cold start más estrictos, es todavía más doloroso."},{"heading":"Generación de Queries","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#generacin-de-queries","content":"Prisma genera su propio plan de query y lo traduce a SQL. A veces produce queries subóptimas:\n\n```typescript\n// Prisma genera: múltiples queries separadas (potencial 1+N)\n// SELECT * FROM \"posts\" WHERE \"published\" = true\n// SELECT * FROM \"users\" WHERE \"id\" IN (...)\n// SELECT * FROM \"comments\" WHERE \"post_id\" IN (...)\n// SELECT * FROM \"users\" WHERE \"id\" IN (...)\n\n// Drizzle genera: lateral joins donde sea posible\n// SELECT ... FROM \"posts\"\n// LEFT JOIN LATERAL (\n// SELECT ... FROM \"comments\"\n// LEFT JOIN \"users\" ON ...\n// WHERE \"comments\".\"post_id\" = \"posts\".\"id\"\n// ORDER BY \"created_at\" DESC LIMIT 5\n// ) ON true\n// LEFT JOIN \"users\" ON \"users\".\"id\" = \"posts\".\"author_id\"\n// WHERE \"posts\".\"published\" = true\n```\n\nEl enfoque de lateral join es una sola query que PostgreSQL puede optimizar holísticamente."},{"heading":"Transacciones y Manejo de Errores","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#transacciones-y-manejo-de-errores","content":"```typescript\nconst newPost = await db.transaction(async (tx) => {\n const [author] = await tx\n .select()\n .from(schema.users)\n .where(eq(schema.users.id, authorId))\n .for('update'); // SELECT FOR UPDATE, lock a nivel fila\n\n if (!author) throw new Error('Autor no encontrado');\n\n const [post] = await tx\n .insert(schema.posts)\n .values({\n title,\n content,\n authorId: author.id,\n published: false,\n })\n .returning();\n\n await tx\n .update(schema.users)\n .set({ lastLoginAt: new Date() })\n .where(eq(schema.users.id, authorId));\n\n return post;\n});\n// La transacción auto-commitea en éxito, auto-rollbackea en throw\n```"},{"heading":"Limitaciones Notables","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#limitaciones-notables","content":"Después de uso extendido en producción con Drizzle, las limitaciones notables son:\n\n1. **El query builder relacional (` db.query.* `) es menos flexible que el builder SQL-like.** Para queries complejas, siempre bajo al API ` select/from/where `. El ` findMany ` con ` with ` está genial para lecturas anidadas simples pero se rompe para cualquier cosa con joins custom o CTEs.\n2. **Los mensajes de error del diff de schema pueden ser crípticos.** Cuando ` drizzle-kit push ` detecta un cambio de tipo de columna, el error a veces no te dice qué tabla o columna cambió.\n3. **No tiene connection pooling built-in.** Se necesita traer el tuyo (` postgres.js ` pool, driver serverless de [Neon](<https://neon.com/>), etc.).\n\nPero estos son cortes de papel comparados con los beneficios fundamentales: cero dependencias de binarios, type safety genuino a nivel SQL, y la libertad de desplegar en cualquier lugar donde corra JavaScript. Drizzle representa la dirección que el espacio de ORMs necesitaba tomar."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#questions","content":""},{"heading":"¿Qué hace diferente a Drizzle ORM de Prisma?","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-1","content":"Drizzle genera SQL a nivel TypeScript con cero overhead en runtime, las queries compilan a strings SQL planos. Prisma usa un query engine binario basado en Rust que agrega ~10-15MB a tu deployment e introduce su propia capa de planificación de queries. La API de Drizzle espeja la sintaxis SQL, mientras que Prisma es más abstraída."},{"heading":"¿Puede Drizzle ORM correr en edge runtimes como Cloudflare Workers?","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-2","content":"Sí. Drizzle no tiene dependencias de binarios nativos, haciéndolo compatible con Cloudflare Workers, Vercel Edge Functions, Deno Deploy y cualquier otro edge runtime."},{"heading":"¿Cómo funcionan las migraciones de Drizzle?","level":3,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#question-3","content":"Drizzle Kit genera archivos de migración SQL diffeando tu schema TypeScript contra el estado actual de la base o snapshots de migraciones anteriores. Obteners archivos .sql planos que es posible revisar, editar y correr con cualquier herramienta de migración."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#article-tips","content":"1. **Tip:** Utilizar ` drizzle-kit push ` para prototipar rápido y ` drizzle-kit generate ` + ` migrate ` para producción. Es común sobre-diseñar pipelines de migración custom antes de darse cuenta de que ` push ` hace exactamente lo necesario para entornos dev, diffea tu schema contra la base y aplica cambios directo.\n2. **Tip:** Definir siempre el schema en un directorio separado ` schema/ ` con un archivo por tabla. Cuando el schema está en un solo ` schema.ts ` y se llega a 40+ tablas, el archivo se vuelve inmanejable y la introspección de drizzle-kit se arrastra.\n3. **Tip:** Envolver queries complejas en CTEs ` db.$with() ` en vez de subqueries, el soporte de CTEs de Drizzle preserva la inferencia de tipos a lo largo de toda la cadena, y el query planner de PostgreSQL maneja los CTEs como barreras de optimización (que es frecuentemente lo que se desea).\n4. **Tip:** Para Neon serverless, configurar ` prepare: false ` en la config de Drizzle. El connection pooler de Neon (pgbouncer en modo transacción) no soporta prepared statements entre conexiones, y se va a tener errores crípticos ` prepared statement does not exist ` bajo carga.\n\n- drizzle\n- orm\n- postgresql\n- type-safe-sql\n- prisma\n- kysely\n- edge-runtime\n- migraciones\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/drizzle-orm-type-safe-sql-postgresql"},{"text":"Drizzle","url":"https://orm.drizzle.team/"},{"text":"Prisma","url":"https://www.prisma.io/"},{"text":"Neon","url":"https://neon.com/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql"},"headline":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","description":"Sintaxis SQL-like que es realmente type-safe, cero overhead en runtime, soporte para edge runtime, y una comparación honesta con Prisma y Kysely.","datePublished":"2026-03-11","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Backend","keywords":"drizzle, orm, postgresql, type-safe-sql, prisma, kysely, edge-runtime, migraciones","wordCount":1157,"timeRequired":"PT6M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/drizzle-orm-type-safe-sql-postgresql"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Drizzle ORM: SQL Type-Safe para PostgreSQL, Migraciones, Prepared Statements y Comparación con Prisma","item":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué hace diferente a Drizzle ORM de Prisma?","acceptedAnswer":{"@type":"Answer","text":"Drizzle genera SQL a nivel TypeScript con cero overhead en runtime, las queries compilan a strings SQL planos. Prisma usa un query engine binario basado en Rust que agrega ~10-15MB a tu deployment e introduce su propia capa de planificación de queries. La API de Drizzle espeja la sintaxis SQL, mientras que Prisma es más abstraída."}},{"@type":"Question","name":"¿Puede Drizzle ORM correr en edge runtimes como Cloudflare Workers?","acceptedAnswer":{"@type":"Answer","text":"Sí. Drizzle no tiene dependencias de binarios nativos, haciéndolo compatible con Cloudflare Workers, Vercel Edge Functions, Deno Deploy y cualquier otro edge runtime."}},{"@type":"Question","name":"¿Cómo funcionan las migraciones de Drizzle?","acceptedAnswer":{"@type":"Answer","text":"Drizzle Kit genera archivos de migración SQL diffeando tu schema TypeScript contra el estado actual de la base o snapshots de migraciones anteriores. Obteners archivos .sql planos que es posible revisar, editar y correr con cualquier herramienta de migración."}}]}]}],"contentHash":"5d15d6468b01c943878cdc6d231e620dea8157c1200f2eb21a32e97d5f89c52a","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/drizzle-orm-type-safe-sql-postgresql","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":62622,"contentBytes":12177}}
