테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

관계형 모델과 트랜잭션

표로 데이터를 담고 SQL로 묻는 방법, 겹침 없이 나누는 정규화, 여러 변경을 한 덩어리로 다루는 트랜잭션과 ACID다.

목차 (6)
  1. 표·행·키
  2. 관계 대수와 선언형 SQL
  3. 조인의 종류와 NULL
  4. 정규화와 이상 현상
  5. 트랜잭션과 ACID
  6. 비정규화를 고르는 때

표·행·키

관계형 모델은 1970년 E. F. Codd가 「A Relational Model of Data for Large Shared Data Banks」에서 내놓았다. 그 전의 데이터베이스는 레코드끼리 포인터로 이어 두고, 프로그램이 그 포인터를 따라가며 데이터를 찾았다. 저장 방식이 바뀌면 프로그램도 고쳐야 했다. Codd의 생각은 데이터를 값만 담긴 표로 보이고, 어디에 어떻게 저장했는지는 숨기자는 것이었다.

고객과 주문 두 테이블. 주문의 고객_id 열이 고객의 기본 키 id를 가리키는 외래 키다고객id이름도시1김하나서울2이두리부산3박세나서울주문id고객_id상품금액101키보드45000113마우스22000121모니터210000외래 키 → 기본 키행기본 키(행마다 다르다)외래 키(다른 표의 키를 가리킨다)행 = 튜플 하나
관계형 모델의 낱말. 표 하나가 릴레이션, 가로줄이 튜플(행), 세로줄이 속성(열)이다. 주문의 고객_id는 고객의 기본 키를 가리키는 외래 키다.
  • 릴레이션(relation, 테이블): 같은 모양의 행을 모은 것. 수학에서는 행의 집합이라 순서가 없고 같은 행이 두 번 있을 수 없다. SQL 테이블은 실용을 위해 중복 행을 허용한다.
  • 속성(attribute, 열): 이름과 자료형(도메인)이 있다. 칸 하나에는 더 나눌 수 없는 값 하나만 둔다.
  • 기본 키(primary key): 행을 하나로 가리키는 열(또는 열 묶음). 같은 값이 두 번 나올 수 없고 비어 있을 수 없다.
  • 외래 키(foreign key): 다른 표의 키를 담은 열. 데이터베이스가 "없는 고객을 가리키는 주문"을 막아 준다(참조 무결성).

키와 외래 키, 값의 범위 같은 규칙은 제약 조건(constraint)으로 표에 선언해 둔다. 응용 프로그램이 몇 개든 같은 규칙이 늘 지켜진다는 것이 장점이다. PostgreSQL 문서의 Constraints 장이 CHECK·NOT NULL·UNIQUE·PRIMARY KEY·FOREIGN KEY를 예와 함께 설명한다.

관계 대수와 선언형 SQL

표를 다루는 연산은 몇 가지뿐이다. 행을 고르는 선택(σ), 열을 고르는 투영(π), 두 표를 조건으로 잇는 조인(⋈), 그리고 합집합·차집합 같은 집합 연산이다. 연산의 입력도 표, 결과도 표라서 얼마든지 이어 붙일 수 있다. 이것이 관계 대수(relational algebra)다.

SQL 문장을 관계 대수 트리로 옮긴 것과, 선택을 조인 아래로 내려 중간 결과를 줄인 같은 뜻의 트리SELECT 이름 FROM 고객 JOIN 주문 ON 고객.id = 주문.고객_idWHERE 금액 >= 100000;적힌 그대로: 조인한 뒤 거른다바꾼 순서: 먼저 거르고 조인π 이름 (1행)σ 금액 ≥ 100000 (1행)⋈ id = 고객_id (3행)고객 3행주문 3행π 이름 (1행)⋈ id = 고객_id (1행)고객 3행σ 금액 ≥ 100000 (1행)주문 3행σ 선택(행 고르기) · π 투영(열 고르기) · ⋈ 조인(두 표를 조건으로 잇기)괄호 안은 그 연산이 내놓는 행 수다. 답은 같고 조인할 행만 줄었다.
SQL 한 문장은 관계 대수 트리 하나와 같다. 연산 순서를 바꿔도 답이 같은 경우가 많아, 데이터베이스는 그중 중간 결과가 가장 작은 순서를 고른다.

