테이블 설계가 먼저인 이유
SQL을 잘 쓰는 것도 중요하지만, 그보다 먼저 테이블을 잘 설계해야 합니다. 테이블 구조가 잘못되면 조회문을 아무리 고쳐도 한계가 있습니다. 반대로 테이블이 잘 설계되어 있으면 데이터가 늘어나도 관리하기 쉽고, 나중에 기능을 추가할 때도 덜 흔들립니다.
테이블 설계는 다음 질문에서 시작합니다.
이 테이블은 무엇을 저장하는가?
한 행은 무엇 하나를 의미하는가?
각 컬럼에는 어떤 값이 들어가는가?
반드시 있어야 하는 값과 없어도 되는 값은 무엇인가?
중복되면 안 되는 값은 무엇인가?
예를 들어 students 테이블의 한 행은 학생 한 명을 의미합니다. 그러면 이름, 이메일, 생년월일, 등록일 같은 컬럼이 자연스럽게 필요합니다.
CREATE TABLE students (
id BIGINT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
birth_date DATE,
score DECIMAL(5, 2) DEFAULT 0.00,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE (email)
);이 문장 하나에는 테이블 설계의 핵심이 거의 들어 있습니다. 컬럼 이름, 데이터 타입, 기본키, 필수값, 기본값, 중복 방지 조건을 함께 정하고 있습니다.

