Chapter 01

데이터베이스란 무엇인가?

1.1 데이터베이스의 정의

데이터베이스(Database)란 체계적으로 정리된 데이터의 집합입니다. 우리가 일상에서 사용하는 전화번호부, 도서관 카탈로그, 쇼핑몰의 상품 목록 모두 넓은 의미의 데이터베이스입니다. 컴퓨터 과학에서 데이터베이스는 전자적으로 저장되고, 효율적으로 접근·관리할 수 있도록 구조화된 데이터 모음을 의미합니다.

1.2 왜 데이터베이스가 필요한가?

엑셀 파일로도 데이터를 관리할 수 있지만, 데이터가 수만~수억 건으로 늘어나면 한계에 부딪힙니다. 데이터베이스를 사용하면 다음과 같은 이점이 있습니다.

  • 대용량 처리 : 수억 건의 데이터도 빠르게 검색할 수 있습니다.
  • 동시 접근 : 여러 사용자가 동시에 데이터를 읽고 쓸 수 있습니다.
  • 데이터 무결성 : 규칙을 설정하여 잘못된 데이터가 들어오는 것을 방지합니다.
  • 보안 : 사용자별 접근 권한을 세밀하게 설정할 수 있습니다.
  • 백업과 복구 : 장애가 발생해도 데이터를 안전하게 복구할 수 있습니다.

1.3 관계형 데이터베이스 (RDBMS)

관계형 데이터베이스(Relational Database Management System)는 데이터를 테이블(표) 형태로 저장합니다. 행(Row)과 열(Column)로 이루어진 2차원 표를 떠올리면 됩니다. 테이블과 테이블 사이의 "관계(Relation)"를 정의하여 데이터를 효율적으로 관리합니다.

RDBMS개발사특징라이선스
MySQLOracle웹 서비스에서 가장 많이 사용오픈소스 / 상용
PostgreSQL커뮤니티고급 기능, 표준 준수오픈소스
SQLite커뮤니티파일 기반, 경량퍼블릭 도메인
Oracle DBOracle대기업 엔터프라이즈상용
SQL ServerMicrosoftWindows 환경 최적화상용 / Express 무료
MariaDBMariaDB 재단MySQL 호환 포크오픈소스

1.4 핵심 용어 정리

용어영문설명
테이블Table데이터를 저장하는 2차원 표
행 (레코드)Row / Record테이블의 한 줄(하나의 데이터)
열 (컬럼)Column / Field테이블의 한 항목(속성)
기본키Primary Key각 행을 고유하게 식별하는 열
외래키Foreign Key다른 테이블의 기본키를 참조하는 열
스키마Schema데이터베이스의 구조 설계도
Tip! 이 튜토리얼에서는 MySQL 문법을 기준으로 설명하되, 표준 SQL(ANSI SQL)과 다른 DBMS 차이점도 함께 안내합니다.
Chapter 02

SQL 소개

2.1 SQL이란?

SQL(Structured Query Language)은 관계형 데이터베이스를 다루기 위한 표준 프로그래밍 언어입니다. 1970년대 IBM에서 처음 개발되었으며, 현재 거의 모든 RDBMS에서 사용됩니다. "에스큐엘" 또는 "시퀄(Sequel)"이라고 읽습니다.

2.2 SQL의 분류

SQL 명령어는 역할에 따라 크게 네 가지로 분류됩니다.

분류전체 이름역할주요 명령어
DDLData Definition Language구조 정의CREATE, ALTER, DROP, TRUNCATE
DMLData Manipulation Language데이터 조작SELECT, INSERT, UPDATE, DELETE
DCLData Control Language권한 제어GRANT, REVOKE
TCLTransaction Control Language트랜잭션 제어COMMIT, ROLLBACK, SAVEPOINT

2.3 SQL의 특징

  • 선언적 언어 : "어떻게(How)" 가 아니라 "무엇을(What)" 원하는지 기술합니다.
  • 대소문자 구분 없음 : SELECT와 select는 동일합니다. (관례상 키워드는 대문자)
  • 세미콜론으로 끝남 : 각 SQL 문장은 세미콜론(;)으로 마무리합니다.
  • 집합 기반 : 한 번에 여러 행을 처리합니다.

2.4 첫 번째 SQL 맛보기

SQL
-- 모든 직원 정보를 조회합니다
SELECT * FROM employees;

위 SQL은 employees 테이블의 모든 열(*)과 모든 행을 가져오라는 의미입니다. 지금은 이해가 안 되어도 괜찮습니다. 앞으로 하나씩 배워 나가겠습니다.

참고 : SQL에서 -- 뒤의 텍스트는 주석(Comment)으로, 실행에 영향을 주지 않습니다. 여러 줄 주석은 /* ... */을 사용합니다.
Chapter 03

실습 환경 설정

3.1 온라인 환경 (설치 불필요)

가장 빠르게 SQL을 연습하는 방법은 온라인 도구를 사용하는 것입니다.

  • DB Fiddle (db-fiddle.com) : MySQL, PostgreSQL, SQLite를 브라우저에서 바로 실행
  • SQLite Online (sqliteonline.com) : SQLite 전용, 빠르고 간편
  • W3Schools SQL Editor : 튜토리얼과 함께 실습 가능

3.2 로컬 설치 - MySQL

실무 환경과 가장 비슷한 로컬 설치를 추천합니다.

Bash (Ubuntu/Debian)
# MySQL 서버 설치
sudo apt update
sudo apt install mysql-server

# 서비스 시작
sudo systemctl start mysql

# 보안 설정 마법사
sudo mysql_secure_installation

# MySQL 접속
mysql -u root -p
Windows (CMD)
-- MySQL Installer를 공식 사이트에서 다운로드 후 설치
-- 설치 후 MySQL Command Line Client 실행
mysql -u root -p

3.3 로컬 설치 - SQLite (가장 간단)

Bash
# 설치
sudo apt install sqlite3

# 데이터베이스 생성 및 접속
sqlite3 tutorial.db

3.4 GUI 도구

  • DBeaver : 무료, 거의 모든 DB 지원, 추천!
  • MySQL Workbench : MySQL 공식 GUI 도구
  • DataGrip : JetBrains 유료 도구, 매우 강력
  • HeidiSQL : 가볍고 빠른 Windows 전용

3.5 실습용 데이터베이스 생성

이 튜토리얼 전체에서 사용할 샘플 데이터베이스를 만들겠습니다. 온라인 쇼핑몰을 가정합니다.

SQL
-- 데이터베이스 생성
CREATE DATABASE shop_db DEFAULT CHARACTER SET utf8mb4;
USE shop_db;

-- 고객 테이블
CREATE TABLE customers (
    customer_id   INT          PRIMARY KEY AUTO_INCREMENT,
    name          VARCHAR(50)  NOT NULL,
    email         VARCHAR(100) UNIQUE NOT NULL,
    phone         VARCHAR(20),
    city          VARCHAR(30),
    join_date     DATE         DEFAULT (CURDATE()),
    grade         VARCHAR(10)  DEFAULT 'BRONZE'
);

-- 상품 테이블
CREATE TABLE products (
    product_id    INT          PRIMARY KEY AUTO_INCREMENT,
    product_name  VARCHAR(100) NOT NULL,
    category      VARCHAR(30),
    price         DECIMAL(10,2) NOT NULL,
    stock         INT          DEFAULT 0,
    created_at    DATETIME     DEFAULT NOW()
);

