본문 바로가기

갈아먹는 BigQuery [2] 빅쿼리 스키마 및 데이터 모델

지난 글

갈아먹는 BigQuery [1] 빅쿼리 소개

들어가며

지난 시간에 빅 쿼리에 대한 간략한 소개와 빅 쿼리의 전신인 Dremel에 대해서 알아보았습니다. 그리고 Dremel의 가장 큰 특징은 Columnar Storage와 Tree Architecture를 살펴봤습니다. 이번에는 빅 쿼리의 재미있는 특징 중에 하나인 Array와 Struct를 소개하면서 빅 쿼리가 지향하는 스키마 설계 방식을 다뤄보겠습니다. 또한 이를 Columnar Storage 상에서 구현하기 위한 Dremel의 데이터 모델을 자세히 알아보겠습니다. 

Big Query 스키마

BigQuery는 RDBMS 처럼 일정한 스키마를 가진 테이블을 생성하고, 정형화 된 데이터(structured data)를 저장합니다. 그렇기 때문에 raw data를 곧바로 저장하는 데이터 레이크로는 그다지 적합하지 않고, 전처리를 거쳐서 정제된 데이터를 적재하는 데이터 웨어하우스로 적합합니다. 그런데 여기서 고민 거리가 하나 생기는데, 빅 쿼리에 데이터를 적재할 테이블의 스키마는 그러면 어떻게 짜는게 바람직 할 것인가? 입니다.

먼저 왼쪽 표 처럼 원본 데이터가 있는 상황을 가정해보겠습니다.[1] 이를 전통적인 RDBMS에서는 테이블에 중복된 정보가 저장되지 않도록 정규화를 적용하여 테이블을 쪼개어 데이터를 저장합니다. 그리고 특정 거래의 상세 정보를 확인하고 싶을 때에는 조인 연산을 통해서 원하는 정보를 가져오도록 합니다. 이 경우 데이터의 중복은 줄어들지만 조인 연산에 많은 비용이 발생하는 단점이 있습니다. 특히 빅 쿼리의 분석 대상인 페타 바이트 단위의 데이터를 테이블 간 조인하는 것은 비효율 적입니다. 그렇기 때문에 빅 쿼리에 적재하기 이전에 쪼개어 놓은 테이블을 다시 합쳐주는 denormalization 과정을 거칩니다.

denormalization을 거친 테이블에서는 다시 중복된 데이터가 저장된다는 문제점이 생깁니다. 또한 중복되는 키 값에 대해서 group by 연산을 수행할 경우 성능이 저하되는 문제점이 있습니다. 이를 해결하기 위해서 빅 쿼리는 Nested와 Repeated 라는 개념을 도입합니다.

Nested와 Repeated Column은 중복되는 값을 한번만 저장해도 되며, 미리 group by 연산을 수행한 것과 같은 효과를 내줍니다. 그 결과 데이터 중복은 줄어들고 성능이 향상됩니다. 그렇다면 빅 쿼리는 어떠한 방식으로 이러한 중첩되고 반복이 발생하는 스키마의 데이터를 컬럼 기반으로 저장할 수 있는 것일까요?

Dremal Data model

빅 쿼리의 전신인 Dremel 논문에 Nested and Repeated 스키마를 Columnar Storage에 저장하기 위한 데이터 모델이 자세히 소개되어 있습니다.[2]. (다소 난이도가 있어서 이해하는데 꽤나 고생했습니다.) 그럼 논문에 제시되어 있는 예시를 차례로 따라가면서 개념을 익혀보도록 하겠습니다.

위 그림은 Dremel에서 데이터를 저장하는 스키마와 그 예시를 보여줍니다. (검색 회사 답게 예시도 웹 문서의 메타데이터 같습니다.) 살펴보면 required, optional, repeated 등의 생소한 단어들이 보입니다. 이를 우리에게 친숙한 json 방식으로 설명해보자면 group이란 여러 필드들이 묶여있는 객체이고, repeated란 배열을 의미합니다.  optional group Links라는 것은 Document에서 있어도 되고 없어도 괜찮은 객체 멤버 변수 Links라는 의미입니다. 메타데이터 r1과 r2를 json으로 바꾸어 표현하면 아래와 같습니다.

얼핏 보기에도 RDBMS 테이블 형식으로 저장하기에는 다소 까다로워 보이는 형태의 데이터로 mongodb와 같은 document oriented nosql 솔루션이 적합해 보입니다. 그러나 Dremel은 이를 컬럼을 기준으로 저장하는 방식을 제안하며, 이러한 복잡성을 핸들링하기 위해서 제안하는 것이 repitition level과 definition level입니다.

Repetition Level

위 도표는 컬럼 기반 저장 방식으로 앞서 소개한 데이터 r1과 r2를 저장한 것입니다. 표 상에서 r은 repetition level, d는 definition level을 의미합니다. json 데이터를 컬럼별로 나누어 저장이 정말 가능하다면, 우리는 이 컬럼별로 저장된 데이터만 보고 원래의 json 데이터를 복원할 수도 있어야 합니다. 그리고 이를 위해서 추가적으로 필요한 정보가 바로 r과 d입니다. 그렇다면 먼저 repetition level이 필요한 배경과 개념에 대해서 알아보겠습니다.

 

