즐겁게 배우는 SQL 9. 제약조건

jiffydev·2021년 7월 11일
0

즐겁게 배우는 SQL

목록 보기
9/13

본 포스트는 박재호님의 유튜브 강의(링크)를 보고 일부 발췌하여 정리한 내용입니다.
사용된 자료, 샘플 데이터 등은 모두 SQLite Tutorial에서 확인할 수 있습니다.

1. 기본 키

기본 키, 외래 키와 같은 제약조건은 이미 이전 포스트에서도 사용해 보았기 때문에 잘 알 것이라 생각하지만, 제약조건을 공부하면서 기본 키를 안 볼 수는 없기 때문에 다시 한번 다루고자 한다.

기본 키는 테이블에서 각 행을 구분할 수 있는 수단이므로 중복되어서는 안된다.

CREATE TABLE table_name(
   column_1 INTEGER NOT NULL PRIMARY KEY,
   ...
);

기본 키가 하나의 컬럼으로 구성될 경우, 테이블 생성 시 컬럼에 PRIMARY KEY를 명시해 주면 된다.

CREATE TABLE table_name(
   column_1 INTEGER NOT NULL,
   column_2 INTEGER NOT NULL,
   ...
   PRIMARY KEY(column_1,column_2,...)
);

M:N 관계에서처럼 두 테이블 사이에 중간 테이블이 있는 경우, 기본키가 두 개 이상의 컬럼으로 구성될 수도 있다. 이 때는 기본키가 될 컬럼을 NOT NULL로 하고 PRIMARY KEY를 따로 선언해 준다.

2. 외래 키

외래 키는 단순히 키가 있고 없고의 문제가 아니라 외래 키에 어떤 행위가 일어났을 때 이를 어떻게 처리할지를 정할 수 있는데, 이 점이 외래 키를 이해하는데 어려움을 겪게 한다.

sqlite에서는 외래 키가 기본적으로 OFF 상태이기 때문에 실습을 하기 전에 항상 PRAGMA foreign_keys = ON;을 실행한 후에 진행해야 한다.

그러면 외래 키를 가진 테이블을 생성해 보자

CREATE TABLE supplier_groups (
	group_id integer PRIMARY KEY,
	group_name text NOT NULL
);

CREATE TABLE suppliers (
    supplier_id   INTEGER PRIMARY KEY,
    supplier_name TEXT    NOT NULL,
    group_id      INTEGER NOT NULL,
    FOREIGN KEY (group_id)
       REFERENCES supplier_groups (group_id) 
);

이렇게 생성한 테이블은 아래와 같은 관계를 갖게 될 것이다.

이제 본격적으로 데이터를 넣어 보자.

INSERT INTO supplier_groups (group_name)
VALUES
   ('Domestic'),
   ('Global'),
   ('One-Time');
   
INSERT INTO suppliers (supplier_name, group_id)
VALUES ('HP', 2);

여기까지는 문제 없이 데이터가 들어간 것을 확인할 수 있을 것이다.
그런데 아래와 같이 없는 group_id를 넣으려고 하면 어떻게 될까?

INSERT INTO suppliers (supplier_name, group_id)
VALUES('ABC Inc.', 4);

해당하는 외래 키가 없어서 에러가 난 것을 볼 수 있다.

한 발 더 나아가, 외래 키의 행위를 정의해 보자.
supplier_groups의 데이터 중 한 행을 삭제할 경우, 이를 참조하고 있는 suppliers의 데이터는 어떻게 될까?

sql에서는 기본적으로 이런 상황을 위한 설정을 가지고 있다.
sqlite에서는 외래 키에 설정을 할 때 다음과 같은 형태로 설정하게 된다.

FOREIGN KEY (foreign_key_columns)
   REFERENCES parent_table(parent_key_columns)
      ON UPDATE action 
      ON DELETE action;

정의 할 수 있는 action은 아래의 5가지가 있으니, 비즈니스 로직에 따라 필요한 것을 선택하면 된다.

  • SET NULL
    참조하고 있는 부모 키가 변경되면 그에 해당하는 자식 키는 NULL로 변경된다.

  • SET DEFAULT
    외래 키의 값을 테이블 생성시에 설정한 기본값으로 변경한다.

  • RESTRICT
    부모 키의 변경을 애초에 허용하지 않는다.

  • NO ACTION
    아무 행동도 하지 않는다. 사실 RESTRICT와 유사한 효과를 가진다.

  • CASCADE
    부모 키가 변경되면, 변경된 것을 자식 키에게도 전달해서 변경시킨다.

3. NOT NULL 제약

테이블을 생성할 때 컬럼이 NULL값을 가질 수 있는지를 설정할 수 있는데, 명시적으로 NOT NULL을 설정하지 않는 한 NULL값을 가질 수 있다.

