Learning
레슨 7 / 8·25분

실전: 쇼핑몰 데이터베이스 설계

프로젝트 개요 — 온라인 쇼핑몰 DB

지금까지 배운 SQL 지식을 총동원하여 온라인 쇼핑몰 데이터베이스를 설계하고 주요 쿼리를 작성합니다. 테이블 관계, 제약조건, 인덱스, 트랜잭션을 실전에서 어떻게 활용하는지 경험합니다.

테이블 설계

sql
-- ── 회원 테이블 ──
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(50) NOT NULL,
    phone VARCHAR(20),
    address TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ── 카테고리 테이블 ──
CREATE TABLE categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    parent_id INT NULL,
    FOREIGN KEY (parent_id) REFERENCES categories(id)
);

-- ── 상품 테이블 ──
CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
    stock INT DEFAULT 0 CHECK (stock >= 0),
    category_id INT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- ── 주문 테이블 ──
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    total_amount DECIMAL(12, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- ── 주문 상세 테이블 ──
CREATE TABLE order_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

인덱스 설정

sql
-- 자주 검색되는 열에 인덱스
CREATE INDEX idx_products_category ON products (category_id);
CREATE INDEX idx_products_price ON products (price);
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_order_items_order ON order_items (order_id);
CREATE INDEX idx_users_email ON users (email);

주요 비즈니스 쿼리

sql
-- 1. 카테고리별 상품 목록 (재고 있는 것만)
SELECT p.id, p.name, p.price, p.stock, c.name AS category
FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE p.is_active = TRUE
  AND p.stock > 0
ORDER BY c.name, p.price;

-- 2. 회원별 총 구매 금액 (상위 10명)
SELECT u.name, u.email,
       COUNT(o.id) AS order_count,
       SUM(o.total_amount) AS total_spent
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
GROUP BY u.id, u.name, u.email
ORDER BY total_spent DESC
LIMIT 10;

-- 3. 인기 상품 TOP 5 (판매량 기준)
SELECT p.name,
       SUM(oi.quantity) AS total_sold,
       SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM products p
INNER JOIN order_items oi ON p.id = oi.product_id
INNER JOIN orders o ON oi.order_id = o.id
WHERE o.status = 'completed'
GROUP BY p.id, p.name
ORDER BY total_sold DESC
LIMIT 5;

-- 4. 월별 매출 통계
SELECT
    DATE_FORMAT(ordered_at, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY month
ORDER BY month DESC;

주문 처리 트랜잭션

sql
-- 주문 생성 트랜잭션
START TRANSACTION;

-- 1. 주문 생성
INSERT INTO orders (user_id, total_amount, status)
VALUES (1, 0, 'pending');
SET @order_id = LAST_INSERT_ID();

-- 2. 주문 아이템 추가 + 재고 확인
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT @order_id, id, 2, price
FROM products
WHERE id = 101 AND stock >= 2;

-- 3. 재고 감소
UPDATE products
SET stock = stock - 2
WHERE id = 101 AND stock >= 2;

-- 4. 주문 총액 갱신
UPDATE orders
SET total_amount = (
    SELECT SUM(quantity * unit_price)
    FROM order_items
    WHERE order_id = @order_id
)
WHERE id = @order_id;

-- 5. 모든 작업 성공 시 확정
COMMIT;
  • 정규화 — 데이터 중복을 최소화하는 테이블 설계
  • 외래 키 — 테이블 간 관계를 명시적으로 정의
  • 인덱스 — 자주 조회되는 열에 설정하여 성능 향상
  • 트랜잭션 — 주문 처리처럼 여러 작업을 원자적으로 실행
  • VIEW — 복잡한 쿼리를 가상 테이블로 저장하여 재사용
💡

실무에서는 ERD(Entity-Relationship Diagram)로 테이블 관계를 먼저 시각화한 후 SQL을 작성합니다. MySQL Workbench, DBeaver, dbdiagram.io 같은 도구를 활용하면 설계가 수월합니다.