Files
2nd/10_Wiki/Topics/Coding/DB_Sql_Builder_vs_ORM.md
T
2026-05-09 22:47:42 +09:00

10 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-sql-builder-vs-orm SQL Builder vs ORM — Drizzle / Kysely / Prisma Coding draft B conceptual 2026-05-09 2026-05-09
database
orm
sql-builder
vibe-coding
language applicable_to
TS
Backend
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 (가장 단순)

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, 가장 인기)

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

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)

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)

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)

npx kysely-codegen --connection-string $DATABASE_URL

→ DB schema 에서 자동 type generate.

Prisma (전통적 ORM)

// 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
}
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)

@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)

@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)

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

// 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)

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

// 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

// 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)

// 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

// 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

# 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

// 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)

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.

🔗 관련 문서