DB Study

🧩 RDB 공통 원리

PostgreSQL을 알아도, Oracle을 알아도, 결국 매일 부딪히는 문제는 같습니다. "동시에 같은 행을 건드리면 어떻게 되는가", "이 쿼리는 왜 느린가". 이 트랙은 특정 제품이 아니라 모든 관계형 DB가 공유하는 원리를 다룹니다. 락·트랜잭션·격리 수준·조인 알고리즘·튜닝 — 한 번 배우면 어느 DB로 옮겨도 그대로 쓰입니다.

📚 5트랙 16장 🧭 벤더 중립 🔐 락 4장 · 🔗 조인 1장 · 🚫 안티패턴 20+ 🎬 애니메이션 2개
🧭
이 트랙과 벤더 가이드의 역할 분담

같은 주제를 두 번 배우는 게 아닙니다. 여기서는 왜 이 문제가 존재하고 어떤 선택지가 있는가를, 벤더 가이드에서는 그 DB가 그걸 어떻게 구현했는가를 봅니다.

주제🧩 RDB 공통 (여기)벤더 가이드
격리 수준 이상현상 5종이 왜 생기는가, 표준 4단계, 벤더별 해석 차이 PG 7장 — 스냅샷·xmin, pg_stat_activity
동시성 제어 MVCC와 2PL의 구조적 차이, 무엇을 포기하고 무엇을 얻는가 PG 3장 — 튜플 버전, Oracle 6장 — Undo
낙관적·비관적·분산 락의 선택 기준과 함정 PG 7장pg_locks, Oracle 8장 — 락 모드
실행계획·조인 NL/Hash/Merge의 비용 모델, 스캔 방식 선택 원리 PG 6장 — EXPLAIN, Oracle 튜닝 팁
인덱스 B+Tree 구조, 선두 컬럼 규칙, 커버링·선택도 PG 5장 — GIN·BRIN 등 6종, Oracle 9장

먼저 이 장면부터 — 재고 1개가 사라지는 순간

커리큘럼을 읽기 전에, 이 트랙 전체가 왜 필요한지 보여주는 장면 하나를 봅시다. 재고 10개짜리 상품을 두 사용자가 동시에 1개씩 주문했습니다. 코드에는 버그가 없습니다. 그런데 재고는 8이 아니라 9가 됩니다.

🛒 상품 A — 재고 10개, 두 주문이 같은 순간에 도착 👤 세션 A — 주문 1건 BEGIN; 👤 세션 B — 주문 1건 BEGIN; products (id=1) stock = 10 stock = 9 stock = 9 stock = 8 ✓ SELECT stock → 10 SELECT stock → 10 UPDATE … stock = 10-1; COMMIT; UPDATE … stock = 10-1; COMMIT; ❌ Lost Update 2건을 팔았는데 재고는 1개만 줄었다 — B가 A의 결과를 덮어씀 🔒 처방 1 — 비관적 락 (Pessimistic Lock) A: SELECT stock FROM products WHERE id=1 FOR UPDATE; → 행에 배타 락 B: SELECT … FOR UPDATE; → ⏳ A가 COMMIT할 때까지 대기(블로킹) B: 대기 해제 후 stock=9를 다시 읽고 → 8로 UPDATE ✓ 👍 충돌이 잦을 때 확실하다  ·  👎 대기·데드락·동시성 저하 — 트랜잭션을 짧게! 🔁 처방 2 — 낙관적 락 (Optimistic Lock) 둘 다 stock=10, version=3을 읽는다 (락 없음) A: UPDATE … SET stock=9, version=4 WHERE id=1 AND version=3; → 1 row ✓ B: UPDATE … WHERE id=1 AND version=3; → 0 rows → 충돌 감지 → 재조회 후 재시도 → 8 ✓ 👍 대기가 없어 처리량이 높다  ·  👎 경합이 심하면 재시도 폭주 — 백오프 필수 🌐 처방 3 — 분산 락 (Distributed Lock) DB 한 대로 못 막는 경우: 앱 인스턴스 3대의 스케줄러가 같은 배치를 동시 실행, 또는 외부 결제 API 호출처럼 DB 트랜잭션 밖에 있는 자원을 보호할 때. SET lock:order:1 <token> NX PX 3000 → 획득한 1대만 진행 · fencing token 동봉 ⚠️ 마지막 질문: 유니크 제약이나 멱등키로 풀 수 있다면 분산 락은 쓰지 않는 게 정답이다.
🎯
이 트랙을 끝내면 답할 수 있는 질문들

