정규화(Normalization)와 비정규화(Denormalization, 역정규화)
우리가 앞서 SQL에서 여러 테이블을 조인(JOIN)하고 그룹화(GROUP BY)하는 유용한 방법들을 배웠습니다. 그렇다면 애초에 "테이블은 어떤 기준으로 쪼개어 설계해야 하며, 또 어떤 상황에서는 다시 합쳐야 할까?"라는 근본적인 아키텍처 고민이 생기게 됩니다.
그 해답이 바로 정규화와 비정규화입니다. 아주 직관적인 비유와 구체적인 사례를 통해 구조적으로 정리해 드릴게요!
'초간단 정규화' 요약장
제1정규화 : 하나의 셀(칸)에는 무조건 단 한 개의 데이터만 넣는다.
한 칸에 쉼표(,) 찍고 억지로 데이터 욱여넣기 방지
제2정규화 : 공동 대장 ID(복합 기본키) 중, 한쪽 대장에게만 아양 떠는 열은 분리한다.
부부 공동 명의 방에 남편 개인 낚시 장비 쑤셔 넣기 방지
*제3정규화 : 대장 ID가 아닌, 일반 데이터끼리 꼬리를 물고 종속되는 관계는 분리한다.
내 수첩에 친구의 사촌 동생 전화번호까지 같이 적어두기 방지
JOIN 해야 합니다. JOIN은 컴퓨터 컴퓨터 자원(CPU, 메모리)을 많이 소모하는 연산입니다. 사용자가 몰릴 때 이 조인 연산 때문에 화면이 멈추는 성능 병목이 생깁니다. 이때 조회 성능을 위해 테이블을 다시 조금 합치는 비정규화를 단행합니다.| 구분 | 정규화 (Normalization) | 비정규화 (Denormalization - 역정규화) |
|---|---|---|
| 핵심 목표 | 데이터 중복 제거, 데이터 일관성(무결성) 확보 | 데이터 조회 속도 향상, 시스템 성능 최적화 |
| 주요 장점 | 저장 공간 절약, 데이터 수정/삭제 시 안전성 보장 | 복잡한 JOIN 연산 감소로 초고속 데이터 검색 가능 |
| 주요 단점 | 테이블 개수가 늘어나 잦은 JOIN으로 조회 성능 저하 가능 | 데이터 중복으로 인해 저장 공간 낭비 및 수정 시 오류 위험 증가 |
| 최적 시스템 | 실시간 원장 거래가 빈번한 OLTP 시스템 (예: 은행 입출금) | 대규모 분석 리포트를 뽑아내는 OLAP / 데이터웨어하우스 |
실무에서 "100% 완벽한 정규화"만을 고집하는 설계자는 좋은 평가를 받기 어렵습니다. 시스템의 성능과 안전성의 균형을 맞추는 것이 설계자의 진짜 역량입니다.
| 분류명 (Abbreviation) | 한글 명칭 | 역할 비유 | 대표적인 명령어 (Commands) |
|---|---|---|---|
| DDL (Data Definition Language) | 데이터 정의어 | 창고 뼈대 짓기 & 가구 배치 | CREATE, ALTER, DROP, TRUNCATE |
| DML (Data Manipulation Language) | 데이터 조작어 | 서랍에 물건 넣고 꺼내기 | SELECT, INSERT, UPDATE, DELETE |
| DCL (Data Control Language) | 데이터 제어어 | 보안요원 열쇠 권한 관리 | GRANT, REVOKE |
| TCL (Transaction Control Language) | 트랜잭션 제어어 | 타임머신 및 최종 도장 찍기 | COMMIT, ROLLBACK, SAVEPOINT |
"창고 건물을 설계하고 수납장을 제작하는 도구"
DDL은 데이터베이스의 구조나 틀(Schema)을 정의하고 변경하는 명령어들입니다. 테이블 안에 들어있는 알맹이(값)가 아니라, 데이터를 담는 수납장 자체의 외형을 만들고 부수는 역할을 합니다.
CREATE: 새로운 데이터베이스나 테이블, 뷰, 인덱스 등을 생성합니다.ALTER: 기존 테이블의 컬럼을 추가/수정/삭제하는 등 구조를 변경합니다.DROP: 테이블이나 데이터베이스 구조 자체를 완전히 철거(삭제)합니다.TRUNCATE: 테이블의 뼈대(구조)는 그대로 남겨둔 채 안의 모든 데이터만 고속으로 전부 비웁니다."서랍 속에 물건을 채우고, 꺼내서 요리하는 도구"
DML은 테이블에 저장되어 있는 실제 데이터(행/Row)를 다루는 명령어들입니다. 데이터 분석가나 AI 개발자가 실무에서 가장 많이 가공하고 쿼리를 작성하게 되는 메인 영역입니다.
SELECT: 데이터베이스에서 원하는 조건의 데이터를 조회하고 검색합니다 (분석의 핵심!).INSERT: 테이블에 새로운 데이터 행을 삽입하여 등록합니다.UPDATE: 이미 저장되어 있는 특정 데이터 값을 수정합니다.DELETE: 테이블에서 조건에 맞는 데이터를 선택하여 지웁니다."창고 출입을 통제하는 보안 관리자의 마스터키"
데이터베이스는 기업의 기밀이나 사용자 정보가 담겨있는 안전구역입니다. DCL은 어떤 사용자에게 어떤 권한을 부여할지, 혹은 뺏을지 제어하여 시스템의 보안과 무결성을 유지합니다.
GRANT: 특정 데이터베이스나 객체에 대한 접근 및 사용 권한을 특정 사용자에게 부여합니다.REVOKE: 이전에 사용자에게 주었던 권한을 다시 안전하게 회수(취소)합니다."낙장불입을 보장하는 최종 승인 및 롤백 장치"
TCL은 이전에 다룬 '트랜잭션(더 이상 쪼갤 수 없는 작업 단위)'을 제어하는 명령어입니다. DML로 조작된 데이터의 변경 사항을 승인하거나 원상복구합니다.
COMMIT: 임시로 실행한 모든 DML 작업 결과가 완벽할 때, 이를 데이터베이스에 영구적으로 확정 저장합니다.ROLLBACK: 작업 도중 예기치 못한 에러가 나거나 수정이 잘못되었을 때, 트랜잭션 시작 이전의 상태로 되돌립니다.SAVEPOINT: 중간중간 북마크(깃발)를 꽂아두어, 문제가 생겼을 때 맨 처음이 아닌 지정한 중간 지점으로만 롤백할 수 있게 도와줍니다.여러분이 친구에게 계좌이체로 1만 원을 송금하는 상황을 가정해 봅시다. 이 작업은 컴퓨터 내부에서 크게 두 단계로 쪼개집니다.
1. 내 계좌에서 1만 원 인출
2. 친구 계좌에 1만 원 입금
만약 시스템 장애로 1번(인출)은 성공했는데, 2번(입금)이 실패했다면 어떻게 될까요?
내 돈 1만 원은 공중으로 날아가 버리고 엄청난 민원이 발생하겠죠. 따라서 이 두 단계는 "둘 다 아주 깔끔하게 성공(All)하거나, 아니면 애초에 아무 일도 없었던 것처럼 둘 다 취소(Nothing)"되어야만 안전합니다. 이 두 과정을 자석처럼 하나로 꽁꽁 묶어주는 기술이 바로 트랜잭션입니다.
안전한 데이터 거래를 보장하기 위해 트랜잭션은 ACID라고 불리는 4가지 전제 조건을 완벽히 만족해야 합니다.
MySQL에서 수동으로 트랜잭션을 조율할 때 사용하는 핵심 명령어입니다.
START TRANSACTIONCOMMIT (확정 저장)COMMIT이 떨어지면 비로소 하나의 트랜잭션이 안전하게 종료됩니다.ROLLBACK (취소 및 원상복구)트랜잭션은 시작되어 끝날 때까지 다음과 같은 단계별 상태를 거치며 생명주기를 가집니다.
[ 활성 (Active) ] ➡️ 정상 실행 중
⬇️
[ 부분 완료 (Partially Committed) ] ➡️ 마지막 연산 종료 후 Commit 대기
⬇️
[ 완료 (Committed) ] ➡️ Commit 성공! DB 영구 반영 완료
(오류 발생 시)
[ 활성 (Active) ] ➡️ 실행 중 오류 발생
⬇️
[ 실패 (Failed) ] ➡️ 연산 중단 상태
⬇️
[ 철회 (Aborted) ] ➡️ Rollback 연산 수행하여 실행 전으로 원상복구
INSERT, UPDATE, DELETE 같은 DML(데이터 조작어)은 에러가 나면 롤백이 가능합니다.CREATE, DROP, ALTER, TRUNCATE 같은 DDL(데이터 정의어) 문법은 트랜잭션 실행 중에 에러가 나서 롤백을 선언하더라도 롤백 대상이 되지 않고 즉시 반영되어 버리니 아주 주의하셔야 합니다!InnoDB로 설정되어 있어야만 이 강력한 트랜잭션 기능을 정상적으로 사용할 수 있습니다. 구버전이나 MyISAM 엔진을 쓰면 트랜잭션이 작동하지 않으니 개발 시작 전 꼭 설계 설정을 체크해 보세요!트랜잭션은 계좌이체처럼 더 이상 쪼갤 수 없는 하나의 작업 단위이며, 데이터의 안전을 위해 ACID 특징을 지키고, 문제가 생기면 ROLLBACK으로 깔끔하게 원상복구하고 성공 시에만 COMMIT으로 저장하는 안전장치입니다.
dbms 마직말 날...
select
b.name as 구매자 명,
p.product_name as 상품명,
p.price as 단가,
pc.guantity as 구매수량,
(p.price * pc.guantity) as 총결제금액
from purchases as pc
inner join buyers as b
on pc.buyers as b
inner join products as p
on pc.product_id = p.product_id;
< 위 명령어의 처리 흐름 단계별 분석 :작동 원리>
컴퓨터가 이 쿼리를 실행하는 내부적인 단계(Execution Order)를 시각적으로 따라가 봅시다.
1단계: FROM purchases AS pc (징검다리 테이블 먼저 놓기)
가장 먼저 활동 기록이 담긴 핵심 영수증 장부(purchases)를 테이블 위에 올려놓습니다. 컴퓨터는 이 테이블을 아주 짧게 pc라는 별칭으로 부르기 시작합니다.
2단계: INNER JOIN buyers AS b ON pc.buyer_id = b.buyer_id (첫 번째 섬 연결)
영수증 장부(pc)에 적힌 고객번호(buyer_id)를 단서로 삼아, 고객 명부 테이블(buyers, 약칭 b)로 찾아갑니다.
고객번호가 똑같은 행끼리 가로로 착 이어 붙입니다. 이 단계를 거치면 이제 숫자로만 되어 있던 고객번호 옆에 '김철수', '이영희' 같은 실제 이름이 생겨납니다.
3단계: INNER JOIN products AS p ON pc.product_id = p.product_id (두 번째 섬 연결)
영수증 장부(pc)에 적힌 상품번호(product_id)를 단서로 삼아, 상품 목록 테이블(products, 약칭 p)로 찾아갑니다.
상품번호가 일치하는 행을 기존에 완성된 큰 표 오른쪽에 추가로 이어 붙입니다. 이제 숫자로만 되어 있던 상품번호 옆에 '아이패드', '마우스' 같은 실제 상품명과 가격 정보가 추가됩니다.
4단계: SELECT ... (원하는 컬럼만 선택하고 계산하기)
3개의 테이블이 완벽하게 가로로 합쳐진 거대한 임시 테이블에서, 우리가 처음에 요청한 구매자명(b.name), 상품명(p.product_name), 단가(p.price), 수량(pc.quantity) 컬럼만 콕 집어서 꺼냅니다.
동시에 p.price * pc.quantity를 계산하여 총결제금액이라는 새로운 결과 컬럼을 만들어 최종 화면에 보여줍니다.