SQL은 이 연산을 직접 늘어놓지 않는다. "금액이 10만 원 이상인 주문을 한 고객의 이름"처럼 무엇을 원하는지만 적는다. 어떤 표를 먼저 읽고 어느 인덱스를 쓰며 조인을 어떤 방법으로 할지는 데이터베이스의 옵티마이저가 정한다. 이렇게 결과만 적는 방식을 선언형(declarative)이라 한다. 위 그림처럼 거르기를 조인 아래로 내리는 변환은 답을 바꾸지 않으면서 일을 줄이는데, 프로그래머가 아니라 데이터베이스가 그 판단을 한다는 점이 핵심이다. 실제 계획을 고르는 과정은 인덱스와 쿼리 실행 노트에 있다.

Carnegie Mellon 대학 데이터베이스 강의의 첫 강의 노트가 관계 대수 연산을 하나씩 그림으로 보여 준다.

조인의 종류와 NULL

조인은 짝이 없는 행을 어떻게 다루느냐로 갈린다. PostgreSQL 튜토리얼의 Joins Between Tables가 날씨 표와 도시 표로 차이를 보여 준다.

조인결과쓰는 때
INNER JOIN조건이 맞는 짝만 남는다. 짝이 없는 행은 양쪽 모두 빠진다주문과 그 주문을 한 고객처럼 짝이 꼭 있는 것
LEFT OUTER JOIN왼쪽 표의 행은 모두 한 번 이상 나오고, 짝이 없으면 오른쪽 열이 NULL로 채워진다주문이 없는 고객까지 포함한 목록. 오른쪽이 NULL인 행만 고르면 "짝이 없는 것" 찾기가 된다
RIGHT OUTER JOINLEFT를 좌우만 바꾼 것대개 표 순서를 바꿔 LEFT로 쓴다
FULL OUTER JOIN양쪽 행이 모두 나오고, 짝이 없는 쪽은 NULL두 목록을 맞대어 어느 쪽에만 있는지 볼 때
CROSS JOIN조건 없이 모든 짝(행 수의 곱)날짜 × 상품처럼 빈칸까지 채운 격자를 만들 때

외부 조인이 만드는 NULL은 값이 아니라 "모른다"는 표시다. 그래서 7 = NULL도 NULL = NULL도 참이 아니라 NULL(알 수 없음)이 되고, WHERE는 참인 행만 남기므로 WHERE 열 = NULL은 아무 행도 고르지 못한다. NULL인지는 IS NULL로, NULL끼리도 같다고 보려면 IS NOT DISTINCT FROM으로 비교한다(Comparison Functions and Operators). LEFT JOIN 뒤에 오른쪽 표의 열을 WHERE에서 거르면 NULL 행이 빠져 INNER JOIN과 같아지는 실수도 같은 이유로 생긴다. 오른쪽 표의 조건은 ON에 둔다.

정규화와 이상 현상

표를 어떻게 나눌지 정하는 기준이 정규화(normalization)다. 목표는 하나의 사실을 한 곳에만 두는 것이다. 같은 사실이 여러 줄에 흩어져 있으면 고칠 때마다 모든 사본을 찾아야 하고, 하나라도 놓치면 데이터가 스스로 모순된다.

학번·이름·과목·교수·연구실을 한 테이블에 담아 최교수의 연구실이 세 번 되풀이되는 모습과 그로 인한 세 가지 이상수강 (키: 학번 + 과목)학번이름과목교수연구실1김하나DB최교수공학관 3011김하나네트워크정교수공학관 2052이두리DB최교수공학관 3013박세나DB최교수공학관 301← 같은 사실이세 줄에되풀이된다갱신 이상최교수가 연구실을 옮기면 세 줄을 모두 고쳐야 한다. 하나만 빠져도 값이 엇갈린다.삽입 이상수강생이 없는 새 과목의 교수·연구실은 넣을 곳이 없다. 키(학번)가 비기 때문이다.삭제 이상김하나가 네트워크 수강을 취소하면 정교수의 연구실 정보까지 함께 사라진다.
정규화하지 않은 수강 테이블. 교수의 연구실이라는 한 가지 사실이 수강 신청 줄마다 되풀이되어 고치기·넣기·지우기에서 이상이 생긴다.

