🗄️
데이터베이스
PlanetScale
Vitess 기반 MySQL
YouTube를 확장한 Vitess 기반의 서버리스 MySQL 플랫폼으로, Git처럼 데이터베이스를 브랜칭하고, 다운타임 없이 스키마를 변경하며, 자동으로 수평 확장되는 MySQL 호환 데이터베이스입니다.
Vitess 기반 MySQL
YouTube를 확장한 Vitess 기반의 서버리스 MySQL 플랫폼으로, Git처럼 데이터베이스를 브랜칭하고, 다운타임 없이 스키마를 변경하며, 자동으로 수평 확장되는 MySQL 호환 데이터베이스입니다.
PlanetScale은 YouTube의 MySQL 클러스터를 관리하기 위해 만들어진 오픈소스 프로젝트 Vitess를 기반으로 한 서버리스 MySQL 플랫폼입니다. 기존 MySQL과 100% 호환되면서도 대규모 트래픽을 처리할 수 있는 수평 확장성을 제공합니다.
YouTube가 개발한 MySQL 샤딩/클러스터링 기술. 단일 MySQL을 여러 샤드로 분산하면서도 애플리케이션에서는 단일 DB처럼 사용 가능.
# PlanetScale CLI 설치
brew install planetscale/tap/pscale
# 로그인
pscale auth login
# 데이터베이스 생성
pscale database create my-app --region us-east
# 개발 브랜치 생성 (main에서 분기)
pscale branch create my-app feature/add-orders
# 브랜치에 연결 (로컬 프록시)
pscale connect my-app feature/add-orders --port 3309
# 다른 터미널에서 MySQL 클라이언트로 접속
mysql -h 127.0.0.1 -P 3309 -u root
# 브랜치에서 스키마 변경
pscale shell my-app feature/add-orders
> CREATE TABLE orders (
> id BIGINT PRIMARY KEY AUTO_INCREMENT,
> user_id BIGINT NOT NULL,
> total DECIMAL(10,2) NOT NULL,
> status ENUM('pending', 'paid', 'shipped') DEFAULT 'pending',
> created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
> INDEX idx_user_id (user_id),
> INDEX idx_status (status)
> );
# Deploy Request 생성 (PR처럼)
pscale deploy-request create my-app feature/add-orders
# Deploy Request 적용 (main에 머지)
pscale deploy-request deploy my-app 1import { connect } from '@planetscale/database';
// PlanetScale Serverless Driver (Edge 호환)
const conn = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
});
// 기본 쿼리
async function getUsers() {
const results = await conn.execute('SELECT * FROM users WHERE active = ?', [true]);
return results.rows;
}
// 파라미터화된 쿼리 (SQL Injection 방지)
async function createOrder(userId: number, items: CartItem[]) {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// 트랜잭션 대신 단일 INSERT (PlanetScale 권장)
const result = await conn.execute(
'INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)',
[userId, total, 'pending']
);
const orderId = result.insertId;
// 주문 상품 일괄 삽입
const values = items.map(item => `(${orderId}, ${item.productId}, ${item.quantity})`).join(',');
await conn.execute(
`INSERT INTO order_items (order_id, product_id, quantity) VALUES ${values}`
);
return { orderId, total };
}
// 복잡한 집계 쿼리
async function getOrderStats(userId: number) {
const results = await conn.execute(`
SELECT
COUNT(*) as total_orders,
SUM(total) as total_spent,
AVG(total) as avg_order_value,
MAX(created_at) as last_order_date
FROM orders
WHERE user_id = ?
AND status != 'cancelled'
`, [userId]);
return results.rows[0];
}generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // PlanetScale은 FK 미지원
}
model User {
id BigInt @id @default(autoincrement())
email String @unique
name String?
orders Order[]
createdAt DateTime @default(now())
@@index([email])
}
model Order {
id BigInt @id @default(autoincrement())
userId BigInt
user User @relation(fields: [userId], references: [id])
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
items OrderItem[]
createdAt DateTime @default(now())
@@index([userId])
@@index([status])
}
model OrderItem {
id BigInt @id @default(autoincrement())
orderId BigInt
order Order @relation(fields: [orderId], references: [id])
productId BigInt
quantity Int
@@index([orderId])
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}# 1. 개발 브랜치에서 스키마 변경
pscale shell my-app dev-branch
# 대용량 테이블에 컬럼 추가 (기존 MySQL에서는 락 발생)
ALTER TABLE orders ADD COLUMN shipping_address TEXT;
ALTER TABLE orders ADD INDEX idx_created_at (created_at);
# 2. Deploy Request 생성
pscale deploy-request create my-app dev-branch --notes "Add shipping address column"
# 3. 스키마 변경 사항 확인
pscale deploy-request diff my-app 1
# 출력 예시:
# -- orders
# + shipping_address TEXT
# + INDEX idx_created_at (created_at)
# 4. Deploy Request 승인 및 적용
# (PlanetScale이 내부적으로 Ghost/pt-online-schema-change 같은 기법 사용)
pscale deploy-request deploy my-app 1
# 결과: 테이블 락 없이 수백만 행 테이블도 스키마 변경 완료// pages/api/orders/[id].ts
import { connect } from '@planetscale/database';
import type { NextApiRequest, NextApiResponse } from 'next';
const conn = connect({
host: process.env.DATABASE_HOST!,
username: process.env.DATABASE_USERNAME!,
password: process.env.DATABASE_PASSWORD!,
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query;
if (req.method === 'GET') {
const result = await conn.execute(
`SELECT o.*, u.name as user_name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = ?`,
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Order not found' });
}
return res.json(result.rows[0]);
}
if (req.method === 'PATCH') {
const { status } = req.body;
await conn.execute(
'UPDATE orders SET status = ? WHERE id = ?',
[status, id]
);
return res.json({ success: true });
}
res.status(405).json({ error: 'Method not allowed' });
}Vitess 아키텍처 특성상 데이터베이스 레벨 Foreign Key를 지원하지 않습니다. 애플리케이션 레벨에서 참조 무결성을 관리해야 합니다.
2024년 기준 무료 플랜이 폐지되었습니다. 취미 프로젝트나 소규모 앱에서는 비용을 고려해야 합니다.
일부 고급 MySQL 기능(저장 프로시저, 트리거 등)이 제한될 수 있습니다. 마이그레이션 전 호환성을 확인하세요.