[1] 기본 키 설정[1.1] 자주 조회되는 열을 기본 키로 지정[1.2] 명확한 기본 키가 없다면 자동 증가(AUTO_INCREMENT) 값 사용[2] 조인 최적화[2.1] 조인되는 열들의 데이터 타입을 동일하게 맞추기[2.2] 테이블 간 조인할 때는 반드시 외래 키 설정[3] 트랜잭션 관리[3.1] 자동 커밋 기능 OFF[3.2] 관련된 작업들은 START TRANSACTION과 COMMIT으로 묶기[3.3] 너무 잦은 커밋, 너무 큰 트랜잭션 피하기[4] 테이블 잠금 관리[4.1] 적절한 잠금 관리[4.2] SELECT ... FOR UPDATE 구문으로 필요한 행만 잠금[5] 저장 공간 최적화[5-1] innodb_file_per_table 활성화하여 테이블별 separate 파일 사용[5-2] 데이터 특성에 따라 테이블/페이지 압축 고려[5.3] 권장 사용 예시[6] 설정 관리
Mysql 공식 InnoDB 테이블에 대한 모범 사례 참조
[1] 기본 키 설정
[1.1] 자주 조회되는 열을 기본 키로 지정
// 실제로 사용되지 않는 AUTO PK 값
CREATE TABLE student_courses (
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 불필요한 AUTO_INCREMENT
student_id BIGINT,
course_id BIGINT,
semester VARCHAR(20),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
SELECT * FROM student_courses
WHERE student_id = 1234 AND course_id = 5678;
// 실제로 사용되는 AUTO PK 값, 각 row 사이 무결성을 지킬 수 있다면 AUTO PK 값은 필요하지 않습니다.
CREATE TABLE student_courses (
student_id BIGINT,
course_id BIGINT,
semester VARCHAR(20),
PRIMARY KEY (student_id, course_id), -- 자주 조회되는 실제 비즈니스 키
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
위 처럼 기본 키를 효과적으로 설정하면
- 불필요한 인덱스 제거로 저장 공간 절약
- 자주 사용되는 조회 조건이 PK에 포함되어 있어 조회 성능 향상
- 실제 비즈니스 규칙을 DB 레벨에서 강제 (한 학생이 같은 과목을 같은 학기에 중복 수강할 수 없음)
와 같은 장점을 얻을 수 있습니다.
[1.2] 명확한 기본 키가 없다면 자동 증가(AUTO_INCREMENT) 값 사용
하지만 실제로 고유 식별 ID가 필요한 경우에는 고유 ID를 만들어 사용해야 합니다.
- 실제로 고유한 식별자가 필요한 엔티티 테이블 (예: 사용자, 주문, 상품 등)
- 이력 관리가 필요한 테이블
- 자연키로 식별하기 어려운 경우
- 자연 키(Natural Keys) :
- 도메인 레벨에서 실제 의미가 있는 데이터를 키로 사용
- 주민등록번호, 사업자등록번호, 이메일 주소 등
- 대리 키(Surrogate key) :
- 의미 없이 인공적으로 생성한 키
- 보통 AUTO_INCREMENT나 UUID 사용
[2] 조인 최적화
[2.1] 조인되는 열들의 데이터 타입을 동일하게 맞추기
// 데이터 타입이 다른 경우
CREATE TABLE users (
id VARCHAR(36), // uuid
name VARCHAR(100),
PRIMARY KEY (id)
);
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT,
user_id BIGINT, // bigint
amount DECIMAL(10,2),
PRIMARY KEY (id)
// fk 키 설정 없음
);
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id; // 타입 변환으로 인한 성능 저하
[2.2] 테이블 간 조인할 때는 반드시 외래 키 설정
- FK 키의 장점
- 모든 PK/FK가 동일한 데이터 타입(BIGINT) 사용
- 적절한 외래키 제약조건으로 데이터 정합성 보장
- 자동 생성되는 인덱스로 조인 성능 최적화
- 비즈니스 식별자(product_code)는 UNIQUE 인덱스로 분리
- 연관 테이블 간의 참조 무결성 보장
[3] 트랜잭션 관리
[3.1] 자동 커밋 기능 OFF
// 자동 커밋 모드 (각 INSERT마다 커밋 발생)
SET autocommit = 1;
// 10,000건의 주문 데이터 입력 (10,000번의 커밋 발생)
INSERT INTO orders (user_id, amount) VALUES (1, 1000);
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 1, 2);
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 2, 1);
문제점
- 매 INSERT마다 디스크 쓰기 발생
- 성능 극도로 저하
- 도중 실패 시 부분적 데이터만 입력될 수 있음
[3.2] 관련된 작업들은 START TRANSACTION과 COMMIT으로 묶기
// 주문 처리 로직 (트랜잭션 없음)
// 주문 생성
INSERT INTO orders (user_id, total_amount) VALUES (1, 5000);
SET order_id = LAST_INSERT_ID();
// 주문 상품 입력
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (order_id, 1, 2);
// 재고 감소
UPDATE products
SET stock = stock - 2
WHERE id = 1;
문제점
- 원자성 보장 못함
- 일관성 깨질 수 있음
- 부분적 데이터만 처리될 수 있음
[3.3] 너무 잦은 커밋, 너무 큰 트랜잭션 피하기
// 한 트랜잭션에서 너무 많은 작업 수행
START TRANSACTION;
// 대량의 데이터 처리
DELETE FROM old_logs WHERE created_at < '2023-01-01'; // 수백만 건
UPDATE users SET status = 'inactive' WHERE last_login < '2023-06-01'; // 수십만 건
INSERT INTO audit_logs SELECT * FROM temporary_logs; // 수백만 건
// 다른 세션의 작업이 오래 대기하게 됨
UPDATE products SET stock = stock - 1 WHERE id = 123;
COMMIT;
문제점
- 트랜잭션 롤백 시 복구가 오래 걸림
- 메모리 사용량 증가
- 다른 트랜잭션 블로킹
- 데드락 가능성 증가
[4] 테이블 잠금 관리
[4.1] 적절한 잠금 관리
이하 Worst Case
- 부적절한 인덱스로 인한 과도한 Row Lock
// 인덱스 없는 컬럼으로 업데이트 (테이블 풀 스캔)
START TRANSACTION;
UPDATE orders
SET status = 'COMPLETED'
WHERE customer_name = 'John' // customer_name에 인덱스 없음
FOR UPDATE; // 테이블 전체 스캔하면서 모든 행을 잠금
COMMIT;
- Lock 범위가 넓은 배치 처리
START TRANSACTION;
// 많은 수의 주문을 한 번에 처리
SELECT * FROM orders
WHERE status = 'PENDING'
AND created_at < '2024-01-01' // 대량의 데이터
FOR UPDATE; // 많은 행을 오래 잠금
// 여러 처리 수행...
UPDATE orders SET status = 'PROCESSING'
WHERE id IN (/* 위에서 선택된 주문 IDs */);
COMMIT;
- 잘못된 순서의 Lock 획득 (데드락 유발)
// 세션 1
START TRANSACTION;
UPDATE users SET points = points - 100 WHERE id = 1;
// 다른 처리
UPDATE orders SET status = 'COMPLETED' WHERE id = 999;
COMMIT;
// 세션 2 (동시에 실행)
START TRANSACTION;
UPDATE orders SET status = 'PROCESSING' WHERE id = 999;
// 다른 처리
UPDATE users SET last_order_date = NOW() WHERE id = 1;
COMMIT;
[4.2] SELECT ... FOR UPDATE 구문으로 필요한 행만 잠금
// 재고 차감 프로세스
START TRANSACTION;
// 재고 확인 및 잠금
SELECT id, stock
FROM products
WHERE id = 123
AND stock > 0
FOR UPDATE; // 다른 트랜잭션이 이 행을 수정하지 못하도록 잠금
// 재고가 충분한 경우만 차감
UPDATE products
SET stock = stock - 1
WHERE id = 123;
COMMIT;
// ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 필요한 특정 주문만 잠금
SELECT * FROM orders
WHERE id = 123
AND status = 'PENDING'
FOR UPDATE SKIP LOCKED; // 이미 잠긴 행은 건너뜀
[5] 저장 공간 최적화
- 데이터 특성에 따라 테이블/페이지 압축 고려
[5-1] innodb_file_per_table 활성화하여 테이블별 separate 파일 사용
// 서버 설정 확인
SHOW VARIABLES LIKE 'innodb_file_per_table';
// 활성화 (my.cnf 또는 my.ini 파일에 설정)
innodb_file_per_table=1
// 압축된 테이블 생성
CREATE TABLE compressed_table (
id INT AUTO_INCREMENT,
name VARCHAR(100),
description TEXT,
PRIMARY KEY (id)
) ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8; // 8KB 압축 (기본 16KB의 절반)
// 기존 테이블 압축 변환
ALTER TABLE existing_table
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8;
[5-2] 데이터 특성에 따라 테이블/페이지 압축 고려
- SELECT 쿼리
- 문법적 차이 없음
SELECT * FROM compressed_table WHERE id = 123;
- 디스크 I/O 감소 (→ 더 빠름)
- CPU 사용량 증가 (압축 해제 시)
- 특히 SSD보다 HDD에서 성능 향상이 더 큼
- 버퍼 풀에서는 압축 해제된 상태로 저장
- 더 많은 데이터를 메모리에 캐시 가능
- INSERT/UPDATE
// 일반 쿼리와 동일
INSERT INTO compressed_table (name, description)
VALUES ('test', 'test description');
[5.3] 권장 사용 예시
- 권장 사용
- 읽기가 많고 쓰기가 적은 테이블
- 텍스트 데이터가 많은 테이블
- 디스크 공간이 제한적인 환경
- 비권장
- OLTP 환경의 트랜잭션이 많은 테이블
- 이미 압축된 데이터(이미지, PDF 등)가 많은 테이블
- CPU 리소스가 제한적인 환경
[6] 설정 관리
sql_mode=NO_ENGINE_SUBSTITUTION 설정은 MySQL이 테이블 생성/변경 시 지정된 스토리지 엔진을 다른 엔진으로 자동 대체하는 것을 방지하는 옵션입니다.
SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION';
CREATE TABLE test_table (
id INT PRIMARY KEY
) ENGINE=InnoDB;
// 지원하지 않는 엔진인 경우 에러 발생:
// ERROR 1286 (42000): Unknown storage engine 'InnoDB'
Share article