"재고 차감에 비관적 락과 낙관적 락 중 뭘 쓰죠?" · "우리 서비스 격리 수준이 Read Committed인데 괜찮나요?" · "이 쿼리에 인덱스가 있는데 왜 Full Scan이 뜨죠?" · "Hash Join이 Nested Loop보다 항상 빠른가요?" · "스케줄러가 인스턴스마다 중복 실행되는데 분산 락이 답인가요?"

커리큘럼 — 5트랙 16장

앞의 순서대로 읽으면 뒤 장의 발판이 됩니다. 각 카드의 "이 장에서 답하는 질문"이 곧 그 장의 학습 목표입니다.

T1. 기초 — 옵티마이저를 이해하기 위한 전제 기초

3~5장의 튜닝 이야기는 전부 이 세 장 위에 서 있습니다. "인덱스를 타지 않는 이유"를 외우지 않고 설명할 수 있게 만드는 것이 목표입니다.

01RDB 모델과 SQL 논리적 실행 순서

우리가 쓰는 순서(SELECT→FROM→WHERE)와 DB가 처리하는 순서(FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT)는 다릅니다. 이 차이 하나가 수많은 "왜 안 되지?"의 원인입니다.

  • WHERE에서는 SELECT 별칭을 못 쓰는데 ORDER BY에서는 되나?
  • WHEREHAVING은 무엇이 다른가?
  • NULL은 왜 = NULL로 비교되지 않는가? 3값 논리란?
기초집필 예정

02페이지·블록과 B+Tree 인덱스 구조

인덱스를 "빠르게 해주는 마법"이 아니라 정렬된 트리 자료구조로 이해합니다. 여기서 복합 인덱스의 선두 컬럼 규칙이 자연스럽게 유도됩니다.

  • 루트→리프 탐색이 왜 로그 시간인가? 레벨 3~4가 왜 흔한가?
  • (a, b, c) 인덱스에서 WHERE b = ?가 왜 못 타는가?
  • 커버링 인덱스·클러스터링 팩터·선택도(selectivity)란?
기초집필 예정

03통계정보와 카디널리티 추정

옵티마이저의 모든 판단은 "이 조건에 몇 행이 걸릴까?"라는 추정에서 출발합니다. 추정이 틀어지면 플랜 전체가 무너집니다.

  • 히스토그램과 NDV로 행 수를 어떻게 추정하는가?
  • 추정 1행 → 실제 100만 행이면 무슨 일이 벌어지는가?
  • 통계가 낡으면 왜 어제 빠르던 쿼리가 오늘 느려지는가?
중급집필 예정

T2. 트랜잭션과 격리 수준 — 정합성의 뼈대 중급

요청하신 "트랜잭션 / 아이솔레이션 레벨"의 본진입니다. 이상현상 5종을 세션 2개 타임라인으로 직접 재현해 보는 실습이 중심입니다.

04트랜잭션과 ACID, 그리고 트랜잭션 경계 설계

ACID 암기가 아니라, 트랜잭션을 어디서 열고 어디서 닫을 것인가라는 실무 설계 문제로 접근합니다. 장애의 상당수가 여기서 시작됩니다.

  • savepoint·중첩 트랜잭션·autocommit은 실제로 어떻게 동작하나?
  • 트랜잭션 안에서 외부 API를 호출하면 왜 위험한가?
  • @Transactional 범위를 넓게 잡으면 무엇이 무너지나?
기초집필 예정

05격리 수준 4단계 × 이상현상 5종

Dirty Read · Non-repeatable Read · Phantom · Lost Update · Write Skew를 각각 재현하고, 어느 격리 수준에서 막히는지 눈으로 확인합니다.

  • Read Committed에서 같은 쿼리가 두 번 다른 결과를 주는 이유는?
  • MySQL의 RR과 PostgreSQL의 RR은 왜 다르게 동작하는가?
  • Serializable만 막을 수 있는 Write Skew는 어떤 모양인가?
중급집필 예정

06MVCC vs 2PL — 동시성 제어의 두 갈래

"읽기가 쓰기를 막지 않는다"는 말이 공짜가 아니라 트레이드오프라는 것을 이해합니다. 두 방식의 비용 구조를 비교합니다.

  • 스냅샷 방식은 무엇을 대가로 읽기 락을 없앴는가?
  • PG는 왜 VACUUM이 필요하고 Oracle은 왜 Undo가 필요한가?
  • SQL Server의 기본 잠금 방식은 왜 읽기가 쓰기를 막는가?
고급집필 예정

T3. 락 — 동시성의 실전 이 트랙의 하이라이트

요청하신 낙관적 락 · 비관적 락 · 분산 락을 각각 한 장씩, 예시 코드와 함께 다룹니다. 앞의 애니메이션이 이 트랙 전체의 예고편입니다.

07DB 락의 종류 — 무엇이 무엇을 막는가

