기본 키와 외래 키
회원 정보를 게시글마다 복사하지 않고 PK·FK로 정체성과 관계를 표현해 회원 게시판의 핵심 테이블을 만듭니다.
관계형 모델에서 테이블은 같은 종류의 행 집합이고 열은 각 값의 도메인을 선언합니다.
한 행을 구별하는 키와 테이블 사이의 관계를 먼저 정하면 반복 문자열보다 안정적으로 사실을 연결할 수 있습니다.
Excel처럼 보이는 표 모양보다 행의 정체성과 참조 규칙이 핵심입니다.
회원 한 명은 여러 posts 행을 가질 수 있고 한 게시글은 회원 한 명에게 속합니다.
회원 이메일·이름을 게시글 행마다 복사하지 않고 posts.author_id가 회원 PK를 참조하는 1:N 관계를 만듭니다.
반복 저장의 문제
한 넓은 테이블에 이메일과 이름을 매 게시글 저장하면 회원 이름 변경이 모든 과거 행 UPDATE가 됩니다.
일부만 수정되면 같은 이메일에 여러 이름이 생기고, 존재하지 않는 이메일도 게시글 행에 자유롭게 들어갑니다.
행 하나만 보면 편하지만 전체 무결성 책임이 커집니다.
CREATE TEMPORARY TABLE flat_board_post (
post_id BIGINT,
member_email VARCHAR(255),
member_name VARCHAR(60),
post_title VARCHAR(80),
view_count BIGINT
);
INSERT INTO flat_board_post VALUES
(1, 'min@example.com', '안동민', 'SELECT 기초', 30),
(2, 'min@example.com', '동민', 'WHERE 복습', 40);
SELECT member_email, COUNT(DISTINCT member_name) AS name_versions
FROM flat_board_post
GROUP BY member_email;같은 이메일의 name_versions가 2지만 어느 게시글의 이름을 현재값으로 사용할지 결정할 수 없습니다.
member_email | name_versions
min@example.com | 2게시글의 사실은 제목·본문·상태·시각이고 회원의 현재 속성은 회원이 소유합니다.
서로 다른 변경 이유를 한 행에 넣어 갱신 이상을 만들었습니다.
게시글이 회원을 가리키는 FK를 두면 이름은 한곳에서 바꾸고 존재하지 않는 회원 참조는 DB가 거절할 수 있습니다.
기본 키와 외래 키의 역할
기본 키는 테이블 안에서 각 행을 유일하게 식별하고 NULL을 허용하지 않습니다.
외래 키는 자식 값이 부모의 PK 또는 UNIQUE 후보 키에 존재하도록 강제합니다.
members 1 : N posts에서 한 author_id가 여러 자식 행에 반복되는 것은 관계 표현이며 회원 속성 중복과 다릅니다.
FK 타입의 부호 유무·길이도 부모와 맞춥니다.
PK·FK·UNIQUE는 표준 관계형 제약입니다.
MySQL InnoDB는 FK 검사를 지원하고 자식 참조 열에 인덱스가 필요합니다.
삭제·수정 동작은 RESTRICT, CASCADE, SET NULL 중 데이터 수명에 맞게 선택합니다.
게시글 보존이 기본이므로 회원 삭제는 RESTRICT를 명시해 의도치 않은 연쇄 삭제를 막습니다.
회원과 게시글의 분리
게시글의 정체성, 작성자 FK, 제목, 본문, 상태, 조회수와 시각을 별도 테이블에 둡니다.
수정 충돌을 감지할 version도 0에서 시작합니다.
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
author_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(80) NOT NULL,
content VARCHAR(5000) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
view_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
published_at DATETIME(6) NULL,
version BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY ix_posts_author_created (author_id, created_at DESC, id DESC),
CONSTRAINT fk_posts_author
FOREIGN KEY (author_id) REFERENCES members (id)
ON UPDATE RESTRICT ON DELETE RESTRICT
) ENGINE = InnoDB;
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at
) VALUES (
1, 'SELECT 기초', 'SELECT가 필요한 열을 고르는 이유를 정리합니다.',
'PUBLISHED', 30,
'2026-07-01 10:00:00', '2026-07-01 10:30:00', '2026-07-01 10:30:00'
);게시글은 author_id 1을 통해 회원과 연결되고 이름은 회원에서 한 번만 읽습니다.
없는 author_id는 FK가 거절합니다.
id | author_id | title | view_count | status
1 | 1 | SELECT 기초 | 30 | PUBLISHED
relationship: members 1 ---< posts 1현재 테이블은 타입과 관계의 골격만 만들었습니다.
상태 문자열, 제목·본문 길이, 게시 상태·시각 조합은 다음 문서에서 검사로 강화합니다.
모든 규칙을 첫 DDL에 한꺼번에 넣기보다 각 제약이 막는 오류를 관찰하고 누적합니다.
이름·이메일이 필요할 때는 JOIN으로 읽으며 편의를 위해 게시글에 복사하지 않습니다.
데이터베이스 객체 확인
SQL 파일을 실행한 뒤에는 “성공했다”는 메시지만 믿지 말고 서버에 어떤 객체가 생겼는지 확인합니다.
SHOW DATABASES는 접속 계정이 볼 수 있는 데이터베이스를, SHOW TABLES는 현재 데이터베이스의 테이블을 보여 줍니다.
DESCRIBE는 열 이름·타입·NULL·키·기본값을 빠르게 확인하는 명령이고, SHOW CREATE TABLE은 엔진·인덱스·제약까지 포함한 실제 DDL을 돌려줍니다.
SELECT DATABASE() AS selected_database;
SHOW DATABASES;
USE board_lab;
SHOW TABLES;
DESCRIBE members;
DESCRIBE posts;
SHOW CREATE TABLE posts;selected_database: board_lab
tables: members, posts
posts PK: id
posts FK: author_id -> members.id
engine: InnoDBDESCRIBE는 학습 중 구조를 빠르게 읽는 데 알맞지만 제약식 전체를 보여 주지는 않습니다.
정확한 FK 규칙이나 검사 식을 검사할 때는 SHOW CREATE TABLE과 information_schema를 사용합니다.
이름이 같은 테이블이 다른 데이터베이스에도 있을 수 있으므로 변경 SQL을 실행하기 전 SELECT DATABASE()로 현재 대상을 확인하는 습관을 들입니다.
객체 삭제도 범위가 다릅니다.
DROP TABLE은 한 테이블의 구조와 데이터를 함께 제거하고, DROP DATABASE는 그 안의 모든 객체를 제거합니다.
아래처럼 별도 연습용 데이터베이스에서만 수명을 확인하고 누적 실습 데이터베이스인 board_lab에는 적용하지 않습니다.
CREATE DATABASE db_object_sandbox
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE TABLE db_object_sandbox.probe (
probe_id BIGINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;
SHOW TABLES FROM db_object_sandbox;
DROP TABLE db_object_sandbox.probe;
DROP DATABASE db_object_sandbox;SQL을 목적에 따라 네 범주로 읽기
구문은 같은 언어에 속하지만 바꾸는 대상과 트랜잭션 의미가 다릅니다.
명령 이름을 외우는 것보다 “구조, 행, 권한, 트랜잭션 중 무엇을 바꾸는가?”를 먼저 묻습니다.
| 범주 | 다루는 대상 | 대표 명령 | 실행 전 확인할 점 |
|---|---|---|---|
| DDL | 데이터베이스 객체의 구조 | CREATE, ALTER, DROP, TRUNCATE | 잠금·암묵적 커밋·복구 절차 |
| DML | 테이블의 행과 조회 결과 | SELECT, INSERT, UPDATE, DELETE | 대상 행·조건·영향 건수 |
| DCL | 계정과 객체 권한 | GRANT, REVOKE | 최소 권한·적용 계정 |
| TCL | 트랜잭션 단위 | START TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT | 자동 커밋·오류 처리·소유자 |
교재에 따라 SELECT를 DML에서 분리해 DQL이라고 부르기도 합니다.
분류 이름이 달라도 조회가 행을 영구 변경하지 않는다는 성질과, DDL이 일반 DML과 같은 방식으로 모두 롤백되지 않는다는 차이는 그대로입니다.
DCL은 처음부터 루트 권한으로 실습하기보다 애플리케이션 계정을 따로 만든 뒤 필요한 테이블 권한만 부여할 때 다룹니다.
TCL의 실제 효과와 격리 수준은 8장에서 두 연결로 확인합니다.
관계 메타데이터 확인
SHOW CREATE TABLE과 참조 메타데이터에서 부모·자식 열과 삭제 규칙을 확인합니다.
회원별 게시글 수는 자식 행을 세어 관계 모양을 검증합니다.
SHOW CREATE TABLE posts;
SELECT
constraint_name,
table_name,
referenced_table_name,
update_rule,
delete_rule
FROM information_schema.referential_constraints
WHERE constraint_schema = 'board_lab'
AND table_name = 'posts';
SELECT author_id, COUNT(*) AS post_count
FROM posts
GROUP BY author_id
ORDER BY author_id;fk_posts_author가 회원을 참조하고 delete_rule이 RESTRICT여야 합니다.
한 회원에 게시글을 더 넣으면 post_count만 늘고 회원 행은 복제되지 않습니다.
대리 키는 업무 의미가 없어도 안정적인 참조점을 제공하지만 업무상 중복을 자동 방지하지 않습니다.
회원에는 이메일 UNIQUE가 별도로 필요합니다.
반대로 자연 키만 PK로 쓰면 값 변경·길이·외부 노출 비용이 자식에 전파될 수 있습니다.
어느 쪽이든 후보 키와 식별 규칙을 문서화합니다.
FK는 다른 서버나 샤드를 가로질러 자동 보장되지 않으므로 분산 경계에서는 별도 일관성 전략이 필요합니다.
author_id 1에 게시글 두 건을 더 넣어 1:N을 확인하고 author_id 999로 INSERT해 오류 1452를 관찰하세요.
회원 이름을 한 번 UPDATE한 뒤 JOIN 결과의 모든 게시글에 새 이름이 보이는지 확인합니다.
FK를 제거한 복사 테이블에서는 고아 행이 들어가는 차이를 비교하되 실제 누적 테이블의 제약을 비활성화하지 않습니다.
FK는 무결성을 지키지만 부모 삭제·대량 가져오기·스키마 변경의 잠금과 순서를 고려해야 합니다.
참조 자식 인덱스가 조인과 삭제 확인을 지원하는지 EXPLAIN으로 뒤에서 측정합니다.
ON DELETE CASCADE는 편리하지만 대량 연쇄 삭제와 감사 손실을 만들 수 있으므로 명시적 수명주기 없이는 선택하지 않습니다.
개인정보 삭제와 게시글 보존 요구가 충돌하면 익명화·분리 보관을 설계합니다.
NULL 허용 FK는 ‘관계 없음’을 허용하고 INNER JOIN에서 빠질 수 있습니다.
필수 회원 소유 관계는 NOT NULL입니다.
부모·자식 타입이 하나는 SIGNED, 하나는 UNSIGNED이면 FK 생성에서 오류가 발생합니다.
FK 이름은 스키마 안에서 추적 가능한 규칙으로 정하고 마이그레이션 롤백 시 같은 이름을 사용합니다.
이름 변경을 이유로 PK를 바꾸지 않으므로 ON UPDATE는 RESTRICT입니다.
이 회원–posts 관계는 ch3 쿼리, ch4 집계, ch5 JOIN의 중심 축입니다.
ch5에서 태그 연결 테이블과 두 FK를 추가하고 ch7에서 복합 인덱스 순서를 측정합니다.
ch9의 ERD(Entity-Relationship Diagram, 개체-관계 다이어그램)에서는 지금의 실제 DDL 관계를 개념·논리 모델로 되돌아보며 코드와 그림의 키 이름을 일치시킵니다.
키와 테이블 분리 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 정체성 | 각 테이블의 한 행이 무엇을 뜻하는가? |
| 소유 | 각 속성의 변경 책임이 어느 테이블에 있는가? |
| PK | 행을 안정적으로 참조할 키가 있는가? |
| FK | 자식이 부모 존재와 타입을 강제하는가? |
| 수명 | 부모 삭제 시 RESTRICT·CASCADE·NULL 중 이유가 있는가? |
반복되는 값이 있다는 이유만으로 테이블을 나누지 않습니다.
서로 다른 정체성과 변경 이유가 있고 키로 관계를 설명할 수 있을 때 분리합니다.
연습 문제
author_id 1에 초안 하나와 게시된 글 하나를 추가한 뒤 회원별 게시글 수를 확인하세요.
이어서 author_id 999 게시글을 시도하고 FK 오류가 어떤 관계를 지켰는지 설명하세요.
해설과 예시 답안
존재하는 부모에는 여러 자식이 들어가고 없는 부모 참조만 오류가 발생합니다.
상태 제약은 다음 문서 전이라 문자열 자체는 아직 완전히 제한되지 않았음을 구분합니다.
INSERT INTO posts (
author_id, title, content, status, view_count,
created_at, updated_at, published_at
) VALUES
(1, 'JOIN 예습', 'JOIN 내용을 게시 전에 정리합니다.', 'DRAFT', 0,
'2026-07-02 09:00:00', '2026-07-02 09:00:00', NULL),
(1, 'WHERE 복습', 'WHERE 조건의 경계를 공개 글로 정리합니다.', 'PUBLISHED', 40,
'2026-07-02 10:00:00', '2026-07-02 10:40:00', '2026-07-02 10:40:00');
SELECT author_id, COUNT(*) AS post_count
FROM posts
GROUP BY author_id;
INSERT INTO posts (
author_id, title, content, status, created_at, updated_at
) VALUES (
999, '고아 게시글', '존재하지 않는 작성자를 참조합니다.', 'DRAFT',
'2026-07-02 12:00:00', '2026-07-02 12:00:00'
);앞의 두 INSERT는 성공하고 마지막 문장만 1452로 오류가 발생해야 합니다.
클라이언트의 자동 커밋 여부와 오류 뒤 트랜잭션 상태도 확인합니다.
핵심 정리
- 테이블의 한 행이 뜻하는 정체성과 속성 소유권을 먼저 정합니다.
- PK는 행을 식별하고 FK는 부모 존재와 관계를 강제합니다.
- 1:N의 자식 FK 반복과 부모 속성 중복을 구분합니다.
- 삭제 동작과 키 타입을 수명주기 규칙으로 명시합니다.
다음 문서에서는 타입 범위와 검사로 값·상태 전이의 무결성을 강화합니다.