-- 주문 테이블
CREATE TABLE orders (
    order_id      INT          PRIMARY KEY AUTO_INCREMENT,
    customer_id   INT          NOT NULL,
    order_date    DATETIME     DEFAULT NOW(),
    total_amount  DECIMAL(12,2),
    status        VARCHAR(20)  DEFAULT 'PENDING',
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

-- 주문 상세 테이블
CREATE TABLE order_items (
    item_id       INT          PRIMARY KEY AUTO_INCREMENT,
    order_id      INT          NOT NULL,
    product_id    INT          NOT NULL,
    quantity      INT          NOT NULL,
    unit_price    DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (order_id)   REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

-- 직원 테이블
CREATE TABLE employees (
    emp_id        INT          PRIMARY KEY AUTO_INCREMENT,
    name          VARCHAR(50)  NOT NULL,
    department    VARCHAR(30),
    position      VARCHAR(30),
    salary        DECIMAL(10,2),
    hire_date     DATE,
    manager_id    INT,
    FOREIGN KEY (manager_id) REFERENCES employees(emp_id)
);

3.6 샘플 데이터 삽입

SQL
-- 고객 데이터
INSERT INTO customers (name, email, phone, city, join_date, grade) VALUES
('김민수', 'minsu@mail.com',   '010-1234-5678', '서울',  '2023-01-15', 'GOLD'),
('이영희', 'younghee@mail.com', '010-2345-6789', '부산',  '2023-03-22', 'SILVER'),
('박철수', 'cheolsu@mail.com',  '010-3456-7890', '대전',  '2023-05-10', 'BRONZE'),
('정수진', 'sujin@mail.com',    '010-4567-8901', '서울',  '2023-07-04', 'GOLD'),
('최동현', 'donghyun@mail.com', '010-5678-9012', '인천',  '2023-09-18', 'SILVER'),
('한지은', 'jieun@mail.com',    '010-6789-0123', '서울',  '2024-01-05', 'BRONZE'),
('윤서준', 'seojun@mail.com',   '010-7890-1234', '대구',  '2024-02-14', 'GOLD'),
('강예린', 'yerin@mail.com',    '010-8901-2345', '부산',  '2024-04-20', 'BRONZE');

-- 상품 데이터
INSERT INTO products (product_name, category, price, stock) VALUES
('무선 키보드',       '전자기기',   45000,   120),
('무선 마우스',       '전자기기',   32000,   200),
('27인치 모니터',     '전자기기',   350000,  45),
('프로그래밍 입문서', '도서',       28000,   300),
('SQL 완전정복',      '도서',       32000,   250),
('노트북 파우치',     '액세서리',   25000,   180),
('USB-C 허브',        '액세서리',   55000,   90),
('기계식 키보드',     '전자기기',   89000,   60),
('데스크 매트',       '액세서리',   18000,   400),
('웹캠 HD',           '전자기기',   65000,   75);

-- 주문 데이터
INSERT INTO orders (customer_id, order_date, total_amount, status) VALUES
(1, '2024-01-10 14:30:00', 77000,  'COMPLETED'),
(1, '2024-02-15 09:00:00', 350000, 'COMPLETED'),
(2, '2024-01-22 11:20:00', 60000,  'COMPLETED'),
(3, '2024-03-05 16:45:00', 28000,  'COMPLETED'),
(4, '2024-03-18 10:10:00', 144000, 'SHIPPED'),
(5, '2024-04-01 13:55:00', 55000,  'SHIPPED'),
(6, '2024-04-12 08:30:00', 89000,  'PENDING'),
(7, '2024-04-20 17:00:00', 415000, 'PENDING'),
(2, '2024-05-03 12:00:00', 32000,  'CANCELLED'),
(1, '2024-05-15 15:30:00', 83000,  'COMPLETED');

-- 주문 상세 데이터
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1, 1, 1, 45000),
(1, 2, 1, 32000),
(2, 3, 1, 350000),
(3, 4, 1, 28000),
(3, 5, 1, 32000),
(4, 4, 1, 28000),
(5, 8, 1, 89000),
(5, 7, 1, 55000),
(6, 7, 1, 55000),
(7, 8, 1, 89000),
(8, 3, 1, 350000),
(8, 10,1, 65000),
(9, 5, 1, 32000),
(10,7, 1, 55000),
(10,4, 1, 28000);

-- 직원 데이터
INSERT INTO employees (name, department, position, salary, hire_date, manager_id) VALUES
('김대표', '경영',   'CEO',     12000000, '2020-01-01', NULL),
('이팀장', '개발',   '팀장',    7000000,  '2020-03-15', 1),
('박사원', '개발',   '사원',    4000000,  '2022-06-01', 2),
('최대리', '개발',   '대리',    5000000,  '2021-09-10', 2),
('정팀장', '마케팅', '팀장',    6500000,  '2020-07-20', 1),
('한사원', '마케팅', '사원',    3800000,  '2023-01-10', 5),
('윤과장', '영업',   '과장',    5500000,  '2021-02-28', 1),
('강사원', '영업',   '사원',    3500000,  '2023-08-15', 7);
Chapter 04

데이터베이스와 테이블 관리

4.1 데이터베이스 생성 · 삭제 · 선택

SQL
-- 데이터베이스 목록 확인
SHOW DATABASES;

-- 데이터베이스 생성
CREATE DATABASE my_app DEFAULT CHARACTER SET utf8mb4;

-- 이미 존재하면 무시
CREATE DATABASE IF NOT EXISTS my_app;

-- 데이터베이스 선택 (사용)
USE my_app;

-- 데이터베이스 삭제
DROP DATABASE my_app;
DROP DATABASE IF EXISTS my_app;

4.2 테이블 생성 (CREATE TABLE)

SQL
CREATE TABLE students (
    id        INT         PRIMARY KEY AUTO_INCREMENT,
    name      VARCHAR(50) NOT NULL,
    age       INT         CHECK (age >= 0 AND age <= 150),
    major     VARCHAR(30) DEFAULT '미정',
    gpa       DECIMAL(3,2),
    enrolled  DATE        DEFAULT (CURDATE())
);

4.3 테이블 구조 확인

SQL
-- 테이블 목록
SHOW TABLES;

-- 테이블 구조 확인
DESC students;
-- 또는
DESCRIBE students;

-- 테이블 생성 SQL 보기
SHOW CREATE TABLE students;

4.4 테이블 수정 (ALTER TABLE)

SQL
-- 열 추가
ALTER TABLE students ADD COLUMN email VARCHAR(100);

-- 열 이름 및 타입 변경
ALTER TABLE students CHANGE COLUMN major department VARCHAR(50);

-- 열 타입만 변경
ALTER TABLE students MODIFY COLUMN name VARCHAR(100) NOT NULL;

-- 열 삭제
ALTER TABLE students DROP COLUMN email;

-- 테이블 이름 변경
ALTER TABLE students RENAME TO learners;

4.5 테이블 삭제

SQL
-- 테이블 완전 삭제 (구조 + 데이터)
DROP TABLE students;
DROP TABLE IF EXISTS students;

-- 데이터만 삭제 (구조 유지, 복구 불가)
TRUNCATE TABLE students;
주의! DROP TABLE과 TRUNCATE TABLE은 되돌릴 수 없습니다. 실행 전 반드시 백업을 확인하세요. DELETE와 달리 TRUNCATE는 WHERE 조건을 사용할 수 없고, AUTO_INCREMENT 값이 초기화됩니다.
Chapter 05

데이터 타입 완벽 가이드

5.1 숫자형 (Numeric)

타입범위크기용도
TINYINT-128 ~ 1271 byte작은 정수, 불리언 대용
SMALLINT-32,768 ~ 32,7672 bytes작은 범위 정수
INT (INTEGER)약 -21억 ~ 21억4 bytes일반 정수 (가장 많이 사용)
BIGINT약 -922경 ~ 922경8 bytes매우 큰 정수
DECIMAL(M,D)고정 소수점가변금액 등 정밀한 숫자
FLOAT부동 소수점4 bytes과학 계산 (근사값)
DOUBLE부동 소수점8 bytes더 높은 정밀도
DECIMAL vs FLOAT : 금액을 다룰 때는 반드시 DECIMAL을 사용하세요. FLOAT/DOUBLE은 근사값을 저장하여 0.1 + 0.2 ≠ 0.3 같은 문제가 발생할 수 있습니다.

5.2 문자형 (String)

타입최대 길이특징용도
CHAR(N)255자고정 길이 (빈 공간은 공백 채움)우편번호, 국가코드
VARCHAR(N)65,535자가변 길이 (실제 길이만큼 저장)이름, 이메일, 주소
TEXT65,535자긴 텍스트게시글 본문
MEDIUMTEXT약 1,600만자매우 긴 텍스트대용량 글
LONGTEXT약 42억자초대용량 텍스트로그, 대량 데이터
ENUM-정해진 값 중 하나성별, 상태값

5.3 날짜/시간형 (Date/Time)

타입형식범위용도
DATEYYYY-MM-DD1000-01-01 ~ 9999-12-31생년월일, 가입일
TIMEHH:MM:SS-838:59:59 ~ 838:59:59시간 간격
DATETIMEYYYY-MM-DD HH:MM:SS1000 ~ 9999년주문일시, 생성일시
TIMESTAMPYYYY-MM-DD HH:MM:SS1970 ~ 2038년수정일시 (시간대 자동 변환)
YEARYYYY1901 ~ 2155연도만 필요할 때

5.4 기타 유용한 타입

SQL
-- BOOLEAN (MySQL에서는 TINYINT(1)의 별칭)
is_active BOOLEAN DEFAULT TRUE,

-- ENUM : 정해진 값만 입력 가능
gender ENUM('M', 'F', 'Other'),

-- JSON (MySQL 5.7+)
metadata JSON,

-- BLOB : 바이너리 데이터 (이미지 등)
profile_img BLOB
Tip! 데이터 타입을 올바르게 선정하는 것은 성능과 저장 공간에 큰 영향을 줍니다. 나이를 저장할 때 INT 대신 TINYINT UNSIGNED(0~255)를 사용하면 메모리를 절약할 수 있습니다.
Chapter 06

SELECT - 데이터 조회의 기본

6.1 SELECT 기본 구조

SQL
-- 기본 문법
SELECT 컬럼1, 컬럼2, ...
FROM 테이블명;

6.2 전체 컬럼 조회 (*)

SQL
-- 고객 테이블의 모든 데이터 조회
SELECT * FROM customers;
실행 결과
+-------------+--------+---------------------+-----------------+------+------------+--------+ | customer_id | name | email | phone | city | join_date | grade | +-------------+--------+---------------------+-----------------+------+------------+--------+ | 1 | 김민수 | minsu@mail.com | 010-1234-5678 | 서울 | 2023-01-15 | GOLD | | 2 | 이영희 | younghee@mail.com | 010-2345-6789 | 부산 | 2023-03-22 | SILVER | | 3 | 박철수 | cheolsu@mail.com | 010-3456-7890 | 대전 | 2023-05-10 | BRONZE | | ... | ... | ... | ... | ... | ... | ... | +-------------+--------+---------------------+-----------------+------+------------+--------+
주의! 실무에서 SELECT *는 되도록 사용하지 마세요. 불필요한 컬럼까지 가져와 성능이 저하됩니다. 필요한 컬럼만 명시하세요.

6.3 특정 컬럼만 조회

SQL
-- 이름과 이메일만 조회
SELECT name, email
FROM customers;

6.4 별칭 (Alias)

SQL
-- 컬럼 별칭 (AS 키워드)
SELECT
    name       AS '고객명',
    email      AS '이메일',
    city       AS '도시',
    grade      AS '등급'
FROM customers;

-- 테이블 별칭
SELECT c.name, c.email
FROM customers AS c;

6.5 DISTINCT - 중복 제거

SQL
-- 어떤 도시의 고객이 있는지 (중복 제거)
SELECT DISTINCT city
FROM customers;

-- 여러 컬럼의 조합에서 중복 제거
SELECT DISTINCT city, grade
FROM customers;
실행 결과 (DISTINCT city)
+------+ | city | +------+ | 서울 | | 부산 | | 대전 | | 인천 | | 대구 | +------+

6.6 산술 연산

SQL
-- 가격에 10% 할인 적용한 가격 계산
SELECT
    product_name AS '상품명',
    price        AS '원가',
    price * 0.9  AS '할인가',
    price - (price * 0.9) AS '할인액'
FROM products;

6.7 연결 연산 (CONCAT)

SQL
-- 이름과 도시를 합쳐서 표시
SELECT
    CONCAT(name, ' (', city, ')') AS '고객정보'
FROM customers;
실행 결과
+------------------+ | 고객정보 | +------------------+ | 김민수 (서울) | | 이영희 (부산) | | 박철수 (대전) | | ... | +------------------+
Chapter 07

WHERE - 조건으로 데이터 필터링

7.1 기본 비교 연산자

연산자의미예시
=같다WHERE city = '서울'
!= 또는 <>같지 않다WHERE grade != 'BRONZE'
>크다WHERE price > 50000
<작다WHERE stock < 100
>=크거나 같다WHERE salary >= 5000000
<=작거나 같다WHERE age <= 30
SQL
-- 서울에 사는 고객 조회
SELECT * FROM customers
WHERE city = '서울';

-- 가격이 50000원 이상인 상품
SELECT product_name, price
FROM products
WHERE price >= 50000;

-- 급여가 500만원 이상인 직원
SELECT name, department, salary
FROM employees
WHERE salary >= 5000000;

7.2 논리 연산자 (AND, OR, NOT)

SQL
-- AND : 두 조건 모두 만족
SELECT * FROM customers
WHERE city = '서울' AND grade = 'GOLD';

-- OR : 둘 중 하나만 만족
SELECT * FROM customers
WHERE city = '서울' OR city = '부산';

-- NOT : 조건의 반대
SELECT * FROM customers
WHERE NOT grade = 'BRONZE';

-- 복합 조건 (괄호로 우선순위 지정)
SELECT * FROM products
WHERE (category = '전자기기' OR category = '액세서리')
  AND price < 50000;

7.3 BETWEEN - 범위 검색

SQL
-- 가격이 30000 ~ 60000 사이인 상품
SELECT product_name, price
FROM products
WHERE price BETWEEN 30000 AND 60000;

-- 날짜 범위
SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';

7.4 IN - 목록에서 검색

SQL
-- 서울, 부산, 대구에 사는 고객
SELECT * FROM customers
WHERE city IN ('서울', '부산', '대구');

-- NOT IN : 목록에 없는 것
SELECT * FROM orders
WHERE status NOT IN ('CANCELLED', 'PENDING');

7.5 LIKE - 패턴 검색

%는 0개 이상의 아무 문자, _는 정확히 1개의 아무 문자를 의미합니다.

SQL
-- '김'으로 시작하는 이름
SELECT * FROM customers
WHERE name LIKE '김%';

-- 이메일에 'mail'이 포함된 고객
SELECT * FROM customers
WHERE email LIKE '%mail%';

-- 이름이 정확히 3글자인 고객
SELECT * FROM customers
WHERE name LIKE '___';

-- '무선'으로 시작하는 상품
SELECT * FROM products
WHERE product_name LIKE '무선%';

7.6 IS NULL / IS NOT NULL

SQL
-- 매니저가 없는 직원 (최상위 관리자)
SELECT * FROM employees
WHERE manager_id IS NULL;

-- 전화번호가 있는 고객
SELECT * FROM customers
WHERE phone IS NOT NULL;
주의! NULL은 "값이 없음"을 나타내며, = NULL이 아닌 IS NULL로 비교해야 합니다. WHERE phone = NULL은 항상 결과가 없습니다.
Chapter 08

정렬(ORDER BY)과 제한(LIMIT)

8.1 ORDER BY - 결과 정렬

SQL
-- 가격 오름차순 (기본값: ASC)
SELECT product_name, price
FROM products
ORDER BY price ASC;

-- 가격 내림차순
SELECT product_name, price
FROM products
ORDER BY price DESC;

-- 여러 컬럼으로 정렬 (1차: 도시 오름차순, 2차: 이름 오름차순)
SELECT name, city, grade
FROM customers
ORDER BY city ASC, name ASC;

-- 별칭이나 컬럼 번호로 정렬
SELECT product_name, price * 0.9 AS discount_price
FROM products
ORDER BY discount_price DESC;
-- 또는 ORDER BY 2 DESC; (2번째 컬럼)

8.2 LIMIT - 결과 수 제한

SQL
-- 상위 5개 비싼 상품
SELECT product_name, price
FROM products
ORDER BY price DESC
LIMIT 5;

-- 가장 최근 주문 3개
SELECT * FROM orders
ORDER BY order_date DESC
LIMIT 3;

8.3 LIMIT + OFFSET - 페이징

SQL
-- 페이지당 5개씩 조회
-- 1페이지 (1~5번째)
SELECT * FROM products ORDER BY product_id LIMIT 5 OFFSET 0;

-- 2페이지 (6~10번째)
SELECT * FROM products ORDER BY product_id LIMIT 5 OFFSET 5;

-- 축약형: LIMIT offset, count
SELECT * FROM products ORDER BY product_id LIMIT 5, 5;
DBMS별 차이 : MySQL/PostgreSQL은 LIMIT, Oracle은 ROWNUM이나 FETCH FIRST, SQL Server는 TOP을 사용합니다.
Chapter 09

INSERT - 데이터 삽입

9.1 단일 행 삽입

SQL
-- 모든 컬럼에 값 지정
INSERT INTO customers (name, email, phone, city, join_date, grade)
VALUES ('송하나', 'hana@mail.com', '010-1111-2222', '광주', '2024-06-01', 'BRONZE');

-- DEFAULT 값이 있는 컬럼은 생략 가능
INSERT INTO customers (name, email, city)
VALUES ('오둘이', 'dooli@mail.com', '제주');

9.2 다중 행 삽입

SQL
INSERT INTO products (product_name, category, price, stock) VALUES
('무선 이어폰',   '전자기기', 79000,  150),
('태블릿 거치대', '액세서리', 22000,  300),
('마우스 패드',   '액세서리', 12000,  500);

9.3 SELECT 결과를 INSERT

SQL
-- VIP 고객 테이블에 GOLD 등급 고객 복사
CREATE TABLE vip_customers LIKE customers;

INSERT INTO vip_customers
SELECT * FROM customers
WHERE grade = 'GOLD';

9.4 INSERT IGNORE와 ON DUPLICATE KEY UPDATE

SQL
-- 중복 키 에러 무시 (삽입 건너뛰기)
INSERT IGNORE INTO customers (name, email, city)
VALUES ('김민수', 'minsu@mail.com', '서울');

-- 중복 시 업데이트 (UPSERT)
INSERT INTO products (product_id, product_name, price, stock)
VALUES (1, '무선 키보드', 43000, 130)
ON DUPLICATE KEY UPDATE
    price = VALUES(price),
    stock = VALUES(stock);
Chapter 10

UPDATE - 데이터 수정

10.1 기본 UPDATE

SQL
-- 특정 고객의 등급 변경
UPDATE customers
SET grade = 'GOLD'
WHERE customer_id = 3;

-- 여러 컬럼 동시 수정
UPDATE customers
SET
    city = '세종',
    phone = '010-9999-0000'
WHERE customer_id = 3;

10.2 조건부 일괄 수정

SQL
-- 모든 상품 가격 10% 인상
UPDATE products
SET price = price * 1.1;

-- 전자기기 카테고리만 5% 할인
UPDATE products
SET price = price * 0.95
WHERE category = '전자기기';

-- 재고가 50 이하인 상품 재고 100 추가
UPDATE products
SET stock = stock + 100
WHERE stock <= 50;
치명적 주의! WHERE 절을 빠뜨리면 모든 행이 수정됩니다! 실무에서는 먼저 SELECT로 대상을 확인한 뒤 UPDATE를 실행하세요.

10.3 안전한 UPDATE 패턴

SQL
-- 1단계: 먼저 SELECT로 대상 확인
SELECT * FROM products
WHERE category = '전자기기' AND stock <= 50;

-- 2단계: 확인 후 UPDATE 실행
UPDATE products
SET stock = stock + 100
WHERE category = '전자기기' AND stock <= 50;
Chapter 11

DELETE - 데이터 삭제

11.1 기본 DELETE

SQL
-- 특정 주문 삭제
DELETE FROM orders
WHERE order_id = 9;

-- 취소된 주문 모두 삭제
DELETE FROM orders
WHERE status = 'CANCELLED';

11.2 DELETE vs TRUNCATE vs DROP

명령어대상WHERE복구(ROLLBACK)AUTO_INCREMENT
DELETE행(데이터)사용 가능가능유지
TRUNCATE전체 데이터불가불가초기화
DROP테이블 자체불가불가테이블 삭제
주의! DELETE도 WHERE 절 없이 실행하면 모든 행이 삭제됩니다. 외래키 관계가 있는 경우, 참조하는 자식 데이터가 있으면 삭제가 거부됩니다.
Chapter 12

집계 함수 (Aggregate Functions)

12.1 주요 집계 함수

함수설명NULL 처리
COUNT(*)행의 수NULL 포함
COUNT(column)NULL이 아닌 행의 수NULL 제외
SUM(column)합계NULL 제외
AVG(column)평균NULL 제외
MAX(column)최대값NULL 제외
MIN(column)최소값NULL 제외
SQL
-- 전체 고객 수
SELECT COUNT(*) AS total_customers
FROM customers;

-- 상품 가격 통계
SELECT
    COUNT(*) AS '상품수',
    SUM(price) AS '가격합계',
    AVG(price) AS '평균가격',
    MAX(price) AS '최고가',
    MIN(price) AS '최저가'
FROM products;

-- 완료된 주문의 총 매출
SELECT
    COUNT(*) AS '주문건수',
    SUM(total_amount) AS '총매출'
FROM orders
WHERE status = 'COMPLETED';

-- 직원 급여 통계
SELECT
    AVG(salary) AS '평균급여',
    MAX(salary) AS '최고급여',
    MIN(salary) AS '최저급여',
    MAX(salary) - MIN(salary) AS '급여차이'
FROM employees;

12.2 COUNT의 다양한 활용

SQL
-- COUNT(*) vs COUNT(column) 차이
SELECT
    COUNT(*)          AS '전체행수',      -- NULL 포함
    COUNT(manager_id) AS '매니저있는직원'  -- NULL 제외
FROM employees;

-- DISTINCT와 함께: 고유 값의 개수
SELECT
    COUNT(DISTINCT city) AS '도시수',
    COUNT(DISTINCT grade) AS '등급수'
FROM customers;
Chapter 13

GROUP BY와 HAVING

13.1 GROUP BY - 그룹별 집계

GROUP BY는 동일한 값을 가진 행을 그룹으로 묶어 집계합니다.

SQL
-- 도시별 고객 수
SELECT
    city      AS '도시',
    COUNT(*) AS '고객수'
FROM customers
GROUP BY city;

-- 카테고리별 상품 통계
SELECT
    category      AS '카테고리',
    COUNT(*)     AS '상품수',
    AVG(price)   AS '평균가격',
    SUM(stock)   AS '총재고'
FROM products
GROUP BY category;

-- 부서별 직원 수와 평균 급여
SELECT
    department    AS '부서',
    COUNT(*)     AS '직원수',
    AVG(salary)  AS '평균급여',
    MAX(salary)  AS '최고급여'
FROM employees
GROUP BY department;
카테고리별 상품 통계 결과
+------------+--------+------------+--------+ | 카테고리 | 상품수 | 평균가격 | 총재고 | +------------+--------+------------+--------+ | 전자기기 | 5 | 116200.00 | 500 | | 도서 | 2 | 30000.00 | 550 | | 액세서리 | 3 | 32666.67 | 670 | +------------+--------+------------+--------+

13.2 여러 컬럼으로 GROUP BY

SQL
-- 도시별, 등급별 고객 수
SELECT
    city, grade,
    COUNT(*) AS cnt
FROM customers
GROUP BY city, grade
ORDER BY city, cnt DESC;

-- 월별, 상태별 주문 통계
SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    status,
    COUNT(*) AS order_count,
    SUM(total_amount) AS total_sales
FROM orders
GROUP BY month, status
ORDER BY month;

13.3 HAVING - 그룹에 조건 걸기

WHERE는 그룹핑 전에, HAVING은 그룹핑 후에 필터링합니다.

SQL
-- 고객이 2명 이상인 도시만
SELECT
    city,
    COUNT(*) AS cnt
FROM customers
GROUP BY city
HAVING cnt >= 2;

-- 평균 급여가 500만원 이상인 부서
SELECT
    department,
    AVG(salary) AS avg_sal
FROM employees
GROUP BY department
HAVING avg_sal >= 5000000;

-- 2건 이상 주문한 고객
SELECT
    customer_id,
    COUNT(*) AS order_count,
    SUM(total_amount) AS total_spent
FROM orders
WHERE status != 'CANCELLED'
GROUP BY customer_id
HAVING order_count >= 2
ORDER BY total_spent DESC;

13.4 SQL 실행 순서

SQL 실행 순서(논리적)

① FROM → ② WHERE → ③ GROUP BY → ④ HAVING → ⑤ SELECT → ⑥ ORDER BY → ⑦ LIMIT

작성 순서와 실행 순서가 다릅니다! HAVING에서 SELECT의 별칭을 사용할 수 있는 것은 MySQL의 확장 기능입니다. 표준 SQL에서는 HAVING 절에서 별칭 대신 집계 함수를 다시 써야 합니다.

13.5 GROUP BY 실전 예제

SQL
-- 등급별 고객 수와 비율
SELECT
    grade,
    COUNT(*) AS cnt,
    ROUND(
        COUNT(*) * 100.0 / (SELECT COUNT(*) FROM customers), 1
    ) AS percentage
FROM customers
GROUP BY grade
ORDER BY cnt DESC;

-- 고객별 첫 주문과 마지막 주문
SELECT
    customer_id,
    MIN(order_date) AS first_order,
    MAX(order_date) AS last_order,
    DATEDIFF(MAX(order_date), MIN(order_date)) AS days_between
FROM orders
GROUP BY customer_id;
Chapter 14

JOIN - 테이블 결합 기초

14.1 JOIN이 필요한 이유

관계형 데이터베이스에서는 데이터를 여러 테이블에 나누어 저장합니다(정규화). 주문 테이블에는 customer_id만 저장하고, 고객 이름은 고객 테이블에 있습니다. 주문 정보와 고객 이름을 함께 보려면 두 테이블을 연결(JOIN)해야 합니다.

14.2 JOIN의 종류 한눈에 보기

JOIN 종류설명결과
INNER JOIN양쪽 모두 일치하는 행만교집합
LEFT JOIN왼쪽 전체 + 오른쪽 일치왼쪽 기준 전체
RIGHT JOIN오른쪽 전체 + 왼쪽 일치오른쪽 기준 전체
FULL OUTER JOIN양쪽 모두 전체합집합 (MySQL 미지원)
CROSS JOIN모든 행의 조합카르테시안 곱
SELF JOIN자기 자신과 조인계층 구조 표현

14.3 INNER JOIN

두 테이블에서 일치하는 행만 반환합니다. 가장 많이 사용되는 JOIN입니다.

SQL
-- 기본 문법
SELECT 컬럼목록
FROM 테이블A
INNER JOIN 테이블B ON 테이블A.키 = 테이블B.키;

-- 주문 정보에 고객 이름 포함하여 조회
SELECT
    o.order_id,
    c.name       AS customer_name,
    c.city,
    o.order_date,
    o.total_amount,
    o.status
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_date DESC;
실행 결과
+----------+---------------+------+---------------------+--------------+-----------+ | order_id | customer_name | city | order_date | total_amount | status | +----------+---------------+------+---------------------+--------------+-----------+ | 10 | 김민수 | 서울 | 2024-05-15 15:30:00 | 83000.00 | COMPLETED | | 9 | 이영희 | 부산 | 2024-05-03 12:00:00 | 32000.00 | CANCELLED | | 8 | 윤서준 | 대구 | 2024-04-20 17:00:00 | 415000.00 | PENDING | | 7 | 한지은 | 서울 | 2024-04-12 08:30:00 | 89000.00 | PENDING | | ... | ... | ... | ... | ... | ... | +----------+---------------+------+---------------------+--------------+-----------+
별칭(Alias) 활용 : JOIN을 사용할 때 테이블 별칭을 지정하면 코드가 간결해집니다. orders o처럼 쓰면 o.order_id로 참조할 수 있습니다.

14.4 여러 테이블 JOIN

SQL
-- 주문 상세 정보: 고객명 + 상품명 + 수량 + 금액
SELECT
    o.order_id       AS '주문번호',
    c.name           AS '고객명',
    p.product_name   AS '상품명',
    p.category       AS '카테고리',
    oi.quantity      AS '수량',
    oi.unit_price    AS '단가',
    oi.quantity * oi.unit_price AS '소계'
FROM order_items oi
INNER JOIN orders o    ON oi.order_id = o.order_id
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN products p  ON oi.product_id = p.product_id
ORDER BY o.order_id, oi.item_id;
실행 결과 (일부)
+----------+--------+------------------+------------+------+--------+--------+ | 주문번호 | 고객명 | 상품명 | 카테고리 | 수량 | 단가 | 소계 | +----------+--------+------------------+------------+------+--------+--------+ | 1 | 김민수 | 무선 키보드 | 전자기기 | 1 | 45000 | 45000 | | 1 | 김민수 | 무선 마우스 | 전자기기 | 1 | 32000 | 32000 | | 2 | 김민수 | 27인치 모니터 | 전자기기 | 1 | 350000 | 350000 | | 3 | 이영희 | 프로그래밍 입문서 | 도서 | 1 | 28000 | 28000 | | 3 | 이영희 | SQL 완전정복 | 도서 | 1 | 32000 | 32000 | | ... | ... | ... | ... | ... | ... | ... | +----------+--------+------------------+------------+------+--------+--------+

14.5 LEFT JOIN (LEFT OUTER JOIN)

왼쪽 테이블의 모든 행을 반환하고, 오른쪽에 일치하는 값이 없으면 NULL로 채웁니다.

SQL
-- 모든 고객 + 주문 정보 (주문이 없는 고객도 포함)
SELECT
    c.name,
    c.email,
    o.order_id,
    o.total_amount,
    o.status
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id;
실행 결과 (주문 없는 고객 = NULL)
+--------+---------------------+----------+--------------+-----------+ | name | email | order_id | total_amount | status | +--------+---------------------+----------+--------------+-----------+ | 김민수 | minsu@mail.com | 1 | 77000.00 | COMPLETED | | 김민수 | minsu@mail.com | 2 | 350000.00 | COMPLETED | | 김민수 | minsu@mail.com | 10 | 83000.00 | COMPLETED | | 이영희 | younghee@mail.com | 3 | 60000.00 | COMPLETED | | ... | ... | ... | ... | ... | | 강예린 | yerin@mail.com | NULL | NULL | NULL | +--------+---------------------+----------+--------------+-----------+
SQL
-- ★ 실무 활용: 주문을 한 번도 하지 않은 고객 찾기
SELECT c.name, c.email, c.join_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

-- ★ 팔린 적 없는 상품 찾기
SELECT p.product_name, p.price, p.stock
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
WHERE oi.item_id IS NULL;

14.6 RIGHT JOIN

SQL
-- RIGHT JOIN : 오른쪽 테이블 기준 전체 표시
SELECT
    o.order_id,
    c.name,
    o.total_amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;
Tip! RIGHT JOIN은 LEFT JOIN의 테이블 순서를 바꾸면 동일한 결과를 얻을 수 있습니다. 실무에서는 LEFT JOIN이 훨씬 많이 사용됩니다.
Chapter 15

JOIN 심화

15.1 SELF JOIN (자기 자신과 조인)

같은 테이블을 두 번 참조하여 조인합니다. 대표적으로 직원-매니저 관계(계층 구조)에 사용됩니다.

SQL
-- 직원과 그 직원의 매니저 이름 함께 조회
SELECT
    e.name        AS '직원명',
    e.department  AS '부서',
    e.position    AS '직급',
    e.salary      AS '급여',
    m.name        AS '상사명',
    m.position    AS '상사직급'
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
실행 결과
+--------+--------+------+----------+--------+----------+ | 직원명 | 부서 | 직급 | 급여 | 상사명 | 상사직급 | +--------+--------+------+----------+--------+----------+ | 김대표 | 경영 | CEO | 12000000 | NULL | NULL | | 이팀장 | 개발 | 팀장 | 7000000 | 김대표 | CEO | | 박사원 | 개발 | 사원 | 4000000 | 이팀장 | 팀장 | | 최대리 | 개발 | 대리 | 5000000 | 이팀장 | 팀장 | | 정팀장 | 마케팅 | 팀장 | 6500000 | 김대표 | CEO | | 한사원 | 마케팅 | 사원 | 3800000 | 정팀장 | 팀장 | | 윤과장 | 영업 | 과장 | 5500000 | 김대표 | CEO | | 강사원 | 영업 | 사원 | 3500000 | 윤과장 | 과장 | +--------+--------+------+----------+--------+----------+

15.2 CROSS JOIN (교차 조인)

양쪽 테이블의 모든 행 조합을 만듭니다(카르테시안 곱). A 테이블 8행 × B 테이블 10행 = 80행

SQL
-- 모든 고객 × 모든 상품 조합
SELECT
    c.name   AS '고객명',
    p.product_name AS '상품명',
    p.price  AS '가격'
FROM customers c
CROSS JOIN products p
LIMIT 10;

-- 실무 활용: 모든 월 × 모든 카테고리 조합 (빈 데이터 포함 리포트)

15.3 USING 절

SQL
-- 조인 컬럼 이름이 동일할 때 USING 사용 가능
SELECT o.order_id, c.name, o.total_amount
FROM orders o
INNER JOIN customers c USING (customer_id);

-- ON 절과 동일한 결과:
-- INNER JOIN customers c ON o.customer_id = c.customer_id

15.4 FULL OUTER JOIN (MySQL 대체법)

MySQL은 FULL OUTER JOIN을 직접 지원하지 않습니다. LEFT JOIN과 RIGHT JOIN을 UNION으로 결합하여 구현합니다.

SQL
-- FULL OUTER JOIN 대체
SELECT c.name, o.order_id, o.total_amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id

UNION

SELECT c.name, o.order_id, o.total_amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

15.5 JOIN 실전 종합 예제

SQL
-- ★ 고객별 총 구매액과 구매 상품 수 (취소 제외)
SELECT
    c.name                            AS '고객명',
    c.grade                           AS '등급',
    COUNT(DISTINCT o.order_id)       AS '주문횟수',
    SUM(oi.quantity)                 AS '총상품수',
    COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS '총구매액'
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
    AND o.status != 'CANCELLED'
LEFT JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY c.customer_id, c.name, c.grade
ORDER BY '총구매액' DESC;
COALESCE 함수는 인자 중 첫 번째로 NULL이 아닌 값을 반환합니다. 주문이 없는 고객의 SUM은 NULL이 되므로, COALESCE로 0으로 변환합니다.
Chapter 16

서브쿼리 (Subquery)

16.1 서브쿼리란?

서브쿼리는 SQL 문 안에 포함된 또 다른 SQL 문입니다. 괄호 ( )로 감싸서 사용하며, 메인 쿼리의 WHERE, FROM, SELECT, HAVING 등 다양한 위치에서 사용할 수 있습니다.

16.2 단일 행 서브쿼리 (WHERE절)

서브쿼리가 하나의 값만 반환할 때, 비교 연산자(=, >, < 등)와 함께 사용합니다.

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

-- 가장 비싼 상품 조회
SELECT * FROM products
WHERE price = (SELECT MAX(price) FROM products);

-- 가장 최근 주문의 상세 정보
SELECT * FROM orders
WHERE order_date = (
    SELECT MAX(order_date) FROM orders
);

-- 개발부서 평균 급여보다 높은 급여를 받는 직원
SELECT name, department, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE department = '개발'
);

