--- id: db-sql-builder-vs-orm title: SQL Builder vs ORM β€” Drizzle / Kysely / Prisma category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, orm, sql-builder, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [SQL builder, Kysely, Drizzle, Prisma, raw SQL, query builder, type-safe SQL] --- # SQL Builder vs ORM > ORM = object-relational mapping. SQL Builder = type-safe SQL. **Drizzle / Kysely (modern), Prisma (popular but binary)**. Raw SQL 도 valid. ## πŸ“– 핡심 κ°œλ… - ORM: object β†’ SQL. - Builder: type-safe SQL string. - Raw: 직접 SQL. - Type-safe: TS κ°€ schema β†’ query type. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### Raw SQL (κ°€μž₯ λ‹¨μˆœ) ```ts import postgres from 'postgres'; const sql = postgres(url); const users = await sql` SELECT id, email FROM users WHERE created_at > ${since} `; // Insert await sql`INSERT INTO users ${sql({ email, name })}`; // Update await sql`UPDATE users SET email = ${email} WHERE id = ${id}`; ``` β†’ κ°€μž₯ 빠름. Type-safety 약함 (manual generic). ### Drizzle (modern, κ°€μž₯ 인기) ```ts import { drizzle } from 'drizzle-orm/postgres-js'; import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; import { eq, and, gt } from 'drizzle-orm'; // Schema export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), email: text('email').unique().notNull(), name: text('name'), createdAt: timestamp('created_at').defaultNow(), }); // Query const db = drizzle(sql); // Select const allUsers = await db.select().from(users).where(eq(users.email, 'a@b.com')); const recent = await db .select({ id: users.id, email: users.email }) .from(users) .where(and( gt(users.createdAt, lastWeek), eq(users.deleted, false) )) .orderBy(desc(users.createdAt)) .limit(20); // Insert const [user] = await db.insert(users).values({ email, name }).returning(); // Update await db.update(users).set({ email: newEmail }).where(eq(users.id, id)); // Delete await db.delete(users).where(eq(users.id, id)); ``` β†’ SQL-style + TS type-safe. ### Drizzle joins ```ts const result = await db .select({ userId: users.id, email: users.email, orderTotal: sum(orders.amount), }) .from(users) .leftJoin(orders, eq(orders.userId, users.id)) .groupBy(users.id, users.email); ``` ### Drizzle relations (eager load) ```ts import { relations } from 'drizzle-orm'; export const usersRelations = relations(users, ({ many }) => ({ orders: many(orders), })); export const ordersRelations = relations(orders, ({ one }) => ({ user: one(users, { fields: [orders.userId], references: [users.id] }), })); // Use const usersWithOrders = await db.query.users.findMany({ with: { orders: true }, where: eq(users.id, id), }); ``` β†’ N+1 μžλ™ 처리. ### Kysely (pure builder) ```ts import { Kysely, PostgresDialect } from 'kysely'; interface DB { users: { id: string; email: string; name: string | null }; orders: { id: string; user_id: string; amount: number }; } const db = new Kysely({ dialect: new PostgresDialect({ pool }) }); const users = await db .selectFrom('users') .where('email', '=', 'a@b.com') .select(['id', 'email']) .execute(); await db .insertInto('users') .values({ id: uuid(), email, name }) .execute(); ``` β†’ 더 SQL-like. Schema 직접 μ •μ˜. ### Kysely codegen (DB β†’ types) ```bash npx kysely-codegen --connection-string $DATABASE_URL ``` β†’ DB schema μ—μ„œ μžλ™ type generate. ### Prisma (전톡적 ORM) ```prisma // schema.prisma model User { id String @id @default(uuid()) email String @unique name String? orders Order[] } model Order { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id]) amount Decimal } ``` ```ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // κ°•λ ₯ + intuitive const user = await prisma.user.findUnique({ where: { id }, include: { orders: true }, }); await prisma.user.update({ where: { id }, data: { email: newEmail }, }); ``` β†’ Pros: μΉœμˆ™. Cons: binary (rust query engine), Edge runtime 어렀움. ### TypeORM (legacy) ```ts @Entity() class User { @PrimaryGeneratedColumn('uuid') id!: string; @Column({ unique: true }) email!: string; } const users = await User.find({ where: { email: '...' } }); ``` β†’ Java Hibernate λΉ„μŠ·. μƒˆ ν”„λ‘œμ νŠΈ ꢌμž₯ X. ### MikroORM (modern OO) ```ts @Entity() class User { @PrimaryKey() id!: string; @Property() email!: string; } const em = orm.em.fork(); const user = await em.findOne(User, { email: '...' }); user.email = 'new@email.com'; await em.flush(); // μžλ™ dirty tracking ``` β†’ Hibernate-like. Strong unit-of-work. ### Bun:sql (modern, fast) ```ts import { sql } from 'bun'; const users = await sql`SELECT * FROM users WHERE id = ${id}`; ``` β†’ Tagged template. Built-in. ### Comparison ``` Drizzle: + Type-safe, SQL-style + Edge friendly + μž‘μ€ bundle - Schema 직접 μ •μ˜ Kysely: + Pure builder, no migration + DB β†’ type μžλ™ - 더 verbose Prisma: + μΉœμˆ™ / intuitive + κ°•λ ₯ docs - Binary engine - Edge 어렀움 Raw SQL: + κ°€μž₯ 빠름 + Full SQL power - Manual type - Manual escape MikroORM: + Java-style + Strong unit-of-work - Smaller community ``` ### Migration ```ts // Drizzle Kit npx drizzle-kit generate // SQL 파일 generate npx drizzle-kit migrate // μ‹€ν–‰ // Prisma Migrate npx prisma migrate dev npx prisma migrate deploy ``` ### Connection pooling (μœ„ [[Backend_Connection_Handling]]) ```ts import { Pool } from 'pg'; const pool = new Pool({ connectionString, max: 20 }); // Drizzle import { drizzle } from 'drizzle-orm/node-postgres'; const db = drizzle(pool); // Kysely const db = new Kysely({ dialect: new PostgresDialect({ pool }) }); // Prisma // Auto pool β€” connection_limit URL param ``` ### Edge runtime ```ts // Drizzle + Neon HTTP (edge) import { neon } from '@neondatabase/serverless'; import { drizzle } from 'drizzle-orm/neon-http'; const sql = neon(process.env.DATABASE_URL!); const db = drizzle(sql); // Cloudflare Workers + D1 import { drizzle } from 'drizzle-orm/d1'; const db = drizzle(env.DB); // Prisma β€” driver adapter import { PrismaNeon } from '@prisma/adapter-neon'; const adapter = new PrismaNeon(neonClient); const prisma = new PrismaClient({ adapter }); ``` ### Transaction ```ts // Drizzle await db.transaction(async (tx) => { await tx.insert(users).values(...); await tx.insert(orders).values(...); }); // Kysely await db.transaction().execute(async (trx) => { await trx.insertInto('users').values(...).execute(); await trx.insertInto('orders').values(...).execute(); }); // Prisma await prisma.$transaction([ prisma.user.create({ data }), prisma.order.create({ data }), ]); // λ˜λŠ” interactive await prisma.$transaction(async (tx) => { const user = await tx.user.create(...); await tx.order.create({ data: { userId: user.id, ... } }); }); ``` ### Raw SQL (escape hatch) ```ts // Drizzle import { sql } from 'drizzle-orm'; await db.execute(sql`UPDATE users SET balance = balance + ${amount}`); // Kysely await sql`UPDATE users SET balance = balance + ${amount}`.execute(db); // Prisma await prisma.$queryRaw`SELECT * FROM users WHERE balance > ${threshold}`; await prisma.$executeRaw`UPDATE users SET balance = ${val}`; ``` ### Type generation ```ts // Drizzle β€” schema κ°€ truth import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; type User = InferSelectModel; type NewUser = InferInsertModel; // Kysely β€” DB schema κ°€ truth import type { Selectable, Insertable, Updateable } from 'kysely'; type User = Selectable; type NewUser = Insertable; ``` ### Schema migration ```bash # Drizzle npx drizzle-kit generate # SQL diff npx drizzle-kit migrate # Prisma npx prisma migrate dev --name add_email_index # Kysely # Custom β€” kysely-migration-cli λ“± ``` ### Performance ``` Raw SQL: κ°€μž₯ 빠름. Bun:sql: raw λΉ„μŠ·. Drizzle: raw 와 거의 κ°™μŒ. Kysely: raw 와 거의 κ°™μŒ. Prisma: 5-20% slower (engine overhead). TypeORM: Variable. β†’ μ°¨μ΄λŠ” 주둜 λ―Έμ„Έ. DB query κ°€ dominant. ``` ### Bundle ``` Raw SQL (postgres-js): ~30 KB Drizzle: ~40 KB Kysely: ~70 KB Prisma client: 10+ MB (binary engine) β†’ Edge / lambda = Drizzle / Kysely. ``` ### When to choose ``` Drizzle: - New project + edge runtime - λΉ λ₯Έ + type-safe - SQL-style Kysely: - Pure builder - κΈ°μ‘΄ DB β†’ schema μžλ™ - λΉ λ₯Έ dev Prisma: - Familiar / 큰 team - Migration κ°•λ ₯ - Edge μ•ˆ critical Raw SQL: - Performance critical - μž‘μ€ query 수 - 직접 control ``` ### N+1 detection ```ts // Drizzle relations const usersWithOrders = await db.query.users.findMany({ with: { orders: true } }); // Without relations = N+1 const users = await db.select().from(users); for (const u of users) { const orders = await db.select().from(orders).where(eq(orders.userId, u.id)); // N+1 } ``` β†’ Always relations / joins. ### DataLoader (GraphQL) ```ts import DataLoader from 'dataloader'; const userLoader = new DataLoader(async (userIds: string[]) => { const users = await db.select().from(usersTable).where(inArray(usersTable.id, userIds)); return userIds.map(id => users.find(u => u.id === id)); }); // μžλ™ batch const a = await userLoader.load('1'); const b = await userLoader.load('2'); // 1 query (batched). ``` ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | 상황 | μΆ”μ²œ | |---|---| | Modern + edge | Drizzle | | κΈ°μ‘΄ DB 점진 λ„μž… | Kysely | | μΉœμˆ™ / quick start | Prisma | | Performance critical | Raw SQL | | Java background | MikroORM | | λ‹¨μˆœ / μž‘μŒ | Bun:sql | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **Raw SQL + escape μ•ˆ 함**: SQL injection. - **λͺ¨λ“  join 직접 multiple query**: N+1. - **ORM 의 lazy load κ°€μ •**: extra query. - **Type generate 무 manual**: drift. - **Big binary (Prisma) on edge**: μ•ˆ 됨. - **Migration μ—†λŠ” schema λ³€κ²½**: drift. - **Connection pool 무**: λ§€ query κ°€ connect. ## πŸ€– LLM ν™œμš© 힌트 - μƒˆ = Drizzle. - κΈ°μ‘΄ DB = Kysely. - μΉœμˆ™ + serverful = Prisma. - Raw SQL 도 OK. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[DB_ORM_Comparison]] - [[Backend_Connection_Handling]] - [[DB_Migration_Safety]]