공유 기본 키와 연결 테이블
중복 프로필과 직접 다대다 저장 오류를 통해 공유 기본 키와 연결 테이블의 복합 후보 키를 설계합니다.
식별 관계가 항상 나쁜 것은 아닙니다.
member_profiles가 회원 없이 존재할 수 없고 회원당 최대 하나라면 member_id를 프로필 PK이자 FK로 쓰는 공유 기본 키가 선택적 1:1을 간결하게 표현합니다.
post_tags 연결도 두 부모 조합 자체가 관계 정체성일 수 있습니다.
관계별 수명과 외부 참조가 달라 키 전략도 다르게 선택합니다.
공유 기본 키는 회원당 최대 한 프로필을, 두 외래 키의 조합은 중복 없는 게시글과 태그 연결을 표현합니다.
#는 PK, →는 FK입니다. 이 그림은 프로필과 태그 연결의 세 FK만 표시합니다. 회원마다 프로필 행이 반드시 생기는 것은 아닙니다.
UNIQUE 없는 일대일 관계
아래 예제는 members 행이 하나 이상, posts 행이 두 개 이상 있는 상태를 전제로 합니다.
profile_id 대리 PK만 두고 member_id 일반 FK를 사용하면 같은 회원의 프로필 여러 행을 DB가 허용합니다.
ERD의 1:1 표시는 DDL 제약이 아니므로 실제 데이터는 1:N이 됩니다.
DROP TABLE IF EXISTS member_profiles_bad;
SET @profile_member_id := (SELECT MIN(id) FROM members);
CREATE TABLE member_profiles_bad (
profile_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
member_id BIGINT UNSIGNED NOT NULL,
bio VARCHAR(500) NULL,
PRIMARY KEY (profile_id),
FOREIGN KEY (member_id) REFERENCES members (id)
);
INSERT INTO member_profiles_bad (member_id, bio) VALUES
(@profile_member_id, '첫 프로필'),
(@profile_member_id, '두 번째 프로필');
SELECT member_id, COUNT(*) FROM member_profiles_bad GROUP BY member_id;같은 member_id에 프로필 두 행이 저장되어 0..1 요구를 위반하지만 SQL 오류는 없습니다.
member_id | COUNT(*)
<first member id> | 2
declared relationship: 1:1
enforced relationship: 1:N일대일은 FK만으로 완성되지 않고 FK의 유일성이 필요합니다.
공유 PK는 하나의 member_id 열에 PK의 유일성과 FK의 참조 제약을 각각 둡니다.
반대로 프로필이 독립 작업 흐름·외부 참조를 가지면 profile_id PK와 member_id UNIQUE가 더 유연합니다.
공유 PK를 모든 확장 테이블의 관습으로 사용하지 않습니다.
ERD 1:1과 DDL 1:N의 차이 확인
- 최대 1 — FK 열이 PK 또는 UNIQUE인지 확인합니다.
- 선택 참여 — 어느 쪽이 0을 허용하는지 FK 위치로 표현합니다.
- 수명 — 프로필이 회원 없이 이전·보존될 수 있는지 봅니다.
- 확장 — 하위 참조가
member_id와profile_id중 무엇을 의미하는지 정합니다.
외래 키 위치와 수명
프로필에 member_id 공유 PK/FK를 두면 회원은 프로필 0..1, 프로필은 회원 정확히 1입니다.
회원 행에 profile_id NULL 허용 FK를 두면 방향은 가능하지만 부모가 확장 테이블을 알아야 하고 여러 선택 1:1마다 NULL 허용 열이 늘어납니다.
M:N post_tags는 두 FK 조합이 최소 후보 키입니다.
연결 행을 다른 기능이 참조하지 않으면 복합 PK가 자연스럽고, 검토·추천 이력이 관계를 단독 참조하면 post_tags_id+조합 UNIQUE를 선택합니다.
선택 참여와 연결 속성으로 키 정하기
- 선택 쪽 — 먼저 존재할 수 있는 주 테이블과 선택 확장 테이블을 구분합니다.
- 유일성 — FK에 PK/UQ가 있어 최대 1을 강제하는지 봅니다.
- 관계 속성 — 원본·신뢰도가 태그 자체가 아니라 게시글-태그 조합에 종속되는지 봅니다.
- 하위 참조 — 연결 행이 독립 ID를 필요로 하는 사용 사례를 찾습니다.
공유 기본 키와 연결 키
현재 요구에서는 프로필과 post_tags 모두 부모 없이 수명이 없고 외부 참조도 작으므로 식별 키를 선택합니다.
DROP TABLE IF EXISTS post_tags;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS member_profiles;
CREATE TABLE member_profiles (
member_id BIGINT UNSIGNED NOT NULL,
bio VARCHAR(500) NULL,
preferred_theme VARCHAR(16) NOT NULL DEFAULT 'SYSTEM',
PRIMARY KEY (member_id),
FOREIGN KEY (member_id) REFERENCES members (id)
ON DELETE CASCADE
);
CREATE TABLE tags (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(80) NOT NULL,
PRIMARY KEY (id), UNIQUE (name)
);
CREATE TABLE post_tags (
post_id BIGINT UNSIGNED NOT NULL,
tag_id BIGINT UNSIGNED NOT NULL,
source VARCHAR(16) NOT NULL,
confidence DECIMAL(5,4) NULL,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts (id),
FOREIGN KEY (tag_id) REFERENCES tags (id),
CHECK (source IN ('MANUAL', 'AUTO')),
CHECK (confidence IS NULL OR confidence BETWEEN 0 AND 1)
);
SET @profile_member_id := (SELECT MIN(id) FROM members);
SET @profile_post_1 := (SELECT MIN(id) FROM posts);
SET @profile_post_2 := (
SELECT MIN(id) FROM posts WHERE id > @profile_post_1
);
INSERT INTO member_profiles (member_id, bio, preferred_theme)
VALUES (@profile_member_id, '회원 게시판 프로필', 'SYSTEM');
INSERT INTO tags (name) VALUES ('mysql');
SET @profile_tag_1 := LAST_INSERT_ID();
INSERT INTO tags (name) VALUES ('modeling');
SET @profile_tag_2 := LAST_INSERT_ID();
INSERT INTO post_tags (post_id, tag_id, source, confidence)
VALUES
(@profile_post_1, @profile_tag_1, 'MANUAL', NULL),
(@profile_post_1, @profile_tag_2, 'AUTO', 0.9500),
(@profile_post_2, @profile_tag_1, 'AUTO', 0.9000);두 번째 프로필과 같은 게시글-태그 조합은 모두 PK 1062로 거절되고 서로 다른 연결은 자유롭게 저장됩니다.
INSERT INTO member_profiles (member_id, bio)
VALUES (@profile_member_id, '중복 프로필');
-- ERROR 1062: member_profiles.PRIMARYINSERT INTO post_tags (post_id, tag_id, source)
VALUES (@profile_post_1, @profile_tag_1, 'MANUAL');
-- ERROR 1062: post_tags.PRIMARY다음 고아 입력은 회원 ID 999999가 존재하지 않는 경우를 전제로 합니다.
INSERT INTO member_profiles (member_id, bio)
VALUES (999999, '고아 프로필');
-- ERROR 1452: member_profiles.member_id FKsecond profile for first member: ERROR 1062
same post + same tag: ERROR 1062
same post + different tag: allowed
same tag + different post: allowed
profile without member: ERROR 1452AUTO 원본일 때만 신뢰도 필수라는 컬럼 간 규칙을 검사로 더 구체화할 수 있습니다.
자동 추천 모델 버전이 연결마다 다르면 post_tags 속성으로 추가합니다.
프로필 CASCADE는 개인 데이터 삭제 요구에 맞을 수 있지만 보존 규칙을 확인합니다.
현재 post_tags의 FK는 기본 RESTRICT입니다. 게시글 삭제 시 CASCADE로 바꿀지는 감사·추천 근거 보존 요구와 별도로 비교합니다.
프로필·태그 위반 예제 데이터 실행하기
- 1:1 위반 — 같은 회원 프로필 두 번 삽입을 시도합니다.
- M:N 예제 데이터 — 위 INSERT의 세 연결을 확인하고, 확장 실험에서는 2 게시글×2 태그 중 남은 한 조합을 추가합니다.
- 관계 속성 — 같은 태그도 게시글별 원본이 다른지 확인합니다.
- 삭제 — 부모 삭제 정책이 프로필과 태그 연결에 각각 맞는지 테스트합니다.
카디널리티와 행 증가 확인
프로필 중복과 조합 중복은 제약상 불가능해야 하며, 게시글별 태그·태그별 게시글 수를 양방향 집계합니다.
SELECT member_id, COUNT(*) AS profile_count
FROM member_profiles GROUP BY member_id HAVING COUNT(*) > 1;
SELECT post_id, COUNT(*) AS tag_count
FROM post_tags GROUP BY post_id;
SELECT tag_id, COUNT(*) AS post_count
FROM post_tags GROUP BY tag_id;
SELECT post_id, tag_id, COUNT(*) AS pair_count
FROM post_tags GROUP BY post_id, tag_id HAVING COUNT(*) > 1;제시한 제약과 INSERT에 따르면 프로필 중복과 조합 중복 쿼리의 예상 결과는 0행입니다.
나머지 두 집계는 한 게시글 여러 태그, 한 태그 여러 게시글이라는 M:N을 실제 수로 보여 줍니다.
공유 PK와 역방향 연결 인덱스
공유 PK 프로필은 member_id만 알아도 JOIN이 간단하고 추가 대리 인덱스가 없습니다.
하지만 프로필 자체를 다른 도메인으로 이전하거나 버전별 여러 행으로 바꾸면 PK 전략을 재검토합니다.
조합 PK의 왼쪽 접두부는 게시글→태그 조회에 맞습니다. 역방향에는 tag_id로 시작하는 인덱스를 확인합니다. InnoDB가 FK용 인덱스를 자동 생성하고 그 레코드에 PK 열을 포함할 수 있으므로 SHOW INDEX와 실행 계획을 보고 중복 인덱스 추가를 피합니다.
프로필 행 폭과 자동 태그 갱신
1:1 분리는 자주 바뀌는 프로필 열의 잠금·행 폭을 회원에서 분리할 수 있지만 조회 JOIN 비용이 생깁니다.
자동 태그 대량 갱신은 수동 행을 덮지 않도록 원본을 조건에 포함하고 조합 UPSERT 정책을 명확히 합니다.
1:1·M:N 키 선택표
- 공유 PK 1:1 — 유일성·참조 결합. 감수할 비용은 독립 수명 제한, 적합한 조건은 완전 종속 확장일 때.
profile_id+UQ — 독립 참조·변경. 감수할 비용은 인덱스 추가, 적합한 조건은 프로필 작업 흐름이 클 때.- 조합 PK 연결 — 관계 정체성 간결. 감수할 비용은 하위 참조 넓음, 적합한 조건은 연결이 단순할 때.
- 연결 ID+UQ — 연결 단독 참조. 감수할 비용은 키·인덱스 증가, 적합한 조건은 관계 이력이 확장될 때.
두 게시글과 두 태그로 네 조합 만들기
- FK 위치 — 회원에
profile_id를 둘 때 선택·확장 비용을 그립니다. - 조합 예제 데이터 — 2×2 조합과 중복을 실행합니다.
- 역방향 인덱스 — 태그→게시글 계획에서 보조 인덱스 전후를 비교합니다.
- 관계 확장 — 추천 근거·검수 상태가 추가될 때 연결 ID가 필요한지 판단합니다.
NULL 프로필·정렬 규칙·신뢰도 예외
- 프로필 없는 회원을 INNER JOIN하면 목록에서 누락되므로 선택 참여 화면은 LEFT JOIN합니다.
- 태그 이름 대소문자 유일성은 정렬 규칙에 따라 달라집니다.
- 신뢰도 DECIMAL 소수 자릿수와 1.0000 경계를 테스트합니다.
- AUTO→MANUAL 전환이 같은 조합 UPDATE인지 별 이력 행인지 요구를 정합니다.
마지막 문서는 관계별로 다른 키 결정을 하나의 논리 모델에 통합하고 현대적 기본값과 예외를 설계 기록으로 남깁니다.
일대일·다대다 키 선택
| 판단 축 | 확인할 질문 |
|---|---|
| 최대 1 | 일대일 FK가 PK 또는 UNIQUE로 강제되는가? |
| 선택 | 선택 확장 쪽에 FK가 있어 NULL 확산을 줄이는가? |
| 종속 | 확장·연결이 부모 없이 독립 수명을 갖는가? |
| 관계 속성 | 두 부모 조합에 속한 값을 연결 테이블에 두었는가? |
| 역방향 | 양방향 조회 인덱스와 하위 참조를 검토했는가? |
식별·비식별을 프로젝트 전체 규칙 하나로 통일하지 않습니다.
프로필, 댓글, post_tags 각각의 수명·참조·변경을 근거로 키를 선택합니다.
연습 문제
회원과 notification_settings는 0..1이고 설정은 회원과 함께 삭제됩니다.
회원과 게시글은 북마크를 사이에 둔 M:N이며 bookmarked_at·memo가 연결 속성입니다.
DDL을 설계하세요.
해설과 예시 답안
설정은 member_id 공유 PK, bookmarks는 (member_id, post_id) 조합 PK를 사용합니다.
같은 회원이 같은 게시글을 한 번만 북마크한다면 조합 PK가 관계의 중복을 직접 막습니다.
CREATE TABLE notification_settings (
member_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
email_enabled BOOLEAN NOT NULL DEFAULT TRUE,
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE CASCADE
);
CREATE TABLE bookmarks (
member_id BIGINT UNSIGNED NOT NULL,
post_id BIGINT UNSIGNED NOT NULL,
bookmarked_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
memo VARCHAR(200) NULL,
PRIMARY KEY (member_id, post_id),
FOREIGN KEY (member_id) REFERENCES members (id),
FOREIGN KEY (post_id) REFERENCES posts (id)
);같은 회원의 같은 게시글 북마크 중복이 거절되고 회원별 설정 두 행이 불가능한지 확인합니다.
핵심 정리
- 일대일은 FK 유일성이 있어야 실제로 최대 1이 됩니다.
- 완전 종속 선택 확장에는 공유 PK가 간결합니다.
- M:N은 연결 테이블과 두 FK로 구현하고 관계 속성을 연결 행에 둡니다.
- 연결의 하위 참조가 커질 때 대리 ID 전환을 검토합니다.
다음 문서에서는 여러 관계의 키 전략을 하나의 논리 모델로 통합하고 설계 근거를 검증합니다.