10 KiB
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 |
|
|
|
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.