조인 테이블 상속
공통 게시글 부모와 텍스트, 이미지, 링크의 하위 테이블을 공유 기본 키로 나누고, 외래 키가 막지 못하는 자식 0개나 2개의 부모를 재현합니다. 유형별 생성 프로시저와 정확히 하나의 자식을 확인하는 감사로 하위 유형의 참여를 관리하고, 투표 유형으로 확장합니다.
공통 post_id와 깨끗한 하위 유형 테이블을 모두 원한다면 조인 테이블 상속(class table inheritance)이 후보입니다. 부모는 제목과 URL, 생성 시각, 판별자를 갖고, 자식은 유형마다의 전용 속성만 저장하며, 작성자 연결은 부모 하나를 참조합니다.
관계형의 외래 키는 자식이 부모 없이 생기는 것은 막지만, 반대 방향으로 부모가 반드시 자식 하나를 가져야 한다는 규칙은 보장하지 못합니다. 같은 부모 ID가 텍스트 게시글과 이미지 게시글의 자식에 동시에 들어가는 것도, 각각의 외래 키만 보면 합법입니다. 그래서 쓰기 프로시저와 완전성의 감사가 필요합니다.
posts_joined는 전역 식별자와 공통의 수명을 맡고, text_post_detail, image_post_detail, link_post_detail은 공유 기본 키로 부모를 참조합니다. 자식의 ID는 새로 만들지 않고 post_id를 그대로 씁니다. 애플리케이션의 역할은 부모와 자식에 직접 INSERT하지 않고 유형별 프로시저를 호출하며, 프로시저가 부모 ID를 만든 뒤 맞는 자식을 넣고 is_complete를 TRUE로 바꿉니다. 읽는 쪽은 완료된 부모만 보여 줍니다.
공유 PK가 보장하는 유형별 참여 수
공통 ID를 만든다. 완전 참여 검사 뒤 완료로 표시한다. 각 단계의 값과 전환 이유를 비교합니다.
JOINED TABLES · PER-TYPE CARDINALITY / WRITE BOUNDARY
각 상세 0..1은 전체 상세가 정확히 1이라는 뜻이 아니다
공유 PK와 FK는 상세가 참조하는 부모의 존재와 유형별 최대 1행을 보장합니다. 부모 판별자에 맞는 상세 하나를 완성하는 규칙은 생성 프로시저와 쓰기 권한·감사가 담당합니다.
#는 PK, →는 FK입니다. 각 상세는 0..1개지만 세 상세의 합계가 1이라는 뜻은 아닙니다. 합계와 판별자 일치는 생성 경로·권한·감사 쿼리로 확인합니다.
정상 생성 경로 · 부모와 상세를 같은 트랜잭션에서 완성
부모 INSERT → TEXT 상세 35자 INSERT → is_complete = TRUE → COMMIT
오류 → ROLLBACK · 실패한 AUTO_INCREMENT 번호는 재사용하지 않음
상세 0개·서로 다른 상세 2개인 반례 모두 FK는 통과합니다. is_complete 플래그 자체도 상세 합계를 증명하지 않습니다. 부모 잠금과 권한 제한을 함께 운영하고, 감사는 합계 1·판별자 일치·미완료 행을 확인합니다. 생성 프로시저는 외부 트랜잭션 없이 호출합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 부모 생성부터 완료 확정까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목·표기 | 각 상세가 0..1행이라는 것은 전체 상세 합계가 정확히 1행이라는 뜻이 아니다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 부모 생성 공통 ID를 만든다. |
|
|
| 세부 생성 같은 부모 ID의 TEXT 세부 행을 쓴다. |
|
|
| 완료 확정 완전 참여 검사 뒤 완료로 표시한다. |
|
|
공유 PK가 보장하는 유형별 참여 수
공통 ID를 만든다. 완전 참여 검사 뒤 완료로 표시한다. 각 단계의 값과 전환 이유를 비교합니다.
JOINED TABLES · PER-TYPE CARDINALITY / WRITE BOUNDARY
각 상세 0..1은 전체 상세가 정확히 1이라는 뜻이 아니다
공유 PK와 FK는 상세가 참조하는 부모의 존재와 유형별 최대 1행을 보장합니다. 부모 판별자에 맞는 상세 하나를 완성하는 규칙은 생성 프로시저와 쓰기 권한·감사가 담당합니다.
#는 PK, →는 FK입니다. 각 상세는 0..1개지만 세 상세의 합계가 1이라는 뜻은 아닙니다. 합계와 판별자 일치는 생성 경로·권한·감사 쿼리로 확인합니다.
정상 생성 경로 · 부모와 상세를 같은 트랜잭션에서 완성
부모 INSERT → TEXT 상세 35자 INSERT → is_complete = TRUE → COMMIT
오류 → ROLLBACK · 실패한 AUTO_INCREMENT 번호는 재사용하지 않음
상세 0개·서로 다른 상세 2개인 반례 모두 FK는 통과합니다. is_complete 플래그 자체도 상세 합계를 증명하지 않습니다. 부모 잠금과 권한 제한을 함께 운영하고, 감사는 합계 1·판별자 일치·미완료 행을 확인합니다. 생성 프로시저는 외부 트랜잭션 없이 호출합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 부모 생성부터 완료 확정까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목·표기 | 각 상세가 0..1행이라는 것은 전체 상세 합계가 정확히 1행이라는 뜻이 아니다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 부모 생성 공통 ID를 만든다. |
|
|
| 세부 생성 같은 부모 ID의 TEXT 세부 행을 쓴다. |
|
|
| 완료 확정 완전 참여 검사 뒤 완료로 표시한다. |
|
|
부모와 하위 행의 불완전성
가져오기 스크립트가 부모만 넣거나 텍스트와 이미지 게시글에 같은 ID를 넣으면, 공통 목록에는 행이 보이지만 전용 속성을 읽을 수 없거나 두 개가 됩니다. 아래 _bad 테이블들은 자식이 부모를 가리키는 외래 키만 있고 다른 검사는 없는 조인 모델입니다. 첫 부모에는 자식을 넣지 않고, 둘째 부모에는 텍스트와 이미지 자식을 모두 넣습니다. 마지막 SELECT는 두 자식 테이블을 LEFT JOIN한 뒤 IS NOT NULL의 참(1)과 거짓(0)을 더해, 부모마다 자식이 몇 개 붙었는지 셉니다.
DROP TABLE IF EXISTS joined_text_bad;
DROP TABLE IF EXISTS joined_image_bad;
DROP TABLE IF EXISTS joined_post_bad;
CREATE TABLE joined_post_bad (
post_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
post_type VARCHAR(16) NOT NULL,
title VARCHAR(160) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE joined_text_bad (
post_id BIGINT UNSIGNED PRIMARY KEY,
body_chars INT NOT NULL,
FOREIGN KEY (post_id) REFERENCES joined_post_bad (post_id)
) ENGINE=InnoDB;
CREATE TABLE joined_image_bad (
post_id BIGINT UNSIGNED PRIMARY KEY,
image_width_px INT NOT NULL,
FOREIGN KEY (post_id) REFERENCES joined_post_bad (post_id)
) ENGINE=InnoDB;
INSERT INTO joined_post_bad (post_type, title) VALUES ('TEXT', '자식 없는 부모');
SET @bad_parent = LAST_INSERT_ID();
INSERT INTO joined_post_bad (post_type, title) VALUES ('TEXT', '자식 두 개인 부모');
SET @double_parent = LAST_INSERT_ID();
INSERT INTO joined_text_bad VALUES (@double_parent, 20);
INSERT INTO joined_image_bad VALUES (@double_parent, 1200);
SELECT p.post_id, p.post_type,
(t.post_id IS NOT NULL) + (i.post_id IS NOT NULL) AS child_count
FROM joined_post_bad AS p
LEFT JOIN joined_text_bad AS t ON t.post_id = p.post_id
LEFT JOIN joined_image_bad AS i ON i.post_id = p.post_id
ORDER BY p.post_id;post_id | post_type | child_count
1 | TEXT | 0
2 | TEXT | 2모든 외래 키가 유효한데도, 첫 부모는 자식이 0개이고 두 번째 부모는 서로 다른 유형의 자식이 2개입니다. 두 번째 부모는 판별자가 TEXT인데 이미지 자식까지 가졌으므로 판별자와도 어긋납니다.
외래 키는 자식이 있을 때 그 부모가 맞는지만 확인합니다. 부모를 만드는 트랜잭션이 끝나는 시점에 자식이 정확히 하나 있어야 한다는, 테이블을 넘나드는 제약은 MySQL의 DDL로 직접 나타낼 수 없습니다. is_complete는 편리한 플래그일 뿐 유일한 판단 기준이 아니므로, 프로시저가 마지막에 TRUE로 바꾸고 읽는 쪽이 거르는 검증 단계로 쓰며, 정기 쿼리가 실제 자식의 수와 유형의 일치를 다시 검증합니다.
공유 기본 키는 소유의 관계를 뜻합니다. 자식 행은 부모 없이 독립된 식별자를 갖지 않고 ON DELETE CASCADE로 부모의 수명을 따르며, 반대로 작성자 연결은 부모를 RESTRICT로 참조해 쓰고 있는 게시글을 실수로 지우지 못하게 합니다.
부모 관계와 하위 유형의 완전성
제목의 수정이나 보관 같은 공통 작업은 부모 한 행에서 하고, 텍스트 게시글의 body_chars 변경은 자식만 잠급니다. 공통 목록은 부모만 읽고, 상세 화면은 유형에 맞는 자식을 조인하며, 실제 비용은 데이터와 실행 계획으로 확인합니다.
생성 프로시저는 판별자를 매개변수로 받는 범용 JSON 파서보다, 유형별 함수의 시그니처가 읽기 쉽습니다. 텍스트 게시글의 생성 프로시저는 body_chars를 받고 이미지의 전용 매개변수는 받지 않습니다. IN 매개변수 자체가 NULL을 금지하지는 않으므로, 실제 값은 자식의 NOT NULL과 CHECK가 검사합니다.
정확히 하나의 자식을 확인하는 공식은 이렇습니다. 각 부모를 세 자식에 LEFT JOIN하고 자식의 존재를 0과 1로 더해, 합이 1이 아니거나 판별자와 실제 자식이 다르면 위반으로 봅니다. 이 쿼리는 마이그레이션의 전후와 백업 복구의 테스트에 넣습니다.
MySQL에는 커밋할 때까지 검사를 미루는 지연 제약이 없으므로, 부모와 자식을 여러 문장으로 만들 때는 트랜잭션과 최소 권한이 중요합니다. 일반 계정의 테이블 INSERT 권한을 거두고, 프로시저의 실행만 허용합니다.
유형별 생성 프로시저
스키마는 공통 부모와 상세 테이블 세 개, 작성자 연결을 의존성의 순서로 만듭니다. 텍스트 게시글 프로시저의 EXIT 처리기는 자식을 저장하다 오류가 나면 부모의 행도 롤백하며, 이미 할당한 AUTO_INCREMENT 번호를 다시 쓴다는 뜻은 아닙니다.
아래의 이미지 데이터도 명시적인 트랜잭션으로 같은 순서를 따르며, 링크의 데이터와 생성 프로시저는 여기서 구현하지 않습니다. 각 생성 프로시저는 자신의 트랜잭션을 시작하고 끝내므로, 이미 시작한 바깥 트랜잭션 안에서 부르지 않습니다. MySQL의 START TRANSACTION은 열려 있던 트랜잭션을 암묵적으로 커밋합니다.
DROP PROCEDURE IF EXISTS create_text_post;
DROP TABLE IF EXISTS member_post_joined;
DROP TABLE IF EXISTS link_post_detail;
DROP TABLE IF EXISTS image_post_detail;
DROP TABLE IF EXISTS text_post_detail;
DROP TABLE IF EXISTS posts_joined;
CREATE TABLE posts_joined (
post_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
post_type VARCHAR(16) NOT NULL,
title VARCHAR(160) NOT NULL,
canonical_url VARCHAR(700) NOT NULL,
is_complete BOOLEAN NOT NULL DEFAULT FALSE,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE KEY uq_joined_canonical_url (canonical_url),
CONSTRAINT chk_posts_joined_type CHECK (post_type IN ('TEXT', 'IMAGE', 'LINK')),
CHECK (is_complete IN (0, 1))
) ENGINE=InnoDB;
CREATE TABLE text_post_detail (
post_id BIGINT UNSIGNED PRIMARY KEY,
body_chars INT UNSIGNED NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts_joined (post_id) ON DELETE CASCADE,
CHECK (body_chars BETWEEN 1 AND 1000000)
) ENGINE=InnoDB;
CREATE TABLE image_post_detail (
post_id BIGINT UNSIGNED PRIMARY KEY,
alt_text VARCHAR(255) NOT NULL,
image_width_px INT UNSIGNED NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts_joined (post_id) ON DELETE CASCADE,
CHECK (image_width_px BETWEEN 1 AND 20000)
) ENGINE=InnoDB;
CREATE TABLE link_post_detail (
post_id BIGINT UNSIGNED PRIMARY KEY,
link_kind VARCHAR(16) NOT NULL,
target_url VARCHAR(1000) NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts_joined (post_id) ON DELETE CASCADE,
CHECK (link_kind IN ('EXTERNAL', 'INTERNAL', 'DOWNLOAD'))
) ENGINE=InnoDB;
CREATE TABLE member_post_joined (
member_id BIGINT UNSIGNED NOT NULL,
post_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (member_id, post_id),
FOREIGN KEY (member_id) REFERENCES members (id) ON DELETE CASCADE,
FOREIGN KEY (post_id) REFERENCES posts_joined (post_id) ON DELETE RESTRICT
) ENGINE=InnoDB;
DELIMITER //
CREATE PROCEDURE create_text_post(
IN p_title VARCHAR(160),
IN p_url VARCHAR(700),
IN p_body_chars INT UNSIGNED,
OUT p_post_id BIGINT UNSIGNED
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;
START TRANSACTION;
INSERT INTO posts_joined (post_type, title, canonical_url)
VALUES ('TEXT', p_title, p_url);
SET p_post_id = LAST_INSERT_ID();
INSERT INTO text_post_detail VALUES (p_post_id, p_body_chars);
UPDATE posts_joined SET is_complete = TRUE WHERE post_id = p_post_id;
COMMIT;
END//
DELIMITER ;
SET @member_id = (SELECT id FROM members ORDER BY id LIMIT 1);
CALL create_text_post('joined 상속 읽기', 'https://example.com/joined', 35, @joined_text);
INSERT INTO member_post_joined VALUES (@member_id, @joined_text);
START TRANSACTION;
INSERT INTO posts_joined (post_type, title, canonical_url)
VALUES ('IMAGE', '잠금 구조 이미지', 'https://example.com/lock-image');
SET @joined_image = LAST_INSERT_ID();
INSERT INTO image_post_detail VALUES (@joined_image, '잠금 범위 구조', 2400);
UPDATE posts_joined SET is_complete = TRUE WHERE post_id = @joined_image;
COMMIT;
SELECT r.post_id, r.post_type, r.is_complete,
(a.post_id IS NOT NULL) AS has_text,
(v.post_id IS NOT NULL) AS has_image
FROM posts_joined AS r
LEFT JOIN text_post_detail AS a ON a.post_id = r.post_id
LEFT JOIN image_post_detail AS v ON v.post_id = r.post_id
ORDER BY r.post_id;post_id | post_type | is_complete | has_text | has_image
1 | TEXT | 1 | 1 | 0
2 | IMAGE | 1 | 0 | 1완료된 두 부모에 판별자와 맞는 자식이 하나씩 있습니다. 다만 이 결과는 정상 경로의 것이며, 모든 직접 DML의 경로까지 제약한다는 뜻은 아닙니다.
텍스트 게시글 자식의 CHECK에서 오류가 나면 EXIT 처리기가 부모의 INSERT도 취소합니다. 그때 LAST_INSERT_ID의 번호에 빈 값이 생길 수 있지만 무결성의 문제는 아니므로, ID가 연속된다는 것을 업무 규칙으로 쓰지 않습니다.
is_complete가 FALSE인 행이 오래 남아 있다면, 중단된 가져오기나 권한의 우회입니다. 경과 시간의 지표를 두고 안전하게 다시 처리하거나 지우며, 완료를 손으로 TRUE로 바꾸는 권한은 일반 쓰기 주체에게 주지 않습니다.
상세 조회에서는 모든 자식을 한꺼번에 조인하지 않습니다. post_type을 먼저 읽고 맞는 상세 테이블만 조회하거나 유형별 뷰를 쓰며, 목록마다 LEFT JOIN을 세 번 하면 행의 폭과 옵티마이저의 비용이 늘어 조인 모델의 장점이 줄어듭니다.
하위 행과 판별자의 검증
첫 감사는 위반한 부모의 실제 자식 수를 보여 주고, 두 번째는 유형과 자식의 존재를 비교합니다. 미완료 상태의 경과 시간과, 작성자 연결이 미완료 부모를 가리키는지도 따로 찾습니다.
SELECT r.post_id, r.post_type,
(a.post_id IS NOT NULL) + (v.post_id IS NOT NULL)
+ (l.post_id IS NOT NULL) AS child_count
FROM posts_joined AS r
LEFT JOIN text_post_detail AS a ON a.post_id = r.post_id
LEFT JOIN image_post_detail AS v ON v.post_id = r.post_id
LEFT JOIN link_post_detail AS l ON l.post_id = r.post_id
WHERE (a.post_id IS NOT NULL) + (v.post_id IS NOT NULL)
+ (l.post_id IS NOT NULL) <> 1;
SELECT r.post_id, r.post_type FROM posts_joined AS r
LEFT JOIN text_post_detail AS a ON a.post_id = r.post_id
LEFT JOIN image_post_detail AS v ON v.post_id = r.post_id
LEFT JOIN link_post_detail AS l ON l.post_id = r.post_id
WHERE (r.post_type = 'TEXT') <> (a.post_id IS NOT NULL)
OR (r.post_type = 'IMAGE') <> (v.post_id IS NOT NULL)
OR (r.post_type = 'LINK') <> (l.post_id IS NOT NULL);
SELECT post_id FROM posts_joined
WHERE is_complete = FALSE
AND created_at < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;
SELECT x.member_id, x.post_id FROM member_post_joined AS x
JOIN posts_joined AS r ON r.post_id = x.post_id
WHERE r.is_complete = FALSE;정상 데이터에서 네 쿼리는 모두 빈 결과입니다. posts_joined에 부모만 직접 넣으면 첫 쿼리가 찾아내고, 미완료 경과 시간의 쿼리는 그 행이 10분을 넘긴 뒤에 찾습니다. 앞의 반례 joined_post_bad는 별도의 테이블이므로 이 쿼리들이 조회하지 않습니다.
텍스트 게시글 부모에 이미지 게시글 자식을 넣으면, 자식의 수는 1이라도 판별자의 쿼리가 잡아냅니다. 배포를 검증할 때 이 보고서가 빈 결과인지 확인할 수는 있지만, 실무에서는 읽는 쪽의 is_complete 필터를 유지해 일부만 끝난 가져오기를 막는 편이 안전합니다.
모든 유형의 생성이 부모의 AUTO_INCREMENT와 공통 URL의 유일 인덱스를 갱신하므로, 쓰기의 처리량이 아주 높다면 순서의 경합과 인덱스 페이지를 측정합니다. 일반적인 회원 게시판에서는 무결성과 참조의 단순함이 더 큰 이득입니다.
잘못 분류한 텍스트 게시글을 이미지 게시글로 바꿀 때는, 판별자의 UPDATE와 자식의 교체를 한 트랜잭션에 두고 바깥의 캐시를 무효화합니다. 식별자를 꼭 유지할 필요가 없다면 새 게시글을 만들고 연결을 옮기는 편이 감사하기 쉽습니다. 자식에 열을 더하는 영향은 그 유형에만 미치고 공통 제목의 변경은 부모 하나만 바꾸므로, 단일 테이블보다 릴리스의 담당을 나누기 좋지만 상세 조회에는 조인이 더해집니다.
| 따져 볼 것 | 질문 |
|---|---|
| 식별자 | 모든 하위 유형을 하나의 전역 post_id로 참조해야 하는가 |
| 공통 | 제목과 URL, 보관처럼 공유하는 수명이 충분한가 |
| 전용 속성 | 전용 열이 많아 단일 테이블의 듬성듬성한 행이 부담인가 |
| 쓰기 | 부모와 자식을 생성 프로시저의 트랜잭션으로 강제할 수 있는가 |
| 감사 | 정확히 하나의 자식을 확인하는 감사를 운영할 준비가 있는가 |
공통 관계와 전용 속성이 모두 크다면 조인 테이블 상속이 균형을 잡아 줍니다. 생성 프로시저와 감사 없이 직접 DML을 허용하면 자식이 0개이거나 여러 개인 부모가 생기므로, 조직의 권한 운영도 이 전략을 고르는 조건입니다.
정리
- 조인 테이블 상속은 공통 식별자를 부모에, 하위 유형의 전용 속성을 공유 기본 키의 자식에 둡니다.
- 외래 키는 자식에서 부모로의 방향만 보장하므로, 자식이 0개이거나 2개인 부모가 생길 수 있습니다.
- 생성 프로시저의 트랜잭션이 부모와 자식을 함께 발행하고, 완료 플래그를 마지막에 켭니다.
- 정확히 하나의 자식과 판별자의 일치는 명시적인 감사로 검증하고, 새 유형을 더할 때 감사의 분기도 함께 넓힙니다.
연습 문제
1. 투표 하위 유형과 생성 프로시저
POLL 하위 유형과 생성 프로시저를 더해 보세요. option_count는 2 이상 20 이하이고 부모의 판별자는 POLL이어야 하며, 생성 중에 CHECK가 실패하면 부모의 행도 남지 않아야 합니다. 기존의 정확히 하나의 자식 감사에는 투표 상세 테이블을 넣어야 합니다.
해설 보기
부모의 판별자 CHECK에 POLL을 더하고, 공유 기본 키를 쓰는 poll_post_detail을 만듭니다. 같은 이름의 CHECK를 한 ALTER 안에서 지우고 다시 만들지 않도록 두 문장으로 나누었습니다. 생성 프로시저는 부모와 투표 상세, 완료 플래그를 한 트랜잭션으로 커밋합니다.
ALTER TABLE posts_joined DROP CHECK chk_posts_joined_type;
ALTER TABLE posts_joined
ADD CONSTRAINT chk_posts_joined_type
CHECK (post_type IN ('TEXT', 'IMAGE', 'LINK', 'POLL'));
DROP TABLE IF EXISTS poll_post_detail;
CREATE TABLE poll_post_detail (
post_id BIGINT UNSIGNED PRIMARY KEY,
option_count SMALLINT UNSIGNED NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts_joined (post_id) ON DELETE CASCADE,
CHECK (option_count BETWEEN 2 AND 20)
) ENGINE=InnoDB;
DROP PROCEDURE IF EXISTS create_poll_post;
DELIMITER //
CREATE PROCEDURE create_poll_post(
IN p_title VARCHAR(160),
IN p_url VARCHAR(700),
IN p_option_count SMALLINT UNSIGNED,
OUT p_post_id BIGINT UNSIGNED
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;
START TRANSACTION;
INSERT INTO posts_joined (post_type, title, canonical_url)
VALUES ('POLL', p_title, p_url);
SET p_post_id = LAST_INSERT_ID();
INSERT INTO poll_post_detail VALUES (p_post_id, p_option_count);
UPDATE posts_joined SET is_complete = TRUE WHERE post_id = p_post_id;
COMMIT;
END//
DELIMITER ;
CALL create_poll_post('다음 글의 난이도는?',
'https://example.com/poll/difficulty', 4, @joined_poll);
CALL create_poll_post('선택지 하나',
'https://example.com/poll/one', 1, @bad_poll);
SELECT post_type, COUNT(*) AS posts FROM posts_joined
GROUP BY post_type ORDER BY post_type;ERROR 3819 (HY000): Check constraint 'poll_post_detail_chk_1' is violated.
post_type | posts
IMAGE | 1
POLL | 1
TEXT | 1정상 호출은 POLL 부모와 투표 상세 한 행을 남깁니다. 선택지가 하나인 호출은 부모를 넣은 뒤 자식의 이름 없는 CHECK poll_post_detail_chk_1에 걸리고, 처리기가 부모까지 롤백하므로 POLL 부모는 하나뿐입니다. 이 호출에 아직 쓰지 않은 새 URL을 준 것은, URL의 UNIQUE보다 자식의 CHECK까지 실제로 도달하게 하려는 것입니다.
감사 쿼리에도 POLL을 넣어, 네 상세의 합계와 판별자를 함께 검사합니다.
SELECT r.post_id, r.post_type,
(t.post_id IS NOT NULL) + (i.post_id IS NOT NULL)
+ (l.post_id IS NOT NULL) + (p.post_id IS NOT NULL) AS child_count
FROM posts_joined AS r
LEFT JOIN text_post_detail AS t ON t.post_id = r.post_id
LEFT JOIN image_post_detail AS i ON i.post_id = r.post_id
LEFT JOIN link_post_detail AS l ON l.post_id = r.post_id
LEFT JOIN poll_post_detail AS p ON p.post_id = r.post_id
WHERE (t.post_id IS NOT NULL) + (i.post_id IS NOT NULL)
+ (l.post_id IS NOT NULL) + (p.post_id IS NOT NULL) <> 1
OR (r.post_type = 'TEXT') <> (t.post_id IS NOT NULL)
OR (r.post_type = 'IMAGE') <> (i.post_id IS NOT NULL)
OR (r.post_type = 'LINK') <> (l.post_id IS NOT NULL)
OR (r.post_type = 'POLL') <> (p.post_id IS NOT NULL);세 부모가 모두 맞는 자식 하나씩을 가지므로 이 감사는 빈 결과입니다.
2. 감사가 잡는 손상
본문의 데이터에서 누군가 INSERT INTO image_post_detail VALUES (1, '잘못 붙인 이미지', 800);을 직접 실행했다고 합시다. 외래 키는 통과하나요? 본문의 두 감사 쿼리는 각각 어떤 결과를 돌려주나요?
해설 보기
부모 1이 있으므로 외래 키는 통과하고, 이미지 상세의 기본 키 1도 처음이므로 INSERT는 성공합니다. 그러면 텍스트 게시글 1이 텍스트와 이미지 자식을 모두 가집니다.
첫 감사는 자식의 합이 2인 1 | TEXT | 2를 돌려주고, 두 번째 감사는 판별자가 TEXT인데 이미지 자식이 있으므로 1 | TEXT를 돌려줍니다. 두 감사가 서로 다른 각도로 같은 손상을 찾아내며, 이런 행이 생기지 않게 하려면 상세 테이블의 직접 INSERT 권한을 거두어야 합니다.
다음 글 타입형 EAV 모델에서는 스키마가 자주 바뀌는 선택적 메타데이터만 타입을 명시한 EAV로 제한하는 방법을 다룹니다.