r과 d가 없다고 가정하고 Name.Language.Code를 보겠습니다.(이를 앞으로 value의 path라고 부르겠습니다.) en-us, en, NULL, en-gb, NULL 데이터가 있습니다만 우리는 이들이 어느 위치에서 등장하는지 알 수 없습니다. 가령 아래 그림 처럼 분명 en이라는 값이 저장되는 위치는 다르지만 컬럼별로 떼어내어 저장할 경우 결과가 동일해집니다. 

이러한 상황을 방지하고자 도입한 개념이 repetition level입니다. 한 마디로 표현하면 자신 이전에 등장한 같은 path의 value와 함께 묶여져 있는 level입니다. 말만으로는 이해하기 어려워서 사례를 통해서 이해해보도록 하겠습니다. 문서 r1을 위에서 아래로 스캔하며 Name.Language.Code 변수들의 repetition level을 정해보겠습니다.

 

가장 먼저 첫 번째 Name 아래 en-us가 보입니다. 이는 이전에 등장한 같은 path의 value가 없었기 때문에 r은 0이 됩니다. 그 다음으로 넘어가면 en이 보입니다. 이는 같은 path를 지닌 en-us와 같은 Name.Language에 묶여있습니다. 그러므로 r=2가 됩니다.

 

아래로 넘어가보면 Name의 두 번째 원소는 아예 Language를 포함하고 있지 않습니다. 그러므로 Name.Language.Code는 Null 값이며, 이때 같은 path를 지닌 이전 값과는 같은 Name 아래에 묶여있습니다. 그러므로 r=1이 됩니다. 

그 아래로 넘어가보면 마지막 Language가 보이고 en-gb라는 Code가 보입니다. 이는 이전에 등장한 동일한 path의 값 en과는 같은 Name에 속하므로 r=1이 됩니다. r2에도 동일한 방식을 적용하여 정리를 해보면 아래와 같은 결과를 얻을 수 있습니다.

 

Definition Level

repetition level을 통해서 우리는 변수들이 등장하는 위치에 대해서 알 수 있게 되었지만, 아직 컬럼 기반 저장 데이터만 보고 json을 복구해 내기에는 부족합니다. 바로 NULL 값의 문제입니다. 이번에는 Name.Language.Country의 예시를 통해서 알아보겠습니다. Name.Language.Country의 컬럼 기반 저장 값은 아래와 같습니다.

 

value가 있는 값들은 그냥 해당 변수의 path의 level이 d가 되며, Name.Language.Country의 경우에는 3이 됩니다. 문제는 NULL 값입니다. r1을 살펴보면 Name 아래의 첫 번째 원소의 Language의 두 번째 원소의 경우 Country 값이 없습니다. 이 경우 Name.Language 까지는 정의되어 있으니, definition level은 2가 됩니다. 반면에 r1의 Name의 두 번째 원소의 경우 아예 Language가 없어서 자연스럽게 Country가 NULL 값을 가집니다. 이 경우 definition level이 1이 됩니다.

이것으로 repetition level과 definition level을 이해해보았습니다. 이 두 가지 값을 추가하는 것만으로 우리는 중첩과 반복이 등장하는 데이터를 컬럼 별로 쪼개어 효과적으로 저장할 수 있었습니다. 이제 컬럼 기반 저장 테이블만 보아도 원본 json 데이터를 복원할 수 있겠죠?

Encoding

이제 각각의 컬럼은 디스크 공간에 블록 단위로 저장됩니다. 각각의 블록은 repetition level과 definition level을 포함하며, value들은 모두 압축되어 저장합니다. NULL 값은 별도로 명시적으로 저장하지 않습니다. (definition level이 원래 path의 level과 같지 않으면 NULL 값인걸 알 수 있으므로) 마찬가지로 NULL 값이 아닌 value들은 definition level이 필요하지 않으므로 이를 저장하지 않습니다. (당연히 path의 level과 definition level은 같을 것이므로). 또한 repetition level과 definition level은 사용가능한 최소한의 비트만 사용하여 저장합니다. 가령 repetition level의 최대 값이 3일 경우 2bit만 사용하여 인코딩합니다.

마치며

지금까지 빅 쿼리가 왜 Nested와 Repeated 속성의 데이터를 저장해야할 필요가 있는지, 그리고 데이터 모델 설계를 통해서 이를 어떻게 극복했는지에 대해서 알아보았습니다. 이러한 기초 데이터 모델 위에 분산 처리 아키텍쳐들이 접목되어 빅 쿼리라는 커다란 시스템을 만들어 낸 것이겠죠? 

 

지난 포스팅에서 빅 쿼리의 큰 특징으로 Columnar Storage와 Tree Execution이 있다고 소개해드렸습니다. 이번 포스팅에서 Columnar Storage를 다루었으니 다음 포스팅에서 Tree Execution을 다뤄봐야겠죠? SQL 쿼리가 실제로 어떻게 수 많은 머신들로 분산되어 실행되는 원리에 대해서 알아보도록 하겠습니다.

 

[1] BigQuery as a data warehousing solution, coursera, google cloud

[2] Dremel: Interactive Analysis of Web-Scale Datasets, google