16.3 다중 행 서브쿼리 (IN, ANY, ALL)

SQL
-- IN : 주문한 적이 있는 고객
SELECT name, email
FROM customers
WHERE customer_id IN (
    SELECT DISTINCT customer_id FROM orders
);

-- NOT IN : 주문한 적 없는 고객
SELECT name, email
FROM customers
WHERE customer_id NOT IN (
    SELECT DISTINCT customer_id FROM orders
);

-- ANY : 어떤 팀장의 급여보다 높은 사원
SELECT name, salary
FROM employees
WHERE salary > ANY (
    SELECT salary FROM employees WHERE position = '팀장'
);

-- ALL : 모든 팀장의 급여보다 높은 사람
SELECT name, salary
FROM employees
WHERE salary > ALL (
    SELECT salary FROM employees WHERE position = '팀장'
);

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

서브쿼리의 결과를 임시 테이블처럼 사용합니다. 반드시 별칭을 지정해야 합니다.

SQL
-- 고객별 주문 통계를 구한 뒤, 10만원 이상 구매자만 조회
SELECT *
FROM (
    SELECT
        c.name,
        c.grade,
        COUNT(o.order_id)    AS order_count,
        SUM(o.total_amount)  AS total_spent
    FROM customers c
    INNER JOIN orders o ON c.customer_id = o.customer_id
    WHERE o.status = 'COMPLETED'
    GROUP BY c.customer_id, c.name, c.grade
) AS stats
WHERE total_spent > 100000
ORDER BY total_spent DESC;

