Pinot 쿼리하기
Pinot 쿼리하기 (Querying Pinot)
Pinot에 쿼리하는 실용적인 입문 페이지예요.
출처: Querying Pinot
본문
Pinot 쿼리는 브로커를 통해 실행되며 SQL로 작성됩니다. 이 페이지는 데이터를 쿼리하려는 사람, 어떤 엔진을 쓸지 이해하려는 사람, 쿼리 튜닝이 필요할 때 어디를 봐야 할지 알고 싶은 사람을 위한 길잡이입니다.
시작 방법
- 쿼리를 Pinot SQL로 작성합니다.
- 단일-스테이지 엔진으로 충분한지, 아니면 조인·서브쿼리 같은 멀티-스테이지 기능이 필요한지 결정합니다.
- 쿼리 옵션으로 런타임 동작을 제어합니다.
- 성능 디버깅이 필요할 때 플랜이나 결과 형태를 검사합니다.
SET useMultistageEngine = true;
SELECT city, COUNT(*)
FROM stores
GROUP BY city
LIMIT 10;
가장 중요한 점
Pinot SQL은 Apache Calcite 파서와 MYSQL_ANSI 방언을 사용합니다. 실제로는 식별자 따옴표, 리터럴 따옴표, 엔진별 기능에 주의를 기울여야 합니다.
느리거나 예상 밖의 쿼리를 디버깅할 때 가장 유용한 후속 페이지는:
- SQL syntax
- Query options
- Grouping algorithm
- Query quotas
- Query cancellation
- Cursor pagination
- Correlation IDs
- Explain plan
- Multi-stage explain plan
- SSE vs MSE
Group-by 특이사항 (기본 LIMIT, 트리밍, ORDER BY)
많은 사용자가 이 동작에 놀랍니다. 세부 사항과 튜닝 손잡이는 Grouping algorithm과 Query options에 있습니다.
기본 LIMIT는 10
**단일-스테이지 엔진(SSE)**에서 쿼리가 LIMIT를 생략하면 브로커가 기본 10행(pinot.broker.default.query.limit)을 적용합니다. 이는 selection 쿼리와 GROUP BY 쿼리 모두에 적용되므로, 명시적 LIMIT 없는 SSE group-by는 최대 10개 그룹을 반환합니다.
-- SSE: 도시가 많아도 최대 10개 그룹 반환
SELECT city, COUNT(*) AS cnt
FROM stores
GROUP BY city;
**멀티-스테이지 엔진(MSE)**은 이 브로커 기본값을 적용하지 않습니다. 그래도 결과 크기에 신경 쓰거나 의도적인 잘림을 원하면 명시적 LIMIT를 설정하세요.
GROUP BY에서 꼬리 트리밍
ORDER BY가 있는 SSE group-by에서 Pinot은 서버가 메모리 한계 안에 머물도록 집계하는 동안 꼬리 그룹을 트리밍할 수 있습니다. 트리밍이 켜져 있으면 후보 크기는 max(minTrimSize, 5 * LIMIT)를 기반으로 하며, 세그먼트 트리밍은 기본 비활성화이고 서버·브로커 리덕션은 각자 기본값을 가집니다. Pinot은 쿼리의 ORDER BY로 후보를 순위 매깁니다. 스테이지별 설정은 Grouping algorithm을 참조하세요.
결과에 대한 시사점:
- 카디널리티가
LIMIT와 트림 임계값에 비해 높으면 결과가 근사치일 수 있습니다. numGroupsLimitReached=true는 그룹 연산자가 하드 그룹 한계에 도달했음을 뜻합니다. 트림 크기를 늘려도 이미 그 한계에서 버려진 그룹은 복구할 수 없습니다.- 더 큰 후보 집합이 필요하면 관련 트림 크기를 키우고, 더 많은 행이 필요하면
LIMIT를 키우세요. 둘 다 메모리 사용량이 늘어납니다.
ORDER BY가 있는 GROUP BY vs 없는 GROUP BY
| 패턴 | 실행 동작 |
|---|---|
GROUP BY ... ORDER BY ... LIMIT N |
각 트림 스테이지가 ORDER BY 표현식으로 후보 그룹을 순위 매기고 낮은 순위의 후보를 버립니다. 이것이 의도된 top-N 형태지만, 후보 집합이 너무 작으면 분산 트리밍이 여전히 결과를 근사치로 만들 수 있습니다. |
GROUP BY ... LIMIT N (ORDER BY 없음) |
순위가 없습니다. 기본적으로 SSE 결과 테이블은 결과 크기에 도달한 뒤 새로운 그룹 키의 수용을 멈추므로 처리 순서가 어떤 N개 키가 살아남는지에 영향을 줄 수 있습니다. |
-- 순서가 있는 top-N 도시(ORDER BY가 순위와 일치할 때 트림이 높은 집계 유지)
SELECT city, COUNT(*) AS cnt
FROM stores
GROUP BY city
ORDER BY cnt DESC
LIMIT 20;
-- 순서 없음: 최대 20개 그룹이지만 정의된 "top" 집합은 아님
SELECT city, COUNT(*) AS cnt
FROM stores
GROUP BY city
LIMIT 20;
ORDER BY 없이는 안정적이거나 순위가 매겨진 그룹 집합을 가정하지 마세요. 결정적인 부분 집합이 필요하면 accurateGroupByWithoutOrderBy=true를 설정하세요. 그러면 SSE가 서버·브로커 리덕션 동안 사전 순으로 가장 작은 그룹 키를 유지합니다. 이것은 집계로 순위를 매기지 않으며 numGroupsLimit로 버려진 키는 복구할 수 없습니다. Query options를 참조하세요.
HAVING과 집계 후(post-aggregation)
SSE에서 HAVING은 집계 이후와 어떤 이전 그룹 트리밍 이후에 병합된 그룹 후보를 필터링합니다. HAVING이 트리밍이 이미 버린 그룹을 선호하면(예: ORDER BY SUM(x) DESC와 함께 HAVING SUM(x) < 100) 일치하는 그룹이 없을 수 있습니다. 트림 크기를 늘리거나 순서를 조정하세요.
SELECT city, COUNT(*) AS cnt, SUM(revenue) AS total
FROM stores
GROUP BY city
HAVING COUNT(*) > 100
ORDER BY total DESC
LIMIT 50;
집계 후(post-aggregation) 표현식은 집계가 계산된 뒤 SELECT, HAVING, ORDER BY에서 집계된 값(과 그룹 키)을 결합합니다:
SELECT
city,
SUM(revenue) AS total,
COUNT(*) AS cnt,
SUM(revenue) / COUNT(*) AS avg_revenue
FROM stores
GROUP BY city
HAVING SUM(revenue) / COUNT(*) > 10
ORDER BY avg_revenue DESC
LIMIT 50;
쿼리 옵션 (이름과 구문)
SET 문이나 OPTION (...)으로 쿼리별 옵션을 전달할 수 있습니다. 인식되는 키는 Query options에 나열된 표준 camelCase 이름(예: timeoutMs, numGroupsLimit, minSegmentGroupTrimSize, useMultistageEngine)에 대소문자 무관하게 매핑됩니다. 표준 철자를 선호하세요. 알 수 없는 이름은 수용되지만 무시될 수 있습니다.
SET timeoutMs = 5000;
SET minSegmentGroupTrimSize = 5000;
SELECT city, COUNT(*) AS cnt
FROM stores
GROUP BY city
ORDER BY cnt DESC
LIMIT 100;
어떤 엔진을 언제 써야 하나
단일-스테이지 실행은 단순한 필터링, 집계, top-K 형태 쿼리의 기본 경로입니다.
단일-스테이지 모드에서 사용할 수 없는 기능이 필요할 때 멀티-스테이지 실행을 사용하세요. 예:
- 조인(joins)
- 서브쿼리(subqueries)
- 윈도우 함수(window functions)
- 더 복잡한 분산 쿼리 형태
요약하면: 단순 필터링·집계·top-K 쿼리는 SSE를, 조인·서브쿼리·윈도우 함수·기타 고급 관계형 연산자가 필요하면 MSE를 사용하세요. 자세한 비교는 SSE vs MSE를 참조하세요.
다음 단계
쿼리 언어 자체는 SQL syntax를 읽고, 제어나 진단이 필요하면 Query options나 Explain plan으로 넘어가세요.
식별자 vs 리터럴
Pinot SQL에서:
- **큰따옴표(")**는 문자열 식별자를 강제하는 데 사용합니다. 예: 컬럼 이름
- **작은따옴표(')**는 문자열 리터럴을 감싸는 데 사용합니다. 문자열 리터럴에 작은따옴표가 포함되어 있으면 작은따옴표로 이스케이프합니다. 예:
'''Pinot'''는 리터럴'Pinot'와 매치됩니다.
이것을 잘못 쓰면 예상 밖의 쿼리 결과가 나올 수 있습니다. 예:
WHERE a='b'는 컬럼a가 문자열 리터럴 값'b'와 같다는 조건WHERE a="b"는 컬럼a가 컬럼b의 값과 같다는 조건
컬럼 이름이 예약 키워드(예: timestamp나 date)나 특수 문자를 사용하면 쿼리에서 이를 참조할 때 큰따옴표를 사용해야 합니다.
참고: 정밀도를 보존하려면 십진 리터럴을 따옴표 안에 정의하세요.
예제 쿼리
Selection
//기본은 limit 10
SELECT *
FROM myTable
SELECT *
FROM myTable
LIMIT 100
SELECT "date", "timestamp"
FROM myTable
Aggregation
SELECT COUNT(*), MAX(foo), SUM(bar)
FROM myTable
Grouping on Aggregation
SELECT MIN(foo), MAX(foo), SUM(foo), AVG(foo), bar, baz
FROM myTable
GROUP BY bar, baz
LIMIT 50
Ordering on Aggregation
SELECT MIN(foo), MAX(foo), SUM(foo), AVG(foo), bar, baz
FROM myTable
GROUP BY bar, baz
ORDER BY bar, MAX(foo) DESC
LIMIT 50
HAVING으로 그룹 필터링
SELECT bar, SUM(foo) AS total
FROM myTable
GROUP BY bar
HAVING SUM(foo) > 1000
ORDER BY total DESC
LIMIT 50
집계 후 표현식
SELECT bar, SUM(foo) AS total, COUNT(*) AS cnt, SUM(foo) / COUNT(*) AS avg_foo
FROM myTable
GROUP BY bar
ORDER BY avg_foo DESC
LIMIT 50
Filtering
SELECT COUNT(*)
FROM myTable
WHERE foo = 'foo'
AND bar BETWEEN 1 AND 20
OR (baz < 42 AND quux IN ('hello', 'goodbye') AND quuux NOT IN (42, 69))
목록의 ID를 성능 좋게 필터링하려면 Filtering with IdSet을 참조하세요.
NULL 조건으로 필터링
SELECT COUNT(*)
FROM myTable
WHERE foo IS NOT NULL
AND foo = 'foo'
AND bar BETWEEN 1 AND 20
OR (baz < 42 AND quux IN ('hello', 'goodbye') AND quuux NOT IN (42, 69))
Selection (Projection)
SELECT *
FROM myTable
WHERE quux < 5
LIMIT 50
Selection 정렬
SELECT foo, bar
FROM myTable
WHERE baz > 20
ORDER BY bar DESC
LIMIT 100
Selection 페이지네이션
ORDER BY 컬럼이 여러 행에서 같은 값을 가지면 결과가 일관되지 않을 수 있음에 주의하세요.
SELECT foo, bar
FROM myTable
WHERE baz > 20
ORDER BY bar DESC
LIMIT 50, 100
와일드카드 매치 (WHERE 절에서만)
아래 예제는 컬럼 airlineName이 U로 시작하는 행의 수를 셉니다:
SELECT COUNT(*)
FROM myTable
WHERE REGEXP_LIKE(airlineName, '^U.*')
GROUP BY airlineName LIMIT 10
참고: REGEXP_LIKE는 세 번째 파라미터로 i 플래그를 써서 대소문자 무시 검색도 지원합니다.
Case-When 문장
Pinot은 CASE-WHEN-ELSE 문장을 지원합니다.
SELECT
CASE
WHEN price > 30 THEN 3
WHEN price > 20 THEN 2
WHEN price > 10 THEN 1
ELSE 0
END AS price_category
FROM myTable
SELECT
SUM(
CASE
WHEN price > 30 THEN 30
WHEN price > 20 THEN 20
WHEN price > 10 THEN 10
ELSE 0
END) AS total_cost
FROM myTable
UDF
Pinot은 현재 함수 주입을 지원하지 않습니다. 함수는 Pinot 내부에서 구현되어야 합니다.
SELECT COUNT(*)
FROM myTable
GROUP BY DATETIMECONVERT(timeColumnName, '1:MILLISECONDS:EPOCH', '1:HOURS:EPOCH', '1:HOURS')
더 많은 예제는 Transform Function in Aggregation Grouping를 참조하세요.
BYTES 컬럼
Pinot은 hex 문자열을 사용한 BYTES 컬럼 쿼리를 지원합니다. 쿼리 응답도 bytes 값을 표현할 때 hex 문자열을 사용합니다.
아래 쿼리는 주어진 UID에 대한 모든 행을 가져옵니다:
SELECT *
FROM myTable
WHERE UID = 'c8b3bce0b378fc5ce8067fc271a34892'