← posts/b.log()

blog92@web:~$ cat posts/db-design-10-denormalization.md

DATABASE3 min read

DB 설계 A to Z 10강 — 반정규화

반정규화를 검토하기 전에 확인할 순서, 컬럼·테이블·관계별 기법, 중복을 동기화하는 수단 비교표, 생성 컬럼·복합 FK·집계 테이블 예제와 결정 기록 양식을 정리한다.

8~9강에서 정규화를 했다면, 이번 편은 근거를 갖고 되돌리는 방법입니다.

1. 반정규화란

정규화된 모델에 의도적으로 중복이나 구조 변경을 도입해 성능이나 단순성을 얻는 것입니다. 핵심은 "정규화를 안 한 것"과 "정규화한 뒤 근거를 갖고 되돌린 것"이 다르다는 점입니다.

2. 반정규화를 검토하기 전에

순서대로 확인합니다.

  1. 인덱스로 해결되는가? (15강)
  2. 쿼리 재작성으로 해결되는가?
  3. 캐시나 조회 전용 복제본으로 해결되는가?
  4. 파티셔닝으로 해결되는가? (16강)
  5. 그래도 안 되면 반정규화

반정규화는 모든 쓰기 경로에 비용을 추가하기 때문에 마지막 수단입니다.

3. 반정규화 기법

분류기법예시
컬럼중복 컬럼fulfillment_log에 order_no 추가 (주문항목 조인 제거)
컬럼파생 컬럼order_item에 progress_pct 저장
컬럼이력 최신값 컬럼order_item에 last_stage_code 저장
테이블집계 테이블일별 주문 단위 처리 집계
테이블테이블 병합1:1 테이블 합치기
테이블수직 분할자주 안 쓰는 대형 컬럼 분리
테이블수평 분할최근 데이터와 과거 데이터 분리 (파티셔닝으로 대체 가능)
관계중복 관계손자 테이블이 조부모 FK를 직접 보유

4. 정합성 보장 수단 비교

반정규화의 진짜 설계 대상은 "중복을 어떻게 동기화할 것인가" 입니다.

수단일관성쓰기 비용복잡도적합한 경우
생성 컬럼 (Generated Column)즉시, DB 보장낮음낮음같은 행 내 계산
복합 FK + ON UPDATE CASCADE즉시, DB 보장중간낮음부모 값 복사 (8강 예제)
트리거즉시, DB 보장높음높음 (숨은 로직)다른 테이블 집계
애플리케이션코드 품질에 의존중간중간트랜잭션 경계가 명확할 때
구체화 뷰 (Materialized View)새로 고침 시점새로 고침 시낮음지연 허용 집계
배치배치 주기배치 시중간대시보드, 리포트
CDC / 이벤트수 초 지연비동기높음시스템 간 복제

5. 예제 1: 생성 컬럼

sql
-- PostgreSQL 12+
ALTER TABLE item_stage
  ADD COLUMN delay_days INT
  GENERATED ALWAYS AS (actual_date - plan_date) STORED;
 
-- MySQL 5.7+
ALTER TABLE item_stage
  ADD COLUMN delay_days INT
  GENERATED ALWAYS AS (DATEDIFF(actual_date, plan_date)) STORED;

같은 행에서 계산되는 파생값은 생성 컬럼이 가장 안전하며, 인덱스도 걸 수 있습니다.

6. 예제 2: 중복 컬럼과 복합 FK

처리 실적을 주문 단위로 집계하려면 fulfillment_log → order_item → orders 조인이 필요합니다. 실적이 수천만 건이면 부담이 큽니다.

sql
ALTER TABLE order_item ADD CONSTRAINT uq_item_order UNIQUE (item_id, order_no);
 
ALTER TABLE fulfillment_log ADD COLUMN order_no VARCHAR(10);
-- 기존 데이터 채운 뒤
ALTER TABLE fulfillment_log ALTER COLUMN order_no SET NOT NULL;
ALTER TABLE fulfillment_log
  ADD CONSTRAINT fk_log_item_order
  FOREIGN KEY (item_id, order_no) REFERENCES order_item (item_id, order_no)
  ON UPDATE CASCADE;

order_no는 중복이지만, 복합 FK 덕분에 주문항목의 주문과 다른 값이 들어갈 수 없습니다. 트리거 없이 DB가 정합성을 보장하는 중복입니다.

7. 예제 3: 집계 테이블

sql
CREATE TABLE daily_order_progress (
    base_date     DATE        NOT NULL,
    order_no      VARCHAR(10) NOT NULL,
    total_tasks   INT         NOT NULL,
    done_tasks    INT         NOT NULL,
    total_man_hours NUMERIC(12,2) NOT NULL,
    refreshed_at  TIMESTAMP   NOT NULL,
    PRIMARY KEY (base_date, order_no)
);

배치로 갱신합니다. 이미 학습한 스테이징 적재 → MERGE/UPSERT 패턴이 그대로 적용됩니다.

sql
INSERT INTO daily_order_progress AS t
       (base_date, order_no, total_tasks, done_tasks, total_man_hours, refreshed_at)
SELECT CURRENT_DATE, oi.order_no,
       COUNT(*), COUNT(st.actual_date),
       COALESCE(SUM(w.mh), 0), now()
FROM order_item oi
JOIN item_stage st ON st.item_id = oi.item_id
LEFT JOIN (SELECT item_id, stage_code, SUM(man_hours) AS mh
           FROM fulfillment_log GROUP BY item_id, stage_code) w
  ON w.item_id = st.item_id AND w.stage_code = st.stage_code
GROUP BY oi.order_no
ON CONFLICT (base_date, order_no) DO UPDATE
SET total_tasks     = EXCLUDED.total_tasks,
    done_tasks      = EXCLUDED.done_tasks,
    total_man_hours = EXCLUDED.total_man_hours,
    refreshed_at    = EXCLUDED.refreshed_at;

refreshed_at 컬럼으로 데이터가 언제 기준인지 사용자에게 노출하는 것이 집계 테이블 설계의 필수 요소입니다.

8. 반정규화 결정 기록

반정규화는 반드시 문서로 남깁니다. 1년 뒤에는 누구도 이유를 기억하지 못합니다.

항목내용
대상fulfillment_log.order_no
문제주문 단위 실적 집계 쿼리 3.2초 (목표 0.5초)
검토한 대안인덱스 추가(효과 미미), 구체화 뷰(실시간성 부족)
선택중복 컬럼 + 복합 FK
정합성 수단FK ON UPDATE CASCADE
비용행당 약 11바이트; order_no는 불변이라 연쇄 갱신은 발생하지 않는다
측정 결과0.3초

10강 정리

  1. 반정규화는 인덱스·쿼리·캐시·파티셔닝 다음의 마지막 수단이다.
  2. 설계의 핵심은 중복 자체가 아니라 동기화 수단이다.
  3. 가능하면 생성 컬럼, 복합 FK처럼 DB가 보장하는 수단을 우선한다.
  4. 집계 테이블에는 기준 시각을 함께 저장한다.
  5. 결정 근거와 측정 결과를 기록한다.
COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..