락을 "걸린다/안 걸린다"가 아니라 모드 × 대상 × 호환성 행렬로 이해합니다. 락 대기 체인을 읽는 법까지.

  • 공유(S)·배타(X)·의도(IS/IX) 락은 서로 어떻게 충돌하는가?
  • 행·페이지·테이블 락과 락 에스컬레이션이란?
  • FK가 걸린 테이블에 INSERT하면 왜 부모 행이 잠기는가?
  • DDL(ALTER TABLE) 한 줄이 왜 서비스 전체를 멈추는가?
중급집필 예정

08비관적 락 — 먼저 잠그고 시작한다

FOR UPDATE 계열의 모든 것. 특히 SKIP LOCKED를 이용한 작업 큐 패턴은 실무에서 바로 쓰는 무기입니다.

  • FOR UPDATEFOR SHARE는 언제 갈리는가?
  • NOWAIT / SKIP LOCKED로 대기를 어떻게 제어하나?
  • 여러 워커가 겹치지 않게 작업을 집어가는 큐를 어떻게 만드나?
  • 락 획득 순서가 왜 데드락을 만들고, 어떻게 표준화하나?
중급집필 예정

09낙관적 락 — 일단 진행하고 커밋에서 검증한다

version 컬럼 한 개로 대기 없는 동시성을 얻는 방법과, 그 대가인 재시도 전략을 설계합니다.

  • version / updated_at / 값 비교(CAS) 중 무엇을 쓸 것인가?
  • 충돌 시 재시도를 몇 번, 얼마 간격으로? (지수 백오프)
  • JPA @Version은 내부적으로 어떤 SQL을 만드는가?
  • 충돌률 몇 %부터 비관적 락이 유리해지는가?
중급집필 예정

10분산 락 — DB 하나로 못 막을 때

가장 오남용되는 도구입니다. 먼저 "정말 필요한가?"를 묻는 체크리스트부터 시작해서, 필요할 때 안전하게 쓰는 법을 봅니다.

  • DB advisory lock / Redis / ZooKeeper·etcd — 무엇을 언제?
  • TTL이 만료됐는데 작업이 안 끝났다면? (fencing token)
  • Redlock 논쟁 — 왜 "안전하지 않다"는 반론이 나왔나?
  • 유니크 제약·멱등키로 락 없이 푸는 방법은 없는가?
고급집필 예정

11데드락과 락 경합 — 실전 진단

실제 데드락 로그를 놓고 누가 무엇을 기다렸는지 역추적하는 훈련. 락 경합은 대개 코드가 아니라 순서의 문제입니다.

  • 데드락 로그에서 두 트랜잭션의 락 순서를 어떻게 읽는가?
  • 핫 로우(인기 상품 한 줄)에 몰리는 경합은 어떻게 분산하나?
  • lock_timeout / innodb_lock_wait_timeout은 얼마가 적절한가?
고급집필 예정

T4. 실행계획과 조인 — 쿼리는 이렇게 실행된다 중급

요청하신 Nested Loop · Hash Join 등 조인 알고리즘이 여기 있습니다. 실행계획을 읽는 법 → 스캔 방식 → 조인 → 집계 순으로, 플랜 트리를 아래에서 위로 읽어 올라가는 순서입니다.

12실행계획 읽는 법 — 벤더 공통 문법

PostgreSQL EXPLAIN ANALYZE, MySQL EXPLAIN ANALYZE, Oracle DBMS_XPLAN은 표기만 다를 뿐 읽는 방법은 같습니다.

  • 플랜 트리는 어디서부터 읽어야 하는가?
  • 추정 행 수 vs 실제 행 수의 괴리를 어디서 찾는가? (튜닝의 8할)
  • cost·rows·actual time·loops는 각각 무엇을 뜻하는가?
중급집필 예정

13스캔 방식 — 인덱스를 타거나, 못 타거나

"인덱스를 만들었는데 왜 안 타나요?"에 대한 완전한 답 8가지를 정리합니다. 가장 자주 받는 질문이자 가장 자주 잘못 진단되는 문제입니다.

  • Full/Seq Scan이 오히려 빠른 경우는 언제인가?
  • Index Only Scan과 Bitmap Scan은 언제 선택되는가?
  • 암묵적 형변환·함수 감싸기·LIKE '%x'가 왜 인덱스를 죽이는가?
중급집필 예정

14조인 알고리즘 — NL · Hash · Sort Merge

조인 문법(INNER/OUTER)이 아니라 조인 알고리즘을 다룹니다. 옵티마이저가 왜 그걸 골랐는지 설명할 수 있게 됩니다.

  • 세 알고리즘의 비용 공식과 손익분기점은?
  • 드라이빙 테이블(선행 집합)은 왜 작은 쪽이어야 하는가?
  • 조인 순서 경우의 수가 폭발하면 옵티마이저는 어떻게 하는가?
  • 세미/안티 조인, 서브쿼리 언네스팅이란?