따라서 반드시 입력이 필요한 값이라면 NOT NULL을 설정하면 된다.
물론 PRIMARY KEY는 NULL이면 안되므로 NOT NULL이란 뜻이 내포되어 있다.
다만 sqlite에서는 버그와 호환성 문제로 기본 키에도 NULL을 허용하고 있으므로 설정할 때 유의해야 한다.

4. UNIQUE 제약

UNIQUE 제약은 컬럼의 각 데이터들이 유일성을 보장하는 제약이다.
말만 들으면 단순히 데이터가 중복되지만 않으면 되는 것 아닌가 싶지만, 실제로 사용하게 되면 몇 가지 고민해야 할 부분이 있다.

4-1. 일반적인 경우

-- column level
CREATE TABLE table_name(
    ...,
    column_name type UNIQUE,
    ...
);

-- table level
CREATE TABLE table_name(
    ...,
    UNIQUE(column_name)
);

위 두개의 방법 중 어떤 것을 선택해도 제약을 설정해도 무방하다.

CREATE TABLE contacts(
    contact_id INTEGER PRIMARY KEY,
    first_name TEXT,
    last_name TEXT,
    email TEXT NOT NULL UNIQUE
);

INSERT INTO contacts(first_name,last_name,email)
VALUES ('John','Doe','john.doe@gmail.com');

테이블을 생성해 데이터를 하나 넣고, 같은 이메일 주소로 데이터를 한 번 더 넣어 보면 에러가 발생하는 것을 볼 수 있다.

4-2. UNIQUE 컬럼이 여러개인 경우

UNIQUE 제약을 여러 컬럼에 적용할 경우 다음과 같이 생성하면 된다.

CREATE TABLE table_name(
    ...,
    UNIQUE(column_name1,column_name2,...)
);

여러 컬럼에 UNIQUE 제약을 걸 경우 어떤 로직으로 처리가 되는지 확인해 보자.

CREATE TABLE shapes(
    shape_id INTEGER PRIMARY KEY,
    background_color TEXT,
    foreground_color TEXT,
    UNIQUE(background_color,foreground_color)
);

INSERT INTO shapes(background_color,foreground_color)
VALUES('red','green');

테이블을 생성하고 데이터를 넣었다.
다시 데이터를 넣는데, 이번에는 설정한 두 개의 UNIQUE 컬럼 중 하나만 같은 데이터인 경우이다.

실행해 보면 에러 메시지 없이 데이터가 잘 들어간 것을 확인할 수 있다.

그렇다면 완전히 똑같은 데이터를 넣게 되면 에러가 날지 테스트해보자.

에러가 발생한다.

결론은 여러 개의 컬럼에 UNIQUE 제약을 설정할 경우, 각 컬럼의 데이터가 모두 이미 있는 데이터여야 제약에 걸린다는 것이다. 만약 한 컬럼이라도 다른 데이터가 있을 경우, unique한 것으로 취급된다.

4-3. 값이 NULL인 경우

UNIQUE 제약을 설정했는데 값이 NULL의 경우는 어떻게 처리할까?
상식적으로 생각해 보면 unique하다고 했으니 NULL이 여러개 있으면 에러가 날 것 같은데, 실제로 확인해 보자.

CREATE TABLE lists(
    list_id INTEGER PRIMARY KEY,
    email TEXT UNIQUE
);

INSERT INTO lists(email)
VALUES(NULL),(NULL);

실제로 sql문을 실행해 보면 에러가 나지 않는 것을 볼 수 있다.

들어간 데이터를 확인해 보면 제약을 걸었음에도 NULL이 중복해서 들어간 것이 보일 것이다.

결론: NULL을 unique하게 만들 수 있는 방법은 없으므로 NULL을 체크하기 위해 UNIQUE 제약을 설정할 수 없다.

5. CHECK 제약

CHECK는 데이터를 넣거나 업데이트할 때 값이 적절한지(무결성을 위반하지 않는지) 확인할 때 사용한다. 만약 지정한 조건에 맞지 않는다면 에러가 발생한다.

CHECK를 정의할 때는 컬럼레벨이나 테이블레벨 어느쪽에서든 가능하나, 서브쿼리를 포함할 수 없음에 유의한다.
또한 CHECK를 수행하게 되면 결과로 0,1이 나오게 되는데, 만약 0이 아닌 값(NULL 등)이 나오면 에러가 나지 않는다.

CREATE TABLE table_name(
    ...,
    column_name data_type CHECK(expression),
    ...
);

CREATE TABLE table_name(
    ...,
    CHECK(expression)
);

