SQL 기초와 활용
SQL 완벽 가이드: 기초부터 고급까지
서론
SQL(Structured Query Language)은 관계형 데이터베이스 관리 시스템(RDBMS)에서 데이터를 조작하고 관리하기 위한 표준 언어입니다. 웹 개발에서 데이터베이스는 핵심적인 역할을 하며, 효율적인 SQL 작성 능력은 개발자에게 필수적입니다. 이 글에서는 SQL의 기초부터 고급 기법까지 실용적인 예제와 함께 다루겠습니다.
1. SQL 기본 문법
1.1 SELECT 문
데이터 조회의 기본:
-- 모든 컬럼 조회
SELECT * FROM users;
-- 특정 컬럼 조회
SELECT id, name, email FROM users;
-- 별칭(Alias) 사용
SELECT
id AS user_id,
name AS user_name,
email AS user_email
FROM users;
1.2 WHERE 절
조건부 데이터 조회:
-- 단일 조건
SELECT * FROM users WHERE age > 18;
-- 여러 조건 (AND, OR)
SELECT * FROM users
WHERE age > 18 AND status = 'active';
SELECT * FROM users
WHERE age > 18 OR status = 'premium';
-- IN 연산자
SELECT * FROM users
WHERE status IN ('active', 'premium', 'vip');
-- LIKE 연산자 (패턴 매칭)
SELECT * FROM users
WHERE email LIKE '%@gmail.com';
-- BETWEEN 연산자
SELECT * FROM users
WHERE age BETWEEN 20 AND 30;
-- NULL 체크
SELECT * FROM users
WHERE email IS NOT NULL;
1.3 ORDER BY
정렬:
-- 오름차순 정렬
SELECT * FROM users ORDER BY name ASC;
-- 내림차순 정렬
SELECT * FROM users ORDER BY created_at DESC;
-- 여러 컬럼 정렬
SELECT * FROM users
ORDER BY status ASC, created_at DESC;
1.4 LIMIT과 OFFSET
결과 제한:
-- 상위 10개만 조회
SELECT * FROM users LIMIT 10;
-- 페이징 처리
SELECT * FROM users
ORDER BY id DESC
LIMIT 10 OFFSET 20; -- 21번째부터 10개
2. 데이터 조작 (DML)
2.1 INSERT
데이터 삽입:
-- 단일 행 삽입
INSERT INTO users (name, email, age)
VALUES ('홍길동', 'hong@example.com', 25);
-- 여러 행 한 번에 삽입
INSERT INTO users (name, email, age)
VALUES
('김철수', 'kim@example.com', 30),
('이영희', 'lee@example.com', 28),
('박민수', 'park@example.com', 32);
-- SELECT 결과를 INSERT
INSERT INTO users_backup (name, email, age)
SELECT name, email, age FROM users
WHERE created_at < '2024-01-01';
2.2 UPDATE
데이터 수정:
-- 단일 행 수정
UPDATE users
SET email = 'newemail@example.com'
WHERE id = 1;
-- 여러 컬럼 수정
UPDATE users
SET
email = 'newemail@example.com',
age = 26,
updated_at = NOW()
WHERE id = 1;
-- 조건부 수정
UPDATE users
SET status = 'inactive'
WHERE last_login < DATE_SUB(NOW(), INTERVAL 1 YEAR);
2.3 DELETE
데이터 삭제:
-- 조건부 삭제
DELETE FROM users WHERE id = 1;
-- 여러 조건 삭제
DELETE FROM users
WHERE status = 'inactive'
AND last_login < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- 전체 삭제 (주의!)
DELETE FROM users;
-- 또는
TRUNCATE TABLE users;
3. 집계 함수
3.1 기본 집계 함수
-- COUNT: 행 개수
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM users WHERE status = 'active';
-- SUM: 합계
SELECT SUM(price) FROM orders;
-- AVG: 평균
SELECT AVG(age) FROM users;
-- MAX/MIN: 최대/최소값
SELECT MAX(price) FROM products;
SELECT MIN(price) FROM products;
3.2 GROUP BY
그룹별 집계:
-- 상태별 사용자 수
SELECT status, COUNT(*) as count
FROM users
GROUP BY status;
-- 연도별 주문 금액 합계
SELECT
YEAR(created_at) as year,
SUM(total_amount) as total
FROM orders
GROUP BY YEAR(created_at);
-- 여러 컬럼으로 그룹화
SELECT
status,
country,
COUNT(*) as count
FROM users
GROUP BY status, country;
3.3 HAVING
그룹 조건 필터링:
-- 사용자 수가 10명 이상인 상태만 조회
SELECT status, COUNT(*) as count
FROM users
GROUP BY status
HAVING COUNT(*) >= 10;
-- 평균 주문 금액이 10000원 이상인 고객
SELECT
user_id,
AVG(total_amount) as avg_amount
FROM orders
GROUP BY user_id
HAVING AVG(total_amount) >= 10000;
4. JOIN 연산
4.1 INNER JOIN
교집합 조인:
-- 사용자와 주문 정보 조인
SELECT
u.name,
u.email,
o.order_id,
o.total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 여러 테이블 조인
SELECT
u.name,
p.product_name,
oi.quantity,
oi.price
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;
4.2 LEFT JOIN
왼쪽 테이블 기준 조인:
-- 모든 사용자와 주문 정보 (주문이 없는 사용자도 포함)
SELECT
u.name,
u.email,
o.order_id,
o.total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 주문이 없는 사용자만 조회
SELECT
u.name,
u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
4.3 RIGHT JOIN
오른쪽 테이블 기준 조인:
-- 모든 주문과 사용자 정보 (사용자 정보가 없는 주문도 포함)
SELECT
u.name,
o.order_id,
o.total_amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
4.4 FULL OUTER JOIN
양쪽 테이블 모두 포함:
-- MySQL에서는 지원하지 않음 (UNION으로 대체)
SELECT
u.name,
o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT
u.name,
o.order_id
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
5. 서브쿼리
5.1 스칼라 서브쿼리
단일 값 반환:
-- 각 사용자의 주문 수 조회
SELECT
name,
email,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
FROM users;
-- 평균보다 높은 주문 금액
SELECT * FROM orders
WHERE total_amount > (SELECT AVG(total_amount) FROM orders);
5.2 인라인 뷰 (FROM 절 서브쿼리)
-- 각 카테고리의 평균 가격보다 높은 상품
SELECT
p.name,
p.price,
p.category_id,
avg_price.avg_price
FROM products p
INNER JOIN (
SELECT category_id, AVG(price) as avg_price
FROM products
GROUP BY category_id
) avg_price ON p.category_id = avg_price.category_id
WHERE p.price > avg_price.avg_price;
5.3 EXISTS와 NOT EXISTS
존재 여부 확인:
-- 주문이 있는 사용자만 조회
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);
-- 주문이 없는 사용자 조회
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);
6. 윈도우 함수
6.1 ROW_NUMBER
행 번호 부여:
-- 각 카테고리별 가격 순위
SELECT
name,
category_id,
price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) as rank
FROM products;
6.2 RANK와 DENSE_RANK
순위 부여:
-- RANK: 동일 값은 같은 순위, 다음 순위는 건너뜀
SELECT
name,
score,
RANK() OVER (ORDER BY score DESC) as rank
FROM students;
-- DENSE_RANK: 동일 값은 같은 순위, 다음 순위는 연속
SELECT
name,
score,
DENSE_RANK() OVER (ORDER BY score DESC) as rank
FROM students;
6.3 집계 윈도우 함수
-- 누적 합계
SELECT
date,
amount,
SUM(amount) OVER (ORDER BY date) as cumulative_sum
FROM daily_sales;
-- 이동 평균
SELECT
date,
amount,
AVG(amount) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) as moving_avg_7days
FROM daily_sales;
-- 파티션별 집계
SELECT
category_id,
name,
price,
AVG(price) OVER (PARTITION BY category_id) as avg_category_price
FROM products;
7. 인덱스와 성능 최적화
7.1 인덱스 생성
-- 단일 컬럼 인덱스
CREATE INDEX idx_email ON users(email);
-- 복합 인덱스
CREATE INDEX idx_status_created ON users(status, created_at);
-- 고유 인덱스
CREATE UNIQUE INDEX idx_unique_email ON users(email);
-- 인덱스 삭제
DROP INDEX idx_email ON users;
7.2 쿼리 최적화
-- 나쁜 예: 인덱스 미사용
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- 좋은 예: 인덱스 활용
SELECT * FROM users
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';
-- 나쁜 예: 와일드카드 앞에 %
SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- 좋은 예: 인덱스 활용 가능
SELECT * FROM users WHERE email LIKE 'user%@gmail.com';
8. 트랜잭션
8.1 트랜잭션 기본
-- 트랜잭션 시작
START TRANSACTION;
-- 작업 수행
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- 커밋 또는 롤백
COMMIT;
-- 또는
ROLLBACK;
8.2 자동 커밋 제어
-- 자동 커밋 비활성화
SET autocommit = 0;
-- 작업 수행
INSERT INTO orders (user_id, total_amount) VALUES (1, 50000);
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 10, 2);
-- 커밋
COMMIT;
-- 자동 커밋 재활성화
SET autocommit = 1;
9. 뷰 (Views)
9.1 뷰 생성
-- 단순 뷰
CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE status = 'active';
-- 복합 뷰
CREATE VIEW user_order_summary AS
SELECT
u.id,
u.name,
u.email,
COUNT(o.id) as order_count,
SUM(o.total_amount) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name, u.email;
-- 뷰 사용
SELECT * FROM active_users;
SELECT * FROM user_order_summary WHERE order_count > 5;
9.2 뷰 수정 및 삭제
-- 뷰 수정
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, created_at, last_login
FROM users
WHERE status = 'active';
-- 뷰 삭제
DROP VIEW active_users;
10. 저장 프로시저와 함수
10.1 저장 프로시저
-- 프로시저 생성
DELIMITER //
CREATE PROCEDURE GetUserOrders(IN user_id INT)
BEGIN
SELECT
o.id,
o.order_date,
o.total_amount,
o.status
FROM orders o
WHERE o.user_id = user_id
ORDER BY o.order_date DESC;
END //
DELIMITER ;
-- 프로시저 호출
CALL GetUserOrders(1);
-- 프로시저 삭제
DROP PROCEDURE GetUserOrders;
10.2 저장 함수
-- 함수 생성
DELIMITER //
CREATE FUNCTION GetOrderCount(user_id INT)
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE order_count INT;
SELECT COUNT(*) INTO order_count
FROM orders
WHERE user_id = user_id;
RETURN order_count;
END //
DELIMITER ;
-- 함수 사용
SELECT
name,
GetOrderCount(id) as order_count
FROM users;
11. 트리거
11.1 트리거 생성
-- INSERT 트리거
DELIMITER //
CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
SET NEW.updated_at = NOW();
END //
DELIMITER ;
-- UPDATE 트리거
DELIMITER //
CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
SET NEW.updated_at = NOW();
END //
DELIMITER ;
-- DELETE 트리거 (로그 기록)
DELIMITER //
CREATE TRIGGER after_user_delete
AFTER DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO user_deletion_log (user_id, deleted_at)
VALUES (OLD.id, NOW());
END //
DELIMITER ;
12. 실전 예제
12.1 복잡한 리포트 쿼리
-- 월별 매출 리포트
SELECT
DATE_FORMAT(order_date, '%Y-%m') as month,
COUNT(DISTINCT user_id) as unique_customers,
COUNT(*) as total_orders,
SUM(total_amount) as total_revenue,
AVG(total_amount) as avg_order_value
FROM orders
WHERE order_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month DESC;
12.2 사용자 활동 분석
-- 사용자별 활동 요약
SELECT
u.id,
u.name,
u.email,
COUNT(DISTINCT o.id) as order_count,
SUM(o.total_amount) as total_spent,
MAX(o.order_date) as last_order_date,
DATEDIFF(NOW(), MAX(o.order_date)) as days_since_last_order
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name, u.email
HAVING order_count > 0
ORDER BY total_spent DESC;
12.3 상위 N개 조회
-- 카테고리별 상위 3개 상품
SELECT
category_id,
name,
price,
rank
FROM (
SELECT
category_id,
name,
price,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY price DESC
) as rank
FROM products
) ranked_products
WHERE rank <= 3;
13. 보안과 SQL 인젝션 방지
13.1 준비된 문 (Prepared Statements)
// Node.js 예제
const mysql = require('mysql2/promise');
// 나쁜 예: SQL 인젝션 취약
const query = `SELECT * FROM users WHERE email = '${email}'`;
// 좋은 예: Prepared Statement
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
);
13.2 입력 검증
-- 입력값 검증 예제
SELECT * FROM users
WHERE id = ?
AND status = 'active'
LIMIT 1;
14. 결론
SQL은 데이터베이스와 상호작용하는 핵심 언어입니다. 효율적인 SQL 작성 능력은 웹 개발에서 필수적이며, 적절한 인덱스 사용, 쿼리 최적화, 트랜잭션 관리 등을 통해 성능과 안정성을 확보할 수 있습니다.
주요 포인트:
- 기본 문법 숙지: SELECT, INSERT, UPDATE, DELETE
- JOIN 활용: 테이블 간 관계 이해
- 집계 함수: GROUP BY, HAVING 활용
- 서브쿼리와 윈도우 함수: 복잡한 데이터 처리
- 성능 최적화: 인덱스 활용, 쿼리 튜닝
- 보안: SQL 인젝션 방지
지속적인 학습과 실습을 통해 SQL 실력을 향상시킬 수 있습니다.