중급집필 예정

15집계·정렬·그룹핑의 비용

조인만큼 자주 병목이 되지만 훨씬 덜 이야기되는 영역. 정렬이 메모리를 넘치면 무슨 일이 벌어지는지 봅니다.

  • Sort Aggregate와 Hash Aggregate는 어떻게 갈리는가?
  • 작업 메모리 초과 시 디스크 spill이 성능에 얼마나 영향을 주나?
  • DISTINCT·윈도우 함수를 남용하면 어디서 터지는가?
고급집필 예정

T5. 튜닝과 안티패턴 — 실무로 내려오기 실전

요청하신 튜닝 기법쓰면 안 되는 쿼리가 여기 모입니다. 개별 기법을 나열하기 전에 방법론을 먼저 세웁니다 — 측정 없이 하는 튜닝은 도박이기 때문입니다.

16SQL 튜닝 방법론과 안티패턴 카탈로그

느린 쿼리를 찾고 → 원인을 좁히고 → 고치고 → 검증하는 루프. 그리고 현업에서 반복해서 발견되는 안티패턴 20+개를 ❌ 나쁜 쿼리 / 왜 / ✅ 개선 3단으로.

  • 느린 쿼리를 어디서 찾는가? (slow log · 대기 이벤트 · top query)
  • 인덱스 추가·쿼리 재작성·스키마 변경 중 무엇을 먼저?
  • 대용량 UPDATE/DELETE를 안전하게 쪼개는 방법은?
  • 딥 페이지네이션(OFFSET 1000000)을 어떻게 없애는가?
고급집필 예정

🔐 락 3종 비교 — 지금 바로 고르기

16장을 다 읽기 전에도 쓸 수 있도록, 세 가지 락의 선택 기준을 한 표로 정리했습니다. (9장·10장의 요약)

구분🔁 낙관적 락🔒 비관적 락🌐 분산 락
전제충돌은 드물다충돌은 자주 난다여러 프로세스/노드가 경쟁한다
구현version 컬럼 + WHERE version = ?SELECT … FOR UPDATERedis SET NX PX, ZooKeeper, DB advisory lock
대기없음 (실패 후 재시도)있음 (블로킹)있음 (TTL·타임아웃)
충돌 감지 시점UPDATE 실행 시 (0행)락 획득 시 (선점)락 획득 시
데드락없음있음 — 락 순서 표준화 필요드묾 — 대신 락 만료 중 이중 실행 위험
주 비용재시도 (CPU·응답시간 분산)동시성 저하 (처리량)네트워크 왕복 + 운영 복잡도
잘 맞는 곳게시글·프로필 수정, 낮은 경합 갱신재고 차감, 잔액 이체, 좌석 예약다중 인스턴스 스케줄러, 외부 API 중복 호출 방지
대표 함정경합 급증 시 재시도 폭주 → 백오프 없으면 장애 증폭트랜잭션이 길면 전체 대기 → 락 구간 최소화TTL 만료 후 두 워커가 동시 진행 → fencing token 필수
🧭
선택 순서 — 위에서부터 내려오세요

1. 락 없이 풀리는가? → UNIQUE 제약, 멱등키, UPDATE … SET stock = stock - 1 WHERE stock >= 1 같은 원자적 단일 문장이면 락은 필요 없습니다.
2. 한 DB 안에서 끝나는가? → 예라면 낙관적(경합 낮음) 또는 비관적(경합 높음).
3. DB 밖 자원까지 보호해야 하는가? → 그때만 분산 락. 그리고 fencing token을 잊지 마세요.

세 가지가 실제 SQL로는 이렇게 생겼습니다.

-- ❶ 락이 필요 없는 경우 — 원자적 단일 문장이 가장 빠르고 안전하다
UPDATE products
   SET stock = stock - 1
 WHERE id = 1
   AND stock >= 1;   -- 0행이면 재고 부족 → 애플리케이션에서 실패 처리

-- ❷ 비관적 락 — 읽는 순간부터 잠근다 (다른 계산이 끼어야 할 때)
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;  -- 여기서 대기 발생
--   … 검증 로직 …   (이 구간이 길수록 전체 처리량이 떨어진다)
UPDATE products SET stock = 8 WHERE id = 1;
COMMIT;

-- ❸ 낙관적 락 — 락 없이 읽고, 커밋 시점에 version으로 검증
SELECT stock, version FROM products WHERE id = 1;   -- stock=10, version=3
UPDATE products
   SET stock = 9, version = 4
 WHERE id = 1 AND version = 3;   -- 0행 = 누군가 먼저 바꿈 → 재조회 후 재시도