어떤 열이 다른 열에 따라 정해지는 관계를 함수 종속(functional dependency)이라 하고 X → Y로 쓴다. 학번을 알면 이름이 정해지므로 학번 → 이름이다. 정규형은 이 종속이 어디를 향하는지로 정의한다.

수강 테이블의 함수 종속: 학번→이름과 과목→교수는 키 일부에 기대는 부분 종속, 교수→연구실은 이행 종속이다. 이를 떼어 내면 학생·수강·과목·교수 네 테이블이 된다학번이름과목교수연구실부분 종속부분 종속이행 종속종속마다 따로 떼어 낸다학생학번이름수강학번과목과목과목교수교수교수연구실2NF: 키(학번 + 과목)의 일부에만 기대는 열(이름, 교수)을 그 일부를 키로 하는 표로 옮긴다.3NF: 키가 아닌 열에 기대는 열(교수 → 연구실)을 다시 떼어 낸다.색 칸은 각 표의 키다. 최교수의 연구실은 이제 교수 표의 한 줄에만 있다.
수강 테이블의 함수 종속과 3NF 분해. 종속 하나를 떼어 낼 때마다 그 종속의 왼쪽을 키로 하는 표가 하나 생긴다. 네 표를 다시 조인하면 원래 표가 그대로 나온다(무손실 분해).
정규형조건위 예에서 고칠 것
1NF칸마다 값 하나. 목록이나 반복 열(과목1, 과목2…)이 없다한 학생의 과목을 한 칸에 "DB, 네트워크"로 담지 않고 줄로 나눈다
2NF1NF이고, 키가 아닌 열이 키 전체에 기댄다(키 일부에만 기대지 않는다)학번 → 이름, 과목 → 교수를 떼어 학생·과목 표로
3NF2NF이고, 키가 아닌 열이 다른 키 아닌 열에 기대지 않는다(이행 종속 없음)교수 → 연구실을 떼어 교수 표로
BCNF모든 함수 종속 X → Y에서 X가 후보 키를 품는다위 분해는 이미 BCNF다

3NF와 BCNF는 대부분 같지만 키가 여럿 겹칠 때 갈린다. 과목마다 교수가 여럿이고 교수는 한 과목만 맡는 (학생, 과목, 교수) 표를 보자. 키는 (학생, 과목)인데 교수 → 과목 종속이 있다. 오른쪽 과목이 키의 일부라 3NF는 통과하지만, 교수가 키가 아니므로 BCNF는 아니다. BCNF로 나누면 (학생, 교수)와 (교수, 과목)이 되는데, 이번에는 "학생은 과목마다 교수 한 명"이라는 규칙을 한 표 안에서 검사할 수 없게 된다. 그래서 실무 설계는 보통 3NF를 기준으로 삼고, BCNF는 이 손해가 없을 때 간다.

트랜잭션과 ACID

계좌 이체는 "A에서 빼기"와 "B에 더하기" 두 변경이다. 첫 번째만 되고 서버가 꺼지면 돈이 사라진다. 트랜잭션은 여러 변경을 하나로 묶어, 묶음 전체가 반영되거나 하나도 반영되지 않게 한다. SQL에서는 BEGIN으로 열고 COMMIT으로 확정하며, ROLLBACK으로 없던 일로 한다. PostgreSQL 튜토리얼의 Transactions 절이 바로 이 이체 예로 설명한다.

A에서 B로 100을 옮기는 트랜잭션: 커밋까지 가면 두 변경이 함께 남고, 중간에 장애가 나면 복구가 A의 변경을 되돌려 둘 다 처음 값으로 돌아간다끝까지 간 경우BEGINA 500 · B 500A −100A 400B +100B 600COMMIT로그 기록 확정결과A 400 · B 600중간에 장애가 난 경우BEGINA 500 · B 500A −100A 400✕ 장애전원이 꺼졌다복구A −100 되돌림결과A 500 · B 500어느 쪽이든 "A만 빠지고 B는 안 늘어난" 중간 상태는 남지 않는다(원자성).COMMIT 응답을 받은 뒤라면 그다음 장애에도 A 400 · B 600이 남는다(지속성).'복구'가 무엇을 되돌릴지 아는 방법은 저장 엔진 노트의 WAL 절에 있다.
이체 트랜잭션. 커밋 응답이 나가기 전에 장애가 나면 복구 과정이 끝나지 않은 변경을 되돌리고, 커밋 응답이 나간 뒤에는 장애가 나도 변경이 남는다.

