관계형 데이터베이스 개념

관계형 데이터베이스(RDBMS)는 데이터를 테이블 형태로 저장하고 관리합니다.

핵심 용어

  • 테이블(Table): 데이터를 저장하는 구조 (엑셀의 시트와 유사)
  • 행(Row): 하나의 레코드 (데이터 항목)
  • 열(Column): 속성 또는 필드
  • 기본키(Primary Key): 각 행을 고유하게 식별하는 열 (중복 불가, NULL 불가)
  • 외래키(Foreign Key): 다른 테이블의 기본키를 참조하는 열
-- 예시: users 테이블
-- id (기본키), name, email, created_at (열)
-- 각 행은 한 명의 사용자 데이터

DDL (Data Definition Language)

테이블 구조를 정의하는 명령어입니다.

CREATE TABLE - 테이블 생성

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    age INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

주요 데이터 타입

타입설명예시
INT정수1, 100, -50
VARCHAR(n)가변 길이 문자열'hello', '안녕하세요'
TEXT긴 텍스트게시글 본문
DECIMAL(p,s)고정 소수점19.99 (p=전체자리, s=소수자리)
DATE날짜'2026-02-16'
TIMESTAMP날짜+시간'2026-02-16 13:20:00'
BOOLEAN참/거짓TRUE, FALSE

ALTER TABLE - 테이블 수정

-- 열 추가
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- 열 삭제
ALTER TABLE users DROP COLUMN age;

-- 열 타입 변경
ALTER TABLE users MODIFY COLUMN name VARCHAR(150);

DROP TABLE - 테이블 삭제

DROP TABLE users; -- 테이블과 모든 데이터 삭제 (주의!)

DML (Data Manipulation Language)

데이터를 조작하는 명령어입니다.

INSERT - 데이터 삽입

-- 단일 행 삽입
INSERT INTO users (name, email, age)
VALUES ('김철수', 'kim@example.com', 28);

-- 여러 행 삽입
INSERT INTO users (name, email, age)
VALUES
    ('이영희', 'lee@example.com', 32),
    ('박민수', 'park@example.com', 25);

SELECT - 데이터 조회

-- 모든 열 조회
SELECT * FROM users;

-- 특정 열만 조회
SELECT name, email FROM users;

-- 중복 제거
SELECT DISTINCT age FROM users;

-- 별칭(alias) 사용
SELECT name AS 이름, email AS 이메일 FROM users;

UPDATE - 데이터 수정

-- 특정 행 수정
UPDATE users
SET age = 29
WHERE name = '김철수';

-- 여러 열 동시 수정
UPDATE users
SET age = 30, email = 'newkim@example.com'
WHERE id = 1;

DELETE - 데이터 삭제

-- 조건에 맞는 행 삭제
DELETE FROM users WHERE age < 20;

-- 모든 행 삭제 (테이블 구조는 유지)
DELETE FROM users;

WHERE 조건절

데이터 필터링에 사용합니다.

기본 연산자

-- 비교 연산자
SELECT * FROM users WHERE age = 28;
SELECT * FROM users WHERE age > 25;
SELECT * FROM users WHERE age <= 30;
SELECT * FROM users WHERE age != 28;

-- 논리 연산자
SELECT * FROM users WHERE age > 20 AND age < 40;
SELECT * FROM users WHERE name = '김철수' OR name = '이영희';
SELECT * FROM users WHERE NOT age = 28;

LIKE - 패턴 매칭

-- 이름이 '김'으로 시작
SELECT * FROM users WHERE name LIKE '김%';

-- 이메일에 'gmail'이 포함
SELECT * FROM users WHERE email LIKE '%gmail%';

-- 이름이 '수'로 끝남
SELECT * FROM users WHERE name LIKE '%수';

-- 두 번째 글자가 '영'
SELECT * FROM users WHERE name LIKE '_영%';

IN - 여러 값 중 하나와 일치

SELECT * FROM users WHERE age IN (25, 28, 32);

-- NOT IN
SELECT * FROM users WHERE age NOT IN (25, 28);

BETWEEN - 범위 조건

SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- 20 이상 30 이하

SELECT * FROM users WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31';

IS NULL - NULL 값 확인

SELECT * FROM users WHERE phone IS NULL;

SELECT * FROM users WHERE phone IS NOT NULL;

JOIN - 테이블 결합

여러 테이블의 데이터를 연결하여 조회합니다.

테스트 데이터 준비

CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);

CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT,
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

INSERT INTO departments VALUES (1, '개발팀'), (2, '디자인팀');
INSERT INTO employees VALUES
    (1, '김철수', 1),
    (2, '이영희', 1),
    (3, '박민수', 2),
    (4, '최지연', NULL);

INNER JOIN

양쪽 테이블에 모두 존재하는 데이터만 조회