-- ❹ 작업 큐 — 여러 워커가 겹치지 않게 집어간다 (비관적 락의 응용)
SELECT id, payload FROM jobs
 WHERE status = 'ready'
 ORDER BY created_at
 LIMIT 10
   FOR UPDATE SKIP LOCKED;   -- 잠긴 행은 대기 없이 건너뛴다
⚠️
분산 락에서 가장 많이 빠뜨리는 것 — fencing token

워커 A가 락을 잡고 작업하다 GC 스톱이나 네트워크 지연으로 멈춘 사이 TTL이 만료되면, 워커 B가 같은 락을 잡습니다. 그리고 A가 깨어나 자기가 아직 락을 가진 줄 알고 작업을 마저 진행합니다 — 둘이 동시에 실행되는 것이죠. 그래서 락을 줄 때 단조 증가하는 번호(fencing token)를 함께 발급하고, 최종 저장소에서 더 낮은 번호의 쓰기를 거부해야 합니다. 락만으로는 상호배제가 보장되지 않습니다.

🧪 격리 수준 × 이상현상 매트릭스

표준(ANSI SQL)이 정의한 것과 실제 제품의 동작은 다릅니다. 아래 표는 표준 기준이고, 벤더 차이는 그 아래 표에서 봅니다.

격리 수준Dirty ReadNon-repeatable ReadPhantom ReadLost UpdateWrite Skew
Read Uncommitted발생발생발생발생발생
Read Committed 대부분의 기본값✗ 차단발생발생발생발생
Repeatable Read✗ 차단표준상 발생 PG·MySQL은 차단✗ (구현별)발생
Serializable✗ 차단
이상현상한 줄 정의현실의 사고 사례
Dirty Read커밋되지 않은 남의 변경을 읽음롤백될 주문을 집계에 포함해 매출이 부풀려짐
Non-repeatable Read같은 행을 두 번 읽었더니 값이 다름화면 상단 잔액과 하단 합계가 어긋남
Phantom Read같은 조건으로 두 번 읽었더니 행 개수가 다름정원 체크 후 예약 확정 사이에 다른 예약이 끼어들어 초과 예약
Lost Update읽고-계산하고-쓰는 사이 남의 갱신이 덮임위 애니메이션의 재고 차감 — 2건 팔고 1개만 차감
Write Skew각자 다른 행을 써서 충돌은 없지만, 둘을 합치면 불변식이 깨짐"당직은 최소 1명" 규칙 아래 두 명이 동시에 당직을 빼서 0명이 됨
🚨
가장 흔한 오해 — "우리는 트랜잭션을 쓰니까 안전하다"

대부분의 서비스가 기본값인 Read Committed에서 돌아갑니다. 이 수준은 Lost Update를 막아주지 않습니다.BEGIN … SELECT … UPDATE … COMMIT으로 감쌌다고 해서 재고 차감이 안전해지지 않습니다. 안전하게 만드는 건 트랜잭션이 아니라 락 또는 원자적 UPDATE 문장입니다.

🔗 조인 알고리즘 3종

같은 JOIN 문법이라도 옵티마이저는 상황에 따라 완전히 다른 알고리즘을 고릅니다. 재생해 보세요.