16.5 SELECT 절의 서브쿼리 (스칼라 서브쿼리)

SQL
-- 각 상품의 가격과 전체 평균 가격 비교
SELECT
    product_name,
    price,
    (SELECT ROUND(AVG(price)) FROM products) AS avg_price,
    price - (SELECT ROUND(AVG(price)) FROM products) AS diff_from_avg
FROM products
ORDER BY diff_from_avg DESC;

-- 각 직원이 속한 부서의 평균 급여 함께 조회
SELECT
    e.name,
    e.department,
    e.salary,
    (SELECT ROUND(AVG(salary))
     FROM employees e2
     WHERE e2.department = e.department
    ) AS dept_avg
FROM employees e;

16.6 EXISTS / NOT EXISTS

서브쿼리에 결과가 "존재하는지"만 확인합니다. 대량 데이터에서 IN보다 성능이 좋은 경우가 많습니다.

SQL
-- 주문이 있는 고객만 조회 (EXISTS)
SELECT c.name, c.email
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
);

-- 주문이 없는 고객 (NOT EXISTS)
SELECT c.name, c.email
FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
);

-- 10만원 이상 주문 이력이 있는 고객
SELECT c.name, c.grade
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
    AND o.total_amount >= 100000
);
EXISTS vs IN 성능 비교
EXISTS는 조건에 맞는 행을 하나만 찾으면 즉시 멈추지만, IN은 서브쿼리 결과 전체를 먼저 생성합니다. 서브쿼리 결과가 큰 경우 EXISTS가, 작은 경우 IN이 더 효율적일 수 있습니다.
Chapter 17