그렇다면 처음 테이블을 만들 때 CHECK를 까먹어서 나중에 추가하는 경우에는 어떻게 할까?
최신 버전에는 바로 추가할 수 있지만, 실습을 하게 되는 웹사이트의 sqlite에서는 불가능하다.
따라서 기존에 테이블 데이터를 바꿀 때 사용한 방법인 새 테이블 만들어서 통째로 옮기기를 사용해야 한다.

6. AUTOINCREMENT 제약

AUTOINCREMENT도 이미 이전 포스트의 예제 등에서 본 적이 있을 것이다.
하지만 구체적인 설명이 없었기에, 이번에 제대로 다루고 넘어가고자 한다.

테이블을 생성하면 WITHOUT ROWID를 따로 하지 않는 한 ROWID라는 컬럼이 자동으로 생성되는데, 이는 각 행을 구별해주는 식별자 역할을 한다.

CREATE TABLE people (
   first_name TEXT NOT NULL,
   last_name TEXT NOT NULL
);

INSERT INTO people (first_name, last_name)
VALUES('John', 'Doe'), ('Lily', 'Bush');

테이블을 만들고 데이터를 넣었다.
다음으로 위에서 우리가 생성하지 않은 rowid라는 항목을 포함해 쿼리해 보자.

SELECT
   rowid,
   first_name,
   last_name
FROM
   people;

sqlite에서 자동으로 생성한 rowid라는 컬럼이 들어가 있는 것을 볼 수 있다.

그런데 테이블을 생성할 때 컬럼을 INTEGER타입의 PRIMARY KEY로 설정하면 어떤 일이 일어나는지 보자.
기존에 생성했던 테이블을 삭제하고 새 테이블을 만든다.

DROP TABLE people;

CREATE TABLE people (
   person_id INTEGER PRIMARY KEY,
   first_name TEXT NOT NULL,
   last_name TEXT NOT NULL
);

INSERT INTO people (first_name, last_name)
VALUES('John', 'Doe'), ('Lily', 'Bush');

person_id라는 컬럼이 추가되었는데, 이 테이블을 쿼리할 때 rowid도 포함해서 쿼리하면 다음과 같이 나온다.

분명히 rowid를 명시했는데 나온 이름은 person_id이다.
결국 INTEGER타입의 PRIMARY KEY로 설정한 컬럼은 rowid를 설정한 것과 같다고 할 수 있다.

위 이미지에서 보이듯, PRIMARY KEY는 순차적으로 증가하는 것처럼 보인다.
그런데 sqlite에서는 rowid 숫자를 직접 넣을 수 있으므로, 가장 큰 숫자를 넣고 데이터를 하나 더 넣어보면 어떻게 될까?
상식적으로 생각하면, 순차적으로 증가하니 가장 큰 숫자를 넣으면 다음에 들어가는 데이터는 오버플로우가 나서 에러가 뜨지 않을까 싶지만 그렇지 않다.
직접 확인해 보자.

rowid가 가질 수 있는 가장 큰 수는 9223372036854775807이다.

INSERT INTO people (person_id,first_name,last_name)
VALUES(	9223372036854775807,'Johnathan','Smith');

가장 큰 숫자까지는 무난하게 들어갔다.
AUTOINCREMENT가 순차적으로 증가한다면 이 다음에 데이터를 넣을 수 없을 것이다.

INSERT INTO people (first_name,last_name)
VALUES('William','Gate');

그런데 데이터를 넣어보니 아무 문제 없이 데이터가 들어갔다.
그리고 데이터를 확인해 보면

생뚱맞은 숫자로 rowid가 들어간 것을 확인할 수 있다.

이것은 무엇을 의미할까?
PRIMARY KEY는 반드시 순차적으로 증가하는 것은 아니라는 뜻이다.
따라서 이를 순차적이라 가정하고 작업을 수행하게 되면 어디선가 잘못된 부분이 나타나게 될 것이다.

그렇다면 순서성을 보장하고 싶을때는 어떻게 해야 할까?
이 때 AUTOINCREMENT를 사용한다.

다만 가이드의 주의사항에도 나와있듯이, AUTOINCREMENT를 적용하면 그만큼 자원에 부담이 되기 때문에 반드시 순서를 지켜야 하는 상황이 아니면 사용하지 않는 것이 좋다.

실제로 데이터를 통해 AUTOINCREMENT를 적용하면 어떤 변화가 있는지 확인해 보자.

DROP TABLE people;

CREATE TABLE people (
   person_id INTEGER PRIMARY KEY AUTOINCREMENT,
   first_name TEXT NOT NULL,
   last_name TEXT NOT NULL
);

INSERT INTO people (person_id, first_name, last_name)
VALUES(9223372036854775807, 'John', 'Doe');

그리고 데이터를 하나 추가하려고 하면

가장 큰 수 다음에 데이터가 들어가니 에러가 발생한 것을 볼 수 있다.

profile
잘 & 열심히 살고싶은 개발자

0개의 댓글