1. SQL 초보자가 자주 하는 실수
✅ 1) SELECT * 남용 (불필요한 데이터 조회)
🔴 문제점:
- 모든 컬럼을 조회하면 불필요한 데이터 전송 → 성능 저하
- 테이블 구조 변경 시 호환성 문제 발생
🟢 해결 방법:
- 필요한 컬럼만 명시적으로 조회
-- ❌ SELECT * 남용
SELECT * FROM users;
-- ✅ 필요한 컬럼만 선택
SELECT id, name, email FROM users;
✅ 2) WHERE 절 없이 UPDATE / DELETE 실행
🔴 문제점:
- WHERE 없이 실행하면 모든 데이터가 수정되거나 삭제됨
- 데이터 손실로 복구가 어려울 수 있음
🟢 해결 방법:
- WHERE 절을 반드시 포함하고, 실행 전에 SELECT로 확인
-- ❌ 모든 데이터가 삭제됨 (대참사 발생 가능!)
DELETE FROM users;
-- ✅ WHERE 조건을 추가하여 안전하게 삭제
DELETE FROM users WHERE id = 123;
✅ 3) 인덱스 미사용 (FULL SCAN 발생)
🔴 문제점:
- 인덱스 없이 검색하면 전체 테이블을 스캔 → 성능 저하
- 특히 데이터가 많을수록 속도 저하 심각
🟢 해결 방법:
- 자주 검색하는 컬럼에 인덱스 생성
- EXPLAIN 명령어로 실행 계획 확인
-- 인덱스 생성
CREATE INDEX idx_users_email ON users(email);
-- 인덱스가 적용된 조회
SELECT * FROM users WHERE email = 'test@example.com';
✅ 4) GROUP BY 사용 시 비효율적인 DISTINCT 사용
🔴 문제점:
- DISTINCT와 GROUP BY를 같이 사용하면 중복 연산 발생
- GROUP BY 자체가 중복을 제거하기 때문에 DISTINCT 불필요
🟢 해결 방법:
- DISTINCT 없이 GROUP BY만 사용
-- ❌ 비효율적인 중복 제거
SELECT DISTINCT category, COUNT(*) FROM products GROUP BY category;
-- ✅ 최적화된 쿼리
SELECT category, COUNT(*) FROM products GROUP BY category;
✅ 5) OR 조건으로 인해 인덱스 비활성화
🔴 문제점:
- OR 연산자가 포함된 경우 인덱스가 제대로 동작하지 않을 수 있음
- 풀 테이블 스캔(FULL SCAN) 발생
🟢 해결 방법:
- UNION ALL을 사용하여 쿼리 분리
- 인덱스 활용이 가능한 IN을 사용
-- ❌ 인덱스 비활성화 발생 가능
SELECT * FROM users WHERE email = 'test@example.com' OR phone = '010-1234-5678';
-- ✅ UNION ALL 사용 (각 조건에 인덱스 적용 가능)
SELECT * FROM users WHERE email = 'test@example.com'
UNION ALL
SELECT * FROM users WHERE phone = '010-1234-5678';
2. SQL 성능 최적화 방법
✅ 1) EXPLAIN으로 실행 계획 확인
- 쿼리 실행 전에 EXPLAIN을 사용하여 실행 계획 분석
- 중요한 필드:
- type: ALL(풀스캔) → index → range → ref → eq_ref → const (좋은 순서)
- key: 사용된 인덱스 확인
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
✅ 2) WHERE 절 최적화 (Index-Friendly Queries)
🔴 잘못된 예:
-- 인덱스 미사용 (함수 사용 시)
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
🟢 올바른 예:
-- 인덱스를 사용할 수 있도록 변경
SELECT * FROM users WHERE email = 'test@example.com';
✅ 3) LIMIT 사용하여 페이징 최적화
🔴 문제점:
- OFFSET이 클 경우 성능 저하 발생
🟢 해결 방법:
- WHERE 조건을 사용하여 직접 조회하는 방식 적용
-- ❌ OFFSET이 클수록 성능 저하
SELECT * FROM users ORDER BY id LIMIT 10000, 20;
-- ✅ WHERE로 직접 탐색 (빠름)
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 20;
✅ 4) JOIN 최적화
🔴 잘못된 예:
- 큰 테이블을 JOIN할 때, 필터링 없이 실행하면 불필요한 연산 증가
🟢 올바른 예:
- JOIN 전에 WHERE로 필터링 → 처리해야 할 데이터 줄이기
-- ❌ 비효율적인 JOIN
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- ✅ JOIN 전에 필터링 적용
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';
✅ 5) EXISTS vs IN 최적화
🔴 비효율적인 IN 사용:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
🟢 EXISTS 사용 (더 빠름):
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
✅ 1) 서브쿼리 vs 조인 (JOIN이 항상 더 좋은가?)
🔴 잘못된 개념:
- 서브쿼리보다 JOIN이 항상 더 빠르다고 생각하는 경우
🟢 올바른 개념:
- JOIN은 큰 데이터셋 처리에 유리
- 서브쿼리는 필터링 조건이 적을 때 유리
- 인덱스가 있는 경우 → EXISTS가 IN보다 빠를 수 있음
-- ❌ 비효율적인 서브쿼리
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
-- ✅ 더 나은 JOIN 활용
SELECT users.* FROM users JOIN orders ON users.id = orders.user_id;
✅ 언제 서브쿼리가 유리한가?
- 데이터셋이 매우 작을 때
- 특정 조건에만 데이터를 필터링할 때
✅ 2) WITH (Common Table Expressions, CTE) 활용
🔴 일반적인 서브쿼리의 문제점:
- 중복 사용되면 비효율적
- 가독성이 떨어짐
🟢 CTE를 사용하면:
- 쿼리 가독성 향상
- 재사용 가능
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
)
SELECT * FROM active_users WHERE name LIKE 'J%';
✅ 3) PARTITION BY (윈도우 함수 활용)
🔴 일반적인 GROUP BY의 문제점:
- 집계 결과는 나오지만, 원본 데이터도 보고 싶을 때 어려움
🟢 윈도우 함수(PARTITION BY) 사용
- GROUP BY 없이도 각 행별 집계 가능
- 순위 계산, 이동 평균 등에서 유용
-- 각 사용자의 총 주문 개수를 함께 조회
SELECT user_id, order_id, COUNT(*) OVER (PARTITION BY user_id) AS total_orders
FROM orders;
2. 데이터베이스 성능 최적화 기법
✅ 1) 테이블 정규화 & 비정규화 전략
정규화(Normalization)
- 중복 최소화
- 데이터 무결성 보장
- JOIN이 많아지면서 읽기 성능이 저하될 수 있음
-- 정규화된 구조: orders와 users 테이블 분리
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
비정규화(Denormalization)
- 읽기 성능 향상
- JOIN을 줄여 빠르게 조회 가능
- 저장 공간이 늘어나고, 데이터 중복 발생
-- 비정규화된 테이블: users 정보를 orders에 포함
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(100) -- 중복 저장
);
✅ 언제 정규화 vs 비정규화를 사용할까?
- 쓰기(INSERT, UPDATE) 성능 중요 → 정규화
- 읽기(SELECT) 성능 중요 → 비정규화
✅ 2) 배치 처리 vs 단일 트랜잭션
🔴 대량 데이터 처리 시 1개씩 INSERT/UPDATE 실행하면 성능 저하
🟢 배치 처리(BULK INSERT)를 활용
-- ❌ 비효율적인 개별 INSERT
INSERT INTO users (id, name) VALUES (1, 'Alice');
INSERT INTO users (id, name) VALUES (2, 'Bob');
-- ✅ 배치 INSERT
INSERT INTO users (id, name) VALUES
(1, 'Alice'),
(2, 'Bob');
✅ 3) 데이터 캐싱 활용 (Redis, Memcached 등)
🔴 SQL 쿼리를 반복 실행하면 성능 저하
🟢 자주 사용하는 데이터는 캐시에 저장하여 속도 향상
- 캐싱 가능한 데이터 예시
- 자주 조회하는 SELECT 결과
- API 응답 캐싱
- 세션 정보 저장
-- MySQL에서 query cache 활성화 (MySQL 5.7 이하)
SET GLOBAL query_cache_size = 5242880; -- 5MB 캐시
→ 최신 MySQL(8.0 이상)에서는 Redis 같은 외부 캐시 활용을 권장
3. 추가적으로 알면 좋은 개념
✅ 1) 트랜잭션(Transaction)과 ACID 원칙
- BEGIN TRANSACTION → COMMIT 또는 ROLLBACK
- 데이터 무결성을 보장하려면 ACID 원칙 준수
Atomicity (원자성) | 모든 작업이 성공하거나 실패해야 함 |
Consistency (일관성) | 데이터 무결성이 유지됨 |
Isolation (격리성) | 트랜잭션 간 간섭 방지 |
Durability (지속성) | 트랜잭션이 완료되면 영구 저장됨 |
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
✅ 2) 락(Lock)과 동시성 제어
🔴 동시에 여러 사용자가 UPDATE 시 데이터 충돌 가능
🟢 락을 사용하여 충돌 방지
- READ UNCOMMITTED → Dirty Read 허용
- READ COMMITTED → 트랜잭션 커밋 후 데이터 조회
- REPEATABLE READ → 같은 트랜잭션 내에서 항상 같은 데이터
- SERIALIZABLE → 가장 높은 격리 수준 (성능 저하 가능)
3. SQL 작성 시 팁 요약
✅ 불필요한 SELECT * 피하기 → 필요한 컬럼만 선택
✅ WHERE 절 없이 DELETE / UPDATE 실행 금지
✅ 자주 조회하는 컬럼에 INDEX 적용
✅ GROUP BY 사용 시 DISTINCT 중복 사용 금지
✅ OR 대신 UNION ALL 또는 IN 사용
✅ 쿼리 실행 전에 EXPLAIN으로 성능 확인
✅ OFFSET 대신 WHERE + LIMIT 사용하여 페이징 최적화
✅ JOIN 전에 WHERE로 필터링하여 성능 최적화
✅ IN 대신 EXISTS 사용하여 서브쿼리 최적화