CREATE TABLE 문 이해하기
CREATE TABLE은 새 테이블을 만드는 명령입니다. 기본 형태는 다음과 같습니다.
CREATE TABLE 테이블명 (
컬럼명 데이터타입 제약조건,
컬럼명 데이터타입 제약조건,
PRIMARY KEY (기본키컬럼)
);예제를 다시 나누어 보겠습니다.
id BIGINT AUTO_INCREMENTid 컬럼은 학생을 구분하는 번호입니다. BIGINT는 큰 정수를 저장할 수 있는 타입이고, AUTO_INCREMENT는 값을 직접 넣지 않아도 MySQL이 자동으로 다음 번호를 넣어 주는 옵션입니다.
name VARCHAR(50) NOT NULLname은 최대 50자까지 저장할 수 있는 문자열입니다. NOT NULL은 값이 반드시 있어야 한다는 뜻입니다. 학생 이름이 없는 행은 허용하지 않겠다는 설계입니다.
email VARCHAR(255) NOT NULL이메일도 문자열이므로 VARCHAR를 사용합니다. 이메일은 로그인이나 연락처로 쓰일 수 있으므로 비워 두지 않게 했습니다.
birth_date DATE생년월일은 날짜만 필요합니다. 시간까지 필요하지 않으므로 DATETIME이 아니라 DATE를 사용합니다. NOT NULL을 붙이지 않았으므로 모르는 경우 비워 둘 수 있습니다.
score DECIMAL(5, 2) DEFAULT 0.00점수처럼 소수점이 필요한 값은 DECIMAL을 사용할 수 있습니다. DECIMAL(5, 2)는 전체 5자리 중 소수점 아래 2자리를 저장한다는 뜻입니다. 예를 들어 100.00, 95.50, 7.25 같은 값을 저장할 수 있습니다.
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP등록일은 날짜와 시간이 모두 필요하므로 DATETIME을 사용합니다. DEFAULT CURRENT_TIMESTAMP는 값을 직접 넣지 않으면 현재 시간이 자동으로 들어가게 합니다.
PRIMARY KEY (id)PRIMARY KEY는 기본키입니다. 기본키는 각 행을 구분하는 대표값입니다. 기본키 값은 중복될 수 없고, 비어 있을 수도 없습니다.
UNIQUE (email)UNIQUE는 중복을 막는 제약 조건입니다. 같은 이메일을 가진 학생이 두 번 등록되지 않게 할 수 있습니다.
제약 조건은 데이터를 지키는 규칙이다
제약 조건은 테이블에 들어오는 데이터를 제한하는 규칙입니다. 애플리케이션 코드에서 검사를 하더라도 데이터베이스에도 최소한의 규칙을 두는 것이 좋습니다. 데이터베이스는 여러 프로그램이 함께 사용할 수 있기 때문입니다.
자주 사용하는 제약 조건은 다음과 같습니다.
| 제약 조건 | 의미 | 예시 |
|---|---|---|
PRIMARY KEY | 행을 구분하는 대표값 | PRIMARY KEY (id) |
NOT NULL | 반드시 값이 있어야 함 | name VARCHAR(50) NOT NULL |
DEFAULT | 값이 없을 때 기본값 사용 | score DECIMAL(5,2) DEFAULT 0.00 |
UNIQUE | 중복값 금지 | UNIQUE (email) |
NOT NULL은 신중하게 정해야 합니다. 반드시 필요한 값에는 붙이는 것이 좋습니다. 하지만 아직 모를 수 있는 값에는 붙이지 않는 편이 낫습니다. 예를 들어 학생 이름은 필수지만, 생년월일은 나중에 입력할 수도 있습니다.
DEFAULT도 편리하지만 아무 값이나 넣으면 안 됩니다. 기본값은 실제 의미가 있어야 합니다. 예를 들어 점수의 기본값을 0.00으로 두면, 아직 채점하지 않은 상태와 실제로 0점을 받은 상태가 구분되지 않을 수 있습니다. 이런 경우에는 NULL을 허용하는 편이 더 정확할 수도 있습니다.
숫자 타입 선택하기: INT, BIGINT, DECIMAL
숫자라고 해서 모두 같은 타입을 쓰면 안 됩니다. 숫자의 성격에 따라 타입을 골라야 합니다.
| 타입 | 사용하기 좋은 경우 | 예시 |
|---|---|---|
INT | 일반적인 정수 | 수량, 순서, 작은 범위의 번호 |
BIGINT | 매우 커질 수 있는 정수 | 사용자 id, 주문 id, 로그 id |
DECIMAL | 정확한 소수 계산이 필요한 값 | 금액, 점수, 비율 |
INT는 일반적인 정수에 적합합니다. 예를 들어 재고 수량, 출석 횟수, 게시글 조회수처럼 정수로 표현되는 값에 사용할 수 있습니다.
view_count INT DEFAULT 0BIGINT는 더 큰 정수가 필요할 때 사용합니다. 특히 기본키로 쓰는 id 값은 시간이 지나면서 계속 증가합니다. 학습용 테이블에서는 INT로도 충분한 경우가 많지만, 실무에서는 데이터가 많이 쌓일 수 있으므로 BIGINT를 선택하는 경우가 많습니다.
id BIGINT AUTO_INCREMENTDECIMAL은 정확한 소수값이 필요할 때 사용합니다. 금액이나 점수처럼 오차가 생기면 안 되는 값에는 DECIMAL이 적합합니다.
price DECIMAL(10, 2) NOT NULLDECIMAL(10, 2)는 전체 10자리 중 소수점 아래 2자리를 저장합니다. 예를 들어 12345678.90까지 저장할 수 있습니다.
문자열 타입 선택하기: VARCHAR와 TEXT
문자열을 저장할 때 가장 자주 만나는 타입은 VARCHAR와 TEXT입니다.
| 타입 | 사용하기 좋은 경우 | 예시 |
|---|---|---|
VARCHAR(n) | 길이 제한이 있는 짧은 문자열 | 이름, 이메일, 제목, 전화번호 |
TEXT | 길이가 긴 본문 | 게시글 내용, 설명문, 메모 |
VARCHAR(n)은 최대 길이를 정해 두는 문자열 타입입니다. 이름, 이메일, 상품명처럼 어느 정도 길이를 예상할 수 있는 값에 사용합니다.
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL여기서 VARCHAR(50)은 최대 50자까지 저장하겠다는 뜻입니다. 너무 작게 잡으면 정상적인 값이 들어가지 못하고, 너무 크게 잡으면 설계 의도가 흐려질 수 있습니다. 실제로 필요한 범위를 생각해서 정하는 것이 좋습니다.
TEXT는 긴 글을 저장할 때 사용합니다. 예를 들어 게시글 본문이나 상세 설명처럼 길이를 예측하기 어려운 값에 적합합니다.
description TEXT다만 짧은 문자열까지 모두 TEXT로 저장하는 것은 좋은 습관이 아닙니다. 이름, 이메일, 제목처럼 검색이나 정렬에 자주 쓰는 값은 VARCHAR를 우선 고려하세요.
날짜와 시간 타입 선택하기: DATE와 DATETIME
날짜와 시간은 문자열로 저장하지 않는 것이 좋습니다. 2026-05-17 같은 값을 VARCHAR로 저장할 수도 있지만, 그렇게 하면 날짜 비교와 정렬이 불편해집니다. MySQL이 날짜로 이해할 수 있도록 날짜 타입을 사용해야 합니다.
| 타입 | 저장 내용 | 예시 |
|---|---|---|
DATE | 날짜만 저장 | 생년월일, 예약일, 개강일 |
DATETIME | 날짜와 시간을 함께 저장 | 가입일시, 주문일시, 수정일시 |
생년월일에는 시간이 필요하지 않습니다.
birth_date DATE가입일이나 주문일처럼 정확한 시각이 필요한 값에는 DATETIME을 사용합니다.
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP수정 시각을 자동으로 갱신하고 싶다면 다음처럼 작성할 수도 있습니다.
updated_at DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMPcreated_at과 updated_at은 실무 테이블에서 자주 사용됩니다. 언제 만들어졌고 언제 바뀌었는지 알 수 있으면 문제를 추적하기 쉽습니다.
AUTO_INCREMENT로 식별자 만들기
AUTO_INCREMENT는 숫자 id를 자동으로 증가시켜 주는 기능입니다. 보통 기본키와 함께 사용합니다.
CREATE TABLE students (
id BIGINT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);데이터를 넣을 때는 id를 직접 넣지 않아도 됩니다.
INSERT INTO students (name, email)
VALUES ('김민수', '[email protected]');MySQL은 자동으로 다음 id를 넣습니다.
SELECT id, name, email
FROM students;결과는 다음과 비슷합니다.
+----+--------+-------------------+
| id | name | email |
+----+--------+-------------------+
| 1 | 김민수 | [email protected] |
+----+--------+-------------------+AUTO_INCREMENT를 사용할 때는 몇 가지를 기억해야 합니다.
첫째, id는 연속된 번호라는 보장이 없습니다. 데이터를 삭제하거나 입력에 실패하면 중간 번호가 비어 있을 수 있습니다. 예를 들어 1번, 2번, 4번은 있고 3번은 없을 수 있습니다. 이것은 오류가 아닙니다.
둘째, id는 의미를 담는 값이 아닙니다. id가 10번이라고 해서 10번째로 중요한 학생이라는 뜻은 아닙니다. 단지 행을 구분하는 식별자일 뿐입니다.
셋째, 외부에 그대로 노출할 값인지 고민해야 합니다. 단순한 게시글 번호처럼 보여도 되는 경우가 있지만, 민감한 데이터에서는 자동 증가 id만으로 접근 권한을 판단하면 위험합니다.
좋은 컬럼 이름과 나쁜 컬럼 이름
테이블 설계에서는 컬럼 이름도 중요합니다. 이름이 명확하면 SQL을 읽기 쉬워지고 실수가 줄어듭니다.
좋은 이름은 의미가 분명합니다.
created_at DATETIME
birth_date DATE
email VARCHAR(255)애매한 이름은 피하는 것이 좋습니다.
date DATETIME
text TEXT
value VARCHAR(100)date라는 컬럼은 가입일인지 수정일인지 예약일인지 알기 어렵습니다. text도 이름인지 설명인지 메모인지 알기 어렵습니다. 컬럼 이름은 조금 길어져도 구체적인 편이 좋습니다.
또한 하나의 컬럼에는 하나의 의미만 담아야 합니다. 예를 들어 주소 전체를 하나의 문자열로 저장할 수도 있지만, 우편번호, 기본 주소, 상세 주소를 따로 검색해야 한다면 컬럼을 나누는 편이 낫습니다.
postal_code VARCHAR(10),
address_line1 VARCHAR(255),
address_line2 VARCHAR(255)반대로 무조건 많이 나누는 것도 좋지 않습니다. 데이터를 어떻게 조회하고 관리할지 생각한 뒤에 나누어야 합니다.
ALTER TABLE로 테이블 구조 변경하기
처음부터 완벽한 테이블을 만들기는 어렵습니다. 서비스를 만들다 보면 컬럼을 추가하거나 이름을 바꾸거나 타입을 변경해야 할 때가 있습니다. 이때 사용하는 명령이 ALTER TABLE입니다.
컬럼 추가하기
학생 테이블에 전화번호 컬럼을 추가해 보겠습니다.
ALTER TABLE students
ADD COLUMN phone VARCHAR(20);컬럼 위치를 지정하고 싶다면 AFTER를 사용할 수 있습니다.
ALTER TABLE students
ADD COLUMN phone VARCHAR(20) AFTER email;위치는 사람이 보기 편하게 정리하는 용도입니다. 조회할 때는 컬럼 순서보다 SELECT 문에서 어떤 컬럼을 선택하느냐가 더 중요합니다.
컬럼 타입 변경하기
이름 길이를 50자에서 100자로 늘리고 싶다면 다음처럼 변경할 수 있습니다.
ALTER TABLE students
MODIFY COLUMN name VARCHAR(100) NOT NULL;타입을 변경할 때는 기존 데이터가 새 타입에 맞는지 확인해야 합니다. 예를 들어 VARCHAR(100)을 VARCHAR(20)으로 줄이면 이미 20자를 넘는 값이 문제가 될 수 있습니다.
컬럼 이름 변경하기
phone 컬럼 이름을 더 명확하게 phone_number로 바꿔 보겠습니다.
ALTER TABLE students
CHANGE COLUMN phone phone_number VARCHAR(20);컬럼 이름을 바꾸면 기존 SQL이나 애플리케이션 코드도 함께 수정해야 합니다. 데이터베이스만 바꾸고 프로그램을 바꾸지 않으면 오류가 발생합니다.
컬럼 삭제하기
더 이상 생년월일을 저장하지 않기로 했다면 컬럼을 삭제할 수 있습니다.
ALTER TABLE students
DROP COLUMN birth_date;컬럼 삭제는 특히 조심해야 합니다. 삭제된 컬럼의 데이터는 사라집니다. 실무에서는 백업을 먼저 확인하고, 해당 컬럼을 사용하는 화면이나 쿼리가 없는지 점검한 뒤 삭제해야 합니다.
ALTER TABLE을 사용할 때의 주의점
작은 학습용 테이블에서는 ALTER TABLE이 금방 끝납니다. 하지만 데이터가 많은 테이블에서는 구조 변경이 오래 걸릴 수 있습니다. 변경 중에 테이블 사용이 느려지거나 제한될 수도 있습니다.
그래서 실무에서는 다음 순서로 안전하게 변경합니다.
새 컬럼을 먼저 추가한다.
기존 데이터를 새 컬럼에 채운다.
애플리케이션이 새 컬럼을 사용하도록 바꾼다.
충분히 확인한 뒤 예전 컬럼을 삭제한다.
예를 들어 phone을 phone_number로 바꿔야 한다면 바로 삭제하지 않고 새 컬럼을 추가한 뒤 데이터를 복사할 수 있습니다.
ALTER TABLE students
ADD COLUMN phone_number VARCHAR(20);
UPDATE students
SET phone_number = phone;그다음 프로그램이 phone_number를 사용하도록 바꾼 뒤, 문제가 없을 때 phone을 삭제합니다.
ALTER TABLE students
DROP COLUMN phone;이런 방식은 번거롭지만 안전합니다. 데이터베이스 변경은 되돌리기 어려운 경우가 많기 때문에 빠른 변경보다 안전한 변경이 더 중요합니다.
실습: 학생 테이블 다시 설계하기
이제 지금까지 배운 내용을 사용해 학생 테이블을 새로 만들어 보겠습니다. 기존 테이블이 있다면 학습용 데이터인지 확인한 뒤 삭제합니다.
DROP TABLE IF EXISTS students;새 테이블을 만듭니다.
CREATE TABLE students (
id BIGINT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
birth_date DATE,
score DECIMAL(5, 2),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE (email)
);데이터를 넣어 봅니다.
INSERT INTO students (name, email, birth_date, score)
VALUES
('김민수', '[email protected]', '2003-04-12', 88.50),
('이지은', '[email protected]', '2002-11-03', 92.00),
('박서준', '[email protected]', NULL, NULL);조회해 봅니다.
SELECT id, name, email, birth_date, score, created_at
FROM students;이번에는 이름 없이 데이터를 넣어 보겠습니다.
INSERT INTO students (email)
VALUES ('[email protected]');이 문장은 실패합니다. name 컬럼에 NOT NULL이 있기 때문입니다. 이것이 제약 조건의 역할입니다. 잘못된 데이터가 들어온 뒤에 고치는 것보다, 처음부터 들어오지 못하게 막는 편이 안전합니다.
이메일 중복도 확인해 봅니다.
INSERT INTO students (name, email)
VALUES ('다른학생', '[email protected]');이미 같은 이메일이 있다면 이 문장도 실패합니다. UNIQUE (email) 조건이 중복을 막기 때문입니다.
테이블 설계 점검 목록
테이블을 만들기 전에 다음 항목을 확인해 보세요.
한 테이블이 하나의 주제를 다루는가?
한 행이 무엇 하나를 의미하는지 분명한가?
각 컬럼의 데이터 타입이 값의 성격과 맞는가?
필수값에는
NOT NULL을 붙였는가?기본값이 필요한 컬럼에는 의미 있는
DEFAULT를 정했는가?각 행을 구분할 기본키가 있는가?
중복되면 안 되는 값에
UNIQUE를 고려했는가?날짜와 시간을 문자열이 아니라 날짜 타입으로 저장하는가?
나중에 변경될 가능성이 큰 컬럼을 무리하게 고정하지 않았는가?
이 목록을 매번 완벽하게 지킬 필요는 없습니다. 중요한 것은 컬럼을 만들 때마다 이유를 생각하는 것입니다. 왜 이 타입인지, 왜 비워 둘 수 있는지, 왜 중복을 허용하는지 설명할 수 있어야 합니다.
정리
테이블 설계는 데이터베이스의 기초 공사입니다. 좋은 SQL은 좋은 테이블 위에서 더 힘을 발휘합니다.
이번 장에서는 CREATE TABLE로 컬럼과 제약 조건을 정의하는 방법을 배웠습니다. INT, BIGINT, DECIMAL, VARCHAR, TEXT, DATE, DATETIME의 사용 기준도 살펴보았습니다. 또한 AUTO_INCREMENT로 식별자를 만드는 방식과 ALTER TABLE로 테이블 구조를 바꿀 때의 주의점도 배웠습니다.
다음 장에서는 테이블 하나를 넘어 여러 테이블을 연결하는 방법을 배웁니다. 학생, 강의, 수강 신청처럼 서로 관련된 데이터를 어떻게 나누고 연결하는지 이해하면 MySQL을 훨씬 더 실무적으로 사용할 수 있습니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.