데이터베이스란 무엇인가?
1.1 데이터베이스의 정의
데이터베이스(Database)란 체계적으로 정리된 데이터의 집합입니다. 우리가 일상에서 사용하는 전화번호부, 도서관 카탈로그, 쇼핑몰의 상품 목록 모두 넓은 의미의 데이터베이스입니다. 컴퓨터 과학에서 데이터베이스는 전자적으로 저장되고, 효율적으로 접근·관리할 수 있도록 구조화된 데이터 모음을 의미합니다.
1.2 왜 데이터베이스가 필요한가?
엑셀 파일로도 데이터를 관리할 수 있지만, 데이터가 수만~수억 건으로 늘어나면 한계에 부딪힙니다. 데이터베이스를 사용하면 다음과 같은 이점이 있습니다.
- 대용량 처리 : 수억 건의 데이터도 빠르게 검색할 수 있습니다.
- 동시 접근 : 여러 사용자가 동시에 데이터를 읽고 쓸 수 있습니다.
- 데이터 무결성 : 규칙을 설정하여 잘못된 데이터가 들어오는 것을 방지합니다.
- 보안 : 사용자별 접근 권한을 세밀하게 설정할 수 있습니다.
- 백업과 복구 : 장애가 발생해도 데이터를 안전하게 복구할 수 있습니다.
1.3 관계형 데이터베이스 (RDBMS)
관계형 데이터베이스(Relational Database Management System)는 데이터를 테이블(표) 형태로 저장합니다. 행(Row)과 열(Column)로 이루어진 2차원 표를 떠올리면 됩니다. 테이블과 테이블 사이의 "관계(Relation)"를 정의하여 데이터를 효율적으로 관리합니다.
| RDBMS | 개발사 | 특징 | 라이선스 |
|---|---|---|---|
| MySQL | Oracle | 웹 서비스에서 가장 많이 사용 | 오픈소스 / 상용 |
| PostgreSQL | 커뮤니티 | 고급 기능, 표준 준수 | 오픈소스 |
| SQLite | 커뮤니티 | 파일 기반, 경량 | 퍼블릭 도메인 |
| Oracle DB | Oracle | 대기업 엔터프라이즈 | 상용 |
| SQL Server | Microsoft | Windows 환경 최적화 | 상용 / Express 무료 |
| MariaDB | MariaDB 재단 | MySQL 호환 포크 | 오픈소스 |
1.4 핵심 용어 정리
| 용어 | 영문 | 설명 |
|---|---|---|
| 테이블 | Table | 데이터를 저장하는 2차원 표 |
| 행 (레코드) | Row / Record | 테이블의 한 줄(하나의 데이터) |
| 열 (컬럼) | Column / Field | 테이블의 한 항목(속성) |
| 기본키 | Primary Key | 각 행을 고유하게 식별하는 열 |
| 외래키 | Foreign Key | 다른 테이블의 기본키를 참조하는 열 |
| 스키마 | Schema | 데이터베이스의 구조 설계도 |
SQL 소개
2.1 SQL이란?
SQL(Structured Query Language)은 관계형 데이터베이스를 다루기 위한 표준 프로그래밍 언어입니다. 1970년대 IBM에서 처음 개발되었으며, 현재 거의 모든 RDBMS에서 사용됩니다. "에스큐엘" 또는 "시퀄(Sequel)"이라고 읽습니다.
2.2 SQL의 분류
SQL 명령어는 역할에 따라 크게 네 가지로 분류됩니다.
| 분류 | 전체 이름 | 역할 | 주요 명령어 |
|---|---|---|---|
| DDL | Data Definition Language | 구조 정의 | CREATE, ALTER, DROP, TRUNCATE |
| DML | Data Manipulation Language | 데이터 조작 | SELECT, INSERT, UPDATE, DELETE |
| DCL | Data Control Language | 권한 제어 | GRANT, REVOKE |
| TCL | Transaction Control Language | 트랜잭션 제어 | COMMIT, ROLLBACK, SAVEPOINT |
2.3 SQL의 특징
- 선언적 언어 : "어떻게(How)" 가 아니라 "무엇을(What)" 원하는지 기술합니다.
- 대소문자 구분 없음 : SELECT와 select는 동일합니다. (관례상 키워드는 대문자)
- 세미콜론으로 끝남 : 각 SQL 문장은 세미콜론(;)으로 마무리합니다.
- 집합 기반 : 한 번에 여러 행을 처리합니다.
2.4 첫 번째 SQL 맛보기
-- 모든 직원 정보를 조회합니다
SELECT * FROM employees;
위 SQL은 employees 테이블의 모든 열(*)과 모든 행을 가져오라는 의미입니다. 지금은 이해가 안 되어도 괜찮습니다. 앞으로 하나씩 배워 나가겠습니다.
-- 뒤의 텍스트는 주석(Comment)으로, 실행에 영향을 주지 않습니다. 여러 줄 주석은 /* ... */을 사용합니다.
실습 환경 설정
3.1 온라인 환경 (설치 불필요)
가장 빠르게 SQL을 연습하는 방법은 온라인 도구를 사용하는 것입니다.
- DB Fiddle (db-fiddle.com) : MySQL, PostgreSQL, SQLite를 브라우저에서 바로 실행
- SQLite Online (sqliteonline.com) : SQLite 전용, 빠르고 간편
- W3Schools SQL Editor : 튜토리얼과 함께 실습 가능
3.2 로컬 설치 - MySQL
실무 환경과 가장 비슷한 로컬 설치를 추천합니다.
# MySQL 서버 설치
sudo apt update
sudo apt install mysql-server
# 서비스 시작
sudo systemctl start mysql
# 보안 설정 마법사
sudo mysql_secure_installation
# MySQL 접속
mysql -u root -p
-- MySQL Installer를 공식 사이트에서 다운로드 후 설치
-- 설치 후 MySQL Command Line Client 실행
mysql -u root -p
3.3 로컬 설치 - SQLite (가장 간단)
# 설치
sudo apt install sqlite3
# 데이터베이스 생성 및 접속
sqlite3 tutorial.db
3.4 GUI 도구
- DBeaver : 무료, 거의 모든 DB 지원, 추천!
- MySQL Workbench : MySQL 공식 GUI 도구
- DataGrip : JetBrains 유료 도구, 매우 강력
- HeidiSQL : 가볍고 빠른 Windows 전용
3.5 실습용 데이터베이스 생성
이 튜토리얼 전체에서 사용할 샘플 데이터베이스를 만들겠습니다. 온라인 쇼핑몰을 가정합니다.
-- 데이터베이스 생성
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 샘플 데이터 삽입
-- 고객 데이터
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);
데이터베이스와 테이블 관리
4.1 데이터베이스 생성 · 삭제 · 선택
-- 데이터베이스 목록 확인
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)
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 테이블 구조 확인
-- 테이블 목록
SHOW TABLES;
-- 테이블 구조 확인
DESC students;
-- 또는
DESCRIBE students;
-- 테이블 생성 SQL 보기
SHOW CREATE TABLE students;
4.4 테이블 수정 (ALTER TABLE)
-- 열 추가
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 테이블 삭제
-- 테이블 완전 삭제 (구조 + 데이터)
DROP TABLE students;
DROP TABLE IF EXISTS students;
-- 데이터만 삭제 (구조 유지, 복구 불가)
TRUNCATE TABLE students;
데이터 타입 완벽 가이드
5.1 숫자형 (Numeric)
| 타입 | 범위 | 크기 | 용도 |
|---|---|---|---|
| TINYINT | -128 ~ 127 | 1 byte | 작은 정수, 불리언 대용 |
| SMALLINT | -32,768 ~ 32,767 | 2 bytes | 작은 범위 정수 |
| INT (INTEGER) | 약 -21억 ~ 21억 | 4 bytes | 일반 정수 (가장 많이 사용) |
| BIGINT | 약 -922경 ~ 922경 | 8 bytes | 매우 큰 정수 |
| DECIMAL(M,D) | 고정 소수점 | 가변 | 금액 등 정밀한 숫자 |
| FLOAT | 부동 소수점 | 4 bytes | 과학 계산 (근사값) |
| DOUBLE | 부동 소수점 | 8 bytes | 더 높은 정밀도 |
5.2 문자형 (String)
| 타입 | 최대 길이 | 특징 | 용도 |
|---|---|---|---|
| CHAR(N) | 255자 | 고정 길이 (빈 공간은 공백 채움) | 우편번호, 국가코드 |
| VARCHAR(N) | 65,535자 | 가변 길이 (실제 길이만큼 저장) | 이름, 이메일, 주소 |
| TEXT | 65,535자 | 긴 텍스트 | 게시글 본문 |
| MEDIUMTEXT | 약 1,600만자 | 매우 긴 텍스트 | 대용량 글 |
| LONGTEXT | 약 42억자 | 초대용량 텍스트 | 로그, 대량 데이터 |
| ENUM | - | 정해진 값 중 하나 | 성별, 상태값 |
5.3 날짜/시간형 (Date/Time)
| 타입 | 형식 | 범위 | 용도 |
|---|---|---|---|
| DATE | YYYY-MM-DD | 1000-01-01 ~ 9999-12-31 | 생년월일, 가입일 |
| TIME | HH:MM:SS | -838:59:59 ~ 838:59:59 | 시간 간격 |
| DATETIME | YYYY-MM-DD HH:MM:SS | 1000 ~ 9999년 | 주문일시, 생성일시 |
| TIMESTAMP | YYYY-MM-DD HH:MM:SS | 1970 ~ 2038년 | 수정일시 (시간대 자동 변환) |
| YEAR | YYYY | 1901 ~ 2155 | 연도만 필요할 때 |
5.4 기타 유용한 타입
-- 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
SELECT - 데이터 조회의 기본
6.1 SELECT 기본 구조
-- 기본 문법
SELECT 컬럼1, 컬럼2, ...
FROM 테이블명;
6.2 전체 컬럼 조회 (*)
-- 고객 테이블의 모든 데이터 조회
SELECT * FROM customers;
SELECT *는 되도록 사용하지 마세요. 불필요한 컬럼까지 가져와 성능이 저하됩니다. 필요한 컬럼만 명시하세요.
6.3 특정 컬럼만 조회
-- 이름과 이메일만 조회
SELECT name, email
FROM customers;
6.4 별칭 (Alias)
-- 컬럼 별칭 (AS 키워드)
SELECT
name AS '고객명',
email AS '이메일',
city AS '도시',
grade AS '등급'
FROM customers;
-- 테이블 별칭
SELECT c.name, c.email
FROM customers AS c;
6.5 DISTINCT - 중복 제거
-- 어떤 도시의 고객이 있는지 (중복 제거)
SELECT DISTINCT city
FROM customers;
-- 여러 컬럼의 조합에서 중복 제거
SELECT DISTINCT city, grade
FROM customers;
6.6 산술 연산
-- 가격에 10% 할인 적용한 가격 계산
SELECT
product_name AS '상품명',
price AS '원가',
price * 0.9 AS '할인가',
price - (price * 0.9) AS '할인액'
FROM products;
6.7 연결 연산 (CONCAT)
-- 이름과 도시를 합쳐서 표시
SELECT
CONCAT(name, ' (', city, ')') AS '고객정보'
FROM customers;
WHERE - 조건으로 데이터 필터링
7.1 기본 비교 연산자
| 연산자 | 의미 | 예시 |
|---|---|---|
| = | 같다 | WHERE city = '서울' |
| != 또는 <> | 같지 않다 | WHERE grade != 'BRONZE' |
| > | 크다 | WHERE price > 50000 |
| < | 작다 | WHERE stock < 100 |
| >= | 크거나 같다 | WHERE salary >= 5000000 |
| <= | 작거나 같다 | WHERE age <= 30 |
-- 서울에 사는 고객 조회
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)
-- 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 - 범위 검색
-- 가격이 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 - 목록에서 검색
-- 서울, 부산, 대구에 사는 고객
SELECT * FROM customers
WHERE city IN ('서울', '부산', '대구');
-- NOT IN : 목록에 없는 것
SELECT * FROM orders
WHERE status NOT IN ('CANCELLED', 'PENDING');
7.5 LIKE - 패턴 검색
%는 0개 이상의 아무 문자, _는 정확히 1개의 아무 문자를 의미합니다.
-- '김'으로 시작하는 이름
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
-- 매니저가 없는 직원 (최상위 관리자)
SELECT * FROM employees
WHERE manager_id IS NULL;
-- 전화번호가 있는 고객
SELECT * FROM customers
WHERE phone IS NOT NULL;
= NULL이 아닌 IS NULL로 비교해야 합니다. WHERE phone = NULL은 항상 결과가 없습니다.
정렬(ORDER BY)과 제한(LIMIT)
8.1 ORDER BY - 결과 정렬
-- 가격 오름차순 (기본값: 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 - 결과 수 제한
-- 상위 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 - 페이징
-- 페이지당 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;
INSERT - 데이터 삽입
9.1 단일 행 삽입
-- 모든 컬럼에 값 지정
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 다중 행 삽입
INSERT INTO products (product_name, category, price, stock) VALUES
('무선 이어폰', '전자기기', 79000, 150),
('태블릿 거치대', '액세서리', 22000, 300),
('마우스 패드', '액세서리', 12000, 500);
9.3 SELECT 결과를 INSERT
-- 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
-- 중복 키 에러 무시 (삽입 건너뛰기)
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);
UPDATE - 데이터 수정
10.1 기본 UPDATE
-- 특정 고객의 등급 변경
UPDATE customers
SET grade = 'GOLD'
WHERE customer_id = 3;
-- 여러 컬럼 동시 수정
UPDATE customers
SET
city = '세종',
phone = '010-9999-0000'
WHERE customer_id = 3;
10.2 조건부 일괄 수정
-- 모든 상품 가격 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;
10.3 안전한 UPDATE 패턴
-- 1단계: 먼저 SELECT로 대상 확인
SELECT * FROM products
WHERE category = '전자기기' AND stock <= 50;
-- 2단계: 확인 후 UPDATE 실행
UPDATE products
SET stock = stock + 100
WHERE category = '전자기기' AND stock <= 50;
DELETE - 데이터 삭제
11.1 기본 DELETE
-- 특정 주문 삭제
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 | 테이블 자체 | 불가 | 불가 | 테이블 삭제 |
집계 함수 (Aggregate Functions)
12.1 주요 집계 함수
| 함수 | 설명 | NULL 처리 |
|---|---|---|
| COUNT(*) | 행의 수 | NULL 포함 |
| COUNT(column) | NULL이 아닌 행의 수 | NULL 제외 |
| SUM(column) | 합계 | NULL 제외 |
| AVG(column) | 평균 | NULL 제외 |
| MAX(column) | 최대값 | NULL 제외 |
| MIN(column) | 최소값 | NULL 제외 |
-- 전체 고객 수
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의 다양한 활용
-- 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;
GROUP BY와 HAVING
13.1 GROUP BY - 그룹별 집계
GROUP BY는 동일한 값을 가진 행을 그룹으로 묶어 집계합니다.
-- 도시별 고객 수
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;
13.2 여러 컬럼으로 GROUP BY
-- 도시별, 등급별 고객 수
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은 그룹핑 후에 필터링합니다.
-- 고객이 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 실행 순서
① FROM → ② WHERE → ③ GROUP BY → ④ HAVING → ⑤ SELECT → ⑥ ORDER BY → ⑦ LIMIT
작성 순서와 실행 순서가 다릅니다! HAVING에서 SELECT의 별칭을 사용할 수 있는 것은 MySQL의 확장 기능입니다. 표준 SQL에서는 HAVING 절에서 별칭 대신 집계 함수를 다시 써야 합니다.
13.5 GROUP BY 실전 예제
-- 등급별 고객 수와 비율
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;
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입니다.
-- 기본 문법
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;
orders o처럼 쓰면 o.order_id로 참조할 수 있습니다.
14.4 여러 테이블 JOIN
-- 주문 상세 정보: 고객명 + 상품명 + 수량 + 금액
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;
14.5 LEFT JOIN (LEFT OUTER JOIN)
왼쪽 테이블의 모든 행을 반환하고, 오른쪽에 일치하는 값이 없으면 NULL로 채웁니다.
-- 모든 고객 + 주문 정보 (주문이 없는 고객도 포함)
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;
-- ★ 실무 활용: 주문을 한 번도 하지 않은 고객 찾기
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
-- 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;
JOIN 심화
15.1 SELF JOIN (자기 자신과 조인)
같은 테이블을 두 번 참조하여 조인합니다. 대표적으로 직원-매니저 관계(계층 구조)에 사용됩니다.
-- 직원과 그 직원의 매니저 이름 함께 조회
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;
15.2 CROSS JOIN (교차 조인)
양쪽 테이블의 모든 행 조합을 만듭니다(카르테시안 곱). A 테이블 8행 × B 테이블 10행 = 80행
-- 모든 고객 × 모든 상품 조합
SELECT
c.name AS '고객명',
p.product_name AS '상품명',
p.price AS '가격'
FROM customers c
CROSS JOIN products p
LIMIT 10;
-- 실무 활용: 모든 월 × 모든 카테고리 조합 (빈 데이터 포함 리포트)
15.3 USING 절
-- 조인 컬럼 이름이 동일할 때 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으로 결합하여 구현합니다.
-- 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 실전 종합 예제
-- ★ 고객별 총 구매액과 구매 상품 수 (취소 제외)
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;
서브쿼리 (Subquery)
16.1 서브쿼리란?
서브쿼리는 SQL 문 안에 포함된 또 다른 SQL 문입니다. 괄호 ( )로 감싸서 사용하며, 메인 쿼리의 WHERE, FROM, SELECT, HAVING 등 다양한 위치에서 사용할 수 있습니다.
16.2 단일 행 서브쿼리 (WHERE절)
서브쿼리가 하나의 값만 반환할 때, 비교 연산자(=, >, < 등)와 함께 사용합니다.
-- 평균 가격보다 비싼 상품
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)
-- 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 절의 서브쿼리 (인라인 뷰)
서브쿼리의 결과를 임시 테이블처럼 사용합니다. 반드시 별칭을 지정해야 합니다.
-- 고객별 주문 통계를 구한 뒤, 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 절의 서브쿼리 (스칼라 서브쿼리)
-- 각 상품의 가격과 전체 평균 가격 비교
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보다 성능이 좋은 경우가 많습니다.
-- 주문이 있는 고객만 조회 (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는 조건에 맞는 행을 하나만 찾으면 즉시 멈추지만, IN은 서브쿼리 결과 전체를 먼저 생성합니다. 서브쿼리 결과가 큰 경우 EXISTS가, 작은 경우 IN이 더 효율적일 수 있습니다.
문자열 함수
17.1 기본 문자열 함수
-- 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 추출 · 자르기 함수
-- 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 치환 · 검색 함수
-- 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 실전 활용 예제
-- ① 이메일에서 도메인 추출
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;
날짜와 시간 함수
18.1 현재 날짜 · 시간 가져오기
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 날짜 요소 추출
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 날짜 연산 (더하기 · 빼기)
-- 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)
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;
| 코드 | 의미 | 예시 |
|---|---|---|
| %Y | 4자리 연도 | 2024 |
| %y | 2자리 연도 | 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 |
| %p | AM / PM | PM |
18.5 실전 예제
-- ① 월별 매출 집계
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;
CASE 표현식
19.1 단순 CASE
CASE는 SQL에서 if-else 논리를 구현합니다. 특정 값에 따라 다른 결과를 반환합니다.
-- 주문 상태를 한글로 변환
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 (조건식)
-- 가격대별 분류
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 + 집계 함수 (피벗 테이블)
행으로 나열된 데이터를 열(가로)로 변환하는 기법입니다.
-- 주문 상태별 건수를 가로로 표시
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를 다른 절에서 사용
-- 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;
뷰 (VIEW)
20.1 뷰란?
뷰(View)는 저장된 SELECT 쿼리입니다. 실제 데이터를 갖고 있지 않고, 조회할 때마다 내부 쿼리가 실행됩니다. 복잡한 쿼리를 이름 붙여 재사용할 수 있는 "가상 테이블"입니다.
20.2 뷰의 장점
- 복잡한 쿼리 단순화 : 자주 쓰는 JOIN, 집계 쿼리를 뷰로 만들면 간단하게 호출 가능
- 보안 강화 : 원본 테이블의 특정 컬럼만 노출하여 민감 정보 숨기기
- 데이터 독립성 : 테이블 구조가 변경되어도 뷰를 수정하면 기존 쿼리 유지 가능
20.3 뷰 생성 · 사용
-- 고객별 주문 요약 뷰
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 뷰 수정 · 삭제
-- 뷰 수정 (또는 없으면 생성)
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;
인덱스 (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 인덱스 생성 · 확인 · 삭제
-- 단일 컬럼 인덱스
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으로 실행 계획 분석
-- 인덱스 없이 조회
EXPLAIN SELECT * FROM customers WHERE city = '서울';
-- type: ALL (전체 스캔) → 느림
-- 인덱스 생성 후 조회
CREATE INDEX idx_city ON customers(city);
EXPLAIN SELECT * FROM customers WHERE city = '서울';
-- type: ref (인덱스 사용) → 빠름
const (최고) → eq_ref → ref → range → index → ALL (최악)ALL이 나오면 인덱스를 활용하지 못하고 있다는 의미입니다.
21.5 인덱스 설계 가이드
- WHERE, JOIN, ORDER BY에 자주 사용되는 컬럼
- 카디널리티(고유 값 수)가 높은 컬럼 (email, phone 등)
- 대용량 테이블 (수만 건 이상)
인덱스가 불필요하거나 해로운 경우
- 소량 데이터 테이블 (수백 건 이하)
- INSERT/UPDATE/DELETE가 매우 빈번한 테이블 (인덱스 갱신 오버헤드)
- 카디널리티가 매우 낮은 컬럼 (성별 M/F, 활성화 Y/N)
트랜잭션 (Transaction)
22.1 트랜잭션이란?
트랜잭션은 하나의 논리적 작업 단위입니다. 여러 SQL 문이 모두 성공하거나, 하나라도 실패하면 전부 취소되어야 하는 상황에서 사용합니다.
대표적인 예시는 계좌 이체입니다. A계좌에서 출금하고 B계좌에 입금하는 두 작업은 반드시 함께 성공해야 합니다. 출금만 되고 입금이 실패하면 돈이 사라지기 때문입니다.
22.2 ACID 속성
| 속성 | 영문 | 설명 | 비유 |
|---|---|---|---|
| 원자성 | Atomicity | 전부 실행 또는 전부 취소 | 전부 아니면 전무(All or Nothing) |
| 일관성 | Consistency | 트랜잭션 전후 데이터 무결성 유지 | 잔액 합계는 항상 동일 |
| 격리성 | Isolation | 동시 트랜잭션이 서로 간섭 안 함 | 각자 독립적으로 실행 |
| 지속성 | Durability | 완료된 트랜잭션은 영구 반영 | 정전 후에도 데이터 유지 |
22.3 기본 사용법
-- 트랜잭션 시작
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 (중간 저장점)
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 설정
-- 현재 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을 명시해야 수동 제어가 됩니다.
제약 조건 (Constraints)
23.1 제약 조건의 종류
| 제약 조건 | 설명 | 예시 |
|---|---|---|
| PRIMARY KEY | 고유 식별자 (NOT NULL + UNIQUE) | id INT PRIMARY KEY |
| FOREIGN KEY | 다른 테이블 참조 | REFERENCES orders(order_id) |
| NOT NULL | NULL 값 불허 | name VARCHAR(50) NOT NULL |
| UNIQUE | 중복 값 불허 | email VARCHAR(100) UNIQUE |
| CHECK | 값 검증 조건 | CHECK (age >= 0) |
| DEFAULT | 기본값 지정 | status DEFAULT 'ACTIVE' |
23.2 종합 실습 : 회원 테이블
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 외래키와 참조 무결성
-- 리뷰 테이블 : 외래키 + 참조 옵션
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 ACTION | RESTRICT와 동일 (표준 SQL 표현) | - |
23.5 제약 조건 추가 · 삭제
-- 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;
실전 프로젝트 - 쇼핑몰 데이터 분석
지금까지 배운 모든 내용을 종합하여, 실무에서 자주 사용하는 분석 쿼리를 작성해 봅니다.
24.1 매출 종합 대시보드
-- 전체 매출 요약 (취소 제외)
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 월별 매출 추이
-- 월별 매출 추이 (완료 + 배송중)
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;
24.3 베스트셀러 TOP 5
-- 판매 수량 기준 베스트셀러
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 고객 분석
-- 고객별 구매 분석 + 활동 상태 판단
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 카테고리별 매출 비중
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 재주문율 분석
-- 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 재고 부족 알림 리포트
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 부서별 인건비 보고서
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;
24.9 도시별 · 등급별 매출 피벗
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 고객 등급 자동 업데이트
-- 구매 금액 기준으로 등급 일괄 업데이트
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;
SELECT * FROM v_customer_summary처럼 간단하게 호출할 수 있습니다.
부록 - SQL 치트시트 & 학습 로드맵
25.1 SQL 핵심 문법 치트시트
/* =============================================
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 실행 순서 (반드시 암기!)
작성 순서 : 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, DELETE | Ch.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 학습에서 가장 중요한 세 가지를 기억하세요.
1. 직접 타이핑하세요 — 복사-붙여넣기 대신 직접 입력하면 손이 기억합니다. 오타를 수정하는 과정에서 문법을 체득합니다.
2. 에러를 두려워하지 마세요 — 에러 메시지를 읽고 해결하는 과정이 가장 효과적인 학습입니다. 모든 숙련된 개발자는 수만 번의 에러를 경험했습니다.
3. 나만의 프로젝트를 만드세요 — 관심 있는 주제(영화, 음악, 운동 기록, 가계부 등)로 직접 DB를 설계하고 데이터를 넣어 보세요. 실제 문제를 풀면서 배우는 것이 가장 빠릅니다.
데이터의 세계에 오신 것을 환영합니다. 이 튜토리얼이 여러분의 SQL 여정에 든든한 첫걸음이 되기를 바랍니다!