데이터베이스 용어 사전
설계·정규화denormalization · 요약 테이블

반정규화

읽기 성능을 위해 일부러 중복을 만드는 설계. 갱신 이상을 되살리는 거래이므로 동기화 책임이 따른다.

정규화로 없앤 중복을 읽기 성능을 위해 의도적으로 되살리는 설계.

왜 되돌리나

정규화 ↑   중복 없음 · 갱신 쉬움 · 무결성 강함
           그러나 조인이 많아져 읽기가 느려진다

반정규화 ↑ 조인 없이 한 번에 읽는다
           그러나 중복된 값을 누군가 동기화해야 한다

흔한 세 가지 형태

-- ① 집계 칼럼 캐싱 — 매번 count(*) 하지 않는다
ALTER TABLE post ADD COLUMN comment_count INT NOT NULL DEFAULT 0;

-- ② 중복 칼럼 — 조인을 없앤다
ALTER TABLE orders ADD COLUMN member_name VARCHAR(50);   -- member 를 조인하지 않으려고

-- ③ 요약 테이블 — 통계를 미리 만들어 둔다
CREATE TABLE daily_sales AS
SELECT date(created_at) AS d, sum(amount) AS total FROM orders GROUP BY 1;

동기화 책임이 따라온다

-- 트리거로 강제하기 — DB 가 책임진다
CREATE OR REPLACE FUNCTION bump_comment_count() RETURNS TRIGGER AS $$
BEGIN
  UPDATE post SET comment_count = comment_count + 1 WHERE id = NEW.post_id;
  RETURN NEW;
END $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_comment_ins AFTER INSERT ON comment
  FOR EACH ROW EXECUTE FUNCTION bump_comment_count();

트리거는 확실하지만 쓰기 경로에 부하와 락을 더한다. 애플리케이션에서 처리하면 가볍지만 경로가 여러 개면 빠뜨리기 쉽다. 어느 쪽이든 틀어졌을 때 바로잡는 배치를 함께 둔다.

-- 정합성 점검·보정 배치 (주기적으로 돌린다)
UPDATE post p SET comment_count = c.cnt
  FROM (SELECT post_id, count(*) cnt FROM comment GROUP BY post_id) c
 WHERE p.id = c.post_id AND p.comment_count <> c.cnt;

PostgreSQL의 구체화 뷰라는 선택지

-- 요약 테이블을 직접 관리하는 대신 DB 에 맡긴다
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date(created_at) AS d, sum(amount) AS total FROM orders GROUP BY 1;

CREATE UNIQUE INDEX ON daily_sales (d);          -- CONCURRENTLY 갱신에 필요
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;   -- 읽기를 막지 않고 갱신

CONCURRENTLY 없이 갱신하면 그동안 조회가 막힌다 — 운영에서 자주 데는 지점이다.

순서가 중요하다

  • ① 3NF 로 설계한다
  • ② 실제로 측정한다 (EXPLAIN ANALYZE · 슬로우 쿼리 로그)
  • ③ 느린 곳만 신중히 반정규화한다

처음부터 성능을 핑계로 넓은 테이블을 만드는 건 조기 최적화다. 대개는 인덱스나 쿼리 수정으로 해결되고, 그 편이 되돌리기도 쉽다.

면접 함정

  • "반정규화는 나쁜 설계" → 측정에 근거한 의도적 선택이면 정당하다. 문제는 측정 없이 하는 것이다.
  • "반정규화하면 빨라진다" → 읽기만 빨라지고 쓰기는 느려진다. 읽기 비중이 높을 때만 이득이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 정규화 — 이상현상·함수종속·1NF~BCNF·반정규화