database-schema-design
npx skills add https://github.com/supercent-io/skills-template --skill database-schema-design
Agent 安装分布
Skill 文档
Database Schema Design
When to use this skill
ì´ ì¤í¬ì í¸ë¦¬ê±°í´ì¼ íë 구체ì ì¸ ìí©ì ëì´í©ëë¤:
- ì ê· íë¡ì í¸: ì ì í리ì¼ì´ì ì ë°ì´í°ë² ì´ì¤ ì¤í¤ë§ ì¤ê³
- ì¤í¤ë§ 리í©í ë§: 기존 ì¤í¤ë§ë¥¼ ì±ë¥ì´ë íì¥ì±ì ìí´ ì¬ì¤ê³
- ê´ê³ ì ì: í ì´ë¸ ê° 1:1, 1:N, N:M ê´ê³ 구í
- ë§ì´ê·¸ë ì´ì : ì¤í¤ë§ ë³ê²½ì¬íì ìì íê² ì ì©
- ì±ë¥ 문ì : ë린 쿼리를 í´ê²°í기 ìí ì¸ë±ì¤ ë° ì¤í¤ë§ ìµì í
ì ë ¥ íì (Input Format)
ì¬ì©ìë¡ë¶í° ë°ìì¼ í ì ë ¥ì íìê³¼ íì/ì í ì ë³´:
íì ì ë³´
- ë°ì´í°ë² ì´ì¤ ì¢ ë¥: PostgreSQL, MySQL, MongoDB, SQLite ë±
- ëë©ì¸ ì¤ëª : ì´ë¤ ë°ì´í°ë¥¼ ì ì¥í ê²ì¸ì§ (ì: ì ììê±°ë, ë¸ë¡ê·¸, SNS)
- 주ì ìí°í°: íµì¬ ë°ì´í° ê°ì²´ë¤ (ì: User, Product, Order)
ì í ì ë³´
- ìì ë°ì´í°ë: ìì(<10K rows), ì¤ê°(10K-1M), ëì©ë(>1M) (기본ê°: ì¤ê°)
- ì½ê¸°/ì°ê¸° ë¹ì¨: Read-heavy, Write-heavy, Balanced (기본ê°: Balanced)
- í¸ëìì ì구ì¬í: ACID íì ì¬ë¶ (기본ê°: true)
- ì¤ë©/íí°ì ë: ëì©ë ë°ì´í° ë¶ì° íì ì¬ë¶ (기본ê°: false)
ì ë ¥ ìì
ì ììê±°ë íë«í¼ì ë°ì´í°ë² ì´ì¤ë¥¼ ì¤ê³í´ì¤:
- DB: PostgreSQL
- ìí°í°: User(ì¬ì©ì), Product(ìí), Order(주문), Review(리뷰)
- ê´ê³:
- Userë ì¬ë¬ Order를 ê°ì§ ì ìì
- Orderë ì¬ë¬ Product를 í¬í¨ (N:M)
- Reviewë Userì Productì ì°ê²°
- ìì ë°ì´í°: 10ë§ ì¬ì©ì, 1ë§ ìí
- ì½ê¸° ì¤ì¬ (ìí ì¡°í ë¹ë²)
Instructions
ë¨ê³ë³ë¡ ì ííê² ë°ë¼ì¼ í ìì ìì를 ëª ìí©ëë¤.
Step 1: ìí°í° ë° ìì± ì ì
íµì¬ ë°ì´í° ê°ì²´ì ê·¸ ìì±ì ìë³í©ëë¤.
ìì ë´ì©:
- ë¹ì¦ëì¤ ì구ì¬íìì ëª ì¬ ì¶ì¶ â ìí°í°
- ê° ìí°í°ì ìì±(ì¹¼ë¼) ëì´
- ë°ì´í° íì ê²°ì (VARCHAR, INTEGER, TIMESTAMP, JSON ë±)
- Primary Key ì§ì (UUID vs Auto-increment ID)
ìì (ì ììê±°ë):
Users (ì¬ì©ì)
- id: UUID PRIMARY KEY
- email: VARCHAR(255) UNIQUE NOT NULL
- username: VARCHAR(50) UNIQUE NOT NULL
- password_hash: VARCHAR(255) NOT NULL
- created_at: TIMESTAMP DEFAULT NOW()
- updated_at: TIMESTAMP DEFAULT NOW()
Products (ìí)
- id: UUID PRIMARY KEY
- name: VARCHAR(255) NOT NULL
- description: TEXT
- price: DECIMAL(10, 2) NOT NULL
- stock: INTEGER DEFAULT 0
- category_id: UUID REFERENCES Categories(id)
- created_at: TIMESTAMP DEFAULT NOW()
Orders (주문)
- id: UUID PRIMARY KEY
- user_id: UUID REFERENCES Users(id)
- total_amount: DECIMAL(10, 2) NOT NULL
- status: VARCHAR(20) DEFAULT 'pending'
- created_at: TIMESTAMP DEFAULT NOW()
OrderItems (주문 ìí - ì¤ê° í
ì´ë¸)
- id: UUID PRIMARY KEY
- order_id: UUID REFERENCES Orders(id) ON DELETE CASCADE
- product_id: UUID REFERENCES Products(id)
- quantity: INTEGER NOT NULL
- price: DECIMAL(10, 2) NOT NULL
Step 2: ê´ê³ ì¤ê³ ë° ì ê·í
í ì´ë¸ ê°ì ê´ê³ë¥¼ ì ìíê³ ì ê·í를 ì ì©í©ëë¤.
ìì ë´ì©:
- 1:1 ê´ê³: Foreign Key + UNIQUE ì ì½
- 1:N ê´ê³: Foreign Key
- N:M ê´ê³: ì¤ê°(Junction) í ì´ë¸ ìì±
- ì ê·í ë 벨 ê²°ì (1NF ~ 3NF)
íë¨ ê¸°ì¤:
- OLTP ìì¤í â 3NFê¹ì§ ì ê·í (ë°ì´í° 무결ì±)
- OLAP/ë¶ì ìì¤í â ë¹ì ê·í íì© (쿼리 ì±ë¥)
- ì½ê¸° ì¤ì¬ â ì¼ë¶ ë¹ì ê·íë¡ JOIN ìµìí
- ì°ê¸° ì¤ì¬ â ìì ì ê·íë¡ ì¤ë³µ ì ê±°
ìì (ERD Mermaid):
erDiagram
Users ||--o{ Orders : places
Orders ||--|{ OrderItems : contains
Products ||--o{ OrderItems : "ordered in"
Categories ||--o{ Products : categorizes
Users ||--o{ Reviews : writes
Products ||--o{ Reviews : "reviewed by"
Users {
uuid id PK
string email UK
string username UK
string password_hash
timestamp created_at
}
Products {
uuid id PK
string name
decimal price
int stock
uuid category_id FK
}
Orders {
uuid id PK
uuid user_id FK
decimal total_amount
string status
timestamp created_at
}
OrderItems {
uuid id PK
uuid order_id FK
uuid product_id FK
int quantity
decimal price
}
Step 3: ì¸ë±ì¤ ì ëµ ì립
쿼리 ì±ë¥ì ìí ì¸ë±ì¤ë¥¼ ì¤ê³í©ëë¤.
ìì ë´ì©:
- Primary Keyë ìëì¼ë¡ ì¸ë±ì¤ ìì±ë¨
- WHERE ì ì ì주 ì¬ì©ëë ì¹¼ë¼ â ì¸ë±ì¤ ì¶ê°
- JOINì ì¬ì©ëë Foreign Key â ì¸ë±ì¤
- ë³µí© ì¸ë±ì¤ ê³ ë ¤ (WHERE col1 = ? AND col2 = ?)
- UNIQUE ì¸ë±ì¤ (email, username ë±)
íì¸ ì¬í:
- ì주 ì¡°íëë ì¹¼ë¼ì ì¸ë±ì¤
- Foreign Key ì¹¼ë¼ì ì¸ë±ì¤
- ë³µí© ì¸ë±ì¤ ìì ìµì í (ì íë ëì ì¹¼ë¼ ë¨¼ì )
- ê³¼ëí ì¸ë±ì¤ ì§ì (INSERT/UPDATE ì±ë¥ ì í)
ìì (PostgreSQL):
-- Primary Keys (ìë ì¸ë±ì¤)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL, -- UNIQUE = ìë ì¸ë±ì¤
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Foreign Keys + ëª
ìì ì¸ë±ì¤
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- ë³µí© ì¸ë±ì¤ (statusì created_at í¨ê» ì¡°í ë¹ë²)
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
-- Products í
ì´ë¸
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
category_id UUID REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_price ON products(price); -- ê°ê²© ë²ì ê²ì
CREATE INDEX idx_products_name ON products(name); -- ìíëª
ê²ì
-- Full-text search (PostgreSQL)
CREATE INDEX idx_products_name_fts ON products USING GIN(to_tsvector('english', name));
CREATE INDEX idx_products_description_fts ON products USING GIN(to_tsvector('english', description));
Step 4: ì ì½ì¡°ê±´ ë° í¸ë¦¬ê±° ì¤ì
ë°ì´í° 무결ì±ì ìí ì ì½ì¡°ê±´ì ì¶ê°í©ëë¤.
ìì ë´ì©:
- NOT NULL: íì ì¹¼ë¼
- UNIQUE: ì¤ë³µ ë¶ê° ì¹¼ë¼
- CHECK: ê° ë²ì ì í (ì: price >= 0)
- Foreign Key + CASCADE ìµì
- Default ê° ì¤ì
ìì:
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
discount_percent INTEGER CHECK (discount_percent >= 0 AND discount_percent <= 100),
category_id UUID REFERENCES categories(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Trigger: updated_at ìë ê°±ì
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Step 5: ë§ì´ê·¸ë ì´ì ì¤í¬ë¦½í¸ ìì±
ì¤í¤ë§ ë³ê²½ì¬íì ìì íê² ì ì©íë ë§ì´ê·¸ë ì´ì ì ìì±í©ëë¤.
ìì ë´ì©:
- UP ë§ì´ê·¸ë ì´ì : ë³ê²½ ì ì©
- DOWN ë§ì´ê·¸ë ì´ì : 롤백
- í¸ëìì ì¼ë¡ ëí
- ë°ì´í° ìì¤ ë°©ì§ (ALTER TABLE ì ì¤í)
ìì (SQL ë§ì´ê·¸ë ì´ì ):
-- migrations/001_create_initial_schema.up.sql
BEGIN;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
parent_id UUID REFERENCES categories(id)
);
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
category_id UUID REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_price ON products(price);
COMMIT;
-- migrations/001_create_initial_schema.down.sql
BEGIN;
DROP TABLE IF EXISTS products CASCADE;
DROP TABLE IF EXISTS categories CASCADE;
DROP TABLE IF EXISTS users CASCADE;
COMMIT;
Output format
ê²°ê³¼ë¬¼ì´ ë°ë¼ì¼ í ì íí íìì ì ìí©ëë¤.
기본 구조
íë¡ì í¸/
âââ database/
â âââ schema.sql # ì ì²´ ì¤í¤ë§
â âââ migrations/
â â âââ 001_create_users.up.sql
â â âââ 001_create_users.down.sql
â â âââ 002_create_products.up.sql
â â âââ 002_create_products.down.sql
â âââ seeds/
â â âââ sample_data.sql # í
ì¤í¸ ë°ì´í°
â âââ docs/
â âââ ERD.md # Mermaid ERD ë¤ì´ì´ê·¸ë¨
â âââ SCHEMA.md # ì¤í¤ë§ ì¤ëª
문ì
âââ README.md
ERD ë¤ì´ì´ê·¸ë¨ (Mermaid íì)
# Database Schema
## Entity Relationship Diagram
\`\`\`mermaid
erDiagram
Users ||--o{ Orders : places
Orders ||--|{ OrderItems : contains
Products ||--o{ OrderItems : "ordered in"
Users {
uuid id PK
string email UK
string username UK
}
Products {
uuid id PK
string name
decimal price
}
\`\`\`
## Table Descriptions
### users
- **Purpose**: Store user account information
- **Indexes**: email, username
- **Estimated rows**: 100,000
### products
- **Purpose**: Product catalog
- **Indexes**: category_id, price, name
- **Estimated rows**: 10,000
Constraints
ë°ëì ì§ì¼ì¼ í ê·ì¹ê³¼ ê¸ì§ ì¬íì ëª ìí©ëë¤.
íì ê·ì¹ (MUST)
-
Primary Key íì: 모ë í ì´ë¸ì Primary Key ì ì
- ë ì½ë ê³ ì ìë³
- 참조 ë¬´ê²°ì± ë³´ì¥
-
Foreign Key ëª ì: ê´ê³ê° ìë í ì´ë¸ì ë°ëì Foreign Key ì¤ì
- ON DELETE CASCADE/SET NULL ìµì ëª ì
- Orphan ë ì½ë ë°©ì§
-
NOT NULL ì ì í ì¬ì©: íì ì¹¼ë¼ì NOT NULL
- NULL íì© ì¬ë¶ ëª íí
- ê¸°ë³¸ê° ì ê³µ ê¶ì¥
ê¸ì§ ì¬í (MUST NOT)
-
EAV í¨í´ ë¨ì©: Entity-Attribute-Value í¨í´ì í¹ë³í ê²½ì°ìë§
- 쿼리 ë³µì¡ë ê¸ì¦
- ì±ë¥ ì í
-
ê³¼ëí ë¹ì ê·í: ì±ë¥ì ìí ë¹ì ê·íë ì ì¤í
- ë°ì´í° ì¼ê´ì± 문ì
- ì ë°ì´í¸ ì´ì ë°ì ìí
-
민ê°ì ë³´ í문 ì ì¥: ë¹ë°ë²í¸, ì¹´ëë²í¸ ë±ì ì ë í문 ì ì¥ ê¸ì§
- í´ì±/ìí¸í íì
- ë²ì ì± ì 문ì
ë³´ì ê·ì¹
- ìµì ê¶í ìì¹: ì í리ì¼ì´ì DB ê³ì ì íìí ê¶íë§ ë¶ì¬
- SQL Injection ë°©ì§: Prepared Statements/Parameterized Queries ì¬ì©
- ë¯¼ê° ì¹¼ë¼ ìí¸í: ê°ì¸ì ë³´ë ìí¸í ì ì¥ ê³ ë ¤
Examples
ì¤ì ì¬ì© ì¬ë¡ë¥¼ íµí´ ì¤í¬ì ì ì© ë°©ë²ì ë³´ì¬ì¤ëë¤.
ìì 1: ë¸ë¡ê·¸ íë«í¼ ì¤í¤ë§
ìí©: Medium ì¤íì¼ì ë¸ë¡ê·¸ íë«í¼ ë°ì´í°ë² ì´ì¤ ì¤ê³
ì¬ì©ì ìì²:
ë¸ë¡ê·¸ íë«í¼ì ìí PostgreSQL ì¤í¤ë§ë¥¼ ì¤ê³í´ì¤:
- ì¬ì©ìë ì¬ë¬ í¬ì¤í¸ë¥¼ ìì±
- í¬ì¤í¸ë ì¬ë¬ í그를 ê°ì§ (N:M)
- ì¬ì©ìë í¬ì¤í¸ì ì¢ìì, ë¶ë§í¬ ê°ë¥
- ëê¸ ê¸°ë¥ (ëëê¸ ì§ì)
ìµì¢ ê²°ê³¼:
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
bio TEXT,
avatar_url VARCHAR(500),
created_at TIMESTAMP DEFAULT NOW()
);
-- Posts
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
content TEXT NOT NULL,
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_published ON posts(published_at);
CREATE INDEX idx_posts_slug ON posts(slug);
-- Tags
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL
);
-- Post-Tag relationship (N:M)
CREATE TABLE post_tags (
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX idx_post_tags_post ON post_tags(post_id);
CREATE INDEX idx_post_tags_tag ON post_tags(tag_id);
-- Likes
CREATE TABLE post_likes (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, post_id)
);
-- Bookmarks
CREATE TABLE post_bookmarks (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, post_id)
);
-- Comments (self-referencing for nested comments)
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
parent_comment_id UUID REFERENCES comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_author ON comments(author_id);
CREATE INDEX idx_comments_parent ON comments(parent_comment_id);
ìì 2: MongoDB ì¤í¤ë§ (NoSQL)
ìí©: ì¤ìê° ì±í ì±ì ìí MongoDB ì¤í¤ë§
ì¬ì©ì ìì²:
ì¤ìê° ì±í
ì±ì MongoDB ì¤í¤ë§ë¥¼ ì¤ê³í´ì¤.
ì½ê¸°ê° ë§¤ì° ë¹ë²íê³ , ë©ìì§ íì¤í 리를 ë¹ ë¥´ê² ì¡°íí´ì¼ í´.
ìµì¢ ê²°ê³¼:
// users collection
{
_id: ObjectId,
username: String, // indexed, unique
email: String, // indexed, unique
avatar_url: String,
status: String, // 'online', 'offline', 'away'
last_seen: Date,
created_at: Date
}
// conversations collection (ë¹ì ê·í - ì½ê¸° ìµì í)
{
_id: ObjectId,
participants: [ // indexed
{
user_id: ObjectId,
username: String,
avatar_url: String
}
],
last_message: { // ë¹ì ê·íë¡ ìµê·¼ ë©ìì§ ë¹ ë¥¸ ì¡°í
content: String,
sender_id: ObjectId,
sent_at: Date
},
unread_counts: { // ê° ì°¸ì¬ìë³ ì½ì§ ìì ë©ìì§ ì
"user_id_1": 5,
"user_id_2": 0
},
created_at: Date,
updated_at: Date
}
// messages collection
{
_id: ObjectId,
conversation_id: ObjectId, // indexed
sender_id: ObjectId,
content: String,
attachments: [
{
type: String, // 'image', 'file', 'video'
url: String,
filename: String
}
],
read_by: [ObjectId], // ì½ì ì¬ì©ì ID ë°°ì´
sent_at: Date, // indexed
edited_at: Date
}
// Indexes
db.users.createIndex({ username: 1 }, { unique: true });
db.users.createIndex({ email: 1 }, { unique: true });
db.conversations.createIndex({ "participants.user_id": 1 });
db.conversations.createIndex({ updated_at: -1 });
db.messages.createIndex({ conversation_id: 1, sent_at: -1 });
db.messages.createIndex({ sender_id: 1 });
ì¤ê³ í¹ì§:
- ì½ê¸° ìµì í를 ìí ë¹ì ê·í (last_message ìë² ë©)
- ì주 ì¡°íëë íëì ì¸ë±ì¤
- ë°°ì´ íë íì© (participants, read_by)
Best practices
íì§ í¥ì
-
ëª ëª ê·ì¹ ì¼ê´ì±: í ì´ë¸/ì¹¼ë¼ ì´ë¦ì snake_case ì¬ì©
- users, post_tags, created_at
- ë³µìí/ë¨ìí ì¼ê´ëê² (í ì´ë¸ì ë³µì, ì¹¼ë¼ì ë¨ì ë±)
-
Soft Delete ê³ ë ¤: ì¤ì ë°ì´í°ë 물리 ìì ëì ë ¼ë¦¬ ìì
- deleted_at TIMESTAMP (NULLì´ë©´ íì±, NOT NULLì´ë©´ ìì ë¨)
- ì¤ìë¡ ìì í ë°ì´í° 복구 ê°ë¥
- ê°ì¬(Audit) ì¶ì
-
Timestamp íì: created_at, updated_atì ëë¶ë¶ í ì´ë¸ì í¬í¨
- ë°ì´í° ì¶ì ë° ëë²ê¹
- ìê³ì´ ë¶ì
í¨ì¨ì± ê°ì
- Partial Indexes: ì¡°ê±´ë¶ ì¸ë±ì¤ë¡ ì¸ë±ì¤ í¬ê¸° ìµìí
CREATE INDEX idx_posts_published ON posts(published_at) WHERE published_at IS NOT NULL; - Materialized Views: ë³µì¡í ì§ê³ 쿼리ë Materialized Viewë¡ ìºì±
- Partitioning: ëì©ë í ì´ë¸ì ë ì§/ë²ì ê¸°ì¤ íí°ì ë
ì주 ë°ìíë 문ì (Common Issues)
문ì 1: N+1 쿼리 문ì
ì¦ì: í ì¿¼ë¦¬ë¡ ì¶©ë¶íë° ì¬ë¬ ë² DB í¸ì¶
ìì¸: JOIN ìì´ ë°ë³µë¬¸ìì ê°ë³ ì¡°í
í´ê²°ë°©ë²:
-- â ëì ì: N+1 queries
SELECT * FROM posts; -- 1ë²
-- ê° postë§ë¤
SELECT * FROM users WHERE id = ?; -- Në²
-- â
ì¢ì ì: 1 query
SELECT posts.*, users.username, users.avatar_url
FROM posts
JOIN users ON posts.author_id = users.id;
문ì 2: ì¸ë±ì¤ ìë Foreign Keyë¡ ì¸í ë린 JOIN
ì¦ì: JOIN ì¿¼ë¦¬ê° ë§¤ì° ë림
ìì¸: Foreign Key ì¹¼ë¼ì ì¸ë±ì¤ ëë½
í´ê²°ë°©ë²:
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
문ì 3: UUID vs Auto-increment ì±ë¥
ì¦ì: UUID Primary Key ì¬ì© ì ì½ì ì±ë¥ ì í
ìì¸: UUIDë ëë¤íì¬ ì¸ë±ì¤ ì¡°ê°í
í´ê²°ë°©ë²:
- PostgreSQL:
uuid_generate_v7()ì¬ì© (ìê° ìì UUID) - MySQL:
UUID_TO_BIN(UUID(), 1)ì¬ì© - ëë Auto-increment BIGINT ì¬ì© ê³ ë ¤
References
ê³µì 문ì
ë구
- dbdiagram.io – ERD ë¤ì´ì´ê·¸ë¨ ìì±
- PgModeler – PostgreSQL 모ë¸ë§ ë구
- Prisma – ORM + ë§ì´ê·¸ë ì´ì
íìµ ìë£
- Database Design Course (freecodecamp)
- Use The Index, Luke – SQL ì¸ë±ì± ê°ì´ë
Metadata
ë²ì
- íì¬ ë²ì : 1.0.0
- ìµì¢ ì ë°ì´í¸: 2025-01-01
- í¸í íë«í¼: Claude, ChatGPT, Gemini
ê´ë ¨ ì¤í¬
- api-design: APIì í¨ê» ì¤í¤ë§ ì¤ê³
- performance-optimization: 쿼리 ì±ë¥ ìµì í
íê·¸
#database #schema #PostgreSQL #MySQL #MongoDB #SQL #NoSQL #migration #ERD