문자열 함수

17.1 기본 문자열 함수

SQL
-- CONCAT : 문자열 연결
SELECT CONCAT('Hello', ' ', 'World');
-- 결과: Hello World

-- CONCAT_WS : 구분자(Separator)로 연결
SELECT CONCAT_WS('-', '2024', '01', '15');
-- 결과: 2024-01-15

-- UPPER / LOWER : 대소문자 변환
SELECT
    UPPER('hello sql') AS upper_case,  -- HELLO SQL
    LOWER('HELLO SQL') AS lower_case;  -- hello sql

-- LENGTH / CHAR_LENGTH
SELECT
    LENGTH('SQL 공부')      AS byte_len,  -- 바이트 수 (UTF8: 10)
    CHAR_LENGTH('SQL 공부') AS char_len;  -- 문자 수 (6)

17.2 추출 · 자르기 함수

SQL
-- SUBSTRING(str, start, length) : 부분 문자열
SELECT SUBSTRING('Hello World', 7, 5);
-- 결과: World

-- LEFT / RIGHT : 왼쪽/오른쪽에서 n글자
SELECT
    LEFT('Hello World', 5)  AS left_str,   -- Hello
    RIGHT('Hello World', 5) AS right_str;  -- World

-- TRIM : 공백 제거
SELECT
    TRIM('  Hello  ')  AS both_trim,   -- Hello
    LTRIM('  Hello  ') AS left_trim,   -- 'Hello  '
    RTRIM('  Hello  ') AS right_trim;  -- '  Hello'

-- SUBSTRING_INDEX : 구분자 기준으로 자르기
SELECT
    SUBSTRING_INDEX('minsu@mail.com', '@', 1)  AS username,  -- minsu
    SUBSTRING_INDEX('minsu@mail.com', '@', -1) AS domain;    -- mail.com

17.3 치환 · 검색 함수

SQL
-- REPLACE : 문자열 치환
SELECT REPLACE('010-1234-5678', '-', '');
-- 결과: 01012345678

-- INSTR / LOCATE : 위치 찾기
SELECT
    INSTR('Hello World', 'World')    AS pos1,  -- 7
    LOCATE('World', 'Hello World')   AS pos2;  -- 7

-- LPAD / RPAD : 패딩(채우기)
SELECT
    LPAD('42', 5, '0')  AS left_pad,   -- 00042
    RPAD('Hi', 5, '.')  AS right_pad;  -- Hi...

-- REPEAT / REVERSE
SELECT
    REPEAT('★', 5) AS stars,     -- ★★★★★
    REVERSE('Hello') AS reversed;  -- olleH

17.4 실전 활용 예제

SQL
-- ① 이메일에서 도메인 추출
SELECT
    name,
    email,
    SUBSTRING_INDEX(email, '@', -1) AS domain
FROM customers;

-- ② 전화번호 마스킹 처리
SELECT
    name,
    CONCAT(
        LEFT(phone, 3), '-****-', RIGHT(phone, 4)
    ) AS masked_phone
FROM customers;

-- ③ 이름 성씨 기준 통계 (한글 1글자 = 성씨)
SELECT
    LEFT(name, 1) AS family_name,
    COUNT(*)       AS cnt
FROM customers
GROUP BY family_name
ORDER BY cnt DESC;

-- ④ 상품명 검색 (대소문자 무시)
SELECT * FROM products
WHERE LOWER(product_name) LIKE '%usb%';

-- ⑤ 주문번호 형식 만들기: ORD-00001
SELECT
    CONCAT('ORD-', LPAD(order_id, 5, '0')) AS order_no,
    total_amount
FROM orders;
전화번호 마스킹 결과
+--------+----------------+ | name | masked_phone | +--------+----------------+ | 김민수 | 010-****-5678 | | 이영희 | 010-****-6789 | | 박철수 | 010-****-7890 | | ... | ... | +--------+----------------+
Chapter 18

날짜와 시간 함수

18.1 현재 날짜 · 시간 가져오기

SQL
SELECT
    NOW()         AS '현재일시',      -- 2024-04-26 14:30:00
    CURDATE()     AS '오늘날짜',      -- 2024-04-26
    CURTIME()     AS '현재시간',      -- 14:30:00
    SYSDATE()     AS '시스템일시',    -- NOW()와 유사
    UTC_TIMESTAMP() AS 'UTC시간';

18.2 날짜 요소 추출

SQL
SELECT
    YEAR(NOW())       AS '연도',     -- 2024
    MONTH(NOW())      AS '월',       -- 4
    DAY(NOW())        AS '일',       -- 26
    HOUR(NOW())       AS '시',       -- 14
    MINUTE(NOW())     AS '분',       -- 30
    SECOND(NOW())     AS '초',       -- 0
    DAYOFWEEK(NOW()) AS '요일번호',  -- 1(일)~7(토)
    DAYNAME(NOW())   AS '요일명',    -- Friday
    QUARTER(NOW())   AS '분기',      -- 2
    WEEK(NOW())      AS '주차';      -- 17

18.3 날짜 연산 (더하기 · 빼기)

SQL
-- DATE_ADD / DATE_SUB
SELECT
    DATE_ADD(NOW(), INTERVAL 7 DAY)     AS '7일후',
    DATE_ADD(NOW(), INTERVAL 1 MONTH)   AS '1달후',
    DATE_ADD(NOW(), INTERVAL 1 YEAR)    AS '1년후',
    DATE_ADD(NOW(), INTERVAL 2 HOUR)    AS '2시간후',
    DATE_SUB(NOW(), INTERVAL 30 DAY)    AS '30일전',
    DATE_SUB(NOW(), INTERVAL 3 MONTH)   AS '3달전';

-- DATEDIFF : 두 날짜 사이의 일수
SELECT
    name, join_date,
    DATEDIFF(CURDATE(), join_date) AS days_since_join
FROM customers;

-- TIMESTAMPDIFF : 특정 단위로 차이 계산
SELECT
    name, hire_date,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE())  AS '근속연수',
    TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) AS '근속개월'
FROM employees;

18.4 날짜 포맷팅 (DATE_FORMAT)

SQL
SELECT
    order_date,
    DATE_FORMAT(order_date, '%Y년 %m월 %d일')  AS '한국식',
    DATE_FORMAT(order_date, '%Y-%m')            AS '년월',
    DATE_FORMAT(order_date, '%W')               AS '요일',
    DATE_FORMAT(order_date, '%p %h:%i:%s')     AS '시간'
FROM orders
LIMIT 3;
코드의미예시
%Y4자리 연도2024
%y2자리 연도24
%m월 (01~12)04
%d일 (01~31)26
%H시 (00~23)14
%h시 (01~12)02
%i분 (00~59)30
%s초 (00~59)00
%W요일 이름Friday
%pAM / PMPM

18.5 실전 예제

SQL
-- ① 월별 매출 집계
SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(total_amount) AS monthly_sales
FROM orders
WHERE status = 'COMPLETED'
GROUP BY month
ORDER BY month;

-- ② 최근 30일 내 주문
SELECT * FROM orders
WHERE order_date >= DATE_SUB(NOW(), INTERVAL 30 DAY);

-- ③ 가입 후 1년이 지난 고객
SELECT name, join_date
FROM customers
WHERE join_date < DATE_SUB(CURDATE(), INTERVAL 1 YEAR);

-- ④ 요일별 주문 분포
SELECT
    DAYNAME(order_date) AS day_name,
    COUNT(*) AS cnt
FROM orders
GROUP BY day_name
ORDER BY FIELD(day_name, 'Monday','Tuesday','Wednesday',
    'Thursday','Friday','Saturday','Sunday');

