---
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<User[]>`
  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<DB>({ 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<DB>({ 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<typeof users>;
type NewUser = InferInsertModel<typeof users>;

// Kysely — DB schema 가 truth
import type { Selectable, Insertable, Updateable } from 'kysely';

type User = Selectable<DB['users']>;
type NewUser = Insertable<DB['users']>;
```

### 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]]