SELECT e.name AS 직원명, d.name AS 부서명
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- 결과:
-- 김철수 | 개발팀
-- 이영희 | 개발팀
-- 박민수 | 디자인팀
-- (최지연은 부서가 NULL이므로 제외)

LEFT JOIN (LEFT OUTER JOIN)

왼쪽 테이블의 모든 데이터 + 오른쪽 테이블의 매칭되는 데이터

SELECT e.name AS 직원명, d.name AS 부서명
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;

-- 결과:
-- 김철수 | 개발팀
-- 이영희 | 개발팀
-- 박민수 | 디자인팀
-- 최지연 | NULL (부서 없는 직원도 포함)

RIGHT JOIN (RIGHT OUTER JOIN)

오른쪽 테이블의 모든 데이터 + 왼쪽 테이블의 매칭되는 데이터

SELECT e.name AS 직원명, d.name AS 부서명
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;

-- 직원이 없는 부서도 포함됨

FULL OUTER JOIN

양쪽 테이블의 모든 데이터 (MySQL은 미지원, UNION으로 구현)

-- MySQL에서는 UNION 사용
SELECT e.name, d.name
FROM employees e LEFT JOIN departments d ON e.department_id = d.id
UNION
SELECT e.name, d.name
FROM employees e RIGHT JOIN departments d ON e.department_id = d.id;

JOIN 다이어그램

INNER JOIN: [A ∩ B]
LEFT JOIN:  [A + (A ∩ B)]
RIGHT JOIN: [(A ∩ B) + B]
FULL OUTER: [A + B]

집계와 그룹화

집계 함수

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2),
    category VARCHAR(50),
    stock INT
);

INSERT INTO products VALUES
    (1, '노트북', 1200000, '전자제품', 10),
    (2, '마우스', 25000, '전자제품', 50),
    (3, '책상', 150000, '가구', 5),
    (4, '의자', 80000, '가구', 8);
-- 총 개수
SELECT COUNT(*) AS 총상품수 FROM products;

-- NULL이 아닌 값만 카운트
SELECT COUNT(stock) FROM products;

-- 합계
SELECT SUM(stock) AS 총재고 FROM products;

-- 평균
SELECT AVG(price) AS 평균가격 FROM products;

-- 최대/최소
SELECT MAX(price) AS 최고가, MIN(price) AS 최저가 FROM products;

GROUP BY - 그룹별 집계

-- 카테고리별 상품 수
SELECT category, COUNT(*) AS 상품수
FROM products
GROUP BY category;

-- 결과:
-- 전자제품 | 2
-- 가구     | 2

-- 카테고리별 평균 가격
SELECT category, AVG(price) AS 평균가격, SUM(stock) AS 총재고
FROM products
GROUP BY category;

HAVING - 그룹 필터링

-- WHERE: 그룹화 전 필터링
-- HAVING: 그룹화 후 필터링

-- 평균 가격이 100,000원 이상인 카테고리
SELECT category, AVG(price) AS 평균가격
FROM products
GROUP BY category
HAVING AVG(price) >= 100000;

ORDER BY - 정렬

-- 오름차순 (기본)
SELECT * FROM products ORDER BY price;

-- 내림차순
SELECT * FROM products ORDER BY price DESC;

-- 여러 열 정렬
SELECT * FROM products
ORDER BY category ASC, price DESC;

서브쿼리

쿼리 안에 또 다른 쿼리를 포함합니다.

스칼라 서브쿼리 (단일 값 반환)

-- 평균 가격보다 비싼 상품
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

인라인 뷰 (FROM절 서브쿼리)

-- 카테고리별 평균 가격을 먼저 구한 뒤, 그 결과를 조회
SELECT category, 평균가격
FROM (
    SELECT category, AVG(price) AS 평균가격
    FROM products
    GROUP BY category
) AS category_avg
WHERE 평균가격 > 100000;

WHERE절 서브쿼리

-- IN 사용
SELECT name FROM products
WHERE category IN (
    SELECT category FROM products WHERE price > 100000
);

-- EXISTS 사용 (존재 여부 확인)
SELECT name FROM products p
WHERE EXISTS (
    SELECT 1 FROM products WHERE category = p.category AND price > 500000
);

데이터 모델링 기초

정규화 (Normalization)

데이터 중복을 제거하고 무결성을 높이는 과정입니다.

1NF (제1정규형)

모든 속성이 원자값(Atomic Value)을 가져야 함

-- 위반 예시
CREATE TABLE orders (
    id INT,
    customer VARCHAR(50),
    products VARCHAR(255) -- '사과, 바나나, 오렌지' (여러 값 저장)
);

-- 1NF 준수
CREATE TABLE order_items (
    order_id INT,
    product VARCHAR(50) -- 각 행에 하나의 상품만
);

