사전 참조 무결성
개념

참조 무결성

gabury1

한 데이터가 다른 데이터를 가리킬 때 가리켜진 쪽이 실제로 있어야 한다는 성질입니다. 가리키는 자리를 아예 비워 두는 것은 괜찮습니다. 없는 것을 가리키는 값만 남지 않으면 됩니다.

상세

회의를 잡으면서 예약표에 3층 회의실을 적어 둡니다. 그 방을 창고로 바꾸려는 쪽은 먼저 거기 잡힌 예약이 있는지 봅니다. 예약이 남아 있으면 방을 그대로 두거나, 그 예약을 다른 방으로 옮기거나, 방 칸을 비워 두고 다시 잡게 합니다.

참조 무결성은 참조하는 쪽에 적힌 값이 참조받는 쪽에 실재하거나, 아예 비어 있어야 한다는 성질입니다. 참조하는 값을 담은 자리를 외래키라고 부릅니다. 참조받는 쪽의 값은 그 테이블에서 행 하나를 가려내는 키입니다. 보통은 기본키입니다. 유일함이 보장되는 다른 키여도 됩니다.

참조 무결성은 없는 것을 가리키는 값을 누군가 막아야 한다고 못 박습니다. 이 성질은 값을 넣을 때만 걸리지 않습니다. 이미 있는 행을 지우거나 고칠 때도 같이 걸립니다. 참조받던 행이 사라지면 그 행을 가리키던 값이 갈 곳을 잃기 때문입니다. 그래서 검사는 양쪽에서 일어납니다. 한쪽은 참조하는 쪽에 값을 넣거나 고칠 때입니다. 다른 쪽은 참조받는 쪽 행을 지우거나 그 키를 고칠 때입니다.

빈 값을 허용한다는 대목은 자주 잊힙니다. 아직 정해지지 않은 참조는 비워 둘 수 있습니다. 빈 값은 아무것도 가리키지 않으므로 깨질 참조가 없습니다. 여러 열을 묶어 하나의 참조로 쓸 때는 그중 일부만 비어 있는 경우를 어떻게 볼지가 따로 정해집니다.

외래키와 참조 무결성은 같은 말이 아닙니다. 외래키는 이 성질을 데이터 정의에 적어 두는 수단입니다. 참조 무결성은 그 수단이 지키려는 성질 자체입니다. 외래키를 선언하지 않아도 성질 자체는 그대로 성립합니다. 다만 그때는 아무도 대신 지켜 주지 않습니다.

성질을 누가 강제하느냐도 따로 정해집니다. 데이터베이스 엔진이 검사해 막을 수 있습니다. 애플리케이션 코드가 스스로 지킬 수도 있습니다. 아무도 안 지키면 어긋난 값이 그대로 쌓입니다. 강제하는 쪽이 달라져도 성질의 뜻은 달라지지 않습니다. 참조 대상이 사라져 홀로 남은 행을 흔히 고아 레코드라고 부릅니다. 참조 무결성이 지켜진다는 말은 그런 행이 하나도 없다는 말입니다.

배경

한 가지 사실을 한 테이블에 다 적으면 같은 내용이 행마다 되풀이됩니다. 그래서 되풀이되는 부분을 따로 떼어 다른 테이블에 둡니다. 원래 자리에는 그것을 가리키는 값만 남깁니다. 이렇게 나눠 담은 뒤로 전에 없던 어긋남이 생겼습니다. 가리키는 쪽은 그대로 남습니다. 가리켜지던 행만 지워지는 일입니다. 남은 값은 아무 데도 닿지 않습니다. 그래도 값 자체는 멀쩡해 보여서 한참 뒤에야 드러납니다.

필요했던 것은 이런 값이 애초에 만들어지지 않게 막는 일이었습니다. 처음에는 프로그램마다 손으로 검사를 넣었습니다. 넣기 전에 대상이 있는지 조회합니다. 지우기 전에는 가리키는 행이 없는지 조회합니다. 같은 데이터를 프로그램 하나만 만질 때는 이 방법이 버팁니다. 프로그램이 여럿이 되면 무너집니다. 하나만 검사를 빠뜨려도 어긋난 값이 들어옵니다. 검사를 넣은 나머지는 그 사실을 모릅니다.