SELECT * FROM users u JOIN orders o ON o.user_id = u.id 👥 users 1,000,000 행 🧾 orders 10,000,000 행 🔁 Nested Loop Join — "하나씩 들고 가서 찾아본다" 외부(드라이빙) WHERE 조건 후 users 12행 ← 작을수록 좋다 행마다 1번씩 (12 loops) 내부: orders 인덱스 idx(user_id) 탐색 B+Tree 3~4 레벨 인덱스 없으면 재앙 ✅ 유리: 외부 결과가 소량이고     내부 조인 키에 인덱스가 있을 때 ❌ 위험: 외부 행 수 추정이 빗나가면     12 → 120만 loops로 폭발 비용 ≈ 외부 행수 N × (내부 인덱스 탐색 비용 log M) 🪣 Hash Join — "작은 쪽으로 사전을 만들고, 큰 쪽을 훑는다" ① Build 단계 작은 쪽(users)으로 해시 테이블 생성 메모리에 올린다 ② Probe 단계 orders 1000만 행을 한 번씩만 스캔 해시로 O(1) 매칭 ✅ 유리: 대량 등가(=) 조인,     인덱스가 없어도 강하다 ❌ 제약: 등가 조인만 가능,     메모리 부족 시 디스크 spill 비용 ≈ N + M (양쪽을 한 번씩) — 단, 해시 테이블이 메모리에 들어갈 때 🤝 Sort Merge Join — "양쪽을 줄 세우고 지퍼처럼 맞춘다" ① 양쪽 정렬 users → id 순 orders → user_id 순 여기가 비싸다 ② 병합 스캔 1 ─ 1 ─ 2 ─ 3 ─ 3 양쪽 포인터 전진 되돌아가지 않는다 ✅ 유리: 이미 정렬되어 있거나     범위 조인(<, >, BETWEEN)일 때 ❌ 부담: 정렬 비용이 크고     메모리를 많이 쓴다 비용 ≈ N log N + M log M (정렬) + N + M (병합) 🔁 Nested Loop 소량 × 인덱스 OLTP 단건 조회 추정 실패에 취약 N × log M 🪣 Hash Join 대량 × 등가 조인 배치·분석 쿼리 메모리에 의존 N + M 🤝 Sort Merge 정렬됨 · 범위 조인 대량 + ORDER BY 정렬 비용이 관건 N logN + M logM 옵티마이저는 이 셋의 예상 비용을 계산해 가장 싼 것을 고른다 — 그래서 통계가 틀리면 잘못 고른다
알고리즘동작최적 조건대략적 비용주요 위험
Nested Loop 외부 행마다 내부를 탐색 외부 결과 소량 + 내부 조인 키 인덱스 N × log M 외부 행 수 추정 실패 시 비용 폭발
Hash Join 작은 쪽 해시 빌드 → 큰 쪽 프로브 대량 등가(=) 조인, 인덱스 없어도 가능 N + M 메모리 부족 시 디스크 spill, 등가 조인 전용
Sort Merge 양쪽 정렬 후 병합 이미 정렬됨 · 범위 조인 · 대량 + ORDER BY N logN + M logM 정렬 비용·메모리 사용량이 큼

🚫 쓰면 안 되는 쿼리 — 안티패턴 카탈로그

16장에서 각각을 실행계획과 함께 파헤치지만, 목록만으로도 당장 코드 리뷰에 쓸 수 있습니다. 빈도 × 파급력 순으로 정렬했습니다.

#안티패턴왜 문제인가대안
1N+1 쿼리목록 1번 + 건별 N번 → 네트워크 왕복이 지배적 비용이 됨조인 / IN 일괄 조회 / ORM fetch join
2OFFSET 딥 페이지네이션OFFSET 1000000은 앞의 100만 행을 읽고 버린다키셋 페이지네이션 (WHERE id < ? ORDER BY id DESC LIMIT n)
3WHERE 컬럼을 함수로 감싸기WHERE DATE(created_at) = ? → 인덱스 사용 불가범위 조건으로 변환하거나 함수 기반 인덱스
4암묵적 형변환문자 컬럼에 숫자 비교 → 컬럼 측이 변환되어 인덱스 무력화타입을 맞춰 바인딩 / 컬럼 타입 정정
5SELECT *불필요한 컬럼·TOAST/LOB까지 읽고, 커버링 인덱스를 놓침필요한 컬럼만 명시
6선행 와일드카드 LIKE '%kw%'B+Tree는 앞에서부터 비교 → 전체 스캔전문 검색 인덱스 / 역순 컬럼 / 검색엔진 분리
7대량 UPDATE·DELETE 한 방긴 트랜잭션 → 락 장기 점유, 로그 폭증, 복제 지연PK 범위로 청킹 후 반복 (배치 커밋)
8트랜잭션 안에서 외부 API 호출외부 지연이 그대로 락 점유 시간이 됨트랜잭션 밖으로 분리 / 아웃박스 패턴
9NOT IN + NULL목록에 NULL이 하나라도 있으면 결과가 통째로 0행NOT EXISTS 또는 LEFT JOIN … IS NULL
10중복을 DISTINCT로 덮기조인 카디널리티 오류를 감추고 정렬 비용까지 추가조인 조건을 바로잡거나 EXISTS로 변경
11거대한 IN 리스트수천 개 바인딩 → 파싱 비용·플랜 캐시 오염임시 테이블 조인 / 배치 분할
12OR 조건 남발서로 다른 컬럼의 OR는 인덱스 결합이 어려움UNION ALL 분리 또는 복합 조건 재설계
13인덱스 과다 생성INSERT/UPDATE마다 모든 인덱스 갱신 → 쓰기 성능 붕괴미사용 인덱스 제거, 복합 인덱스로 통합
14페이징 없는 전체 조회데이터 증가에 비례해 언젠가 반드시 터진다필수 LIMIT + 커서 기반 순회
15COUNT(*) 전체 건수 매 요청대형 테이블에서 매번 전체 스캔근사치 통계 / 카운터 테이블 / "다음 페이지 유무"만 확인
16상관 서브쿼리 반복 실행외부 행마다 서브쿼리 재실행조인으로 평탄화 또는 윈도우 함수
17커서 루프 (RBAR)행 단위 반복 처리 — 집합 연산의 장점을 버림집합 기반 단일 문장으로 재작성
18읽고-계산하고-쓰기락 없이 하면 Lost Update (맨 위 애니메이션)원자적 UPDATE / 낙관적·비관적 락
19ORDER BY RAND()전체 행에 난수를 매겨 정렬PK 랜덤 샘플링 / 미리 계산된 랜덤 컬럼
20인덱스 없는 FK부모 삭제·갱신 시 자식 전체 스캔 + 락 확대모든 FK 컬럼에 인덱스 생성