-- ⑤ 분기별 매출
SELECT
    CONCAT(YEAR(order_date), ' Q', QUARTER(order_date)) AS quarter,
    SUM(total_amount) AS revenue
FROM orders
WHERE status != 'CANCELLED'
GROUP BY quarter
ORDER BY quarter;
Chapter 19

CASE 표현식

19.1 단순 CASE

CASE는 SQL에서 if-else 논리를 구현합니다. 특정 값에 따라 다른 결과를 반환합니다.

SQL
-- 주문 상태를 한글로 변환
SELECT
    order_id,
    status,
    CASE status
        WHEN 'COMPLETED' THEN '완료'
        WHEN 'SHIPPED'   THEN '배송중'
        WHEN 'PENDING'   THEN '대기중'
        WHEN 'CANCELLED' THEN '취소됨'
        ELSE '알수없음'
    END AS '상태한글',
    total_amount
FROM orders;

19.2 검색 CASE (조건식)

SQL
-- 가격대별 분류
SELECT
    product_name,
    price,
    CASE
        WHEN price >= 100000 THEN '★★★ 고가'
        WHEN price >= 50000  THEN '★★ 중가'
        WHEN price >= 20000  THEN '★ 저가'
        ELSE '초저가'
    END AS '가격대'
FROM products
ORDER BY price DESC;

-- 급여 등급 분류
SELECT
    name, department, salary,
    CASE
        WHEN salary >= 10000000 THEN 'S등급'
        WHEN salary >= 6000000  THEN 'A등급'
        WHEN salary >= 4000000  THEN 'B등급'
        ELSE 'C등급'
    END AS salary_grade
FROM employees
ORDER BY salary DESC;

-- 재고 상태 표시
SELECT
    product_name, stock,
    CASE
        WHEN stock = 0   THEN '품절'
        WHEN stock <= 30  THEN '부족'
        WHEN stock <= 100 THEN '보통'
        ELSE '충분'
    END AS stock_status
FROM products;

19.3 CASE + 집계 함수 (피벗 테이블)

행으로 나열된 데이터를 열(가로)로 변환하는 기법입니다.

SQL
-- 주문 상태별 건수를 가로로 표시
SELECT
    COUNT(*) AS '전체',
    SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS '완료',
    SUM(CASE WHEN status = 'SHIPPED'   THEN 1 ELSE 0 END) AS '배송중',
    SUM(CASE WHEN status = 'PENDING'   THEN 1 ELSE 0 END) AS '대기',
    SUM(CASE WHEN status = 'CANCELLED' THEN 1 ELSE 0 END) AS '취소'
FROM orders;

-- 카테고리별 가격대 분포 피벗
SELECT
    category,
    SUM(CASE WHEN price >= 100000 THEN 1 ELSE 0 END) AS '10만원이상',
    SUM(CASE WHEN price >= 50000 AND price < 100000 THEN 1 ELSE 0 END) AS '5~10만원',
    SUM(CASE WHEN price < 50000 THEN 1 ELSE 0 END) AS '5만원미만'
FROM products
GROUP BY category;

-- 월별 상태별 매출 피벗
SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    SUM(CASE WHEN status = 'COMPLETED' THEN total_amount ELSE 0 END) AS completed_sales,
    SUM(CASE WHEN status = 'SHIPPED'   THEN total_amount ELSE 0 END) AS shipped_sales,
    SUM(total_amount) AS total_all
FROM orders
GROUP BY month
ORDER BY month;

19.4 CASE를 다른 절에서 사용

SQL
-- ORDER BY에서 CASE 사용: 커스텀 정렬
SELECT name, grade
FROM customers
ORDER BY
    CASE grade
        WHEN 'GOLD'   THEN 1
        WHEN 'SILVER' THEN 2
        WHEN 'BRONZE' THEN 3
    END;

-- UPDATE에서 CASE 사용: 조건별 일괄 수정
UPDATE products
SET price = CASE
    WHEN category = '전자기기' THEN price * 0.9   -- 10% 할인
    WHEN category = '도서'     THEN price * 0.95  -- 5% 할인
    ELSE price
END;
Chapter 20

뷰 (VIEW)

20.1 뷰란?

뷰(View)는 저장된 SELECT 쿼리입니다. 실제 데이터를 갖고 있지 않고, 조회할 때마다 내부 쿼리가 실행됩니다. 복잡한 쿼리를 이름 붙여 재사용할 수 있는 "가상 테이블"입니다.

20.2 뷰의 장점

  • 복잡한 쿼리 단순화 : 자주 쓰는 JOIN, 집계 쿼리를 뷰로 만들면 간단하게 호출 가능
  • 보안 강화 : 원본 테이블의 특정 컬럼만 노출하여 민감 정보 숨기기
  • 데이터 독립성 : 테이블 구조가 변경되어도 뷰를 수정하면 기존 쿼리 유지 가능

20.3 뷰 생성 · 사용

SQL
-- 고객별 주문 요약 뷰
CREATE VIEW v_customer_summary AS
SELECT
    c.customer_id,
    c.name,
    c.grade,
    c.city,
    COUNT(o.order_id)                AS order_count,
    COALESCE(SUM(o.total_amount), 0) AS total_spent,
    MAX(o.order_date)                AS last_order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
    AND o.status != 'CANCELLED'
GROUP BY c.customer_id, c.name, c.grade, c.city;

-- 뷰를 일반 테이블처럼 조회!
SELECT * FROM v_customer_summary
WHERE total_spent > 100000
ORDER BY total_spent DESC;

-- 보안용 뷰: 급여 정보를 숨긴 직원 뷰
CREATE VIEW v_employees_public AS
SELECT emp_id, name, department, position, hire_date
FROM employees;

-- 상품 카테고리별 통계 뷰
CREATE VIEW v_category_stats AS
SELECT
    category,
    COUNT(*)         AS product_count,
    ROUND(AVG(price)) AS avg_price,
    SUM(stock)       AS total_stock,
    MIN(price)       AS min_price,
    MAX(price)       AS max_price
FROM products
GROUP BY category;

20.4 뷰 수정 · 삭제

SQL
-- 뷰 수정 (또는 없으면 생성)
CREATE OR REPLACE VIEW v_category_stats AS
SELECT
    category,
    COUNT(*) AS product_count,
    ROUND(AVG(price)) AS avg_price
FROM products
GROUP BY category;

-- 뷰 목록 확인
SHOW FULL TABLES WHERE Table_type = 'VIEW';

-- 뷰 삭제
DROP VIEW IF EXISTS v_customer_summary;
주의! 집계 함수, GROUP BY, DISTINCT, JOIN 등을 포함한 뷰는 INSERT/UPDATE/DELETE가 불가능합니다. 단순한 뷰(단일 테이블, 집계 없음)만 데이터 수정이 가능합니다.
Chapter 21

인덱스 (INDEX)

21.1 인덱스란?

인덱스는 책의 "색인(목차)"과 같습니다. 특정 컬럼의 값과 해당 행의 위치를 미리 정리해 두어, 데이터를 빠르게 찾을 수 있게 합니다. 인덱스가 없으면 테이블 전체를 처음부터 끝까지 검색(Full Table Scan)해야 하지만, 인덱스가 있으면 바로 원하는 위치로 점프합니다.

21.2 인덱스 종류

종류설명생성 방법
PRIMARY KEY기본키 인덱스 (자동 생성)테이블 생성 시 자동
UNIQUE INDEX중복 불가 인덱스CREATE UNIQUE INDEX
일반 INDEX중복 허용 인덱스CREATE INDEX
복합 INDEX여러 컬럼 조합CREATE INDEX ... ON (col1, col2)
FULLTEXT전문 검색용CREATE FULLTEXT INDEX

21.3 인덱스 생성 · 확인 · 삭제

SQL
-- 단일 컬럼 인덱스
CREATE INDEX idx_customers_city
ON customers(city);

-- 복합 인덱스 (여러 컬럼)
CREATE INDEX idx_orders_cust_date
ON orders(customer_id, order_date);

-- 유니크 인덱스
CREATE UNIQUE INDEX idx_customers_email
ON customers(email);

-- 인덱스 확인
SHOW INDEX FROM customers;

-- 인덱스 삭제
DROP INDEX idx_customers_city ON customers;

-- ALTER TABLE로 인덱스 추가/삭제
ALTER TABLE products ADD INDEX idx_category (category);
ALTER TABLE products DROP INDEX idx_category;

21.4 EXPLAIN으로 실행 계획 분석

SQL
-- 인덱스 없이 조회
EXPLAIN SELECT * FROM customers WHERE city = '서울';
-- type: ALL (전체 스캔) → 느림

-- 인덱스 생성 후 조회
CREATE INDEX idx_city ON customers(city);
EXPLAIN SELECT * FROM customers WHERE city = '서울';
-- type: ref (인덱스 사용) → 빠름
EXPLAIN 결과 해석 (type 컬럼 - 성능 순)

const (최고) → eq_refrefrangeindexALL (최악)

ALL이 나오면 인덱스를 활용하지 못하고 있다는 의미입니다.

21.5 인덱스 설계 가이드

인덱스를 만들면 좋은 경우
- WHERE, JOIN, ORDER BY에 자주 사용되는 컬럼
- 카디널리티(고유 값 수)가 높은 컬럼 (email, phone 등)
- 대용량 테이블 (수만 건 이상)

인덱스가 불필요하거나 해로운 경우
- 소량 데이터 테이블 (수백 건 이하)
- INSERT/UPDATE/DELETE가 매우 빈번한 테이블 (인덱스 갱신 오버헤드)
- 카디널리티가 매우 낮은 컬럼 (성별 M/F, 활성화 Y/N)
Chapter 22

트랜잭션 (Transaction)

22.1 트랜잭션이란?

트랜잭션은 하나의 논리적 작업 단위입니다. 여러 SQL 문이 모두 성공하거나, 하나라도 실패하면 전부 취소되어야 하는 상황에서 사용합니다.

대표적인 예시는 계좌 이체입니다. A계좌에서 출금하고 B계좌에 입금하는 두 작업은 반드시 함께 성공해야 합니다. 출금만 되고 입금이 실패하면 돈이 사라지기 때문입니다.

22.2 ACID 속성

속성영문설명비유
원자성Atomicity전부 실행 또는 전부 취소전부 아니면 전무(All or Nothing)
일관성Consistency트랜잭션 전후 데이터 무결성 유지잔액 합계는 항상 동일
격리성Isolation동시 트랜잭션이 서로 간섭 안 함각자 독립적으로 실행
지속성Durability완료된 트랜잭션은 영구 반영정전 후에도 데이터 유지

22.3 기본 사용법

SQL
-- 트랜잭션 시작
START TRANSACTION;

-- 작업 1: 주문 생성
INSERT INTO orders (customer_id, total_amount, status)
VALUES (1, 45000, 'PENDING');

-- 작업 2: 재고 차감
UPDATE products
SET stock = stock - 1
WHERE product_id = 1 AND stock > 0;