그래서 검사를 코드 쪽에서 데이터 쪽으로 옮겼습니다. 어느 열이 어느 열을 가리키는지 데이터 정의에 적어 둡니다. 그 규칙은 엔진이 지킵니다. 관계형 모델은 이렇게 지켜져야 하는 성질에 참조 무결성이라는 이름을 붙였습니다. 이 규칙을 관계형 모델의 무결성 규칙으로 다듬어 적은 글이 E. F. 코드의 1979년 논문 「Extending the Database Relational Model to Capture More Meaning」입니다.

갈래

참조받는 쪽 행을 지우거나 그 키를 고치면 그 행을 가리키던 값이 갈 곳을 잃습니다. 이때 무엇을 할지가 갈래를 가르는 축입니다. 이 선택을 참조 동작이라고 부릅니다. 행을 지울 때와 키를 고칠 때에 각각 따로 정합니다. 이름은 다섯입니다.

flowchart TD
    A["참조받는 행을 지우거나 키를 고침"] --> B{"가리키던 행이 남아 있나"}
    B -->|없다| C["그대로 진행"]
    B -->|있다| D["참조 동작이 정한 대로"]
    D --> E["막는다: NO ACTION · RESTRICT"]
    D --> F["따라 바꾼다: CASCADE · SET NULL · SET DEFAULT"]

NO ACTION

따로 적지 않으면 이것이 됩니다. 참조받는 쪽에서 지우는 일 자체는 일단 진행됩니다. 그래도 제약은 여전히 만족되어야 하므로 대개 오류로 끝난다고 PostgreSQL 공식 문서는 적습니다.

이 동작이 남겨 두는 여지는 검사를 미룰 수 있다는 것입니다. 제약 검사를 트랜잭션 뒤로 미뤄 두면 그 사이에 다른 명령이 상황을 고칠 수 있습니다. 참조받는 테이블에 알맞은 행을 다시 넣거나, 갈 곳을 잃은 행을 지우는 식입니다.

RESTRICT

NO ACTION 보다 엄격한 설정입니다. 가리켜지는 행을 지우는 일 자체를 막습니다. PostgreSQL 공식 문서는 RESTRICT 가 검사를 트랜잭션 뒤로 미루는 것을 허용하지 않는다고 적습니다. 두 동작을 가르는 자리가 여기입니다.

엔진마다 이 구분을 그대로 두지는 않습니다. MySQL 공식 문서는 InnoDB 에서 NO ACTION 이 RESTRICT 와 같다고 적습니다. 참조받는 테이블에 연결된 값이 있으면 삭제나 수정이 즉시 거부됩니다. 미뤄진 검사를 지원하는 NDB 클러스터 엔진에서만 NO ACTION 이 미뤄진 검사를 뜻합니다. SQLite 공식 문서는 RESTRICT 처리가 문장 끝이나 트랜잭션 끝이 아니라 값이 바뀌는 즉시 일어난다고 적습니다.

CASCADE

참조받는 행이 지워지면 그것을 가리키던 행도 같이 지웁니다. 키가 바뀌면 가리키던 값도 새 값으로 같이 바꿉니다. 한 번의 삭제가 다른 테이블로 번져 나갑니다.

번짐이 어디까지 가는지는 눈에 잘 안 띕니다. MySQL 공식 문서는 이 방식으로 번진 삭제와 수정이 트리거를 깨우지 않는다고 적습니다.

SET NULL

참조받는 행이 지워지거나 키가 바뀌면 가리키던 값을 빈 값으로 만듭니다. 참조는 끊기지만 가리키던 행 자체는 남습니다.

가리키는 열에 빈 값을 금지해 두었으면 이 동작은 성립하지 않습니다. MySQL 공식 문서는 SET NULL 을 지정할 때 자식 테이블의 그 열을 NOT NULL 로 선언하지 않았는지 확인하라고 적습니다.

SET DEFAULT

가리키던 값을 그 열의 기본값으로 바꿉니다. 빈 값 대신 미리 정해 둔 값으로 떨어뜨리는 셈입니다.

기본값이라고 제약을 면제받지는 않습니다. PostgreSQL 공식 문서는 기본값에 해당하는 행이 참조받는 테이블에 없으면 그 작업이 실패한다고 적습니다. 엔진에 따라 이 동작을 아예 받지 않기도 합니다. MySQL 공식 문서는 파서가 이 구문을 알아보기는 하지만 InnoDB 와 NDB 둘 다 이 절이 들어간 테이블 정의를 거부한다고 적습니다.

예시