트랜잭션이 지키는 네 성질의 머리글자가 ACID다.

  • 원자성(Atomicity): 전부 아니면 전무. 실패한 트랜잭션의 흔적은 남지 않는다. 엔진은 되돌리기용 기록(undo)으로 이를 지킨다.
  • 일관성(Consistency): 트랜잭션은 제약 조건을 지키는 상태에서 지키는 상태로만 옮겨 간다. 나머지 셋과 달리 데이터베이스 혼자가 아니라 응용 프로그램의 올바른 트랜잭션과 함께 지키는 성질이다.
  • 격리성(Isolation): 동시에 도는 트랜잭션이 서로의 중간 상태를 보지 않는다. 얼마나 엄격하게 막을지는 격리 수준으로 고른다(동시성과 격리 수준 노트).
  • 지속성(Durability): 커밋이 성공했다고 답했으면 그 뒤 전원이 꺼져도 남는다. 엔진은 커밋 전에 로그를 디스크에 먼저 써서 지킨다(저장 엔진 노트의 WAL).

ACID의 C는 분산 시스템에서 말하는 일관성(모든 복제본이 같은 값을 보는가)과 이름만 같고 뜻이 다르다. 그 이야기는 복제·샤딩·합의 노트의 CAP 절에서 다룬다.

비정규화를 고르는 때

정규화는 쓰기를 안전하게 하지만 읽을 때 조인이 늘어난다. 읽기가 압도적으로 많고 조인 비용이 문제가 될 때 일부러 사본을 두는 것이 비정규화(denormalization)다.

정규화하면 이름이 고객 표 한 곳에만 있어 읽을 때 조인하고, 비정규화하면 주문마다 이름을 복사해 조인 없이 읽지만 이름이 바뀌면 여러 줄을 고쳐야 한다정규화비정규화주문id고객_id금액1014500011322000121210000고객id이름1김하나3박세나주문(고객 이름 복사)id고객_id고객_이름금액101김하나45000113박세나22000121김하나210000읽기: 주문과 고객을 조인한다이름 바꾸기: 고객 한 줄만 고친다같은 사실은 한 곳에만 있다읽기: 주문 한 표만 읽는다이름 바꾸기: 김하나가 든 모든 줄을 고친다한 줄이라도 빠지면 이름이 엇갈린다
같은 데이터를 정규화한 모양과 비정규화한 모양. 비정규화는 조인 한 번을 없애는 대신, 복사한 값이 바뀔 때 고칠 곳을 늘린다.
  • 미리 계산한 값: 게시글마다 댓글 수를 열로 두면 목록 화면이 댓글 표를 셀 필요가 없다. 댓글을 넣고 지울 때 같은 트랜잭션에서 함께 고쳐야 한다.
  • 그때의 값을 남겨야 할 때: 주문서의 배송지와 가격은 고객이 나중에 주소를 바꾸거나 상품 가격이 올라도 주문 당시 값이어야 한다. 이것은 사본이 아니라 다른 사실(주문 시점의 값)이므로 정규화 위반도 아니다.
  • 분석용 표: 집계 질의가 큰 표 여러 개를 조인하는 데이터 웨어하우스는 사실 표 하나에 차원 표를 붙인 스타 스키마처럼 의도적으로 넓은 표를 쓴다.
  • 분산 데이터베이스: 표가 여러 서버에 흩어지면 조인이 네트워크 왕복이 되므로, 함께 읽는 데이터를 한 행(문서)에 모으는 쪽이 낫기도 하다.

순서는 늘 같다. 먼저 정규화해서 설계하고, 측정해서 느린 읽기를 찾은 뒤, 그 읽기 하나를 위해 사본을 두고 그 사본을 누가 언제 고칠지(트리거, 같은 트랜잭션, 배치 작업)를 함께 정한다. 사본을 고치는 길이 정해지지 않은 비정규화는 위 그림의 이상 현상을 그대로 되살린다. 실체화 뷰(materialized view)처럼 데이터베이스가 사본 관리를 맡아 주는 기능을 먼저 살펴볼 만하다.