카테고리 없음

SQL 작성시 초보자가 실수할 수 있는 원리와 개념, 또 더 효율적인 쿼리

JUNGKEUNG 2025. 3. 15. 23:41
반응형

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 시 데이터 충돌 가능
🟢 락을 사용하여 충돌 방지

sql
복사편집
-- 행 레벨 락 적용 (선택한 행만 잠금) SELECT * FROM users WHERE id = 1 FOR 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 사용하여 서브쿼리 최적화