PostgreSQL

PostgreSQL 튜토리얼은 cities 에 없는 도시가 weather 에 들어가지 못하게 막는 일을 참조 무결성을 지키는 것이라고 부릅니다.

SQL
CREATE TABLE cities (
        name     varchar(80) primary key,
        location point
);

CREATE TABLE weather (
        city      varchar(80) references cities(name),
        temp_lo   int,
        temp_hi   int,
        prcp      real,
        date      date
);

references cities(name) 한 마디가 이 열의 값이 cities.name 에 실재해야 한다고 선언합니다. 없는 도시를 넣으면 이렇게 거부됩니다.

INSERT INTO weather VALUES ('Berkeley', 45, 53, 0.0, '1994-11-28');
ERROR:  insert or update on table "weather" violates foreign key constraint "weather_city_fkey"
DETAIL:  Key (city)=(Berkeley) is not present in table "cities".

어느 제약을 어겼는지와 어느 값이 없는지가 메시지에 같이 적힙니다.

SQLite

같은 성질이라도 기본으로 지켜 주는지는 엔진마다 다릅니다. SQLite 공식 문서는 외래키 제약이 하위 호환을 위해 기본으로 꺼져 있다고 적습니다. 데이터베이스 연결마다 따로 켜야 한다고 덧붙입니다.

sqlite> PRAGMA foreign_keys;
0
sqlite> PRAGMA foreign_keys = ON;
sqlite> PRAGMA foreign_keys;
1

같은 문서는 앞으로 판이 바뀌어 기본으로 켜질 수도 있다고 덧붙입니다. 그러니 기본값을 가정하지 말고 언제나 이 문장을 쓰라고 적습니다.

Django

강제하는 쪽이 반드시 엔진이어야 하는 것은 아닙니다. Django 의 ForeignKey 는 가리킬 모델과 on_delete 를 위치 인자로 반드시 받습니다.

Python
from django.db import models

class Manufacturer(models.Model):
    name = models.TextField()

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)

on_delete 는 참조받던 객체가 지워질 때 가리키던 객체를 어떻게 할지 정합니다. Django 공식 문서는 이 인자가 그 갱신을 Django 가 하는지 데이터베이스가 하는지까지 정한다고 적습니다. 데이터베이스 쪽 변형은 관련 객체를 가져오지 않아 비용이 덜 듭니다. 대신 같은 모델 안에서는 파이썬 쪽 변형과 섞어 쓸 수 없습니다. DO_NOTHING 만 예외로 섞입니다.

MongoDB

참조를 적어 두되 그것을 따라가는 일을 엔진이 대신해 주지 않는 자리도 있습니다. MongoDB 공식 문서는 한 문서의 _id 를 다른 문서에 적어 두는 방식을 수동 참조라고 부릅니다. 같은 문서는 애플리케이션이 연관 데이터를 얻으려면 두 번째 조회를 낸다고 적습니다.

컬렉션 이름까지 같이 적는 DBRef 를 써도 마찬가지입니다. 같은 문서는 DBRef 를 풀려면 애플리케이션이 추가 조회를 내야 하며, 일부 드라이버가 도우미 메서드를 제공하지만 해석이 자동으로 되지는 않는다고 적습니다.

관련 항목

참조를 맞대는 키

외래키 · 기본키 · 후보 키 · 참조 키

참조 관계에서 갈리는 테이블 역할

부모 테이블 · 자식 테이블 · 테이블

참조를 비우거나 막는 제약

NULL · NOT NULL · 제약조건 · MATCH SIMPLE · MATCH FULL

검사를 뒤로 미뤄야 하는 참조 상황

순환 참조 · 자기참조 제약 · DEFERRABLE · 트랜잭션

위반이 발생했을 때 마주치는 이름

고아 레코드 · SQLSTATE 23503 · 메시지

이 성질을 강제하는 제품과 엔진

데이터베이스 · PostgreSQL · MySQL · SQLite · Django · MongoDB

제품마다 다른 어휘로 나타나는 이름

PRAGMA foreign_keys · 컬렉션 · 클러스터

이 성질을 좌우하는 데이터 설계

정규화 · 데이터 모델링 · EAV(Entity-Attribute-Value, 엔티티-속성-값) · 샤딩

참조 검사가 영향을 주고받는 실행 장치

트리거 · 인덱스 · 격리 수준

다른 이름: referential integrity · 참조무결성