대표적인 SQL-on-Hadoop 기반 쿼리 엔진인 Hive와 Presto를 중심으로, 구조화된 데이터를 생성하고 이를 효율적으로 질의하는 방식을 살펴보자. Hive는 배치 기반의 처리에 강점을 가진 반면, Presto는 대화식 쿼리에 특화된 쿼리 엔진이다. 두 시스템은 상호 보완적인 관계를 이루며 데이터 파이프라인 내에서 함께 사용될 수 있다.
| 데이터 마트 구축의 파이프라인

실제 쿼리 엔진을 사용하여 데이터 마트를 만들기까지의 흐름을 살펴보자. 위 그림은 Hive와 Presto를 결합한 데이터 파이프라인이다. 파이프라인의 구조는 다음과 같다.
1. 데이터 구조화 (Hive)
- 분산 스토리지(HDFS 등)에 저장된 비구조화 데이터(텍스트, 로그 파일, csv 등)의 스키마를 정의해 구조화한다.
- Hive는 이 과정을 수행하면서 데이터를 열 기반 DB로 저장하여 분석의 효율성을 높인다.
- 이때 생성되는 테이블은 팩트/디멘젼 테이블로 구성되며, 모두 Hive의 메타스토어에 등록되어 정보를 관리한다.
[여기서 잠깐] Hive 메타스토어란?
Hive에서 생성한 테이블, 컬럼, 파티션, 데이터 형식, 파일 위치 등의 정보를 저장하는 메타데이터 저장소. 크게 메타데이터 저장소와 Metastore Service로 나뉜다.
| 메타데이터 저장소(DB) | MySQL, PostgreSQL 등 관계형 데이터베이스에 저장 |
| Metastore Service | Hive나 Presto 같은 쿼리 엔진이 메타 데이터를 조회할 수 있게 해주는 API 서버 |
스키마를 변경하거나 테이블을 추가해도 메타스토어에만 반영하면, 다양한 분석 도구가 이를 즉시 인식할 수 있다.
2.데이터 집약 (Presto)
- Presto는 쿼리 집약 및 분석 처리를 수행한다.
- Presto는 대화형 쿼리 엔진(SQL 기반)으로, 빠른 응답 시간과 병렬 처리 성능을 바탕으로 실시간 분석 쿼리를 실행할 수 있다.
- Hive의 메타스토어에 접근해 테이블 정의를 그대로 활용한다.
3. 결과 저장 및 시각화
- Presto로 집계된 분석 결과는 데이터 마트에 저장되고, 이는 MPP 기반의 데이터베이스처럼 비정규화된 테이블로 구성된다.
- 이 마트는 SQL 기반 시각화 도구와 연동되어 사용자에게 즉시 분석 결과를 제공할 수 있는 환경을 만든다.
| Hive를 활용한 데이터 구조화
앞서 살펴본 데이터 파이프라인에서 Hive는 분산 스토리지에 저장된 비정형 데이터를 읽어들여 구조화하는 역할을 담당한다. Hive를 활용해 데이터를 구조화하는 과정을 구체적으로 알아보자.
외부 테이블(external table)
외부 테이블은 실제 데이터 파일(CSV 등)을 Hive가 직접 소유하지 않고, 외부 경로의 파일을 그대로 참조하는 방식을 말한다. 쉽게 말해 외부에 있는 특정 파일을 참고해 마치 거기에 테이블이 존재하는 것처럼 읽어들인다.
아래 예시 쿼리에서는 'access_log_csv'라는 테이블명을 참고해 외부 테이블을 정의한다.
CREATE EXTERNAL TABLE access_log_csv(
time string, request string, status int, bytes int
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde'
STORED AS TEXTFILE LOCATION '/var/log/access_log/'
TBLPROPERTIES ('skip.header.line.count'='1');
Hive를 비롯한 대부분의 SQL-on-Hadoop의 쿼리 엔진은 MPP 데이터베이스처럼 데이터를 내부로 가져오지 않아도 텍스트 파일을 그대로 집계할 수 있다. 아래 예시와 같이 쿼리를 실행하면 외부 테이블로 지정한 경로에 포함된 모든 CSV 파일이 로드되고 집계된다.
SELECT status, count(*)
FROM access_log_csv
GROUP BY status LIMIT 2;
쿼리 실행 과정
1. Hive는 access_log_csv 라는 외부 테이블일 가리키는 csv 파일을 읽는다.
2. status별로 몇 개씩 있는지 세서 바로 보여준다. (집계)
요약하자면 Hive나 Presto 같은 SQL-on-Hadoop 쿼리 엔진은 데이터를 복사해서 내부로 옮기지 않아도, 외부에 저장된 파일을 그 자리에서 바로 읽어서 분석할 수 있다. 이처럼 데이터를 다른 데로 옮기지 않고도 바로 분석할 수 있다는 점은, 특히 로그 데이터처럼 계속 쌓이는 데이터를 빠르게 분석할 때 유용하다. 필요한 정보만 쿼리해서 즉시 얻을 수 있기 때문이다.
하지만 이것은 비효율적이다. 쿼리를 실행할 때마다 매번 텍스트를 읽어들이기 때문에 빠르다고 할 수 없다. 따라서 효율성을 위해 열 지향 스토리지로 변환할 필요가 있다.
| 열 지향 스토리지로의 변환
데이터 집계를 고속화하기 위해 테이블을 열 지향 스토리지 형식인 ORC 형식으로 변환한다.
Hive는 테이블마다 개별적으로 스토리지 형식을 지정할 수 있다. 아래 쿼리를 보자.
ORC 형식의 테이블 'access_log_orc'로 변환
CREATE TABLE access_log_orc STORED AS ORC AS
SELECT
cast(time AS timestamp) time,
request,
status,
cast(bytes AS bigint) bytes
FROM access_log_csv;
⏱️ Time taken: 15.993 seconds
이 쿼리는 기존 CSV 데이터를 ORC라는 열 지향 포맷으로 바꾸어 access_log_orc라는 새로운 테이블로 저장하는 과정이다. 약 16초 정도가 걸렸는데, 처음 변환하는 데는 시간이 조금 걸린다.
변환된 ORC 테이블로 집계 쿼리 실행
SELECT status, count(*) cnt
FROM access_log_orc
GROUP BY status
LIMIT 2;
OK
200 1701534
302 46573
Time taken: 1.567 seconds, Fetched: 2 row(s)
이 쿼리는 변환된 ORC 테이블에 집계를 실행한 결과이다. 결과는 CSV와 동일하지만, 걸린 시간은 1.5초로 훨씬 빠르게 진행되었음을 알 수 있다. .
왜 ORC로 바꿀까? CSV 파일 vs ORC 파일
| CSV 파일 | ORC 파일 | |
| 저장 방식 | 행(row) 단위 저장 | 열(column) 단위 저장 |
| 쿼리 속도 | 느림 (매번 전체 읽음) | 빠름 (필요한 열만 읽음) |
| 파일 크기 | 큼 | 작음 (최대 90% 압축됨) |
| 분석 적합성 | 반복 분석에 부적절 | 반복 분석에 최적화 |
CSV와 같은 텍스트 기반의 데이터를 구조가 단순해 다루기 쉽지만, 분석용 쿼리를 반복해서 실행할 때는 비효율적이다. 그래서 Hive에서는 이 데이터를 ORC(열 지향 포맷)으로 변환해 읽기 성능을 크게 개선한다. 변환에는 시간이 걸리지만 변환 후에는 쿼리 속도가 CSV와 비교해 5-10배 이상 빨라진다.
그럼 왜 Hive를 사용할까?
변환 작업은 시간이 오래 걸리는 배치 처리에 해당한다. Hive는 대용량 데이터 처리에 최적화되어 있어 이러한 배치 처리 작업에 적합한 엔진이다. 또한 Hive는 단순히 쿼리를 실행하는 것을 넘어, 문자열 처리, 날짜 포맷 변경, 정규 표현식 활용 등 복잡한 전처리 작업까지 지원학 때문에, 대규모 데이터를 정제하고 분석하기에 효과적이다.
| Hive로 비정규화 테이블 작성하기
데이터의 구조화가 완료되고 나면 데이터 마트를 구축해야 한다. 즉, 테이블을 결합하고 집계해 비정규화 테이블을 만든다. 이때 우리는 한 가지 질문을 던질 수 있다.
Presto 같은 대화형 쿼리 엔진을 사용할까, Hive 같은 배치형 쿼리 엔진을 사용할까?
어떤 쿼리 엔진을 사용할 지는 수행하려는 태스크의 목적에 따라 다르다. 빠른 응답이 중요하다면 Presto를, 대용량 데이터 배치 처리가 필요하다면 Hive를 선택하는 것이 좋다.
구체적인 예시를 살펴보자.
e커머스 플랫폼이 있다. Hive를 통해 구조화한 다음과 같은 두 개의 테이블이 존재한다.
- orders : 사용자 주문 정보 (수억 건)
- users : 사용자 정보 (수만 건)
어떤 상황에 어떤 엔진이 적합할까?
지금 어떤 상품이 제일 잘 팔리고 있을까? Presto로 알아보자!
마케팅팀이 주문 데이터를 기준으로 어떤 상품이 가장 잘 팔리고 있는지를 빠르게 확인하고 싶어한다면, 데이터가 완전히 정제되어 있지 않아도 Presto로 orders 테이블을 쿼리해 실시간 집계가 가능하다.
쿼리를 살펴보자.
SELECT product_id, COUNT(*) AS order_count
FROM orders
WHERE order_time >= now() - interval '1 hour'
GROUP BY product_id
ORDER BY order_count DESC
LIMIT 10;
이 쿼리는 몇 초만에 결과를 반환한다. 실시간 대시보드나 임시 분석에 딱 좋다.
주문 데이터를 모아 사용자별 누적 구매액을 알고 싶다면? Hive로 알아보자!
이번에는 사용자별 누적 구매액, 평균 구매 주기 등을 계산해야 하는 상황이 발생했다. 이것은 수억 건의 데이터를 결합하고 집계해야 하는 무거운 작업이다. 이런 경우, Hive로 안정적으로 처리하고 ORC로 저장하는 것이 ㄷ ㅓ적절하다.
쿼리를 살펴보자.
CREATE TABLE user_order_summary STORED AS ORC AS
SELECT
u.user_id,
u.gender,
u.age,
SUM(o.order_amount) AS total_spent
FROM
users u
JOIN
orders o
ON
u.user_id = o.user_id
GROUP BY
u.user_id, u.gender, u.age;
이 쿼린느 시간이 오래 걸리더라도 안정적으로 모든 데이터를 끝까지 처리한다.
그래서 언제 뭐가 적합할까?
결과를 저장해두고 여러 번 반복해서 사용할 데이터라면 Hive로 미리 비정규화 테이블을 만들어 ORC로 저장해두고, Presto로 빠르게 조회하는 것이 좋다.
수억 건 이상을 처리하거나 매일 정기적으로 갱신해야 하는 배치 단위의 작업이라면 Hive로 처리하는 것이 더 안정적이고 효율적이다.
| 서브 쿼리 안에서 레코드 수 줄이기
비정규화 테이블을 만드는 데 오랜 시간이 걸리는 것은 흔한 일이다. 그렇기 때문에 '효율적인 쿼리'를 작성하는 것이 중요하다.
Hive 쿼리를 효율적으로 작성하기 위한 방법들을 알아보자.
서브 쿼리 안에서 레코드 수 줄이기
Hive 쿼리는 배치 처리 구조이기 때문에 데이터의 양에 따라 성능에 크게 영향을 받을 수 밖에 없다.
먼저 비효율적인 쿼리를 살펴보자.
SELECT ...
FROM access_log a
JOIN users b ON b.id = a.user_id
WHERE b.created_at = '2017-01-01'
이 쿼리는 JOIN을 먼저 실행한다. access_log와 users를 결합한 후, WHERE 조건을 필터링한다.

즉, 모든 데이터를 읽고 결합한 뒤에 필요한 행을 걸러낸다. 이런 경우 중간 데이터가 과도하게 생성되어 오버헤드가 발생한다.
개선된 쿼리를 보자.
SELECT ...
FROM (
SELECT *
FROM access_log
WHERE time >= TIMESTAMP '2017-01-01 00:00:00'
) a
JOIN users b ON b.id = a.user_id
WHERE b.created_at = '2017-01-01'
이 쿼리는 서브쿼리 안에서 access_log를 먼저 필터링한다. 즉, 처음부터 줄어든 데이터만 JOIN에 참여한다. 결과적으로는 중간 데이터량이 대폭 감소되어 처리 시간이 단축된다.
JOIN은 두 테이블을 전부 읽고 연결하기 때문에, 필터링이 늦을수록 중간 결과가 커져 성능 저하가 발생한다. 특히 팩트 테이블의 경우 데이터가 많은 쪽이기 때문에 JOIN 전에 줄여야 효율적이다.
데이터 편향 피하기
고속화를 방해하는 또 다른 원인은 데이터의 편향(data skew)이다.
[여기서 잠깐] 분산시스템에서의 데이터 편향이란?
분산 시스템에서 일부 노드에만 데이터가 쏠리는 현상을 말한다. 이런 경우 특정 노드만 과부하가 걸려 전체 쿼리 성능이 심각하게 저하된다.
날짜별로 고유 사용자 수를 계산하는 쿼리를 실행한다고 생각해보자.
SELECT date, COUNT(DISTINCT user_id)
FROM access_log
GROUP BY date;
이 쿼리는 중복된 user_id를 제거하기 위해 같은 날짜의 데이터를 한 곳에 모아서 처리해야 한다.
그런데 만약 1월 1일 데이터가 100만 건이고, 다른 날은 1천 건밖에 없다면? 분산 쿼리 엔진은 여러 대의 노드가 나눠서 데이터를 처리하기 때문에, 1월 1일 데이터만 한 노드에 몰려 과부하가 발생한다. 결과적으로 전체 쿼리가 그 노드가 끝날 때까지 기다려야 해서 성능 저하가 발생한다.
즉, 가장 느린 노드가 전체 분산 시스템의 속도를 결정하게 된다. 이것을 병렬 처리 시스템의 병목이라고도 한다.
그래서 중요한 건 데이터를 쿼리할 때 모든 노드가 비슷한 양의 데이터를 처리하도록 설계하는 것! 이것이 데잍 ㅓ편향을 피하는 이유이다.
그 방법은 아래 쿼리를 통해 살펴보자.
SELECT date, count(*)
FROM (
SELECT DISTINCT date, user_id
FROM access_log
) t
GROUP BY date;
1. 먼저 중복을 제거한 테이블을 서브쿼리로 만든다.
2. 다시 GROUP BY를 수행한다.
이렇게 하면 중복 제거가 분산된 노드에서 병렬로 수행되어 전체 처리 속도가 대폭 향상된다.
'Database' 카테고리의 다른 글
| [Database] 나에게 맞는 데이터 분석 프레임워크 선택하기 (feat. Hive, Presto, Spark) (2) | 2025.07.30 |
|---|---|
| [Database] 분산 처리 프레임워크 (4) 대화형 쿼리 엔진 Presto의 구조 (3) | 2025.07.30 |
| [Database] 분산처리 프레임워크 (2) Hadoop이란? 개념부터 동작 원리까지 (1) | 2025.07.28 |
| [Database] 분산 처리 프레임워크 (1) 구조화 데이터 vs 비구조화 데이터 (1) | 2025.07.28 |
| [Database] 빅데이터 시대에 '열 기반 데이터베이스'가 중요한 이유는 무엇일까? (2) | 2025.07.25 |