Apache Parquet
고친 사람 github-actions[bot]
Apache Parquet 는 표 데이터를 파일로 적어 두고 필요한 열만 골라 읽게 해 주는 파일 포맷입니다. 같은 열의 값을 한데 모아 적습니다. 그래서 금액 열 하나만 더하는 분석 질의는 다른 열을 읽지 않고 지나갑니다. 클라우드 저장소에 분석용 데이터를 쌓을 때 흔히 이 포맷을 씁니다.
쉽고 빠른 이해
Parquet 는 표 데이터를 열마다 모아 파일에 적습니다. 주문 기록 1억 건을 파일로 적어 두고 금액 열만 꺼내 합계를 내는 식입니다.
이게 없으면 분석할 때마다 쓰지도 않을 열까지 전부 읽습니다. 쉼표로 값을 나눠 한 줄에 한 행씩 적은 텍스트 파일이라면 금액만 더하려 해도 고객 이름까지 다 읽고 버립니다.
- 행을 알맞은 수만큼 묶습니다
- 묶음 안에서 같은 열의 값끼리 모아 적고 크기를 줄입니다
- 파일 끝에 열마다 놓인 위치와 가장 작은 값, 가장 큰 값을 적어 둡니다
읽는 쪽은 파일 끝부터 봅니다. 필요한 열이 놓인 구간만 찾아 읽습니다. 찾는 값이 들어 있을 수 없는 묶음은 건너뜁니다.
대가는 고쳐 쓰기입니다. 행 하나를 바꾸려 해도 파일을 새로 써야 합니다. 행 하나를 꺼내 보는 일도 열마다 흩어진 값을 모아야 해서 품이 듭니다.
상세
Apache Parquet 는 Apache Software Foundation 이 관리하는 오픈 소스 파일 포맷입니다. 파일 안에 바이트를 어떤 순서와 모양으로 적을지 정합니다. 그 규칙대로 읽고 쓰는 라이브러리도 여러 언어로 나와 있습니다.
이 절은 행으로 적는 파일과 열로 적는 파일을 견주는 데서 출발합니다. 그다음 Parquet 파일 안이 어떻게 나뉘는지, 읽는 쪽이 그 구조로 무엇을 건너뛰는지 봅니다. 끝으로 이 포맷이 못 하는 일과 이 포맷을 고르는 경우를 봅니다. 예는 처음부터 끝까지 주문 테이블 하나로 듭니다. 필요한 대목에서는 그 테이블에 열을 하나씩 더해 봅니다.
행으로 적는 파일
아래 주문 테이블을 예로 듭니다. 열은 주문 번호, 고객, 금액 셋입니다.
| id | customer | amount |
|---|---|---|
| 1 | kim | 3000 |
| 2 | lee | 5000 |
| 3 | kim | 1200 |
CSV(Comma-Separated Values)는 이런 표를 한 줄에 한 행씩 적습니다. 값 사이는 쉼표로 나눕니다. 이렇게 한 행의 값을 이어서 적는 방식을 행 지향 저장이라고 부릅니다.
분석 질의는 많은 행을 훑되 열은 몇 개만 쓰는 질의입니다. 지난달 매출 합계나 고객별 주문 수를 내는 질의가 그렇습니다. 이런 질의를 돌리는 일을 온라인 분석 처리라고 부릅니다.
행 지향 파일에서 금액 합계를 내면 곤란해집니다. 금액은 줄마다 끝에 붙어 있습니다. 금액만 꺼내려 해도 주문 번호와 고객 이름을 모두 읽고 버려야 합니다.
열이 셋이면 버리는 양이 크지 않습니다. 분석용 테이블은 열이 수십에서 수백 개인 경우가 흔합니다. 그중 두세 열만 쓰는 질의라면 읽은 바이트 대부분을 버리는 셈입니다.
열로 적는 파일
열 지향 저장은 같은 열의 값을 한데 모아 적습니다. 위 표를 두 방식으로 적으면 값이 놓이는 순서가 이렇게 갈립니다.
행으로 1,kim,3000 | 2,lee,5000 | 3,kim,1200
열로 1,2,3 | kim,lee,kim | 3000,5000,1200
열로 적은 쪽에서는 금액 값 셋이 붙어 있습니다. 합계를 내는 쪽은 그 구간만 읽으면 됩니다.
같은 열의 값은 타입이 같고 서로 닮았습니다. 고객 열에는 같은 이름이 자주 되풀이됩니다. 금액 열에는 숫자만 있습니다. 닮은 값이 붙어 있으면 압축이 잘 됩니다. Parquet 파일이 작아지는 힘도 대부분 여기서 나옵니다.
행 그룹과 열 청크
열로 적는다고 해서 한 열을 파일 처음부터 끝까지 한 덩어리로 적지는 않습니다. 그러면 쓰는 쪽이 마지막 행을 받을 때까지 모든 값을 메모리에 들고 있어야 합니다.
Parquet 는 먼저 행을 알맞은 수만큼 묶습니다. 이 묶음이 행 그룹입니다. 쓰는 쪽은 행 그룹 하나를 다 모으면 파일에 적고 메모리를 비웁니다.
행 그룹 안에서는 열마다 값을 모아 적습니다. 한 행 그룹 안에 있는 한 열의 값 모음이 열 청크입니다. 열이 셋이면 행 그룹마다 열 청크가 셋 생깁니다.
열 청크는 다시 페이지 여러 장으로 나뉩니다. 페이지는 값을 줄이고 풀 때 한 번에 다루는 단위입니다. 뒤의 「인코딩과 압축」에서 이 단위를 다시 봅니다.
파일 맨 앞과 맨 끝에는 PAR1 네 글자가 붙습니다. 읽는 쪽은 이 글자로 Parquet 파일인지 확인합니다. 맨 끝 PAR1 바로 앞에는 푸터와 그 길이가 옵니다. 푸터는 이 파일이 어떻게 생겼는지 적은 요약입니다. 다음 소절에서 자세히 봅니다.
아래 그림은 행 그룹이 둘이고 열이 둘인 파일을 앞에서 뒤로 그린 것입니다. 위에 있을수록 파일 앞쪽입니다. 칸 하나가 열 청크 하나이고, 같은 줄의 두 칸이 한 행 그룹입니다.
block-beta columns 2 m1["PAR1"]:2 a1["그룹 1 · id"] b1["그룹 1 · amount"] a2["그룹 2 · id"] b2["그룹 2 · amount"] f["푸터"]:2 l["푸터 길이 · 4바이트"]:2 m2["PAR1"]:2
끝에 붙는 푸터
푸터에는 이 파일의 스키마가 먼저 들어 있습니다. 스키마는 열마다 이름과 타입을 적은 목록입니다. 읽는 쪽은 스키마를 보고 어떤 열이 있는지 압니다.
푸터에는 열 청크마다 파일의 몇 번째 바이트에서 시작하는지도 적힙니다. 읽는 쪽은 이 위치를 보고 필요한 열 청크로 곧장 건너갑니다.
열 청크마다 통계도 붙습니다. 그 청크에 든 값의 최솟값과 최댓값, 값이 비어 있는 칸의 수 같은 것입니다. 이 통계가 다음 소절에서 볼 건너뛰기의 재료입니다.
읽는 쪽은 파일 끝에서 출발합니다. 맨 끝 4바이트가 PAR1 인지 보고, 그 앞 4바이트에서 푸터 길이를 읽습니다. 그 길이만큼 앞으로 돌아가 푸터를 읽습니다.
요약을 끝에 두는 까닭은 쓰는 쪽에 있습니다. 열 청크의 위치와 통계는 값을 다 쓰고 나서야 정해집니다. 그래서 쓰는 쪽은 값을 모두 적은 뒤 마지막에 푸터를 붙입니다.
그 대가로 파일은 푸터를 쓰고 닫기 전까지 읽을 수 없습니다. 쓰는 도중에 프로그램이 멈추면 푸터 없는 파일이 남습니다. 이런 파일은 앞부분 값이 멀쩡해도 읽지 못합니다.
푸터는 코드로 들여다볼 수 있습니다. 파이썬에서는 Apache Arrow 프로젝트의 라이브러리 pyarrow 를 씁니다. Arrow 는 읽어 들인 열 데이터를 메모리에 어떤 모양으로 둘지 정하는 프로젝트입니다. Parquet 와 어떻게 다른지는 뒤의 「다른 파일 포맷과 견주기」에서 봅니다.
아래는 앞의 주문 테이블을 행 그룹 둘로 나눠 적은 파일을 읽는 코드입니다. 첫 행 그룹에는 1번과 2번 주문이 들어 있습니다. 주석은 각 줄이 돌려주는 값입니다.
import pyarrow.parquet as pq
f = pq.ParquetFile("orders.parquet")
f.metadata.num_row_groups # 2
f.metadata.num_columns # 3
rg = f.metadata.row_group(0)
st = rg.column(2).statistics
st.min # 3000
st.max # 5000
row_group(0) 은 첫 행 그룹의 정보입니다. column(2) 는 셋째 열인 amount 의 열 청크입니다. statistics 가 그 청크의 통계입니다. 값을 하나도 풀지 않고 푸터만 읽어 얻은 숫자들입니다.
읽지 않고 건너뛰기
Parquet 를 읽는 도구는 두 가지로 읽는 양을 줄입니다. 하나는 열을 고르는 것입니다. 다른 하나는 행 그룹을 건너뛰는 것입니다. 둘 다 푸터만 보고 정합니다.
첫째는 질의에 나오는 열의 열 청크만 읽는 것입니다. 금액 합계를 내는 질의라면 amount 열 청크만 읽고 id 와 customer 는 열지 않습니다. 이렇게 필요한 열만 읽게 하는 최적화를 프로젝션 푸시다운(projection pushdown)이라고 부릅니다.
둘째는 조건에 맞는 값이 들어 있을 수 없는 행 그룹을 건너뛰는 것입니다. 금액이 10000 원 넘는 주문을 찾는다고 해 봅니다. 어떤 행 그룹의 금액 최댓값이 5000 이면 그 행 그룹에는 찾는 행이 없습니다.
이렇게 조건을 읽는 단계까지 내려보내 미리 거르는 최적화를 프레디케이트 푸시다운(predicate pushdown)이라고 부릅니다. 프레디케이트는 참과 거짓을 가리는 조건입니다. SQL(Structured Query Language)의 WHERE 뒤에 쓰는 식이 그 예입니다.
아래 그림은 푸터를 읽은 뒤 행 그룹마다 내리는 판단을 그린 것입니다.
flowchart TD
A["푸터를 읽는다"] --> B["행 그룹 하나의 통계를 본다"]
B --> C{"조건에 맞는 값이 들어 있을 수 있나"}
C -->|아니다| D["이 행 그룹을 건너뛴다"]
C -->|그렇다| E["질의에 나오는 열의 열 청크만 읽는다"]
D --> F["다음 행 그룹으로 간다"]
E --> F
건너뛰기는 비슷한 값이 행 그룹마다 모여 있을 때 효과가 큽니다. 주문 테이블에 주문 시각 열을 하나 더했다고 해 봅니다. 주문을 시각 순서로 적었다면 행 그룹마다 시각 범위가 겹치지 않습니다. 그러면 하루치를 찾는 질의는 대부분의 행 그룹을 건너뜁니다.
값이 뒤섞여 있으면 사정이 달라집니다. 행 그룹마다 최솟값과 최댓값의 폭이 넓어져 어느 조건이든 「들어 있을 수 있다」가 됩니다. 그래서 자주 거르는 열로 정렬한 뒤 파일을 쓰는 경우가 많습니다.
인코딩과 압축
Parquet 는 페이지마다 값을 두 단계로 줄입니다. 먼저 인코딩으로 값을 적는 방식을 바꿉니다. 그 결과를 범용 압축 알고리즘으로 한 번 더 줄입니다.
인코딩은 여럿입니다. 쓰는 쪽 라이브러리가 열마다 고릅니다. 흔히 쓰는 넷은 이렇습니다.
| 인코딩 | 하는 일 | 크게 줄어드는 열 |
|---|---|---|
| 사전 인코딩 | 되풀이되는 값에 번호를 붙이고 번호만 적는다 | 고객 이름처럼 같은 값이 자주 나오는 열 |
| 런 렝스 인코딩 | 같은 값이 연달아 나오면 값과 횟수만 적는다 | 정렬된 열 · 사전 번호 |
| 비트 패킹 | 작은 정수를 필요한 비트 수만큼만 써서 적는다 | 사전 번호처럼 범위가 좁은 정수 |
| 델타 인코딩 | 앞 값과의 차이만 적는다 | 차례로 늘어나는 번호나 시각 |
사전 인코딩을 고객 열로 보겠습니다. 고객 값이 kim, lee, kim 이면 kim 에 0, lee 에 1 을 붙인 사전을 따로 둡니다. 값은 0, 1, 0 으로 적습니다. 긴 문자열이 수천 번 나와도 문자열 자체는 사전에 한 번만 적힙니다.
런 렝스 인코딩(run-length encoding)은 연달아 나오는 같은 값을 한 쌍으로 적습니다. 주문 테이블에 배송 상태 열을 하나 더했다고 해 봅니다. 그 열에 「배송 완료」가 1000번 이어지면 「배송 완료, 1000번」 하나로 적는 식입니다.
인코딩을 거친 페이지는 Snappy, gzip, Zstandard 같은 범용 압축 알고리즘으로 한 번 더 줄입니다. 어느 알고리즘으로 줄였는지는 열 청크마다 푸터에 적습니다. 읽는 쪽은 그 표시를 보고 같은 알고리즘으로 풉니다.
알고리즘마다 줄이는 정도와 푸는 데 드는 계산이 다릅니다. 파일을 더 작게 줄이는 알고리즘일수록 대개 풀 때 계산이 더 듭니다.
타입과 스키마
CSV 는 모든 값을 글자로 적습니다. 3000 이 숫자인지 글자인지는 읽는 쪽이 짐작해야 합니다.
Parquet 파일은 이진 파일입니다. 사람이 읽는 글자가 아니라 컴퓨터가 쓰는 바이트 값을 적습니다. 텍스트 편집기로 열면 알아볼 수 없는 글자가 섞여 나옵니다.
Parquet 는 열마다 타입을 정해 푸터의 스키마에 적습니다. 숫자는 숫자로, 날짜는 날짜로 적힌 채 저장됩니다. 읽는 쪽이 값을 짐작할 필요가 없습니다.
타입은 두 겹입니다. 아래 겹은 바이트로 어떻게 적느냐를 정하는 물리 타입입니다. 32비트 정수인 INT32, 길이가 제각각인 바이트 배열인 BYTE_ARRAY 가 물리 타입입니다.
위 겹은 값의 뜻을 정하는 논리 타입입니다. 날짜는 INT32 에 적고 논리 타입으로 「날짜」를 붙입니다. 문자열은 BYTE_ARRAY 에 적고 「문자열」을 붙입니다.
목록이나 객체 안의 객체처럼 겹친 값도 담습니다. 주문 테이블에 주문마다 상품 목록을 붙였다고 해 봅니다. 1번 주문에는 펜과 공책이, 2번 주문에는 컵이 들어 있습니다. Parquet 는 목록을 통째로 두지 않고 맨 안쪽 값인 상품 이름을 열 하나로 풀어 적습니다.
상품 펜 공책 컵
표시 새 이어 새
풀어 적으면 어디까지가 한 주문의 목록인지가 사라집니다. 그래서 값마다 작은 숫자를 옆에 적어 둡니다. 위의 「새」와 「이어」는 그 숫자를 말로 옮긴 것입니다. 읽는 쪽은 「이어」가 붙은 공책을 앞의 펜과 같은 목록으로 묶어 1번 주문의 목록을 다시 짜 맞춥니다.
객체 스토리지 위의 Parquet
Parquet 파일은 흔히 객체 스토리지에 둡니다. 객체 스토리지는 파일을 이름 붙은 덩어리로 맡아 두는 저장소입니다. Amazon S3(Simple Storage Service)와 Google Cloud Storage 가 그런 저장소입니다.
객체 스토리지는 파일의 일부 바이트 구간만 골라 내려받게 해 줍니다. Parquet 를 읽는 도구는 이 기능으로 푸터가 든 끝 구간을 먼저 받습니다. 그다음 필요한 열 청크의 구간만 이어서 받습니다. 네트워크 너머의 큰 파일에서도 받는 양이 필요한 열만큼으로 줄어듭니다.
데이터 레이크는 여러 곳에서 생긴 데이터를 파일로 한곳에 쌓아 둔 저장소입니다. 그 안에서 분석용 테이블 하나는 Parquet 파일 여럿으로 놓입니다. Spark, Athena, BigQuery 같은 도구가 이 파일들을 SQL 로 조회합니다.
이 포맷이 못 하는 일
Parquet 는 여러 번 읽는 일에 맞춰 설계됐습니다. 그 대가로 못 하는 일이 있습니다.
| 못 하는 일 | 까닭 |
|---|---|
| 파일 안의 값을 제자리에서 고치기 | 값이 인코딩과 압축을 거쳐 페이지 안에 빽빽이 들어 있다 |
| 파일 끝에 행을 덧붙이기 | 파일 끝을 푸터가 차지하고 있다 |
| 쓰는 도중에 읽기 | 푸터를 붙이기 전에는 열 청크가 어디 있는지 모른다 |
| 행 하나를 적은 비용으로 꺼내기 | 한 행의 값이 열 청크마다 흩어져 있다 |
| 텍스트 편집기로 열어 읽기 | 이진 파일이다 |
고치기와 덧붙이기가 안 되므로 행을 바꾸거나 더하려면 새 파일을 씁니다. 그래서 한 테이블은 대개 Parquet 파일 여럿으로 이루어집니다. 파일 여럿을 한 테이블로 묶고 고치기와 지우기를 흉내 내는 일은 Apache Iceberg 같은 테이블 포맷이 위에서 맡습니다.
주문 한 건을 번호로 찾아 화면에 보여 주는 일에는 맞지 않습니다. 열 수만큼 서로 다른 구간을 읽어 한 행을 다시 짜 맞춰야 합니다. 그런 일은 관계형 데이터베이스가 맡습니다.
작게 자주 쓰면 이 포맷의 장점이 줄어듭니다. 몇 행짜리 파일은 열 청크가 작아 압축할 것도 건너뛸 것도 적습니다. 파일마다 푸터를 따로 읽어야 해서 파일 수가 늘수록 여는 비용만 커집니다.
이것이 작은 파일 문제입니다. 작은 파일 여럿을 큰 파일 하나로 합쳐 다시 쓰는 컴팩션으로 풉니다.
다른 파일 포맷과 견주기
분석용 데이터를 파일로 적는 포맷은 Parquet 말고도 여럿입니다. 가르는 축은 셋입니다. 행으로 적느냐 열로 적느냐, 글자로 적느냐 바이트 값으로 적느냐, 스키마를 파일 안에 담느냐입니다.
| 포맷 | 적는 방향 | 적는 방식 | 파일 안의 스키마 |
|---|---|---|---|
| CSV | 행 | 글자 | ✗ |
| 한 줄에 하나씩 적은 JSON(JavaScript Object Notation) | 행 | 글자 | ✗ |
| Apache Avro | 행 | 이진 | ✓ |
| ORC(Optimized Row Columnar) | 열 | 이진 | ✓ |
| Parquet | 열 | 이진 | ✓ |
JSON 은 값을 중괄호와 따옴표로 감싸 적는 글자 포맷입니다. 한 줄에 한 행씩 적으면 로그처럼 계속 이어 쓰기 쉽습니다. 대신 줄마다 열 이름이 되풀이되고 타입은 읽는 쪽이 짐작합니다.
Avro 는 행 단위로 적는 이진 포맷입니다. 행 하나씩 이어 쓰기 쉽습니다. 그래서 메시지를 끝없이 이어 보내는 Kafka 같은 스트림에 실을 때 흔히 씁니다. 열 몇 개만 읽는 분석에서는 행 지향 파일과 같은 곤란을 겪습니다.
ORC 는 Parquet 와 같은 열 지향 이진 포맷입니다. 행을 묶고 그 안에서 열마다 모으고 통계를 붙이는 구조도 닮았습니다. Hive 는 분산 저장소에 쌓인 파일을 SQL 로 조회하게 해 주는 도구입니다. ORC 는 Hive 둘레에서 자라 그쪽 도구와 함께 자주 쓰입니다.
Apache Arrow 는 이름이 비슷해 헷갈리는 이웃입니다. Arrow 도 값을 열로 모아 둡니다. Arrow 가 정하는 것은 파일이 아니라 메모리 안에서의 모양입니다.
Parquet 는 디스크에 오래 두는 파일입니다. Arrow 는 읽어 들인 뒤 계산하는 동안의 모양입니다. 그래서 Parquet 파일을 읽어 Arrow 모양으로 올리는 식으로 둘을 함께 씁니다.
Parquet 를 고르는 경우
많은 행을 쌓아 두고 열 몇 개씩 모아 계산하는 일에 맞습니다. 로그와 주문 기록을 날마다 쌓아 두고 한꺼번에 집계하는 배치 처리가 그렇습니다.
한 번 쓰고 여러 번 읽는 데이터에 맞습니다. 쓸 때 인코딩과 압축에 들인 품을 읽을 때마다 돌려받습니다.
행을 자주 고치거나 한 건씩 꺼내 보는 데이터에는 맞지 않습니다. 그런 데이터는 데이터베이스에 두었다가 분석할 몫만 Parquet 로 내보내는 경우가 많습니다.
데이터가 작고 사람이 직접 열어 볼 일이 잦으면 CSV 가 덜 번거롭습니다. 파일을 여는 데 별도 도구가 필요 없기 때문입니다.
관련 항목
Apache Parquet 의 상위 분류
파일 포맷 · 열 지향 포맷 · 열 지향 저장 · 이진 포맷 · 직렬화 · Apache Software Foundation · 오픈 소스
Apache Parquet 와 겨루는 파일 포맷
ORC · Apache Avro · CSV · JSON · JSON Lines
Apache Parquet 파일을 이루는 구성 요소
행 그룹 · 열 청크 · 푸터 · 스키마 · 메타데이터 · 열 통계
Apache Parquet 가 값을 줄이는 인코딩과 압축
사전 인코딩 · 런 렝스 인코딩 · 비트 패킹 · 델타 인코딩 · 압축 · Snappy · gzip · Zstandard · LZ4
Apache Parquet 파일을 읽고 쓰는 도구
Apache Arrow · Apache Spark · pandas · DuckDB · Trino · Amazon Athena · BigQuery · Apache Hive
Apache Parquet 파일 위에 얹히는 테이블 포맷
테이블 포맷 · Apache Iceberg · Delta Lake · Apache Hudi
Apache Parquet 파일이 놓이는 저장소
객체 스토리지 · Amazon S3 · Google Cloud Storage · Azure Blob Storage · HDFS · 데이터 레이크
Apache Parquet 로 읽는 양을 줄이는 최적화
프로젝션 푸시다운 · 프레디케이트 푸시다운 · 파티셔닝 · 정렬
Apache Parquet 와 맞세워지는 저장 방식
행 지향 저장 · 관계형 데이터베이스 · 온라인 트랜잭션 처리 · 데이터베이스
Apache Parquet 를 쓰는 데이터 작업
데이터 엔지니어링 · 온라인 분석 처리 · 배치 처리 · 데이터 웨어하우스 · 데이터 파이프라인 · ETL
Apache Parquet 를 굴릴 때 나는 운영 문제
다른 이름: Parquet · 파케이 · 아파치 파케이