- 데이터 중복 감소: 고객이 주문할 때마다 고객의 주소를 저장한다고 상상해보세요. 주소가 변경되면 여러 곳에서 업데이트해야 합니다! 정규화는 정보를 한 곳에 저장하고 다른 곳에서 참조하도록 하여 반복을 최소화합니다.
- 데이터 무결성 향상: 중복이 적으면 불일치 가능성도 줄어듭니다. 주소를 한 곳에서 업데이트하면 필요한 모든 곳에서 업데이트됩니다.
- 이상 현상 방지: 정규화는 다음과 같은 문제를 방지합니다:
- 삽입 이상: 관련 정보가 없어서 새 데이터를 추가하기 어려운 경우
- 업데이트 이상: 여러 행에서 동일한 정보를 업데이트해야 하는 경우
- 삭제 이상: 관련 없어 보이는 것을 삭제할 때 실수로 중요한 정보를 잃는 경우
- 이해 및 유지보수 용이: 정규화된 데이터베이스는 일반적으로 논리적으로 더 잘 구조화되어 있어 이해하고, 쿼리하고, 수정하기가 더 쉽습니다.
Drizzle Relations 기본 개념
데이터베이스, 특히 관계형 데이터베이스에서 관계(relation)의 개념은 매우 중요합니다. “관계”는 서로 다른 데이터 조각들 사이의 연결과 링크로 생각할 수 있습니다. 실생활에서 사람들이 서로 관계를 맺고, 사물들이 카테고리와 연결되는 것처럼, 데이터베이스는 관계를 사용하여 서로 다른 유형의 정보가 어떻게 연결되고 함께 작동하는지를 모델링합니다.
정규화
정규화(Normalization)는 중복성(중복)을 줄이고 데이터 무결성(정확성과 일관성)을 향상시키기 위해 데이터베이스의 데이터를 조직화하는 과정입니다. 어질러진 파일 캐비닛을 정리하는 것과 같다고 생각하면 됩니다. 모든 종류의 문서를 한 폴더에 우겨 넣는 대신, 논리적인 폴더와 카테고리로 정리하여 모든 것을 찾고 관리하기 쉽게 만드는 것입니다.
정규화가 왜 중요한가요?
정규화는 종종 “정규형”(1NF, 2NF, 3NF 등)의 관점에서 설명됩니다. 세부 사항은 매우 기술적일 수 있지만, 핵심 개념은 간단합니다:
1NF (제1정규형): 원자값
목표: 각 컬럼은 단일하고 분할 불가능한 값을 가져야 합니다. 단일 셀 내에 반복되는 데이터 그룹이 없어야 합니다.
예시: 123 Main St, City, USA를 저장하는 단일 address 컬럼을 갖는 대신,
street_address, city, state, zip_code와 같은 별도의 컬럼으로 나눕니다.
-- Unnormalized (violates 1NF)
CREATE TABLE Customers_Unnormalized (
customer_id INT PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255) -- Problem: Multiple pieces of info in one column
);
-- Normalized to 1NF
CREATE TABLE Customers_1NF (
customer_id INT PRIMARY KEY,
name VARCHAR(255),
street_address VARCHAR(255),
city VARCHAR(255),
state VARCHAR(255),
zip_code VARCHAR(10)
);2NF (제2정규형): 키의 일부에 종속된 중복 데이터 제거
목표: 복합 기본 키(두 개 이상의 컬럼으로 구성된 기본 키)를 가진 테이블에 적용됩니다. 제2정규형은 모든 비키 속성이 복합 기본 키의 일부가 아닌 전체에 완전히 종속되도록 합니다.
order_items라는 테이블이 있다고 상상해봅시다. 이 테이블은 주문 내의 항목을 추적하며, 단일 주문에 동일한 제품이 여러 개 있을 수 있기 때문에
복합 기본 키(order_id, product_id)를 사용합니다(이 간단한 예제에서는 명확성을 위해 각 제품이 주문당 한 번만 나타난다고 가정하지만,
복합 키 논리는 여전히 적용됩니다).
시각적 예제 확장하기
CREATE TABLE OrderItems_Unnormalized (
order_id INT,
product_id VARCHAR(10),
product_name VARCHAR(100),
product_price DECIMAL(10, 2),
quantity INT,
order_date DATE,
PRIMARY KEY (order_id, product_id) -- Composite Primary Key
);
INSERT INTO OrderItems_Unnormalized (order_id, product_id, product_name, product_price, quantity, order_date) VALUES
(101, 'A123', 'Laptop', 1200.00, 1, '2023-10-27'),
(101, 'B456', 'Mouse', 25.00, 2, '2023-10-27'),
(102, 'A123', 'Laptop', 1200.00, 1, '2023-10-28'),
(103, 'C789', 'Keyboard', 75.00, 1, '2023-10-29');+------------------------------------------------------------------------------------+
| OrderItems_Unnormalized |
+------------------------------------------------------------------------------------+
| PK (order_id, product_id) | product_name | product_price | quantity | order_date |
+------------------------------------------------------------------------------------+
| 101, A123 | Laptop | 1200.00 | 1 | 2023-10-27 |
| 101, B456 | Mouse | 25.00 | 2 | 2023-10-27 |
| 102, A123 | Laptop | 1200.00 | 1 | 2023-10-28 |
| 103, C789 | Keyboard | 75.00 | 1 | 2023-10-29 |
+------------------------------------------------------------------------------------+문제: 동일한 product_id가 서로 다른 주문에 나타날 때마다 product_name과 product_price가 반복됩니다.
이러한 속성은 복합 기본 키(order_id, product_id)의 일부인 product_id에만 종속되며, 전체 키에 종속되지 않습니다.
이것이 부분 종속성입니다.
제2정규형을 달성하려면, 부분적으로 종속된 속성(product_name, product_price)을 제거하고 새 테이블의 기본 키에 완전히 종속되는
별도의 테이블에 배치해야 합니다.
제2정규형 정규화: 시각적 설명
+-------------------+ 1:M +---------------------------+
| Products | <---------- | OrderItems_2NF |
+-------------------+ +---------------------------+
| PK product_id | | PK (order_id, product_id) |
| product_name | | quantity |
| product_price | | order_date |
+-------------------+ | FK product_id |
+---------------------------+CREATE TABLE Products (
product_id VARCHAR(10) PRIMARY KEY,
product_name VARCHAR(100),
product_price DECIMAL(10, 2)
);
CREATE TABLE OrderItems_2NF (
order_id INT,
product_id VARCHAR(10),
quantity INT,
order_date DATE,
PRIMARY KEY (order_id, product_id), -- Composite Primary Key remains
FOREIGN KEY (product_id) REFERENCES Products(product_id) -- Foreign Key to Products
);
-- Insert data into Products
INSERT INTO Products (product_id, product_name, product_price) VALUES
('A123', 'Laptop', 1200.00),
('B456', 'Mouse', 25.00),
('C789', 'Keyboard', 75.00);
-- Insert data into OrderItems_2NF (referencing Products)
INSERT INTO OrderItems_2NF (order_id, product_id, quantity, order_date) VALUES
(101, 'A123', 1, '2023-10-27'),
(101, 'B456', 2, '2023-10-27'),
(102, 'A123', 1, '2023-10-28'),
(103, 'C789', 1, '2023-10-29');3NF (제3정규형): 비키 속성에 종속된 중복 데이터 제거
목표: 다른 비키 속성에 종속된 데이터를 제거합니다. 이는 이행적 종속성을 제거하는 것입니다.
문제: suppliers 테이블이 있다고 가정해봅시다. zip_code, city, state를 포함한 공급업체 정보를 저장합니다. supplier_id가 기본 키입니다.
CREATE TABLE suppliers (
supplier_id VARCHAR(10) PRIMARY KEY,
supplier_name VARCHAR(255),
zip_code VARCHAR(10),
city VARCHAR(100),
state VARCHAR(50)
);
INSERT INTO suppliers (supplier_id, supplier_name, zip_code, city, state) VALUES
('S1', 'Acme Corp', '12345', 'Anytown', 'NY'),
('S2', 'Beta Inc', '67890', 'Otherville', 'CA'),
('S3', 'Gamma Ltd', '12345', 'Anytown', 'NY');+---------------------------------------------------------------+
| suppliers |
+---------------------------------------------------------------+
| PK supplier_id | supplier_name | zip_code | city | state |
+---------------------------------------------------------------+
| S1 | Acme Corp | 12345 | Anytown | NY |
| S2 | Beta Inc | 67890 | Otherville | CA |
| S3 | Gamma Ltd | 12345 | Anytown | NY |
+---------------------------------------------------------------+해결책: 제3정규형을 달성하려면, 비키 속성에 종속된 속성(zip_code에 종속된 city, state)을 제거하고
비키 속성 자체(zip_code)를 키로 하는 별도의 테이블에 배치합니다.
제3정규형 정규화: 시각적 설명
+-------------------+ 1:M +--------------------+
| zip_codes | <---------- | suppliers |
+-------------------+ +--------------------+
| PK zip_code | | PK supplier_id |
| city | | supplier_name |
| state | | FK zip_code |
+-------------------+ +--------------------+CREATE TABLE zip_codes (
zip_code VARCHAR(10) PRIMARY KEY,
city VARCHAR(100),
state VARCHAR(50)
);
CREATE TABLE suppliers (
supplier_id VARCHAR(10) PRIMARY KEY,
supplier_name VARCHAR(255),
zip_code VARCHAR(10), -- Foreign Key to zip_codes
FOREIGN KEY (zip_code) REFERENCES zip_codes(zip_code)
);
-- Insert data into zip_codes
INSERT INTO zip_codes (zip_code, city, state) VALUES
('12345', 'Anytown', 'NY'),
('67890', 'Otherville', 'CA');
-- Insert data into suppliers (referencing zip_codes)
INSERT INTO suppliers (supplier_id, supplier_name, zip_code) VALUES
('S1', 'Acme Corp', '12345'),
('S2', 'Beta Inc', '67890'),
('S3', 'Gamma Ltd', '12345');4NF, 5NF, 6NF, EKNF, ETNF, DKNF와 같은 추가 정규형이 있습니다. 여기서는 다루지 않지만,
가이드 및 튜토리얼 섹션에서 이들에 대한 전용 튜토리얼을 제공할 예정입니다.
데이터베이스 관계
일대일 관계
일대일 관계에서 테이블 A의 각 레코드는 테이블 B의 최대 하나의 레코드와 관련되며, 테이블 B의 각 레코드는
테이블 A의 최대 하나의 레코드와 관련됩니다. 매우 직접적이고 배타적인 쌍입니다.
사용 사례 및 예시
- 사용자 프로필과 사용자 계정 세부정보: 웹사이트를 생각해보세요. 각 사용자 계정(Users 테이블)은 더 상세한 정보를 포함하는 정확히 하나의 사용자 프로필(UserProfiles 테이블)을 가질 수 있습니다.
- 직원과 주차 공간: Employees 테이블과 ParkingSpaces 테이블. 각 직원은 최대 하나의 주차 공간을 할당받을 수 있으며, 각 주차 공간은 최대 하나의 직원에게 할당됩니다.
- 조직화를 위한 테이블 분할: 때로는 더 나은 조직화나 보안상의 이유로 매우 넓은 테이블을 두 개로 분할하여 그들 사이에 일대일 관계를 유지할 수 있습니다.
Table A (One Side) Table B (One Side)
+---------+ +---------+
| PK (A) | <---------> | FK (A) | (Foreign Key referencing Table A)
| ... | | ... |
+---------+ +---------+일대다 관계
일대다 관계에서 테이블 A의 하나의 레코드는 테이블 B의 여러 레코드와 관련될 수 있지만, 테이블 B의 각 레코드는
테이블 A의 최대 하나의 레코드와 관련됩니다. “부모-자식” 관계로 생각할 수 있습니다.
사용 사례 및 예시
- 고객과 주문: 한 명의 고객은 여러 주문을 할 수 있지만, 각 주문은 한 명의 고객에게만 속합니다.
- 저자와 책: 한 명의 저자는 여러 권의 책을 쓸 수 있지만(지금은 간단히 말해서), 각 책은 한 명의 주 저자가 씁니다.
- 부서와 직원: 하나의 부서는 여러 직원을 가질 수 있지만, 각 직원은 하나의 부서에만 속합니다.
Table A (One Side) Table B (Many Side)
+---------+ +---------+
| PK (A) | ----------> | FK (A) | (Foreign Key referencing Table A)
| ... | | ... |
+---------+ +---------+
(One) (Many)다대다 관계
다대다 관계에서 테이블 A의 하나의 레코드는 테이블 B의 여러 레코드와 관련될 수 있으며, 테이블 B의 하나의 레코드는
테이블 A의 여러 레코드와 관련될 수 있습니다. 보다 복잡한 양방향 관계입니다.
사용 사례 및 예시
- 학생과 강좌: 한 명의 학생은 여러 강좌에 등록할 수 있으며, 하나의 강좌는 여러 학생이 등록할 수 있습니다.
- 상품과 카테고리: 하나의 상품은 여러 카테고리에 속할 수 있으며(예: “티셔츠”는 “의류” 및 “여름 의류” 카테고리에 속할 수 있음), 하나의 카테고리는 여러 상품을 포함할 수 있습니다.
- 저자와 책: 책은 여러 저자가 쓸 수 있으며, 저자는 여러 권의 책을 쓸 수 있습니다.
Table A (Many Side) Junction Table Table B (Many Side)
+---------+ +-------------+ +---------+
| PK (A) | -------->| FK (A) | <----| FK (B) |
| ... | | FK (B) | | ... |
+---------+ +-------------+ +---------+
(Many) (Junction) (Many)다대다 관계는 두 주 테이블 간의 외래 키로 직접 구현되지 않습니다.
대신 중간 테이블(연관 테이블 또는 브리징 테이블이라고도 함)이 필요합니다.
이 테이블은 두 테이블의 레코드를 연결하는 중개자 역할을 합니다.
-- Table for Students (Many side)
CREATE TABLE students (
iid INT PRIMARY KEY,
name VARCHAR(255)
);
-- Table for Courses (Many side)
CREATE TABLE courses (
id INT PRIMARY KEY,
name VARCHAR(255),
credits INT
);
-- Junction Table: Enrollments (Connects Students and Courses - M-M relationship)
CREATE TABLE enrollments (
id INT PRIMARY KEY AUTO_INCREMENT, -- Optional, but good practice for junction tables
student_id INT,
course_id INT,
enrollment_date DATE,
-- Composite Foreign Keys (often part of a composite primary key or unique constraint)
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id),
UNIQUE KEY (student_id, course_id) -- Prevent duplicate enrollments for the same student and course
);외래 키를 사용하는 이유는?
외래 키 제약조건을 단순히 데이터를 검증하는 방법으로 생각할 수 있습니다 - 외래 키 컬럼에 값을 입력할 때 그 값이 실제로 다른 테이블의 기본 키 컬럼에 존재하는지 확인하는 것입니다. 그리고 부분적으로는 맞습니다! 이 값 검사가 외래 키가 사용하는 메커니즘입니다.
하지만 이 검증이 최종 목표가 아니라, 훨씬 더 큰 목적을 위한 수단이라는 것을 이해하는 것이 중요합니다. 외래 키 제약조건은 근본적으로 다음에 관한 것입니다:
1. 관계의 명시적 정의 및 강제
고객과 주문 간의 일대다 같은 관계에 대해 논의했습니다.
외래 키는 SQL 언어가 데이터베이스에 전달하는 방식입니다:
데이터베이스야, 여기서 일대다 관계를 강제하고 싶어. Orders 테이블의 customer_id 컬럼의 모든 값은 Customers 테이블의 유효한 customer_id와 일치해야 해.
이것은 단순한 제안이 아니라, 데이터베이스가 적극적으로 강제하는 제약조건입니다. 외래 키 덕분에 데이터베이스는 관계를 인식하게 됩니다.
2. 참조 무결성 유지
- 이것은 관계의 맥락에서 “데이터 무결성”의 핵심입니다. 참조 무결성은 테이블 간의 관계가 시간이 지나도 일관되고 유효하게 유지됨을 의미합니다.
- 외래 키는 고아 레코드를 방지합니다. 고아 레코드가 무엇일까요? 고객-주문 예제에서, Orders 테이블에 존재하지만 Customers 테이블에 해당 고객이 없는 주문이 고아가 될 것입니다. 외래 키는 이런 일이 발생하는 것을 방지합니다(또는 주문이 있는 고객을 삭제하려고 할 때 어떻게 될지 제어합니다 - CASCADE, SET NULL 등을 통해).
- 고아를 방지하는 것이 왜 중요할까요? 고아 레코드는 데이터의 논리적 구조를 깨뜨립니다. 고객 없는 주문이 있으면 중요한 컨텍스트를 잃게 됩니다. 쿼리가 신뢰할 수 없게 되고, 리포트가 부정확해지며, 애플리케이션의 로직이 망가질 수 있습니다.
예시:
외래 키가 없으면, Customers 테이블에서 고객을 실수로 삭제할 수 있으며
그들의 주문이 여전히 Orders 테이블에 존재합니다. 갑자기, 더 이상 존재하지 않는
고객을 가리키는 주문이 생깁니다! 외래 키 제약조건은 이러한 데이터 불일치를 방지합니다.3. 데이터베이스 설계 및 이해 촉진
- 외래 키는 단순히 기술적 강제에 관한 것이 아니라, 데이터베이스 설계 문서의 중요한 부분이기도 합니다.
- 데이터베이스 스키마에서 외래 키를 보면 즉시 알 수 있습니다:
테이블 'X'는 테이블 'Y'와 이런 방식으로 관련되어 있다.이것은 관계의 명확한 시각적이고 구조적인 지표입니다. - 이로 인해 데이터베이스를 이해하고, 유지보수하고, 시간이 지남에 따라 발전시키기가 더 쉬워집니다. 새로운 개발자들은 데이터베이스의 다른 부분들이 어떻게 연결되어 있는지 빠르게 파악할 수 있습니다.
본질적으로, 외래 키 제약조건은 단순히 값을 확인하는 것이 아니라 다음에 관한 것입니다:
- 데이터 관계의 규칙 정의
- 데이터베이스 수준에서 해당 규칙을 적극적으로 강제
- 해당 관계 내에서 데이터 무결성과 일관성 보장
- 데이터베이스를 더 견고하고, 신뢰할 수 있으며, 이해하기 쉽게 만들기
외래 키를 사용하지 않는 이유는?
매우 유익하지만, 외래 키를 재고하거나 주의해서 사용해야 하는 몇 가지 시나리오가 있습니다. 이는 일반적으로 엣지 케이스이며 종종 트레이드오프를 수반합니다.
1. 매우 높은 쓰기 환경에서의 성능 오버헤드
- 시나리오: 극도로 높은 볼륨의 트랜잭션 시스템(예: 실시간 로깅, 매우 높은 빈도의 거래 플랫폼, 대규모 IoT 데이터 수집).
- 설명: 외래 키가 있는 테이블에 데이터를 삽입하거나 업데이트할 때마다 데이터베이스 시스템은 참조 무결성을 보장하기 위해 검사를 수행해야 합니다. 극도로 높은 쓰기 시나리오에서는 이러한 검사가 작지만 잠재적으로 눈에 띄는 성능 오버헤드를 발생시킬 수 있습니다.
2. 분산 데이터베이스 시스템 및 크로스-노드 외래 키:
- 시나리오: 데이터가 여러 데이터베이스 노드 또는 클러스터에 분산된 시스템(샤딩된 데이터베이스, 클라우드 환경 및 마이크로서비스에서 일반적).
- 설명: 크로스-노드 외래 키는 상당한 복잡성과 성능 오버헤드를 초래할 수 있습니다. 참조 무결성 검증은 노드 간 통신을 필요로 하여 지연 시간이 증가합니다. 일관성을 유지하는 데 필요한 분산 트랜잭션도 더 복잡하고 로컬 트랜잭션보다 성능이 떨어질 수 있습니다. 이러한 아키텍처에서는 애플리케이션 수준의 데이터 무결성 검사 또는 최종 일관성 모델이 대안으로 고려될 수 있습니다.
3. 레거시 시스템 및 비관계형 데이터와의 데이터 통합:
- 시나리오: 관계형 데이터베이스를 이전 레거시 시스템 또는 비관계형 데이터 저장소(예: NoSQL, 플랫 파일, 외부 API)와 통합.
- 설명: 레거시 시스템 또는 비관계형 데이터는 외래 키에 의해 강제되는 참조 무결성 규칙을 일관되게 준수하지 않을 수 있습니다. 이러한 시나리오에서 외래 키를 부과하면 데이터 가져오기 문제, 데이터 불일치가 발생할 수 있으며, 대신 복잡한 데이터 변환 또는 애플리케이션 수준의 무결성 관리가 필요할 수 있습니다. 외부 소스의 데이터 품질과 일관성을 신중하게 평가하고, 데이터베이스 수준에서 외래 키를 엄격하게 강제하는 대신 애플리케이션 로직 또는 ETL 프로세스에 의존하여 데이터 무결성을 보장해야 할 수 있습니다.
PlanetScale 팀의 문서에서 훌륭한 설명을 확인할 수도 있습니다.
다형성 관계
다형성 관계는 단일 관계가 다른 유형의 엔티티나 테이블을 가리킬 수 있도록 하는 고급 개념입니다. 공통점을 공유하는 다양한 종류의 데이터가 있을 때 더 유연하고 적응 가능한 관계를 만드는 것입니다.
activities 로그가 있다고 상상해보세요. 활동은 comment, like 또는 share일 수 있습니다.
이러한 각 activity 유형은 서로 다른 세부 정보를 가지고 있습니다. 각 활동 유형과 관련된 것들에 대해 별도의 테이블과
관계를 만드는 대신, 다형성 접근 방식을 사용할 수 있습니다.
일반적인 시나리오 및 예시
- 댓글/리뷰: “댓글”은 기사, 제품, 비디오 등 다양한 유형의 콘텐츠와 관련될 수 있습니다. Comments 테이블에 별도의 article_id, product_id, video_id 컬럼을 두는 대신, 다형성 관계를 사용할 수 있습니다.
+---------------------+
| **Comments** |
+---------------------+
| PK comment_id |
| commentable_type | ------> [Polymorphic Relationship]
| commentable_id | -------->
| user_id |
| comment_text |
| ... |
+---------------------+
^
|
+---------------------+ +---------------------+ +---------------------+
| **Articles** | | **Products** | | **Videos** |
+---------------------+ +---------------------+ +---------------------+
| PK article_id | | PK product_id | | PK video_id |
| ... | | ... | | ... |
+---------------------+ +---------------------+ +---------------------+- 알림: 알림은 사용자, 주문, 시스템 이벤트 등과 관련될 수 있습니다.
+----------------------+
| **Notifications** |
+----------------------+
| PK notification_id |
| notifiable_type | ------> [Polymorphic Relationship]
| notifiable_id | -------->
| user_id |
| message |
| ... |
+----------------------+
^
|
+---------------------+ +---------------------+ +-----------------------+
| **Users** | | **Orders** | | **System Events** |
+---------------------+ +---------------------+ +-----------------------+
| PK user_id | | PK order_id | | PK event_id |
| ... | | ... | | ... |
+---------------------+ +---------------------+ +-----------------------+다형성 관계는 더 복잡하며 종종 애플리케이션 수준에서 처리되거나 고급 데이터베이스 기능을 사용합니다(특정 데이터베이스 시스템에 따라). 표준 SQL은 일반 외래 키와 동일한 방식으로 다형성 외래 키 제약조건을 강제하는 직접적인 내장 지원이 없습니다.