실습 프로젝트: 쇼핑몰 DB
지금까지 배운 DDL(CREATE TABLE, ALTER TABLE)과 DML(INSERT, UPDATE, DELETE, SELECT)을 종합하여 실제 쇼핑몰 데이터베이스를 설계하고 운영하는 실습을 진행합니다. 단순히 SQL 문법을 따라 치는 것이 아니라, 왜 이렇게 설계하는가, 실무에서 이 쿼리를 어떤 상황에서 쓰는가를 함께 생각하면서 진행합니다.
이 실습에서는 회원, 상품, 주문, 주문상세의 4개 테이블로 구성된 쇼핑몰 스키마를 만들고, 데이터를 삽입한 뒤, 다양한 비즈니스 시나리오에 맞는 조회·수정·삭제를 수행합니다. 각 SQL에 대해 실행 결과와 그 의미를 함께 설명합니다.
스키마 설계
먼저 쇼핑몰에 필요한 테이블을 설계합니다. 3장에서 학습한 CREATE TABLE을 활용합니다.
-- 회원 테이블
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
grade VARCHAR(10) DEFAULT 'BRONZE',
created_at DATE DEFAULT CURRENT_DATE
);
-- 상품 테이블
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INT NOT NULL CHECK (price >= 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
category VARCHAR(50),
description VARCHAR(500),
created_at DATE DEFAULT CURRENT_DATE
);
-- 주문 테이블
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(user_id),
order_date DATE NOT NULL DEFAULT CURRENT_DATE,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total_amount INT DEFAULT 0
);
-- 주문 상세 테이블
CREATE TABLE order_items (
item_id INT PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
product_id INT NOT NULL REFERENCES products(product_id),
quantity INT NOT NULL CHECK (quantity > 0),
unit_price INT NOT NULL CHECK (unit_price >= 0)
);이 스키마의 설계 포인트를 짚어봅시다.
1. users.email에 UNIQUE → 동일 이메일로 중복 가입 방지
2. products.price와 stock에 CHECK → 음수 가격/재고 방지
3. orders.user_id에 FK → 존재하지 않는 회원의 주문 방지
4. order_items.order_id에 FK + ON DELETE CASCADE
→ 주문 삭제 시 주문 상세도 함께 삭제
5. order_items.product_id에 FK (CASCADE 없음)
→ 상품 삭제 시 주문 이력 보존이 필요하므로 RESTRICT
6. 주문과 주문상세를 분리한 이유:
→ 하나의 주문에 여러 상품이 포함될 수 있음 (1:N 관계)
→ 정규화 원칙에 따른 설계데이터 삽입
테이블에 실습용 데이터를 넣습니다. 실제 쇼핑몰의 데이터를 모방하여 다양한 조회 시나리오를 연습할 수 있도록 구성합니다.
INSERT INTO users (user_id, username, email, password, grade, created_at)
VALUES (1, 'kim_dev', 'kim@example.com', 'hashed_pw1', 'GOLD', DATE '2024-01-15');
INSERT INTO users (user_id, username, email, password, grade, created_at)
VALUES (2, 'lee_design', 'lee@example.com', 'hashed_pw2', 'BRONZE', DATE '2024-02-20');
INSERT INTO users (user_id, username, email, password, grade, created_at)
VALUES (3, 'park_pm', 'park@example.com', 'hashed_pw3', 'SILVER', DATE '2024-03-10');
INSERT INTO users (user_id, username, email, password, grade, created_at)
VALUES (4, 'choi_data', 'choi@example.com', 'hashed_pw4', 'BRONZE', DATE '2024-05-05');
INSERT INTO users (user_id, username, email, password, grade, created_at)
VALUES (5, 'jung_back', 'jung@example.com', 'hashed_pw5', 'GOLD', DATE '2024-06-20');INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (1, '맥북 프로 16', 3690000, 10, '전자기기', '최신 M3 칩 탑재');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (2, '에어팟 프로', 359000, 50, '전자기기', '노이즈 캔슬링 지원');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (3, '클린 코드', 33000, 100, '도서', '로버트 C. 마틴 저');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (4, '기계식 키보드', 89000, 30, '전자기기', '체리 청축');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (5, '모니터 암', 45000, 0, '가구', '듀얼 모니터용, 현재 품절');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (6, '리팩터링', 44000, 80, '도서', '마틴 파울러 저');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (7, '무선 마우스', 35000, 200, '전자기기', '인체공학 설계');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (8, '스탠딩 데스크', 450000, 5, '가구', '전동 높이 조절');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (9, 'USB-C 허브', 55000, 150, '전자기기', '7-in-1 멀티포트');
INSERT INTO products (product_id, name, price, stock, category, description)
VALUES (10, '실용주의 프로그래머', 38000, 60, '도서', '앤드류 헌트 저');INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (1, 1, DATE '2024-06-01', 'DELIVERED', 4408000);
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (2, 1, DATE '2024-07-15', 'SHIPPED', 33000);
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (3, 2, DATE '2024-08-01', 'PAID', 177000);
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (4, 3, DATE '2024-08-15', 'DELIVERED', 3690000);
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (5, 5, DATE '2024-09-01', 'PENDING', 89000);
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (6, 4, DATE '2024-09-10', 'CANCELLED', 359000);-- 주문1: kim이 맥북 1개 + 에어팟 2개 구매
INSERT INTO order_items VALUES (1, 1, 1, 1, 3690000);
INSERT INTO order_items VALUES (2, 1, 2, 2, 359000);
-- 주문2: kim이 클린코드 1권 구매
INSERT INTO order_items VALUES (3, 2, 3, 1, 33000);
-- 주문3: lee가 키보드 1개 + 리팩터링 2권 구매
INSERT INTO order_items VALUES (4, 3, 4, 1, 89000);
INSERT INTO order_items VALUES (5, 3, 6, 2, 44000);
-- 주문4: park이 맥북 1개 구매
INSERT INTO order_items VALUES (6, 4, 1, 1, 3690000);
-- 주문5: jung이 키보드 1개 구매
INSERT INTO order_items VALUES (7, 5, 4, 1, 89000);
-- 주문6: choi가 에어팟 1개 구매 (취소됨)
INSERT INTO order_items VALUES (8, 6, 2, 1, 359000);
COMMIT;기본 조회 실습
단일 테이블 조회
SELECT name, price, stock
FROM products
WHERE category = '전자기기'
ORDER BY price DESC;name | price | stock
------------------+----------+------
맥북 프로 16 | 3690000 | 10
에어팟 프로 | 359000 | 50
기계식 키보드 | 89000 | 30
USB-C 허브 | 55000 | 150
무선 마우스 | 35000 | 200SELECT name, stock, price
FROM products
WHERE stock > 0
ORDER BY stock DESC;name | stock | price
----------------------+-------+--------
무선 마우스 | 200 | 35000
USB-C 허브 | 150 | 55000
클린 코드 | 100 | 33000
리팩터링 | 80 | 44000
실용주의 프로그래머 | 60 | 38000
에어팟 프로 | 50 | 359000
기계식 키보드 | 30 | 89000
맥북 프로 16 | 10 | 3690000
스탠딩 데스크 | 5 | 450000모니터 암(stock=0)이 결과에서 빠져 있습니다. WHERE stock > 0 조건에 의해 필터링되었습니다.
SELECT name, price
FROM products
WHERE price BETWEEN 30000 AND 100000
ORDER BY price;name | price
----------------------+--------
클린 코드 | 33000
무선 마우스 | 35000
실용주의 프로그래머 | 38000
리팩터링 | 44000
모니터 암 | 45000
USB-C 허브 | 55000
기계식 키보드 | 89000BETWEEN은 양쪽 끝값을 포함합니다(30000 이상 100000 이하).
SELECT name, price FROM products WHERE name LIKE '%프로%';name | price
--------------+--------
맥북 프로 16 | 3690000
에어팟 프로 | 359000
실용주의 프로그래머 | 38000'프로'가 포함된 모든 상품이 조회됩니다. LIKE '%프로%'는 인덱스를 사용할 수 없으므로 대용량 테이블에서는 성능에 주의해야 합니다.
SELECT username, email, grade
FROM users
WHERE grade IN ('GOLD', 'SILVER')
ORDER BY grade, username;username | email | grade
-----------+--------------------+-------
kim_dev | kim@example.com | GOLD
jung_back | jung@example.com | GOLD
park_pm | park@example.com | SILVER집계 함수 활용
SELECT category,
COUNT(*) AS 상품수,
AVG(price) AS 평균가격,
MIN(price) AS 최저가격,
MAX(price) AS 최고가격,
SUM(stock) AS 총재고
FROM products
GROUP BY category
ORDER BY 상품수 DESC;category | 상품수 | 평균가격 | 최저가격 | 최고가격 | 총재고
---------+--------+----------+---------+----------+------
전자기기 | 5 | 845600 | 35000 | 3690000 | 440
도서 | 3 | 38333 | 33000 | 44000 | 240
가구 | 2 | 247500 | 45000 | 450000 | 5SELECT name, price, category
FROM products
WHERE price = (SELECT MAX(price) FROM products);name | price | category
--------------+----------+----------
맥북 프로 16 | 3690000 | 전자기기서브쿼리가 먼저 실행되어 최대 가격(3690000)을 구하고, 외부 쿼리가 그 가격에 해당하는 상품을 찾습니다.
SELECT name, price, category
FROM products
WHERE price >= (SELECT AVG(price) FROM products)
ORDER BY price DESC;name | price | category
--------------+----------+----------
맥북 프로 16 | 3690000 | 전자기기
스탠딩 데스크 | 450000 | 가구
에어팟 프로 | 359000 | 전자기기전체 평균 가격(약 383,800원) 이상인 상품 3개가 조회됩니다.
다중 테이블 조회 (JOIN 미리보기)
아직 JOIN을 정식으로 배우지 않았지만, 여러 테이블의 데이터를 결합하는 쿼리를 미리 경험해 봅시다.
SELECT u.username, o.order_id, o.order_date, o.status, o.total_amount
FROM users u
JOIN orders o ON u.user_id = o.user_id
ORDER BY o.order_date DESC;username | order_id | order_date | status | total_amount
------------+----------+------------+-----------+-------------
choi_data | 6 | 2024-09-10 | CANCELLED | 359000
jung_back | 5 | 2024-09-01 | PENDING | 89000
park_pm | 4 | 2024-08-15 | DELIVERED | 3690000
lee_design | 3 | 2024-08-01 | PAID | 177000
kim_dev | 2 | 2024-07-15 | SHIPPED | 33000
kim_dev | 1 | 2024-06-01 | DELIVERED | 4408000kim_dev가 두 번 나옵니다. 한 회원이 여러 주문을 할 수 있기 때문입니다. 이것이 1:N 관계의 특성입니다.
SELECT u.username,
p.name AS product_name,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price AS subtotal
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN users u ON o.user_id = u.user_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY o.order_date, oi.item_id;username | product_name | quantity | unit_price | subtotal
-----------+-----------------+----------+------------+---------
kim_dev | 맥북 프로 16 | 1 | 3690000 | 3690000
kim_dev | 에어팟 프로 | 2 | 359000 | 718000
kim_dev | 클린 코드 | 1 | 33000 | 33000
lee_design | 기계식 키보드 | 1 | 89000 | 89000
lee_design | 리팩터링 | 2 | 44000 | 88000
park_pm | 맥북 프로 16 | 1 | 3690000 | 3690000
jung_back | 기계식 키보드 | 1 | 89000 | 89000
choi_data | 에어팟 프로 | 1 | 359000 | 3590003개 테이블을 JOIN하여 회원-주문-상품을 하나의 결과로 연결했습니다. 이것이 관계형 데이터베이스의 핵심 능력입니다. 정규화로 분리된 데이터를 JOIN으로 다시 결합하여 원하는 정보를 추출합니다.
SELECT u.username, u.email, u.created_at
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;LEFT JOIN은 왼쪽 테이블(users)의 모든 행을 유지하고, 오른쪽 테이블(orders)에 매칭되는 행이 없으면 NULL로 채웁니다. WHERE o.order_id IS NULL 조건으로 주문이 없는 회원만 필터링합니다.
SELECT p.name, p.stock, p.category
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
WHERE oi.item_id IS NULL;name | stock | category
----------------------+-------+---------
모니터 암 | 0 | 가구
스탠딩 데스크 | 5 | 가구
무선 마우스 | 200 | 전자기기
USB-C 허브 | 150 | 전자기기
실용주의 프로그래머 | 60 | 도서비즈니스 시나리오별 조회
실제 쇼핑몰을 운영하면서 자주 필요한 쿼리들을 연습합니다.
SELECT p.name,
SUM(oi.quantity) AS 총판매수량,
SUM(oi.quantity * oi.unit_price) AS 총매출
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status != 'CANCELLED'
GROUP BY p.product_id, p.name
ORDER BY 총매출 DESC
FETCH FIRST 3 ROWS ONLY; -- Oracle/PostgreSQL (MySQL: LIMIT 3)name | 총판매수량 | 총매출
--------------+-----------+--------
맥북 프로 16 | 2 | 7380000
에어팟 프로 | 2 | 718000
기계식 키보드 | 2 | 178000취소된 주문(CANCELLED)은 제외하고 매출을 계산합니다. 실무에서는 이런 조건을 빠뜨리면 잘못된 매출 보고서를 만들게 됩니다.
SELECT EXTRACT(MONTH FROM o.order_date) AS 월,
COUNT(DISTINCT o.order_id) AS 주문수,
SUM(o.total_amount) AS 월매출
FROM orders o
WHERE o.status NOT IN ('CANCELLED', 'PENDING')
GROUP BY EXTRACT(MONTH FROM o.order_date)
ORDER BY 월;월 | 주문수 | 월매출
---+--------+---------
6 | 1 | 4408000
7 | 1 | 33000
8 | 2 | 3867000SELECT u.username, u.grade,
COUNT(o.order_id) AS 주문횟수,
SUM(o.total_amount) AS 총구매액
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.status NOT IN ('CANCELLED')
AND u.grade = 'GOLD'
GROUP BY u.user_id, u.username, u.grade
ORDER BY 총구매액 DESC;SELECT p.name, p.stock, p.category,
COALESCE(SUM(oi.quantity), 0) AS 총판매수량
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
WHERE p.stock <= 10
GROUP BY p.product_id, p.name, p.stock, p.category
HAVING COALESCE(SUM(oi.quantity), 0) > 0
ORDER BY p.stock;COALESCE는 NULL을 0으로 대체합니다. LEFT JOIN으로 판매 이력이 없는 상품도 포함하되, HAVING 절로 판매 이력이 있는 상품만 필터링합니다. 재고가 부족한데 판매가 되고 있는 상품이 우선 보충 대상입니다.
UPDATE 실습
가격 변경
-- 변경 전 확인
SELECT name, price FROM products WHERE category = '전자기기';
-- 할인 적용
UPDATE products SET price = price * 0.9
WHERE category = '전자기기';
-- 변경 후 확인
SELECT name, price FROM products WHERE category = '전자기기';변경 전: 변경 후:
name | price name | price
맥북 프로 16 | 3690000 맥북 프로 16 | 3321000
에어팟 프로 | 359000 에어팟 프로 | 323100
기계식 키보드 | 89000 기계식 키보드 | 80100
무선 마우스 | 35000 무선 마우스 | 31500
USB-C 허브 | 55000 USB-C 허브 | 49500-- 10% 할인을 되돌리려면 0.9로 나누기
UPDATE products SET price = ROUND(price / 0.9)
WHERE category = '전자기기';ROUND를 사용하는 이유는 나눗셈에서 소수점이 발생할 수 있기 때문입니다. 실무에서는 할인 전 원래 가격을 별도 컬럼(original_price)에 저장하는 것이 안전합니다.
주문 상태 변경
UPDATE orders
SET status = CASE
WHEN status = 'PENDING' AND order_date < DATE '2024-09-05' THEN 'EXPIRED'
WHEN status = 'SHIPPED' AND order_date < DATE '2024-08-01' THEN 'DELIVERED'
ELSE status
END
WHERE status IN ('PENDING', 'SHIPPED');여러 조건에 따라 서로 다른 상태로 변경합니다. ELSE status를 넣어서 조건에 해당하지 않는 행은 변경하지 않습니다.
회원 등급 재산정
UPDATE users u
SET grade = (
SELECT CASE
WHEN COALESCE(SUM(o.total_amount), 0) >= 3000000 THEN 'GOLD'
WHEN COALESCE(SUM(o.total_amount), 0) >= 1000000 THEN 'SILVER'
ELSE 'BRONZE'
END
FROM orders o
WHERE o.user_id = u.user_id
AND o.status NOT IN ('CANCELLED')
);
-- 결과 확인
SELECT username, grade FROM users ORDER BY username;서브쿼리 UPDATE를 사용하여 각 회원의 총 구매액을 기준으로 등급을 재산정합니다. COALESCE로 주문이 없는 회원(NULL)은 0으로 처리합니다.
DELETE 실습
취소된 주문 처리
-- 삭제 전 확인
SELECT oi.item_id, o.order_id, o.status, p.name
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.status = 'CANCELLED';
-- 삭제 실행
DELETE FROM order_items
WHERE order_id IN (
SELECT order_id FROM orders WHERE status = 'CANCELLED'
);
-- 취소 주문 자체도 삭제
DELETE FROM orders WHERE status = 'CANCELLED';DELETE 순서가 중요합니다. order_items를 먼저 삭제하고 orders를 삭제해야 합니다. 순서를 바꾸면 order_items의 FK 제약 때문에 orders 삭제가 실패합니다(ON DELETE CASCADE가 설정되어 있다면 자동으로 처리되지만, 명시적으로 하는 것이 안전합니다).
품절 상품 정리
-- 확인
SELECT p.name, p.stock
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
WHERE p.stock = 0
AND oi.item_id IS NULL;
-- 삭제
DELETE FROM products
WHERE stock = 0
AND product_id NOT IN (
SELECT DISTINCT product_id FROM order_items
);주문 이력이 있는 상품은 삭제하면 안 됩니다. FK 제약 때문에 에러가 발생하기도 하지만, 비즈니스적으로도 과거 주문 내역에서 어떤 상품을 샀는지를 알 수 없게 되기 때문입니다.
고급 조회 실습
-- MySQL
SELECT p.name,
GROUP_CONCAT(DISTINCT u.username ORDER BY u.username) AS 구매회원
FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
JOIN users u ON o.user_id = u.user_id
WHERE o.status != 'CANCELLED'
GROUP BY p.product_id, p.name;
-- PostgreSQL
SELECT p.name,
STRING_AGG(DISTINCT u.username, ', ' ORDER BY u.username) AS 구매회원
FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
JOIN users u ON o.user_id = u.user_id
WHERE o.status != 'CANCELLED'
GROUP BY p.product_id, p.name;SELECT p.category,
SUM(oi.quantity * oi.unit_price) AS 카테고리매출,
ROUND(
SUM(oi.quantity * oi.unit_price) * 100.0 /
(SELECT SUM(quantity * unit_price) FROM order_items),
1
) AS 비중_퍼센트
FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status != 'CANCELLED'
GROUP BY p.category
ORDER BY 카테고리매출 DESC;SELECT u.username,
COUNT(o.order_id) AS 주문횟수,
MIN(o.order_date) AS 첫주문일,
MAX(o.order_date) AS 최근주문일
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.status NOT IN ('CANCELLED')
GROUP BY u.user_id, u.username
HAVING COUNT(o.order_id) >= 2;트랜잭션 실습
실무에서의 트랜잭션 처리 패턴을 연습합니다.
BEGIN; -- START TRANSACTION (MySQL)
-- 1. 주문 생성
INSERT INTO orders (order_id, user_id, order_date, status, total_amount)
VALUES (7, 2, CURRENT_DATE, 'PAID', 89000);
-- 2. 주문 상세 생성
INSERT INTO order_items (item_id, order_id, product_id, quantity, unit_price)
VALUES (9, 7, 4, 1, 89000);
-- 3. 재고 차감
UPDATE products SET stock = stock - 1
WHERE product_id = 4;
-- 4. 재고 확인 (음수 방지)
-- PostgreSQL/Oracle에서는 CHECK 제약이 이를 자동으로 방지
SELECT stock FROM products WHERE product_id = 4;
-- stock이 음수가 아니면 확정
COMMIT;
-- 문제가 있다면: ROLLBACK;이 트랜잭션은 주문 생성, 주문 상세 기록, 재고 차감을 하나의 원자적 단위로 처리합니다. 세 단계 중 하나라도 실패하면 ROLLBACK으로 전체를 취소해야 합니다.
BEGIN;
-- 마일리지 차감 (회원1 → 회원2로 포인트 이전)
UPDATE users SET grade = 'SILVER' WHERE user_id = 1; -- 등급 변경
UPDATE users SET grade = 'GOLD' WHERE user_id = 2; -- 등급 변경
-- 변경 확인
SELECT user_id, username, grade FROM users WHERE user_id IN (1, 2);
-- 확인 후 확정
COMMIT;실습 정리 및 데이터 원복
-- 실습에서 변경한 데이터를 원복하려면 처음부터 다시 시작
-- 방법 1: ROLLBACK (COMMIT 전이라면)
ROLLBACK;
-- 방법 2: 테이블 재생성
DROP TABLE order_items;
DROP TABLE orders;
DROP TABLE products;
DROP TABLE users;
-- 위의 CREATE TABLE과 INSERT를 다시 실행실습에서 배운 핵심 정리
1. INSERT — 컬럼 리스트를 항상 명시하고, FK 순서를 지켜야 한다
(부모 테이블 먼저 삽입)
2. SELECT — WHERE, ORDER BY, GROUP BY, HAVING의 조합으로
다양한 비즈니스 요구를 처리할 수 있다
3. JOIN — 정규화로 분리된 데이터를 다시 결합하는 핵심 도구이다
LEFT JOIN으로 "없는 것"도 찾을 수 있다
4. UPDATE — 서브쿼리, CASE를 활용하면 복잡한 일괄 수정이 가능하다
항상 SELECT로 먼저 확인하고 실행한다
5. DELETE — 순서가 중요하다 (자식 먼저, 부모 나중에)
삭제 전 영향 범위를 반드시 확인한다
6. 트랜잭션 — 관련 DML을 하나의 단위로 묶어 원자성을 보장한다
문제가 있으면 ROLLBACK으로 전체를 취소한다다음 장에서는 여러 테이블을 연결하여 데이터를 추출하는 조인과 서브쿼리를 다루겠습니다.