-- 작업 3: 주문 상세 추가
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (LAST_INSERT_ID(), 1, 1, 45000);

-- 모두 성공 → 확정
COMMIT;

-- 문제 발생 시 → 전체 취소
-- ROLLBACK;

22.4 SAVEPOINT (중간 저장점)

SQL
START TRANSACTION;

INSERT INTO orders (customer_id, total_amount, status)
VALUES (2, 60000, 'PENDING');
SAVEPOINT sp_order_created;

UPDATE products SET stock = stock - 1 WHERE product_id = 99;
-- 상품 99번이 없어서 문제 발생!

-- sp_order_created 시점으로 되돌리기
ROLLBACK TO SAVEPOINT sp_order_created;

-- INSERT는 유지, UPDATE만 취소된 상태
-- 다시 올바른 작업 수행
UPDATE products SET stock = stock - 1 WHERE product_id = 2;

COMMIT;

22.5 AUTO COMMIT 설정

SQL
-- 현재 autocommit 상태 확인
SELECT @@autocommit;   -- 1 = 활성화(기본값)

-- autocommit 비활성화 (모든 SQL을 수동으로 COMMIT해야 함)
SET autocommit = 0;

-- 다시 활성화
SET autocommit = 1;
주의!
- MySQL의 InnoDB 엔진만 트랜잭션을 지원합니다. MyISAM은 지원하지 않습니다.
- DDL(CREATE, ALTER, DROP)은 실행 즉시 암묵적으로 COMMIT됩니다.
- 기본값으로 autocommit=1이므로, 개별 SQL은 자동으로 COMMIT됩니다. START TRANSACTION을 명시해야 수동 제어가 됩니다.
Chapter 23

제약 조건 (Constraints)

23.1 제약 조건의 종류

제약 조건설명예시
PRIMARY KEY고유 식별자 (NOT NULL + UNIQUE)id INT PRIMARY KEY
FOREIGN KEY다른 테이블 참조REFERENCES orders(order_id)
NOT NULLNULL 값 불허name VARCHAR(50) NOT NULL
UNIQUE중복 값 불허email VARCHAR(100) UNIQUE
CHECK값 검증 조건CHECK (age >= 0)
DEFAULT기본값 지정status DEFAULT 'ACTIVE'

23.2 종합 실습 : 회원 테이블

SQL
CREATE TABLE members (
    -- PRIMARY KEY + AUTO_INCREMENT
    member_id   INT          PRIMARY KEY AUTO_INCREMENT,

    -- NOT NULL + UNIQUE
    username    VARCHAR(30)  NOT NULL UNIQUE,
    email       VARCHAR(100) NOT NULL UNIQUE,

    -- CHECK 제약
    age         INT          CHECK (age >= 14 AND age <= 120),

    -- DEFAULT 값
    status      ENUM('ACTIVE', 'INACTIVE', 'BANNED')
                DEFAULT 'ACTIVE',

    -- DEFAULT 현재 시간
    created_at  TIMESTAMP    DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP    DEFAULT CURRENT_TIMESTAMP
                ON UPDATE CURRENT_TIMESTAMP
);

23.3 외래키와 참조 무결성

