Database Theory
목차
데이터베이스가 해결하는 문제
데이터베이스는 데이터를 안전하고 일관성 있게 저장하고 조회하기 위한 시스템이다. 단순히 파일에 JSON이나 CSV를 저장하는 것과 달리, 데이터베이스는 여러 사용자가 동시에 접근하는 상황에서도 데이터가 깨지지 않도록 도와준다.
1
2
3
4
애플리케이션
-> SQL
-> 데이터베이스 엔진
-> 메모리/디스크
파일 저장과 데이터베이스의 가장 큰 차이는 데이터베이스가 구조, 제약 조건, 동시성, 트랜잭션, 인덱스를 함께 제공한다는 점이다.
1
2
파일 저장 직접 형식과 동시성 처리를 책임져야 함
데이터베이스 저장, 조회, 제약, 트랜잭션, 복구를 엔진이 담당
예를 들어 두 사용자가 동시에 같은 계좌 잔액을 수정한다면 단순 파일 저장은 충돌을 직접 막아야 한다. 데이터베이스는 트랜잭션과 lock 또는 MVCC 같은 방식으로 이런 문제를 다룬다.
테이블, 행, 열
관계형 데이터베이스에서 테이블은 같은 형태의 데이터를 모아 둔 구조다. 행(row)은 하나의 기록이고, 열(column)은 그 기록의 속성이다.
1
2
3
4
5
6
7
users table
┌────┬───────┬─────────────────┐
│ id │ name │ email │
├────┼───────┼─────────────────┤
│ 1 │ shin │ shin@example │
│ 2 │ kim │ kim@example │
└────┴───────┴─────────────────┘
SQL로는 다음처럼 표현할 수 있다.
1
2
3
4
5
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
);
테이블 설계에서 중요한 것은 “어떤 데이터를 하나의 행으로 볼 것인가”다. 사용자, 주문, 상품처럼 개별적으로 식별되고 수명이 있는 개념은 보통 별도 테이블이 된다.
키와 제약 조건
primary key는 행을 식별하는 대표 키다. 같은 테이블 안에서 각 행은 primary key로 구분되어야 한다.
1
2
3
4
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
primary key가 필요한 이유는 특정 행을 안정적으로 식별하기 위해서다. 이름이나 이메일은 바뀔 수 있지만, 내부 식별자인 id는 보통 바꾸지 않는다.
foreign key는 다른 테이블의 행을 참조하는 키다. 데이터 정합성을 지키는 데 도움을 준다.
1
2
3
4
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id)
);
이 제약이 있으면 존재하지 않는 사용자에게 주문을 연결하는 실수를 막을 수 있다.
제약 조건은 애플리케이션 버그가 있더라도 데이터베이스 최종 방어선 역할을 한다.
1
2
3
4
5
NOT NULL 값이 반드시 있어야 함
UNIQUE 중복 금지
PRIMARY KEY 행 식별
FOREIGN KEY 관계 정합성 보장
CHECK 값의 조건 제한
인덱스
인덱스는 조회를 빠르게 하기 위한 자료구조다. 책의 목차나 색인처럼, 모든 행을 처음부터 끝까지 읽지 않고 필요한 위치를 빨리 찾게 해준다.
1
CREATE INDEX idx_orders_user_id ON orders(user_id);
다음 쿼리가 자주 실행된다면 user_id 인덱스가 도움이 될 수 있다.
1
2
3
SELECT *
FROM orders
WHERE user_id = 1;
하지만 인덱스는 공짜가 아니다. 데이터를 추가, 수정, 삭제할 때 인덱스도 함께 갱신해야 하므로 쓰기 비용이 증가한다.
1
2
인덱스 장점 조회 속도 향상
인덱스 비용 쓰기 비용 증가, 저장 공간 사용
그래서 모든 컬럼에 인덱스를 만드는 것은 좋은 전략이 아니다. 조회 패턴과 데이터 분포를 보고 필요한 곳에 만들어야 한다.
트랜잭션과 ACID
트랜잭션은 여러 작업을 하나의 논리적 단위로 묶는 것이다. 모두 성공하거나 모두 실패해야 하는 작업에 필요하다.
예를 들어 계좌 이체는 출금과 입금이 함께 성공해야 한다.
1
2
3
4
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
COMMIT;
중간에 실패하면 rollback해야 한다.
1
ROLLBACK;
ACID는 트랜잭션이 보장해야 하는 성질이다.
1
2
3
4
Atomicity 전부 성공하거나 전부 실패
Consistency 제약 조건을 깨지 않는 일관된 상태 유지
Isolation 동시에 실행되는 트랜잭션끼리 간섭 제어
Durability commit된 데이터는 장애 후에도 보존
트랜잭션이 필요한 이유는 현실의 비즈니스 작업이 여러 데이터 변경으로 이루어지는 경우가 많기 때문이다. 주문 생성, 결제, 재고 차감 같은 작업은 중간 상태가 외부에 노출되면 안 된다.
정규화와 모델링
정규화는 데이터 중복을 줄이고 정합성을 높이기 위해 데이터를 적절한 테이블로 나누는 설계 방법이다.
예를 들어 주문 테이블에 사용자 이름과 이메일을 매번 복사하면 사용자의 이메일이 바뀔 때 모든 주문 행을 수정해야 한다.
1
2
나쁜 중복 예
orders(id, user_name, user_email, product_name, price)
사용자와 주문을 나누면 중복을 줄일 수 있다.
1
2
users(id, name, email)
orders(id, user_id, total_price)
정규화가 도움이 되는 경우는 데이터 정합성이 중요하고 중복 변경이 위험할 때다. 반대로 조회 성능이나 단순한 읽기 모델이 중요할 때는 일부러 중복을 두는 denormalization이 필요할 수 있다.
1
2
정규화 중복 감소, 정합성 향상
비정규화 조회 단순화, 성능 개선 가능, 중복 관리 필요
좋은 모델링은 정규화 규칙만 따르는 것이 아니라, 실제 조회 패턴과 변경 패턴을 함께 고려한다.
동시성 제어
동시성 제어는 여러 트랜잭션이 동시에 실행될 때 데이터가 이상한 상태가 되지 않도록 관리하는 기술이다.
대표적인 문제는 다음과 같다.
1
2
3
4
lost update 동시에 수정하면서 한쪽 변경이 사라짐
dirty read commit되지 않은 데이터를 읽음
non-repeatable read 같은 행을 다시 읽었는데 값이 바뀜
phantom read 같은 조건 조회에서 행 개수가 달라짐
데이터베이스는 lock, isolation level, MVCC 같은 방식으로 동시성을 제어한다.
1
2
lock 다른 트랜잭션의 접근을 막거나 제한
MVCC 여러 버전의 데이터를 유지해 읽기와 쓰기 충돌을 줄임
동시성 제어는 성능과 정합성 사이의 균형이다. isolation을 강하게 하면 안전하지만 느려질 수 있고, 약하게 하면 빠르지만 애플리케이션이 더 조심해야 한다.
쿼리 실행 계획
쿼리 실행 계획은 데이터베이스가 SQL을 실제로 어떻게 실행할지 정한 계획이다. SQL은 선언형 언어라 사용자는 “무엇을 원한다”를 말하고, 데이터베이스 엔진이 “어떻게 찾을지”를 결정한다.
1
2
3
4
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1;
실행 계획에서는 인덱스를 쓰는지, 전체 테이블을 스캔하는지, JOIN 순서는 어떤지 등을 볼 수 있다.
1
2
3
4
Seq Scan 테이블 전체를 읽음
Index Scan 인덱스를 사용해 찾음
Nested Loop 한쪽 결과를 기준으로 반복 JOIN
Hash Join 해시 테이블을 만들어 JOIN
쿼리가 느릴 때는 단순히 인덱스를 추가하기보다 실행 계획을 먼저 봐야 한다. 데이터 양, 조건 선택도, 통계 정보, JOIN 방식에 따라 병목이 달라진다.
1
2
3
4
5
느린 쿼리 분석 순서
-> 실행 계획 확인
-> 실제 rows와 예상 rows 비교
-> 인덱스 사용 여부 확인
-> JOIN과 정렬 비용 확인