DB - 반정규화
데이터베이스 스터디 4주차에서 학습하고 정리한 내용입니다.
1. 반정규화
정규화를 도입하면 왜 성능 상의 문제가 생길 수 있을까? 정규화는 데이터 중복을 피하기 위해 하나의 테이블이 여러 테이블로 분리가 될 가능성이 높다. 이 경우 모든 데이터를 조회하기 위해서는 조인 과정을 거쳐야 한다. 이러한 조인 과정은 추가적인 비용이 발생한다.
따라서 의도적으로 정규화 원칙을 위배하여 데이터 무결성 측면에서 손실은 감수하는 대신 성능 향상의 이점을 가져올 수 있다. 이를 반정규화라고 한다. 반정규화의 장점이 정규화의 단점이 되고 반정규화의 단점이 정규화의 장점이 되므로 이 둘을 비교하면 다음과 같다.
정규화 | 반정규화 | |
---|---|---|
데이터 무결성 | 데이터 무결성을 준수하여 일관되고 정확한 데이터를 관리할 수 있다. | 데이터 무결성을 만족하지 못하여 데이터 불일치, 데이터 중복 등이 발생할 수 있다. |
조회 측면 | 테이블이 많을 가능성이 높아 조인 연산으로 조회 성능이 감소할 수 있다. | 정규화에 비해 테이블의 수가 적어 조인 연산이 적어 조회 성능이 좋다. 또한 반정규화는 자주 조회하는 데이터를 중복으로 저장하여 조회 성능을 더욱 향상 시킬 수 있다. |
쓰기 측면 | 중복된 데이터가 없어 삽입과 수정 시 고려 해야 할 사항들이 적다. | 중복된 데이터가 있어 데이터 일관성을 유지하기 위해 서는 삽입과 수정 시 중복된 데이터 전부를 변경해야 할 수도 있다. |
반정규화는 일반적으로 논리적 데이터 모델링에서 정규화를 도입한 이후인 물리적 데이터 모델링 단계에서 수행된다. 또한 반정규화를 남용하거나 잘못된 반정규화를 적용할 경우 오히려 성능이 저하될 수 있으므로 반정규화는 정규화를 진행한 이후 필수적인 곳에서만 적용해야 한다.
2. 반정규화 도입 예시
대량의 데이터를 처리할 때 파티션, 인덱스 설계 등을 통해 해결할 수 없을 때가 있다. 이러한 성능 문제를 예방하기 위해 반정규화를 도입하여 조회 성능을 향상할 수 있다.
집계/요약 테이블 추가
대량의 데이터를 읽어 부서, 일별로 집계하는 상황이 많을 것으로 예상될 때 집계 테이블을 설계할 수 있다. 이 때 다양한 집계 처리를 포함할 수 있도록 공통된 조건을 분석하여 설계하는 것이 좋다.
컬럼 추가
반정규화 과정에서 컬럼 추가를 통해 DB 성능을 향상할 수 있으나 데이터 일관성을 유지하는데 추가적인 비용이 발생하므로 남용하여선 안된다. 데이터 일관성을 유지하기 위한 업무 규칙을 도출하고 주기적으로 데이터 일관성이 깨지지 않는지 점검해야 한다.
부모 테이블에 인조키(Surrogate Key)가 기본 키인 경우
- student 테이블
id | student_id | student_name |
---|---|---|
1 | 202103242 | Kim |
2 | 201903215 | Park |
3 | 202305423 | Lee |
4 | 202304251 | Jung |
이 테이블의 경우 인조키가 기본키이고 학번이 비즈니스에서 식별 가능한 자연키이다. 많은 경우 학생의 성적, 수강중인 과목들을 조회할 때 비즈니스에서 식별 가능한 자연키를 함께 조회하게 된다.
- student 테이블의 자연키인 student_id를 추가한 score 테이블
student_fk | student_id | subject_id | score |
---|---|---|---|
1 | 202103242 | 1 | A0 |
1 | 202103242 | 3 | B+ |
2 | 201903215 | 2 | A+ |
3 | 202305425 | 2 | B0 |
따라서 이 경우 부모 테이블이 가지고 있는 자연키를 자식 테이블에 추가로 저장하면 매번마다 이 학생 테이블을 조인할 필요 없이 성적, 수강중인 과목들을 조회할 수 있다. 위와 같이 성적 테이블을 저장한다면 성적을 조회하는 쿼리에서 학생의 학번만 필요할 때 학생 테이블을 조인하지 않고 반환할 수 있다. 학생의 이름 등 추가적인 정보가 필요할 때만 학생과 성적 테이블을 조인하면 된다.
하지만 이 방식은 데이터가 중복이 되어 데이터의 일관성이 깨질 수 있다. 학번이 바뀌는 경우 학생 테이블과 성적 테이블 모두 학번을 변경해야 할 것이다. 따라서 부모 테이블의 자연키가 자주 갱신된다면 이러한 반정규화를 도입할 때 성능상의 이점을 크게 누리지 못할 수 있다.
자주 사용되는 조건이 서로 다른 테이블에 분산된 경우
- order 테이블
order_id | order_date | customer_id |
---|---|---|
1 | 2024-09-01 | 1 |
2 | 2024-09-04 | 1 |
3 | 2024-08-25 | 2 |
4 | 2024-08-12 | 3 |
- order_product 테이블
order_id | product_id | order_quantity | order_price |
---|---|---|---|
1 | 15 | 1 | 20000 |
1 | 24 | 3 | 9000 |
2 | 17 | 5 | 50000 |
… | … | … | … |
특정 기간에 발생한 주문 건 중 특정 상품이 얼마나 판매되었는지에 대한 조회가 자주 있으면 어떻게 될까? 이 경우 order_date와 product_id가 두 테이블에 분산되어 있으므로 조인이 필수적으로 수행되어야 한다. 특정 기간에 발생한 주문이 10만 건이고 특정 기간에 판매된 특정 상품 데이터의 수가 100개라 하자.
조인을 활용한 방식은 먼저 특정 기간에 발생한 주문을 필터링한 이후 이 데이터 내에서 특정 상품의 데이터를 필터링하므로 10만 건의 데이터를 전부 탐색해야 한다.
- order_date를 추가하여 반정규화를 적용한 order_product 테이블
order_id | order_date | product_id | order_quantity | order_price |
---|---|---|---|---|
1 | 2024-09-01 | 15 | 1 | 20000 |
1 | 2024-09-01 | 24 | 3 | 9000 |
2 | 2024-09-04 | 17 | 5 | 50000 |
… | … | … | … |
위와 같이 반정규화를 적용하면 (order_date, product_id) 인덱스를 추가할 수 있다. 이렇게 인덱스도 함께 추가하면 주문 내역 테이블에서 100건의 데이터를 조회하고 주문 테이블에서 필요한 데이터만 추가적으로 가져오면 되므로 처리량을 크게 줄일 수 있다.
부모 테이블에 컬럼을 추가하기
- 집계 컬럼을 추가한 customer 테이블
customer_id | … | average_review_score | review_count |
---|---|---|---|
1 | 4.3 | 82 | |
2 | 2.8 | 21 | |
3 | 3.9 | 43 |
고객 테이블이 있고 리뷰 테이블이 있다고 가정하자. 고객이 작성한 평균 리뷰와 리뷰 수를 미리 저장한다면 매번 리뷰 테이블에서 값을 계산할 필요가 없으므로 성능이 향상된다. 계산 로직이 복잡하거나 여러 테이블을 읽어야 결과를 추출할 수 있는 경우도 컬럼을 추가하여 결과 값을 저장할 수 있다.
테이블 분할(Table Partitioning)
테이블 파티셔닝은 데이터 무결성에 영향을 미치지 않지만 정규화된 구조를 의도적으로 분할하여 변경하는 과정으로 반정규화의 한 기법으로 볼 수 있다. 테이블의 데이터를 처리할 때는 속성의 수(가로)와 튜플의 수(세로)를 곱한 값이 처리 범위가 된다. 면적을 줄이기 위해서는 가로를 줄이거나 세로를 줄이는 방법이 있다.
테이블 수직분할
테이블 수직분할은 가로를 줄이는 방법으로 테이블의 컬럼을 두 개 이상의 테이블로 나누어 관리하는 방법이다.
- post_content를 포함하여 레코드 크기가 아주 큰 post 테이블
post_id | post_title | writer_id | post_content |
---|---|---|---|
1 | “반정규화” | 1 | “정규화를 도입하면 왜 성능 상의 문제가 생길 수 있을까? …” |
2 | “정규화” | 1 | “정규화는 데이터의 중복을 제거하거나 최소화하고 데이터 종속이 논리적으로 표현되도록 데이터를 재구성하는 과정이다 . …” |
보통 게시글의 내용을 담은 컬럼은 아주 많은 텍스트를 포함하여 게시글 데이터의 대부분을 차지하게 된다. 데이터베이스는 페이지 단위로 데이터를 저장하게 되는데 게시글을 같이 저장하면 게시글 레코드의 크기가 아주 커져서 페이지 당 저장되는 레코드의 수가 적어지게 된다. 따라서 게시글의 목록과 같이 게시글의 내용이 필요없는 경우에도 많은 페이지를 불러와야 하기 때문에 많은 I/O 작업으로 인한 성능 저하가 발생할 수 있다.
이 경우 post_content를 수직분할하여 새로운 테이블로 구성할 수 있다.
- 레코드 크기가 작아진 post 테이블
post_id | post_title | writer_id |
---|---|---|
1 | “반정규화” | 1 |
2 | “정규화” | 1 |
- post_content 테이블
post_id | post_content |
---|---|
1 | “정규화를 도입하면 왜 성능 상의 문제가 생길 수 있을까? …” |
2 | “정규화는 데이터의 중복을 제거하거나 최소화하고 데이터 종속이 논리적으로 표현되도록 데이터를 재구성하는 과정이다 . …” |
이렇게 하면 게시글 테이블의 레코드 크기는 작아지므로 페이지 당 저장할 수 있는 레코드 수는 많아진다. 따라서 수직 분할하기 전과 비교하여 적은 수의 페이지만 불러오므로 성능이 향상된다.
게시글 내용을 포함한 전체 정보를 제공할 때에는 조인을 이용할 수 있다.
테이블 수평분할
테이블 수평분할은 DBMS가 제공하는 테이블 파티션을 활용할 수 있다. 수평분할을 하지 않고 하나의 큰 테이블에 저장하면 인덱스도 커지고 그만큼 물리적인 메모리 공간도 많이 필요해진다. 만약 데이터의 수가 아주 많아 인덱스가 차지하는 메모리가 실질적인 물리 메모리보다 커지게 된다면 쿼리 처리가 상당히 느려지게 된다.
또한 분산 데이터베이스 시스템의 경우 테이블을 수평분할하여 분산된 서버에 테이블을 나누어 저장하게 된다면 서버에 가해지는 부하를 줄일 수 있다. 이렇게 분산 데이터베이스 시스템에서 테이블을 여러 서버에 분산하여 저장하는 방법을 샤딩이라고 한다.
Reference
백은빈, 이성욱 편저, Real MySQL 8.0, 위키북스
유동오 편저, 핵심 데이터 모델링, DBian