가장 자주 보는 세 가지는 미리 코드로 봐 둡시다.

-- ❌ 1. 딥 페이지네이션 — 100만 행을 읽고 버린 뒤 10행을 준다
SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 1000000;

-- ✅ 키셋(커서) 페이지네이션 — 마지막으로 본 id부터 10행만 읽는다
SELECT * FROM orders
 WHERE id < 8481920            -- 직전 페이지의 마지막 id
 ORDER BY id DESC LIMIT 10;


-- ❌ 2. 컬럼을 함수로 감싸면 idx(created_at)를 쓸 수 없다
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-28';

-- ✅ 범위 조건으로 바꾸면 인덱스를 그대로 탄다
SELECT * FROM orders
 WHERE created_at >= '2026-07-28'
   AND created_at <  '2026-07-29';


-- ❌ 3. NOT IN 목록에 NULL이 섞이면 결과가 통째로 0행이 된다
SELECT * FROM users
 WHERE id NOT IN (SELECT user_id FROM blacklist);  -- user_id에 NULL 존재 시

-- ✅ NOT EXISTS는 NULL에 안전하고 대개 더 빠르다
SELECT * FROM users u
 WHERE NOT EXISTS (
       SELECT 1 FROM blacklist b WHERE b.user_id = u.id);

📈 SQL 튜닝 방법론 — 순서가 8할

기법을 아무리 많이 알아도 순서가 틀리면 시간만 씁니다. 위에서부터 내려오세요. 아래로 갈수록 비용과 리스크가 커집니다.

단계무엇을 하는가먼저 확인할 것
0. 측정느린 쿼리를 데이터로 특정한다slow query log, 누적 실행시간 상위 쿼리, 대기 이벤트
1. 실행계획 확인추정 행 수와 실제 행 수의 괴리를 찾는다통계가 최신인가? 이것만으로 해결되는 경우가 많다
2. 쿼리 재작성불필요한 작업을 없앤다안티패턴 표에 걸리는 게 있는가? 안 쓰는 컬럼·조인·정렬은?
3. 인덱스접근 경로를 만든다쓰기 비용은? 기존 인덱스로 커버되지 않는가?
4. 스키마·모델링정규화 조정, 파티셔닝, 집계 테이블영향 범위가 크다 — 앞 단계로 안 될 때만
5. 파라미터·하드웨어메모리·병렬도·커넥션 풀 조정대개 마지막. 쿼리 문제를 장비로 덮으면 비용만 는다
📌
튜닝 전에 항상 묻는 세 가지

이 쿼리가 정말 병목인가? (전체 응답시간에서 차지하는 비중)   결과 행이 정말 다 필요한가? (페이징·집계로 줄일 수 있나)   실시간이어야 하는가? (배치·캐시·비동기로 옮길 수 있나) — 셋 중 하나만 걸려도 SQL을 건드리지 않고 끝나는 경우가 많습니다.

🏷 벤더 차이 매트릭스

"공통 원리"를 배웠어도 실무에서는 제품 차이에 걸립니다. 자주 부딪히는 지점만 모았습니다.

항목PostgreSQLMySQL (InnoDB)OracleSQL Server
기본 격리 수준Read CommittedRepeatable ReadRead CommittedRead Committed (잠금 기반)
동시성 제어MVCC (튜플 버전)MVCC (Undo 로그)MVCC (Undo 세그먼트)기본 2PL, RCSI 옵션
RR에서 Phantom차단 (스냅샷)차단 (갭 락)RR 미지원RR에서 발생
지원 격리 수준RC · RR · Serializable(SSI)RU · RC · RR · SerializableRC · SerializableRU · RC · RR · Snapshot · Serializable
읽기가 쓰기를 막나아니오아니오아니오기본 설정에서는 막음
SKIP LOCKED9.5+ 지원8.0+ 지원지원READPAST 힌트
락 대기 타임아웃lock_timeoutinnodb_lock_wait_timeoutFOR UPDATE WAIT nSET LOCK_TIMEOUT
실행계획 확인EXPLAIN (ANALYZE, BUFFERS)EXPLAIN ANALYZE (8.0.18+)DBMS_XPLAN.DISPLAY_CURSOR실제 실행 계획 (SSMS)
정리/공간 회수VACUUM 필요퍼지 스레드Undo 자동 관리고스트 정리