2NF (제2정규형)

1NF를 만족하고, 부분 함수 종속 제거

기본키가 복합키일 때, 기본키 일부에만 종속되는 컬럼이 있으면 안 됨.

-- 위반 예시 (order_id + product_id가 복합 기본키)
CREATE TABLE order_details (
    order_id INT,
    product_id INT,
    customer_name VARCHAR(50), -- order_id에만 종속 (문제!)
    product_name VARCHAR(50),  -- product_id에만 종속 (문제!)
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

-- 2NF 준수: 테이블 분리
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50)
);

CREATE TABLE order_items (
    order_id INT,
    product_id INT,
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(50)
);

3NF (제3정규형)

2NF를 만족하고, 이행 함수 종속 제거

기본키가 아닌 컬럼이 다른 일반 컬럼을 결정하면 안 됨.

-- 위반 예시
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT,
    department_name VARCHAR(50) -- department_id에 종속 (문제!)
);

-- 3NF 준수: department_name 분리
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT
);

CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);

ERD (Entity Relationship Diagram)

테이블 간 관계를 시각화한 도표

  • 1:1 관계: 사용자 - 프로필
  • 1:N 관계: 부서 - 직원
  • N:M 관계: 학생 - 수업 (중간 테이블 필요)

실전 예제: 쇼핑몰 DB 설계

요구사항

  • 사용자가 상품을 주문할 수 있음
  • 한 주문에 여러 상품이 포함될 수 있음
  • 각 상품의 수량과 가격을 기록

ERD 설계

users (1) ─── (N) orders (1) ─── (N) order_items (N) ─── (1) products

테이블 생성

-- 1. 사용자 테이블
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. 상품 테이블
CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10,2) NOT NULL,
    stock INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 3. 주문 테이블
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending', -- pending, completed, cancelled
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 4. 주문 상세 테이블 (N:M 관계 해소)
CREATE TABLE order_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL,
    price DECIMAL(10,2) NOT NULL, -- 주문 당시 가격 기록
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

샘플 데이터 삽입

-- 사용자 생성
INSERT INTO users (name, email, password) VALUES
    ('김철수', 'kim@example.com', 'hashed_pw_1'),
    ('이영희', 'lee@example.com', 'hashed_pw_2');

-- 상품 생성
INSERT INTO products (name, description, price, stock) VALUES
    ('무선 키보드', '블루투스 지원', 45000, 20),
    ('게이밍 마우스', 'RGB LED', 65000, 15),
    ('모니터 암', '듀얼 모니터 지원', 89000, 10);

-- 주문 생성
INSERT INTO orders (user_id, total_amount, status) VALUES
    (1, 110000, 'completed');

-- 주문 상세 (김철수가 키보드 1개, 마우스 1개 구매)
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
    (1, 1, 1, 45000),
    (1, 2, 1, 65000);

실용 쿼리

-- 1. 사용자별 총 주문 금액
SELECT u.name, SUM(o.total_amount) AS 총구매액
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

-- 2. 가장 많이 팔린 상품 TOP 3
SELECT p.name, SUM(oi.quantity) AS 판매량
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY 판매량 DESC
LIMIT 3;

-- 3. 특정 주문의 상세 내역
SELECT o.id AS 주문번호,
       u.name AS 고객명,
       p.name AS 상품명,
       oi.quantity AS 수량,
       oi.price AS 단가,
       (oi.quantity * oi.price) AS 소계
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.id = 1;

-- 4. 재고가 10개 이하인 상품 알림
SELECT name, stock
FROM products
WHERE stock <= 10
ORDER BY stock ASC;

-- 5. 최근 30일간 주문이 없는 사용자
SELECT u.name, u.email
FROM users u
WHERE u.id NOT IN (
    SELECT DISTINCT user_id
    FROM orders
    WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
);

마무리

SQL 학습 로드맵

  1. 기본 CRUD → SELECT, INSERT, UPDATE, DELETE
  2. 조건과 정렬 → WHERE, ORDER BY, LIMIT
  3. 집계와 그룹화 → GROUP BY, HAVING, 집계 함수
  4. 조인 → INNER JOIN, LEFT JOIN
  5. 서브쿼리 → WHERE절, FROM절 서브쿼리
  6. 데이터 모델링 → 정규화, ERD 설계
  7. 실전 DB 설계 → 요구사항 분석 → 테이블 설계 → 쿼리 작성

추가 학습 주제

  • 인덱스(Index)와 쿼리 최적화
  • 트랜잭션(Transaction)과 ACID
  • 뷰(View), 프로시저(Procedure), 트리거(Trigger)
  • NoSQL과의 차이점

SQL은 백엔드 개발의 필수 기술입니다. 직접 데이터베이스를 설계하고 쿼리를 작성하며 익숙해지세요!