Primary Key
기본 키
테이블에서 각 행(row)을 고유하게 식별하는 열 또는 열의 조합입니다. NOT NULL과 UNIQUE 제약을 암묵적으로 가지며, 인덱스가 자동 생성되어 빠른 조회를 지원합니다.
기본 키
테이블에서 각 행(row)을 고유하게 식별하는 열 또는 열의 조합입니다. NOT NULL과 UNIQUE 제약을 암묵적으로 가지며, 인덱스가 자동 생성되어 빠른 조회를 지원합니다.
Primary Key(기본 키)는 관계형 데이터베이스에서 테이블의 각 레코드를 유일하게 식별하는 가장 중요한 제약조건입니다. PK는 두 가지 핵심 특성을 가집니다: 중복 불가(UNIQUE)와 NULL 불허(NOT NULL). 이를 통해 테이블의 어떤 두 행도 동일한 PK 값을 가질 수 없으며, 모든 행은 반드시 PK 값을 가져야 합니다.
자연 키(Natural Key) vs 대리 키(Surrogate Key)는 PK 설계의 핵심 선택입니다. 자연 키는 비즈니스에서 의미 있는 값(이메일, 주민번호 등)을 사용하고, 대리 키는 시스템이 생성한 의미 없는 값(AUTO_INCREMENT, UUID)을 사용합니다. 대부분의 경우 대리 키가 권장되는데, 자연 키는 비즈니스 규칙 변경 시 문제가 될 수 있기 때문입니다.
자동 증가(Auto Increment)는 대리 키 구현의 일반적인 방법입니다. MySQL은 AUTO_INCREMENT, PostgreSQL은 SERIAL 또는 IDENTITY, SQL Server는 IDENTITY를 사용합니다. 숫자가 순차적으로 증가하여 삽입 순서를 알 수 있고, 인덱스 효율도 좋습니다. 단, 분산 환경에서는 UUID가 더 적합할 수 있습니다.
복합 Primary Key(Composite PK)는 두 개 이상의 열로 PK를 구성합니다. 다대다 관계의 중간 테이블에서 주로 사용됩니다. 예를 들어 학생-수강 테이블에서 (student_id, course_id)를 복합 PK로 설정할 수 있습니다.
PK에는 클러스터형 인덱스(Clustered Index)가 자동 생성됩니다. 이는 테이블의 물리적 정렬 순서를 결정하며, PK 기반 조회가 매우 빠릅니다. Foreign Key가 참조하는 대상이 되어 테이블 간 관계의 기준점 역할을 합니다.
-- Primary Key 예제 - SQL DDL
-- ============================================
-- 1. 단일 컬럼 Primary Key
-- ============================================
-- PostgreSQL - SERIAL 사용
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- MySQL - AUTO_INCREMENT 사용
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- PostgreSQL 15+ - IDENTITY 사용 (표준 SQL)
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL
);
-- ============================================
-- 2. 복합 Primary Key (Composite PK)
-- ============================================
-- 다대다 관계 중간 테이블
CREATE TABLE student_courses (
student_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
grade CHAR(2),
-- 복합 PK 정의
PRIMARY KEY (student_id, course_id),
-- Foreign Keys
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
);
-- 이력/버전 테이블
CREATE TABLE product_prices (
product_id INTEGER NOT NULL,
valid_from DATE NOT NULL,
price DECIMAL(10, 2) NOT NULL,
valid_to DATE,
PRIMARY KEY (product_id, valid_from),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- ============================================
-- 3. UUID를 Primary Key로 사용
-- ============================================
-- PostgreSQL
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id INTEGER NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- MySQL 8.0+
CREATE TABLE orders (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
customer_id INT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 4. 자연 키(Natural Key) 예시
-- ============================================
-- 국가 코드를 PK로 사용 (ISO 3166-1)
CREATE TABLE countries (
code CHAR(2) PRIMARY KEY, -- 'KR', 'US', 'JP'
name VARCHAR(100) NOT NULL,
continent VARCHAR(50)
);
-- 설정 테이블 (키-값 구조)
CREATE TABLE app_settings (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 5. Primary Key 조회 성능
-- ============================================
-- PK를 통한 조회 (가장 빠름)
SELECT * FROM users WHERE id = 12345;
-- 실행 계획 확인
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 12345;
-- Index Scan using users_pkey (PK 인덱스 사용)
-- ============================================
-- 6. Primary Key 관리
-- ============================================
-- 기존 테이블에 PK 추가
ALTER TABLE legacy_table
ADD PRIMARY KEY (id);
-- PK 삭제 (MySQL)
ALTER TABLE users DROP PRIMARY KEY;
-- PK 삭제 (PostgreSQL)
ALTER TABLE users DROP CONSTRAINT users_pkey;
-- 시퀀스 현재값/다음값 확인 (PostgreSQL)
SELECT currval('users_id_seq');
SELECT nextval('users_id_seq');
-- 시퀀스 재설정
ALTER SEQUENCE users_id_seq RESTART WITH 1000;
-- AUTO_INCREMENT 값 설정 (MySQL)
ALTER TABLE users AUTO_INCREMENT = 1000;
# Primary Key 예제 - Python + SQLAlchemy
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime
import uuid
Base = declarative_base()
# ============================================
# 1. 자동 증가 Primary Key
# ============================================
class User(Base):
"""자동 증가 PK 사용"""
__tablename__ = 'users'
# 자동 증가 PK (PostgreSQL SERIAL / MySQL AUTO_INCREMENT)
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String(255), unique=True, nullable=False)
name = Column(String(100), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# 관계
orders = relationship('Order', back_populates='user')
def __repr__(self):
return f""
# ============================================
# 2. UUID Primary Key
# ============================================
class Order(Base):
"""UUID PK 사용 - 분산 시스템에 적합"""
__tablename__ = 'orders'
# UUID PK
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
total_amount = Column(Integer, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship('User', back_populates='orders')
# ============================================
# 3. 복합 Primary Key
# ============================================
class StudentCourse(Base):
"""복합 PK - 다대다 중간 테이블"""
__tablename__ = 'student_courses'
# 복합 PK
student_id = Column(Integer, ForeignKey('students.id'),
primary_key=True)
course_id = Column(Integer, ForeignKey('courses.id'),
primary_key=True)
enrolled_at = Column(DateTime, default=datetime.utcnow)
grade = Column(String(2))
# 관계
student = relationship('Student', back_populates='enrollments')
course = relationship('Course', back_populates='enrollments')
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
enrollments = relationship('StudentCourse', back_populates='student')
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
enrollments = relationship('StudentCourse', back_populates='course')
# ============================================
# 4. 자연 키(Natural Key)
# ============================================
class Country(Base):
"""자연 키 사용 - ISO 국가 코드"""
__tablename__ = 'countries'
# 자연 키 PK
code = Column(String(2), primary_key=True) # 'KR', 'US'
name = Column(String(100), nullable=False)
continent = Column(String(50))
# ============================================
# 5. PK 사용 예제
# ============================================
def main():
engine = create_engine('postgresql://localhost/mydb', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
try:
# 사용자 생성 (PK 자동 할당)
user = User(email='test@example.com', name='홍길동')
session.add(user)
session.flush() # PK 값 확정
print(f"생성된 사용자 ID: {user.id}") # 자동 생성된 ID
# UUID PK 주문 생성
order = Order(user_id=user.id, total_amount=50000)
session.add(order)
session.flush()
print(f"생성된 주문 UUID: {order.id}")
# PK로 조회 (가장 빠름)
found_user = session.query(User).get(user.id) # deprecated in 2.0
# SQLAlchemy 2.0 스타일
found_user = session.get(User, user.id)
# 복합 PK로 조회
enrollment = session.get(StudentCourse, (1, 1)) # (student_id, course_id)
session.commit()
except Exception as e:
session.rollback()
print(f"에러: {e}")
finally:
session.close()
if __name__ == '__main__':
main()
// Primary Key 예제 - Node.js + Prisma
// ============================================
// 1. Prisma 스키마 (schema.prisma)
// ============================================
/*
// 자동 증가 PK
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
orders Order[]
}
// UUID PK
model Order {
id String @id @default(uuid())
userId Int
totalAmount Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
// 복합 PK
model StudentCourse {
studentId Int
courseId Int
enrolledAt DateTime @default(now())
grade String?
student Student @relation(fields: [studentId], references: [id])
course Course @relation(fields: [courseId], references: [id])
@@id([studentId, courseId]) // 복합 PK
}
// 자연 키
model Country {
code String @id @db.Char(2) // 'KR', 'US'
name String
continent String?
}
// CUID (Collision-resistant Unique ID)
model Post {
id String @id @default(cuid())
title String
content String?
createdAt DateTime @default(now())
}
*/
// ============================================
// 2. Prisma Client 사용
// ============================================
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
// 자동 증가 PK - 생성 시 ID 자동 할당
const user = await prisma.user.create({
data: {
email: 'test@example.com',
name: '홍길동'
}
});
console.log('생성된 사용자 ID:', user.id); // 1, 2, 3...
// UUID PK - 생성 시 UUID 자동 할당
const order = await prisma.order.create({
data: {
userId: user.id,
totalAmount: 50000
}
});
console.log('생성된 주문 UUID:', order.id); // 550e8400-e29b-41d4...
// PK로 단일 레코드 조회 (가장 빠름)
const foundUser = await prisma.user.findUnique({
where: { id: user.id }
});
// 복합 PK로 조회
const enrollment = await prisma.studentCourse.findUnique({
where: {
studentId_courseId: { // 복합 PK 이름
studentId: 1,
courseId: 1
}
}
});
// 자연 키 사용
const country = await prisma.country.create({
data: {
code: 'KR',
name: '대한민국',
continent: 'Asia'
}
});
// 자연 키로 조회
const korea = await prisma.country.findUnique({
where: { code: 'KR' }
});
// PK 기반 관계 조회
const userWithOrders = await prisma.user.findUnique({
where: { id: user.id },
include: {
orders: true // FK가 참조하는 관계
}
});
// 대량 삽입 시 PK 반환
const users = await prisma.user.createMany({
data: [
{ email: 'user1@example.com', name: '사용자1' },
{ email: 'user2@example.com', name: '사용자2' }
],
// skipDuplicates: true // 중복 PK 무시
});
console.log('생성된 레코드 수:', users.count);
// PK 존재 여부 확인
const exists = await prisma.user.findUnique({
where: { id: 999 },
select: { id: true } // PK만 조회 (효율적)
});
if (!exists) {
console.log('사용자가 존재하지 않습니다');
}
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
"사용자 테이블 PK는 이메일 말고 자동 증가 숫자로 하는 게 좋겠습니다. 이메일은 변경될 수 있고, FK로 참조하는 테이블이 많을 때 문제가 됩니다. 이메일은 UNIQUE 인덱스로 별도 관리하면 됩니다."
"마이크로서비스 환경에서는 UUID를 PK로 사용하는 게 낫습니다. 여러 서비스에서 동시에 레코드를 생성해도 충돌이 없어요. 다만 인덱스 효율이 조금 떨어지니까 ULID나 Snowflake ID도 고려해보세요."
"PK로 조회하면 클러스터 인덱스를 타서 가장 빠릅니다. 목록 조회할 때도 WHERE id IN (1, 2, 3) 형태로 PK 배열을 넘기면 효율적이에요. 가능하면 PK 기반 조회를 최대한 활용하세요."
이메일, 전화번호, 주민번호 등 변경 가능한 값을 PK로 사용하면 안 됩니다. 값이 변경될 때 FK로 참조하는 모든 테이블을 수정해야 합니다.
삭제된 레코드의 PK 값을 재사용하지 마세요. 로그, 감사 기록에서 혼란이 생기고 외래 키 참조 문제가 발생할 수 있습니다.
3개 이상의 열로 복합 PK를 구성하면 JOIN이 복잡해지고 FK 설정이 어려워집니다. 대리 키 추가를 고려하세요.
대리 키(자동 증가/UUID) 우선 사용, 의미 있는 이름(user_id)보다 일관된 id 사용, 분산 환경에서는 UUID/ULID 고려, 복합 PK는 중간 테이블에만 제한적 사용.