✍️ 이해도 체크

재고 차감 로직을 BEGIN → SELECT stock → 앱에서 -1 계산 → UPDATE → COMMIT으로 구현했고, DB는 기본 격리 수준(Read Committed)입니다. 동시 주문 시 재고가 잘못 차감되는 문제가 보고되었습니다. 원인으로 가장 정확한 것은?
✅ 전형적인 Lost Update입니다. 트랜잭션으로 감쌌다는 사실 자체는 이 문제를 막아주지 않습니다 — Read Committed에서 두 세션은 모두 커밋된 값(10)을 정상적으로 읽었고, 각자 계산한 9를 쓴 것뿐이니 DB 입장에서는 아무 위반도 없습니다. 해결책은 세 가지입니다: UPDATE … SET stock = stock - 1 WHERE stock >= 1처럼 원자적 단일 문장으로 바꾸거나, SELECT … FOR UPDATE(비관적 락)로 읽는 순간부터 잠그거나, version 컬럼(낙관적 락)으로 커밋 시점에 충돌을 감지해 재시도하는 것입니다.
배치 집계 쿼리가 1000만 행 테이블 두 개를 = 조건으로 조인합니다. 조인 키에는 인덱스가 없습니다. 옵티마이저가 선택할 가능성이 가장 높고, 또 실제로 가장 적합한 알고리즘은?
Hash Join입니다. 인덱스 없이 1000만 × 1000만을 Nested Loop로 처리하면 내부 테이블을 매번 전체 스캔하게 되어 사실상 끝나지 않습니다. Hash Join은 한쪽으로 해시 테이블을 만들고 다른 쪽을 한 번 훑는 N + M 비용이라 대량 등가 조인에 가장 적합합니다. Sort Merge도 가능하지만 양쪽 정렬 비용(N logN + M logM)이 추가되므로, 이미 정렬돼 있거나 범위 조인일 때가 아니면 보통 Hash에 밀립니다. 다만 Hash Join은 해시 테이블이 작업 메모리를 넘으면 디스크로 spill되어 급격히 느려지므로, 이 경우 작은 쪽이 build 대상으로 선택됐는지 실행계획에서 확인해야 합니다.

🧭 추천 학습 경로

16장을 순서대로 읽는 게 기본이지만, 목적이 뚜렷하다면 아래처럼 골라 읽어도 됩니다.

🌱 백엔드 개발자 (2주)

01 → 04 → 05 → 08 → 09 → 13 → 14 → 16

매일 쓰는 순서 그대로입니다. 실행 순서로 SQL의 오해를 걷어내고, 트랜잭션 경계와 락 두 종류를 잡은 뒤, 인덱스가 안 먹는 이유와 조인으로 마무리합니다.

🎤 면접·기술 리뷰 대비 (3일)

05 → 06 → 08 → 09 → 14 + 락 비교표 · 벤더 매트릭스

가장 자주 나오는 질문 셋 — "격리 수준 설명해 보세요", "낙관적 락과 비관적 락의 차이는?", "Nested Loop와 Hash Join은 언제 갈리나요?"에 집중합니다.

🚒 지금 장애 대응 중

11 → 07 → 12 + 안티패턴 표

데드락·락 경합부터 보고, 실행계획으로 범인을 좁힙니다. 벤더별 진단 쿼리는 PG 14장·Oracle 12장에 있습니다.

🌐 분산 시스템 설계

04 → 05 → 06 → 09 → 10

트랜잭션 경계와 격리 수준을 확실히 한 뒤 분산 락으로 갑니다. 순서를 지키세요 — 10장만 먼저 읽으면 안 써도 될 곳에 분산 락을 쓰게 됩니다.

🚧
이 페이지는 커리큘럼 설계도입니다

16개 챕터는 순차적으로 집필됩니다. 각 챕터는 다른 가이드와 동일한 형식 — 단계별 애니메이션 + 두 세션 실습 SQL + 이해도 퀴즈 — 으로 만들어집니다. 집필 순서는 T3 락(07~11) → T4 조인(12~15) → T2 트랜잭션(04~06) → T5 튜닝(16) → T1 기초(01~03)로, 요청 빈도가 높은 주제부터 진행합니다.

🔗
지금 바로 이어서 볼 것

이 페이지의 주제를 특정 DB에서 확인하고 싶다면 — PostgreSQL 7장 · 트랜잭션과 격리 수준(실제 pg_locks 진단), PG 3장 · MVCC, Oracle 8장 · 트랜잭션과 락, Oracle 쿼리 튜닝 팁, PG 6장 · 플래너와 EXPLAIN.