정규화된 스키마와 비정규화된 스키마 비교
NoSQL 솔루션에서 널리 쓰이게 된 대표적인 기법 중 하나는
JOIN 지원이 없을 때 데이터를 비정규화하는 것으로, 모든 통계나 관련 행을 상위 행의 컬럼과 중첩 객체로 저장하는 방식입니다. 예를 들어 블로그용 예시 스키마에서는 각 게시물의 모든 Comments를 객체 배열로 저장할 수 있습니다.
비정규화를 사용해야 하는 경우
- 변경이 드문 테이블이거나, 데이터가 분석용 쿼리에서 사용 가능해지기까지 어느 정도 지연을 허용할 수 있는 테이블은 비정규화하십시오. 즉, 데이터를 Batch로 완전히 다시 로드할 수 있는 경우입니다.
- 다대다 관계는 비정규화하지 마십시오. 이렇게 하면 단일 원본 행이 변경되었을 때 많은 행을 업데이트해야 할 수 있습니다.
- 카디널리티가 높은 관계는 비정규화하지 마십시오. 한 테이블의 각 행이 다른 테이블의 수천 개 관련 항목과 연결되어 있다면, 이를
배열로 표현해야 합니다. 즉, 원시 유형 또는 튜플의배열가 됩니다. 일반적으로 1000개가 넘는 튜플을 담은 배열은 권장되지 않습니다. - 모든 컬럼을 중첩 객체로 비정규화하기보다는, materialized view를 사용하여 통계 정보만 비정규화하는 방안을 고려하십시오(아래 참조).
자주 업데이트되는 데이터에는 비정규화를 피하십시오
- 테이블의 행이 변경될 때 올바른 조인 SQL 문을 트리거해야 합니다. 이상적으로는 조인에 포함된 모든 객체를 업데이트해서는 안 되며, 영향을 받은 객체만 업데이트해야 합니다. 올바른 행만 효율적으로 filter하도록 조인을 수정하고, 이를 고처리량 환경에서 구현하려면 외부 도구나 추가 엔지니어링이 필요합니다.
- ClickHouse에서의 행 업데이트는 신중하게 관리해야 하며, 이로 인해 복잡성이 추가됩니다.
따라서 일반적으로는 비정규화된 모든 객체를 주기적으로 다시 로드하는 배치 업데이트 프로세스를 사용합니다.
비정규화의 실용적인 사례
AnswerCount 및 CommentCount 같은 통계가 포함되도록 비정규화된 Posts 테이블을 가정해 보겠습니다. 원본 데이터는 이러한 형태로 제공됩니다. 하지만 실제로는 이 정보가 자주 변경될 가능성이 높기 때문에, 오히려 이를 정규화하는 편이 나을 수 있습니다. 또한 이러한 컬럼 중 상당수는 다른 테이블을 통해서도 확인할 수 있습니다. 예를 들어 게시물의 댓글은 PostId 컬럼과 Comments 테이블을 통해 확인할 수 있습니다. 이 예시에서는 게시물이 Batch 프로세스로 다시 로드된다고 가정합니다.
또한 Posts를 분석용 주 테이블로 간주하므로, 다른 테이블을 Posts에 비정규화하는 경우만 고려합니다. 일부 쿼리에서는 반대 방향으로 비정규화하는 것도 적절할 수 있으며, 이 경우에도 앞서와 같은 고려 사항이 적용됩니다.
다음 각 예시에서는 두 테이블을 조인해 함께 사용해야 하는 쿼리가 존재한다고 가정합니다.
Posts and Votes
Score 컬럼은 이러한 통계를 나타내는 값으로, 총 찬성표 수에서 반대표 수를 뺀 값입니다. 이상적으로는 간단한 lookup으로 쿼리 시점에 이러한 통계를 조회할 수 있으면 됩니다( 딕셔너리 참조).
Users와 Badges
Users와 Badges를 살펴보겠습니다.
먼저 다음 명령으로 데이터를 삽입합니다.
예를 들어 배지 수와 같은 Badges 통계를 Users에 비정규화하고자 할 수 있습니다. 이 데이터셋에서 삽입 시점에 딕셔너리를 사용할 때 이러한 예시를 살펴봅니다.
Posts 및 PostLinks
PostLinks는 사용자가 서로 관련 있거나 중복된 것으로 간주하는 Posts를 연결합니다. 다음 쿼리는 스키마와 로드 명령을 보여줍니다:
간단한 통계 예시
INSERT INTO SELECT 문을 사용합니다.
일대다 관계에 복합 타입 활용하기
- 이름이 지정된 Tuple - 관련 구조를 컬럼 집합으로 표현할 수 있습니다.
- Array(Tuple) 또는 Nested - 이름이 지정된 튜플의 배열이며, Nested라고도 합니다. 각 항목은 하나의 객체를 나타냅니다. 일대다 관계에 적용할 수 있습니다.
PostLinks를 Posts에 비정규화하는 방법을 보여줍니다.
각 게시물에는 앞서 PostLinks 스키마에서 본 것처럼 다른 게시물에 대한 여러 링크가 포함될 수 있습니다. Nested 타입을 사용하면 이러한 연결된 게시물과 중복 게시물을 다음과 같이 표현할 수 있습니다.
flatten_nested=0 설정을 사용한다는 점에 유의하십시오. 중첩 데이터 평탄화는 비활성화하는 것이 좋습니다.
OUTER JOIN 쿼리를 사용한 INSERT INTO SELECT로 이 비정규화를 수행할 수 있습니다:
여기서 걸린 시간에 주목하십시오. 약 2분 만에 6,600만 개의 행을 비정규화했습니다. 뒤에서 살펴보겠지만, 이 작업은 일정에 맞춰 실행하도록 스케줄링할 수 있습니다.조인 전에
groupArray 함수를 사용해 PostLinks를 각 PostId별 배열로 묶은 점에 주목하십시오. 그런 다음 이 배열을 LinkedPosts와 DuplicatePosts라는 두 개의 하위 목록으로 필터링하며, 이 과정에서 외부 조인으로 생긴 빈 결과도 제외합니다.
몇 개의 행을 조회해 새로 비정규화된 구조를 확인할 수 있습니다:
비정규화 오케스트레이션 및 스케줄링
배치
INSERT INTO SELECT를 통해 로드된 후 ClickHouse를 사용해 이 변환을 수행하는 방법을 살펴보았습니다. 이는 주기적인 배치 변환에 적합합니다.
주기적인 배치 로드 프로세스를 수용할 수 있다면, ClickHouse에서 이를 오케스트레이션하는 방법으로 몇 가지 옵션이 있습니다:
- 갱신 가능 구체화 뷰 - 갱신 가능 구체화 뷰를 사용하면 쿼리를 주기적으로 실행하도록 예약하고, 그 결과를 대상 테이블로 보낼 수 있습니다. 쿼리가 실행되면 뷰는 대상 테이블이 원자적으로 갱신되도록 보장합니다. 즉, 이 작업을 예약하는 ClickHouse 네이티브 방식을 제공합니다.
- 외부 도구 - dbt 및 Airflow와 같은 도구를 사용해 변환을 주기적으로 예약 실행할 수 있습니다. dbt용 ClickHouse 통합은 새 버전의 대상 테이블을 생성한 뒤, 현재 쿼리를 처리하는 버전과 이를 원자적으로 교체하여 이 작업이 원자적으로 수행되도록 보장합니다(EXCHANGE 명령 사용).