The Relational Model — Let One Table State One Fact
한국어 원문으로 표시합니다.
한 줄 요약
관계형 모델은 데이터를 행과 열의 표로 표현하고, 행을 유일하게 식별하는 키와 표 사이의 참조를 제약으로 강제해 데이터가 스스로 규칙을 지키게 만든다.
왜 이게 필요했나
관계형 모델 이전에는 데이터가 파일 구조나 포인터로 엮여 있었고, 조회 방법이 저장 구조에 묶여 있었다. 저장 방식을 바꾸면 프로그램을 다 고쳐야 했다.
관계형 모델의 결정적 아이디어는 무엇을 원하는지만 적고 어떻게 찾을지는 데이터베이스가 정하게 하자는 것이다. SQL 이 선언형인 이유가 여기 있다. 우리가 WHERE status = 'paid' 라고만 쓰면, 인덱스를 탈지 전체를 훑을지는 옵티마이저가 그때의 통계를 보고 정한다.
어떻게 동작하나
용어부터 정리하면, 표는 릴레이션, 행은 튜플, 열은 속성이다. 여기에 세 종류의 키가 붙는다.
- 기본 키 — 행을 유일하게 식별한다. NULL 일 수 없다.
- 후보 키 — 기본 키가 될 수 있었던 다른 유일 속성. 대리 키를 쓰더라도 자연 키가 있다면 유일 제약으로 반드시 남겨야 한다.
- 외래 키 — 다른 표의 키를 참조한다. 참조 무결성을 데이터베이스가 강제한다.
NULL 은 관계형 모델에서 가장 많이 오해되는 부분이다. NULL 은 "값이 없다"가 아니라 "값을 모른다"에 가깝다. 그래서 NULL = NULL 은 참이 아니라 미지(unknown) 이고, WHERE email = NULL 은 아무 행도 반환하지 않는다. IS NULL 을 써야 하는 이유다. 이 성질은 곳곳에 파급된다. count(*) 는 행을 세지만 count(email) 은 NULL 이 아닌 값만 센다. NOT IN 목록에 NULL 이 하나라도 있으면 결과가 전부 사라진다.
제약을 데이터베이스에 두는 이유도 짚고 넘어갈 만하다. "이 값은 항상 0보다 크다"는 규칙을 애플리케이션에만 두면 세 경로로 깨진다. 운영 중 수동 SQL, 배치 스크립트, 나중에 붙은 다른 서비스다. 제약은 이 셋을 모두 막는다.
현장에서 만나는 모습
타입 선택은 되돌리기 가장 어려운 결정이다. PostgreSQL 에서 컬럼 타입 변경은 대개 테이블 전체 재작성이고 그동안 강한 잠금이 걸린다. 반면 인덱스는 언제든 추가하고 지울 수 있다. 그래서 타입과 키에는 시간을 쓰고 인덱스는 실제 쿼리 패턴을 보고 나중에 정하는 순서가 합리적이다.
자주 틀리는 두 가지만 짚는다. 금액에 부동소수점을 쓰면 합계가 1원씩 어긋난다. 정확한 계산이 필요하면 numeric 을 쓴다. 그리고 시각 컬럼에 아무 생각 없이 timestamp 라고 쓰면 시간대 없는 타입이 된다. 어떤 순간을 가리키는 값에는 timestamptz 를 써야 하며, 두 타입의 저장 크기는 8바이트로 같으므로 공간을 아끼려고 시간대 없는 타입을 고를 이유는 없다.
정규화는 어디까지 하는가
표를 나누는 규칙에는 이름이 붙어 있지만, 실무에서 외워야 할 것은 세 단계까지이고 그 셋은 사실 한 문장으로 줄어든다. 한 열에는 값 하나만 담고, 열은 전부 기본 키 전체에 달려 있어야 하며, 키가 아닌 열끼리 서로를 결정해서는 안 된다.
- 제1정규형 — 한 칸에
"010-1111-2222, 010-3333-4444"처럼 값을 여러 개 넣지 않는다. 이렇게 담으면 "이 번호를 쓰는 사람 찾기" 가 문자열 검색이 되고 인덱스를 탈 수 없다. - 제2정규형 — 기본 키가
(주문번호, 상품번호)인 표에 상품 이름을 두면, 그 값은 키의 절반에만 달려 있다. 같은 상품이 주문마다 반복 저장되고, 이름을 고칠 때 한 줄을 빠뜨리면 그때부터 같은 상품에 이름이 둘이 된다. - 제3정규형 — 주문 표에
우편번호와도시를 함께 두면, 도시는 주문이 아니라 우편번호에 달려 있다. 우편번호가 바뀌면 도시도 같이 고쳐야 하는데 그 규칙은 어디에도 적혀 있지 않다.
세 가지가 막는 것은 결국 하나입니다. 같은 사실이 여러 곳에 적히면 언젠가 그중 하나만 고쳐지고, 그때부터 데이터베이스는 서로 모순되는 두 이야기를 동시에 들려줍니다.
그렇다고 무조건 잘게 나누는 것이 정답은 아니다. 나눌수록 조인이 늘고, 목록 화면 하나를 그리려고 표 대여섯 개를 엮게 된다. 그래서 읽기가 압도적으로 많고 그 값이 거의 바뀌지 않을 때에 한해 일부러 중복을 남기는 선택을 한다. 주문에 그 시점의 상품명과 가격을 복사해 두는 것이 대표적인 예다. 이것은 정규화 위반이 아니라 오히려 정확한 모델링이다. 주문 시점의 가격은 상품의 현재 가격과 다른 사실이기 때문이다.
거꾸로, 자주 바뀌는 값을 성능을 이유로 복사해 두면 반드시 대가를 치른다. 회원 등급을 주문 표에 복사해 두면 등급이 바뀔 때마다 과거 주문까지 따라 바뀌어야 하는지 아닌지를 매번 판단해야 하고, 그 판단은 코드 여러 곳에 흩어진다. 판단 기준은 이렇게 세우면 됩니다. 그 값이 "지금의 사실" 이면 참조하고, "그때의 사실" 이면 복사합니다.
다음 실습에서 할 것
진짜 PostgreSQL 16 이 뜬 파드에서 이커머스 스키마를 조회한다. 특히 이메일이 NULL 인 고객을 찾는 단계에서 = NULL 이 왜 통하지 않는지 손으로 확인하게 된다.