SQL
-- 리뷰 테이블 : 외래키 + 참조 옵션
CREATE TABLE reviews (
    review_id    INT PRIMARY KEY AUTO_INCREMENT,
    product_id   INT NOT NULL,
    customer_id  INT NOT NULL,
    rating       INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    title        VARCHAR(100),
    content      TEXT,
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- 상품 삭제 시 → 리뷰도 삭제 (CASCADE)
    FOREIGN KEY (product_id)
        REFERENCES products(product_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE,

    -- 고객 삭제 시 → 리뷰 삭제 거부 (RESTRICT)
    FOREIGN KEY (customer_id)
        REFERENCES customers(customer_id)
        ON DELETE RESTRICT
);

23.4 외래키 참조 옵션

옵션부모 행 삭제 시 동작사용 예시
RESTRICT자식 데이터가 있으면 삭제 거부 (기본값)고객 삭제 시 주문이 있으면 거부
CASCADE자식 데이터도 함께 삭제게시글 삭제 시 댓글도 삭제
SET NULL자식의 외래키를 NULL로 변경부서 삭제 시 직원 부서를 NULL
NO ACTIONRESTRICT와 동일 (표준 SQL 표현)-

23.5 제약 조건 추가 · 삭제

SQL
-- CHECK 제약 추가
ALTER TABLE products
ADD CONSTRAINT chk_price_positive CHECK (price > 0);

-- UNIQUE 제약 추가
ALTER TABLE products
ADD CONSTRAINT uq_product_name UNIQUE (product_name);

-- 외래키 추가
ALTER TABLE order_items
ADD CONSTRAINT fk_items_product
FOREIGN KEY (product_id) REFERENCES products(product_id);

-- CHECK 삭제
ALTER TABLE products DROP CHECK chk_price_positive;

-- 외래키 삭제
ALTER TABLE order_items DROP FOREIGN KEY fk_items_product;

-- UNIQUE 삭제
ALTER TABLE products DROP INDEX uq_product_name;
Tip! 제약 조건에 이름(CONSTRAINT name)을 명시하면, 나중에 삭제하거나 수정할 때 편합니다. 이름을 생략하면 시스템이 자동으로 생성한 이름을 찾아야 하므로 번거롭습니다.
Chapter 24

실전 프로젝트 - 쇼핑몰 데이터 분석

지금까지 배운 모든 내용을 종합하여, 실무에서 자주 사용하는 분석 쿼리를 작성해 봅니다.

24.1 매출 종합 대시보드

SQL
-- 전체 매출 요약 (취소 제외)
SELECT
    COUNT(DISTINCT o.order_id)   AS '총주문수',
    COUNT(DISTINCT o.customer_id) AS '구매고객수',
    SUM(o.total_amount)            AS '총매출',
    ROUND(AVG(o.total_amount))     AS '평균주문액',
    MAX(o.total_amount)            AS '최대주문액',
    MIN(o.total_amount)            AS '최소주문액'
FROM orders o
WHERE o.status != 'CANCELLED';

24.2 월별 매출 추이

SQL
-- 월별 매출 추이 (완료 + 배송중)
SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    COUNT(*)                          AS orders,
    SUM(total_amount)                 AS revenue,
    ROUND(AVG(total_amount))          AS avg_order,
    COUNT(DISTINCT customer_id)       AS unique_buyers
FROM orders
WHERE status IN ('COMPLETED', 'SHIPPED')
GROUP BY month
ORDER BY month;
실행 결과 예시
+---------+--------+-----------+-----------+--------------+ | month | orders | revenue | avg_order | unique_buyers| +---------+--------+-----------+-----------+--------------+ | 2024-01 | 2 | 137000.00 | 68500 | 2 | | 2024-02 | 1 | 350000.00 | 350000 | 1 | | 2024-03 | 2 | 172000.00 | 86000 | 2 | | 2024-04 | 2 | 144000.00 | 72000 | 2 | | 2024-05 | 1 | 83000.00 | 83000 | 1 | +---------+--------+-----------+-----------+--------------+

24.3 베스트셀러 TOP 5

SQL
-- 판매 수량 기준 베스트셀러
SELECT
    p.product_name                       AS '상품명',
    p.category                           AS '카테고리',
    SUM(oi.quantity)                    AS '총판매수량',
    SUM(oi.quantity * oi.unit_price)    AS '총매출',
    p.stock                              AS '현재재고'
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN orders o  ON oi.order_id = o.order_id
WHERE o.status != 'CANCELLED'
GROUP BY p.product_id, p.product_name, p.category, p.stock
ORDER BY SUM(oi.quantity) DESC
LIMIT 5;

24.4 VIP 고객 분석

SQL
-- 고객별 구매 분석 + 활동 상태 판단
SELECT
    c.name                                   AS '고객명',
    c.email                                  AS '이메일',
    c.grade                                  AS '등급',
    c.city                                   AS '도시',
    COUNT(o.order_id)                       AS '주문횟수',
    SUM(o.total_amount)                     AS '총구매액',
    ROUND(AVG(o.total_amount))              AS '평균구매액',
    MAX(o.order_date)                       AS '마지막주문',
    DATEDIFF(CURDATE(), MAX(o.order_date)) AS '경과일',
    CASE
        WHEN DATEDIFF(CURDATE(), MAX(o.order_date)) <= 30
            THEN '활성'
        WHEN DATEDIFF(CURDATE(), MAX(o.order_date)) <= 90
            THEN '준활성'
        WHEN DATEDIFF(CURDATE(), MAX(o.order_date)) <= 180
            THEN '휴면위험'
        ELSE '휴면'
    END                                      AS '활동상태'
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status != 'CANCELLED'
GROUP BY c.customer_id, c.name, c.email, c.grade, c.city
ORDER BY SUM(o.total_amount) DESC;

24.5 카테고리별 매출 비중

SQL
SELECT
    p.category                                AS '카테고리',
    SUM(oi.quantity)                         AS '판매수량',
    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 order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
ORDER BY SUM(oi.quantity * oi.unit_price) DESC;

24.6 재주문율 분석

SQL
-- 2번 이상 주문한 고객 비율
SELECT
    total_buyers,
    repeat_buyers,
    ROUND(repeat_buyers * 100.0 / total_buyers, 1)
        AS '재구매율(%)'
FROM (
    SELECT
        COUNT(DISTINCT customer_id) AS total_buyers,
        SUM(CASE WHEN cnt >= 2 THEN 1 ELSE 0 END)
            AS repeat_buyers
    FROM (
        SELECT customer_id, COUNT(*) AS cnt
        FROM orders
        WHERE status != 'CANCELLED'
        GROUP BY customer_id
    ) AS order_counts
) AS stats;

24.7 재고 부족 알림 리포트

SQL
SELECT
    p.product_id,
    p.product_name,
    p.category,
    p.stock                            AS '현재재고',
    COALESCE(SUM(oi.quantity), 0)      AS '총판매량',
    CASE
        WHEN p.stock = 0   THEN '품절'
        WHEN p.stock <= 30  THEN '긴급보충'
        WHEN p.stock <= 50  THEN '보충권장'
        WHEN p.stock <= 100 THEN '보통'
        ELSE '충분'
    END                                 AS '재고상태'
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id, p.product_name, p.category, p.stock
ORDER BY p.stock ASC;

24.8 부서별 인건비 보고서

SQL
SELECT
    department                                 AS '부서',
    COUNT(*)                                  AS '인원',
    FORMAT(SUM(salary), 0)                    AS '급여합계',
    FORMAT(ROUND(AVG(salary)), 0)              AS '평균급여',
    FORMAT(MAX(salary), 0)                    AS '최고급여',
    FORMAT(MIN(salary), 0)                    AS '최저급여',
    ROUND(
        SUM(salary) * 100.0
        / (SELECT SUM(salary) FROM employees), 1
    )                                          AS '인건비비중(%)'
FROM employees
GROUP BY department
ORDER BY SUM(salary) DESC;
실행 결과 예시
+--------+------+-----------+----------+------------+----------+----------------+ | 부서 | 인원 | 급여합계 | 평균급여 | 최고급여 | 최저급여 | 인건비비중(%) | +--------+------+-----------+----------+------------+----------+----------------+ | 개발 | 3 |16,000,000 |5,333,333 | 7,000,000 |4,000,000 | 34.0 | | 경영 | 1 |12,000,000 |12,000,000| 12,000,000 |12,000,000| 25.5 | | 마케팅 | 2 |10,300,000 |5,150,000 | 6,500,000 |3,800,000 | 21.9 | | 영업 | 2 | 9,000,000 |4,500,000 | 5,500,000 |3,500,000 | 19.1 | +--------+------+-----------+----------+------------+----------+----------------+

24.9 도시별 · 등급별 매출 피벗

SQL
SELECT
    c.city AS '도시',
    COALESCE(SUM(
        CASE WHEN c.grade = 'GOLD'
        THEN o.total_amount END
    ), 0) AS 'GOLD매출',
    COALESCE(SUM(
        CASE WHEN c.grade = 'SILVER'
        THEN o.total_amount END
    ), 0) AS 'SILVER매출',
    COALESCE(SUM(
        CASE WHEN c.grade = 'BRONZE'
        THEN o.total_amount END
    ), 0) AS 'BRONZE매출',
    COALESCE(SUM(o.total_amount), 0) AS '합계'
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
    AND o.status != 'CANCELLED'
GROUP BY c.city
ORDER BY COALESCE(SUM(o.total_amount), 0) DESC;

24.10 고객 등급 자동 업데이트

SQL
-- 구매 금액 기준으로 등급 일괄 업데이트
UPDATE customers c
INNER JOIN (
    SELECT
        customer_id,
        SUM(total_amount) AS total
    FROM orders
    WHERE status != 'CANCELLED'
    GROUP BY customer_id
) AS stats ON c.customer_id = stats.customer_id
SET c.grade = CASE
    WHEN stats.total >= 500000 THEN 'GOLD'
    WHEN stats.total >= 200000 THEN 'SILVER'
    ELSE 'BRONZE'
END;
실전 Tip! 위와 같은 분석 쿼리를 뷰(VIEW)로 저장해 두면, 매번 복잡한 쿼리를 작성할 필요 없이 SELECT * FROM v_customer_summary처럼 간단하게 호출할 수 있습니다.
Chapter 25

부록 - SQL 치트시트 & 학습 로드맵

25.1 SQL 핵심 문법 치트시트

SQL Cheat Sheet
/* =============================================
   SQL 치트시트 - 한눈에 보는 핵심 문법
   ============================================= */

/* ── 1. 데이터 조회 ── */
SELECT col1, col2           -- 조회할 컬럼
FROM table_name             -- 테이블
WHERE condition             -- 행 필터
GROUP BY col               -- 그룹핑
HAVING group_condition      -- 그룹 필터
ORDER BY col ASC|DESC     -- 정렬
LIMIT n OFFSET m;          -- 결과 제한

/* ── 2. 데이터 조작 ── */
INSERT INTO t (c1, c2) VALUES (v1, v2);
INSERT INTO t (c1) VALUES (v1), (v2), (v3);
UPDATE t SET c1 = v1 WHERE condition;
DELETE FROM t WHERE condition;

/* ── 3. 테이블 관리 ── */
CREATE TABLE t (col TYPE constraints);
ALTER TABLE t ADD COLUMN col TYPE;
ALTER TABLE t MODIFY COLUMN col NEW_TYPE;
ALTER TABLE t DROP COLUMN col;
DROP TABLE IF EXISTS t;
TRUNCATE TABLE t;

/* ── 4. JOIN ── */
FROM a INNER JOIN b ON a.id = b.a_id  -- 교집합
FROM a LEFT JOIN  b ON a.id = b.a_id  -- 왼쪽 전체
FROM a RIGHT JOIN b ON a.id = b.a_id  -- 오른쪽 전체
FROM a CROSS JOIN b                    -- 모든 조합

/* ── 5. 집계 함수 ── */
COUNT(*) | COUNT(col) | COUNT(DISTINCT col)
SUM(col) | AVG(col) | MAX(col) | MIN(col)

/* ── 6. WHERE 조건 연산자 ── */
=  !=  >  <  >=  <=
AND | OR | NOT
BETWEEN a AND b
IN (v1, v2, v3) | NOT IN (...)
LIKE '%패턴%'     -- %: 0개 이상 문자, _: 1개 문자
IS NULL | IS NOT NULL
EXISTS (subquery) | NOT EXISTS (subquery)

/* ── 7. 문자열 함수 ── */
CONCAT(s1, s2)  |  CONCAT_WS(sep, s1, s2)
SUBSTRING(str, start, len)
LEFT(str, n)  |  RIGHT(str, n)
UPPER(str)  |  LOWER(str)
TRIM(str)  |  LTRIM(str)  |  RTRIM(str)
REPLACE(str, from, to)
LPAD(str, len, pad)  |  RPAD(str, len, pad)
LENGTH(str)  |  CHAR_LENGTH(str)
SUBSTRING_INDEX(str, delim, count)

/* ── 8. 날짜 함수 ── */
NOW()  |  CURDATE()  |  CURTIME()
YEAR(d) | MONTH(d) | DAY(d) | HOUR(d)
DATE_ADD(date, INTERVAL n UNIT)
DATE_SUB(date, INTERVAL n UNIT)
DATEDIFF(date1, date2)
TIMESTAMPDIFF(UNIT, date1, date2)
DATE_FORMAT(date, '%Y-%m-%d')

/* ── 9. 숫자 함수 ── */
ROUND(n, d)  |  CEIL(n)  |  FLOOR(n)
ABS(n)  |  MOD(a, b)  |  POWER(a, b)
FORMAT(n, d)  -- 천 단위 쉼표 포맷

/* ── 10. NULL 처리 ── */
IFNULL(col, default_val)
COALESCE(v1, v2, v3, ...)  -- 첫 NOT NULL
NULLIF(a, b)  -- a=b이면 NULL

/* ── 11. CASE 표현식 ── */
CASE col
    WHEN val1 THEN result1
    WHEN val2 THEN result2
    ELSE default_result
END

CASE
    WHEN condition1 THEN result1
    WHEN condition2 THEN result2
    ELSE default_result
END

/* ── 12. 트랜잭션 ── */
START TRANSACTION;
-- SQL 작업들 ...
COMMIT;      -- 확정
ROLLBACK;    -- 취소
SAVEPOINT sp1;
ROLLBACK TO SAVEPOINT sp1;

/* ── 13. 뷰 / 인덱스 ── */
CREATE VIEW v AS SELECT ...;
DROP VIEW IF EXISTS v;
CREATE INDEX idx ON t(col);
DROP INDEX idx ON t;
EXPLAIN SELECT ...;  -- 실행 계획

25.2 자주 하는 실수 & 해결법

실수증상해결법
WHERE 없이 UPDATE / DELETE전체 행이 변경/삭제됨!항상 SELECT로 대상 먼저 확인
WHERE col = NULL결과가 항상 비어 있음IS NULL / IS NOT NULL 사용
GROUP BY 누락에러 또는 잘못된 결과집계 함수 외 모든 SELECT 컬럼을 GROUP BY에
따옴표 혼동문법 에러문자열 → '작은따옴표', 식별자 → `백틱`
JOIN 시 모호한 컬럼명ambiguous column 에러테이블별칭.컬럼명으로 명시
LIMIT 없이 대량 조회서버 과부하, 타임아웃개발 시 항상 LIMIT 추가
외래키 순서 무시삽입/삭제 에러삽입: 부모→자식, 삭제: 자식→부모
FLOAT로 금액 저장소수점 오차 (0.1+0.2≠0.3)금액은 반드시 DECIMAL 사용
SELECT * 남용불필요한 데이터 전송, 느림필요한 컬럼만 명시
인덱스 과다 생성INSERT/UPDATE 느려짐실제 조회 패턴에 맞게 최소한으로

25.3 SQL 실행 순서 (반드시 암기!)

SQL은 작성 순서와 실행 순서가 다릅니다!

작성 순서 : SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT

실행 순서 :
FROM (테이블 지정) → ② WHERE (행 필터링) → ③ GROUP BY (그룹핑) → ④ HAVING (그룹 필터링) → ⑤ SELECT (컬럼 선택, 계산) → ⑥ ORDER BY (정렬) → ⑦ LIMIT (결과 제한)

이 순서를 이해하면 WHERE에서 별칭을 못 쓰는 이유, HAVING과 WHERE의 차이 등이 자연스럽게 이해됩니다.

25.4 학습 로드맵

단계기간학습 내용이 튜토리얼
Level 1 입문1~2주SELECT, WHERE, ORDER BY, LIMIT, INSERT, UPDATE, DELETECh.1 ~ Ch.11
Level 2 기초2~4주집계 함수, GROUP BY, HAVING, JOIN, 서브쿼리Ch.12 ~ Ch.16
Level 3 중급1~2개월함수, CASE, 뷰, 인덱스, 트랜잭션, 제약조건Ch.17 ~ Ch.23
Level 4 고급2~3개월윈도우 함수, CTE, 재귀 쿼리, 저장 프로시저, 트리거추가 학습
Level 5 실무지속데이터 모델링, 정규화, 쿼리 최적화, 파티셔닝, 복제추가 학습

25.5 마치며

축하합니다! 전 25장의 SQL 기초 완전정복 튜토리얼을 모두 마치셨습니다.

SQL은 데이터를 다루는 모든 직군의 필수 기술입니다. 프론트엔드, 백엔드, 데이터 분석, 마케팅, PM 등 어떤 분야에서든 SQL을 알면 업무 효율이 비약적으로 향상됩니다. 이 튜토리얼의 예제를 직접 실행해 보면서 반복 학습하세요.

SQL 학습에서 가장 중요한 세 가지를 기억하세요.

SQL 학습의 핵심 3원칙

1. 직접 타이핑하세요 — 복사-붙여넣기 대신 직접 입력하면 손이 기억합니다. 오타를 수정하는 과정에서 문법을 체득합니다.

2. 에러를 두려워하지 마세요 — 에러 메시지를 읽고 해결하는 과정이 가장 효과적인 학습입니다. 모든 숙련된 개발자는 수만 번의 에러를 경험했습니다.

3. 나만의 프로젝트를 만드세요 — 관심 있는 주제(영화, 음악, 운동 기록, 가계부 등)로 직접 DB를 설계하고 데이터를 넣어 보세요. 실제 문제를 풀면서 배우는 것이 가장 빠릅니다.

데이터의 세계에 오신 것을 환영합니다. 이 튜토리얼이 여러분의 SQL 여정에 든든한 첫걸음이 되기를 바랍니다!