목표는 모든 테이블이 PK를 갖도록하는 것이지만, PK의 본질을 혼동하면 안티패턴을 초래할수 있음
까다로운 부분은 PK로 사용할 컬럼을 선정하는 일임
이런 테이블에는 테이블로 모델링한 영역에서는 아무런 의미도 가지지 않는 인위적인 값을 저장할 새로운 칼럼이 필요함
이 칼럼을 PK로 사용하면(만약 이것이 적절하다면), 다른 속성 칼럼에는 중복값이 들어가는 것을 허용하는 반면 특정 행에 유일하게 접근할수있게됨
이러한 형태의 PK를 가상키(pseudokey)또는 대체키(surrogate key)라 함
여러 클라이언트가 동시에 새로운 행을 삽입하는 경우에도 각 행의 가상키값이 유일하게 할당되는 것을 보장하기 위해,
대부분의 DBMS는 트랜잭션 격리범위 밖에서 유일한 정수 값을 생성하는 매커니즘을 제공함
기능 | 지원 데이터베이스 |
---|---|
AUTO_INCREMENT | MySQL |
GENERATOR | Firebird, InterBase |
IDENTITY | DB2, Derby, Microsoft SQL Server, Sybase |
ROWID | SQLite |
SEQUENCE | DB2, Firebird, Informix, Ingres, Oracle , PostgreSQL |
SERIAL | MySQL, PostgreSQL |
가상키는 유용한 기능이지만, PK를 선언하는 유일한 방법은 아니다.
책이나 기사, 프로그래밍 프레임워크는 데이터베이스 내 모든 테이블이 다음과 같은 특성을 가지는 PK칼럼을 가지도록 하는 문화적 관례를 만들었음
모든 테이블에 id란 이름의 컬럼이있는것은 너무도 흔해져 이게PK와 동의어가 되어 버렸음
SQL을 배우는 프로그래머들은 PK가 항상 다음과 같은 식으로 정의되는 칼럼이라는 잘못된 생각을 갖게 됨
CREATE TABLE Bugs (
id SERIAL PRIMARY KEY,
description VARCHAR(1000),
-- . . .
);
모든 테이블에 id칼럼을 추가하는 것은, 그 사용을 이상하게 만드는 몇가지 효과를 초래함
테이블 안의 다른 칼럼이 자연키로 사용될 수 있는 상황에서 조차 단지 통념에 따라 id칼럼을 PK로 정의한것을 봤을 것임
그 다른 컬럼에 UNIQUE제약조건이 설정되어 있는 경우도 있음.
예를들 Bugs테이블에서는 프로젝트 코드를 앞에 붙여 bug_id를 만들수 있을 것임
CREATE TABLE Bugs (
id SERIAL PRIMARY KEY,
bug_id VARCHAR(10) UNIQUE,
description VARCHAR(1000),
-- . . .
);
INSERT INTO Bugs (bug_id, description, ...)
VALUES ('VIS-078', 'crashes on save', ...);
이예에서 bug_id칼럼은 각 행을 유일하게 식별할 수 있도록 해준다는 면에서 id와 사용목적이 동일함
복합키는 여러 칼럼을 포함함
복합키가 사용되는 전형적인 예는 BugsProducts와 같은 교차테이블안에서다.
PK는 특정한 bug_id와 product_id값의 조합이 테이블안에서 한 번만 나타난다는 것을 보장해야함
각 값이 다른 쌍으로 여러번 나타날수 있을지라도 말이다.
그러나 id칼럼을 PK로 사용하는 경우에는 유일해야 하는 두 칼럼에 제약조건이 적용되지 않음
CREATE TABLE BugsProducts (
id SERIAL PRIMARY KEY,
bug_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
INSERT INTO BugsProducts (bug_id, product_id)
VALUES (1234, 1), (1234, 1), (1234, 1); -- duplicates are permitted
Bug와 Products를 연결하기위해 이 교차 테이블을 사용할때, 중복 때문에 의도하지 않은 결과가 발생함
중복을 방지하기 위해서는 id뿐만 아니라 다른 두 칼럼에 UNIQUE제약조건을 걸어줘야 함
CREATE TABLE BugsProducts (
id SERIAL PRIMARY KEY,
bug_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
UNIQUE KEY (bug_id, product_id),
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
그러나 이 투 칼럼에 UNIQUE제약조건을 걸어야 한다면, id칼럼은 불필요한 것임
id란 이름은 너무 일반적이기 때문에 아무런 의미도 갖기 못함
이는 PK칼럼 이름이 동일한 두 테이블을 조인할 때 특이 문제가됨
원래의 위치 대신 이름만으로 칼럼을 참조한다면 애플리케이션 코드에서 버그의 id와 계정의id를 어떻게 구분할 것인가?
이는 PHP와 같은 동적언어에서는 특히 문제가 됨.
쿼리 결과가 연관배열에 담겨 반환되는데, 쿼리에 칼럼 별명(alias)를 지정하지 않으면 한 칼럼이 다른 칼럼을 덮어서써버리기 때문임
id칼럼의 이름은 쿼리의 으미를 명확하게 하는데도 도움이 되지않음
그러나 칼럼 이름이 bug_id와 account_id로 되어있다면 쿼리 결과를 읽기도 휠씬 쉬울것임
SELECT * FROM Bugs AS b JOIN BugsProducts AS bp ON (b.bug_id = bp.bug_id);
SQL은 두 테이블의 조인을 표현하는 좀더 간략한 문법도 지원함
양쪽 테이블에 칼럼이름이 같다면 앞의 쿼리는 다음과 같이 다시 작성할수 있음
SELECT * FROM Bugs JOIN BugsProducts USING (bug_id);
그러나 모든 테이블이 id란 이름의 가상키를 PK로 정의해야 한다면, 종속된 테이블에서의FK칼럼 이름은 참조하는 PK칼럼의 이름과 같을수 없게됨
따라서 약간은 장황한 ON문법을 사용해야함
SELECT * FROM Bugs AS b JOIN BugsProducts AS bp ON (b.id = bp.bug_id);
어떤 개발자는 사용하기 어렵다는 이유로 복합키를 거부하며, 키를 비교할때 모든 칼럼을 비교해야함.
복합 PK를 참조하는 FK는 자신도 복합 FK가 되어야하며, 복합키를 하려면 타이핑을 더 해야함
테이블에서 PK칼럼이름으로 id(지나치게 일반적인 이름)가 사용되고 있으면 이 안티패턴의 징후로 볼수 있음
좀더 의미 있는 이름 대신 id를 선호해야할 이유가 없음
다음과 같은 말 또한 이 안티패턴의 증거가 될수 있음
PK는 제약조건이지 데이터 타입이 아니다.
데이터 타입이 인덱스르 지원하기만 하면, 어느 칼럼도는 칼럼의 묶음에 대해서도 PK를 선언할수있음
도한 테이블의 특정칼럼을 PK로 잡지 않고도 자동증가하는 정수값을 가지도록정의할수 있음
이 두 개념은 서로 독립적인 것임
PK에 의미 있는 이름을 선택해야함
이 이름은 PK가 식별하는 엔터티의 타입을 나타내야함
예를들어 Bugs 테이블의 PK는 bug_id가되어야 함
FK에서도 가능하다면 같은 칼럼이름을 사용해야함
이는 종종PK이름이 스키나 매에서 유일해야함을 뜻함
하나가 다른쪽의 FK가 아닌 한, 동일한 PK이름이 다른 테이블에 나오면 안됨
그러나 예외사항으로 연결의 본질을 더 잘 표현하는 경우라면, FK를 자신의 참조하는 PK이름과 다른게하는 것도 괜찮음
CREATE TABLE Bugs (
-- . . .
reported_by BIGINT UNSIGNED NOT NULL,
FOREIGN KEY (reported_by) REFERENCES Accounts(account_id)
);
메타데이터 명명 규칙을 설명하는 업계 표준이 있음.
ISO/IEC 11179라는 표준인데, 정보기술 시스템에서 메타데이터의 분류체계 관리에 대한 가이드 라인임
다른 말로하면, 테이블 이름과 칼럼이름을 의미있게 짓는 방법임
다른 ISO 표준과 마찬가지로, 이 문서도 매우 난해하지만 "SQL Progra,,omg Style"에서 SQL에 실용적으로 적용하는 방법이 나옴
객체-관계 프레임워크는 id란 이름의 가상키가 사용될 것을 기대하지만, 다른 이름을 사용하도록 재설정하는 것도 허용함
다음은 Ruby on Rails에서의 예다.
class Bug < ActiveRecord::Base
set_primary_key "bug_id"
end
유일함을 보장되고, NULL값을 가지는 경우가없고, 행을 식별하는 용도로 사용할수 있는 속성이 테이블에 있다면,
단지 통념을 따르기 위해 가상키를 추가해야한다는 의무감을 느낄필요가 없음
처음에는 자연키로 손색이 없어 보인던 칼럼이 나중에 알고 보니 적법하게 중복을 허용하는 것으로 밝혀질수도 있으며, 이런 경우에는 가상키를 사용할수 있음
복합키가 적절한 경우에는 이를 사용하기바람
BugProducts테이블에 서와 같이 여러 칼럼의 조합으로 행을 가장 잘 식별할수 있다면, 이 칼럼조합을 복합킬로 사용해야함
CREATE TABLE BugsProducts (
bug_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (bug_id, product_id),
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
INSERT INTO BugsProducts (bug_id, product_id)
VALUES (1234, 1), (1234, 2), (1234, 3);
INSERT INTO BugsProducts (bug_id, product_id)
VALUES (1234, 1); -- error: duplicate entry
복합 PK를 참조하는 FK또는 복합키가 되어야 함에 유의하기 바람
종속되는 테이블에 이렇게 칼럼 조합을 중복해야 하는 것은 안 좋아 보이지만, 장점도 있음
중복된 칼럼 값을 얻을때 조인을 안해도 되기때문에 쿼리가단순해짐
SQL Antipatterns Tip
관례는 도움이 될때만 좋은 것이다