SQL Reference

SQL Reference (SQL Reference)

Apache Pinot의 단일 스테이지 엔진(SSE)과 멀티 스테이지 엔진(MSE)이 지원하는 SQL 문법, 연산자, 절의 전체 레퍼런스예요.

출처: SQL Reference

본문

Pinot는 Apache Calcite SQL 파서를 MYSQL_ANSI 방언과 함께 사용합니다. 이 페이지는 Pinot가 지원하는 모든 SQL 문장, 절, 연산자를 문서화하고, 단일 스테이지 엔진(SSE)과 멀티 스테이지 엔진(MSE) 사이에서 동작이 다른 곳을 짚어 줍니다.

{% hint style="info" %} JOIN, 서브쿼리, 윈도우 함수, 집합 연산 같은 MSE 전용 기능을 쓰려면 쿼리 전에 SET useMultistageEngine = true;로 멀티 스테이지 엔진을 활성화하세요. 자세한 내용은 multi-stage query engine을 참고하세요. {% endhint %}


지원되는 문장

Pinot는 다음 최상위 문장 유형을 지원합니다:

문장 설명
SELECT 하나 이상의 테이블에서 데이터 조회
SET 세션의 쿼리 옵션 설정 (예: SET useMultistageEngine = true)
EXPLAIN PLAN FOR 쿼리를 실행하지 않고 쿼리 실행 계획 표시
-- 쿼리 옵션을 설정한 뒤 쿼리 실행
SET useMultistageEngine = true;
SELECT COUNT(*) FROM myTable WHERE city = 'San Francisco';
-- 실행 계획 확인
EXPLAIN PLAN FOR
SELECT COUNT(*) FROM myTable GROUP BY city;

SELECT 문법

Pinot에서 SELECT 문의 전체 문법은 다음과 같습니다:

SELECT [ DISTINCT ] select_expression [, select_expression ]*
FROM table_reference
[ WHERE filter_condition ]
[ GROUP BY group_expression [, group_expression ]* ]
[ HAVING having_condition ]
[ ORDER BY order_expression [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ] [, ...] ]
[ LIMIT count ]
[ OFFSET offset ]
[ OPTION ( key = value [, key = value ]* ) ]

두 쿼리 엔진 모두 GROUP BY 절에서 ROLLUP(...), CUBE(...), GROUPING SETS (...) 같은 그룹핑 구성을 받아들입니다. 문법과 제한은 GROUP BY를 참고하세요.

컬럼 표현식

select_expression은 다음 중 하나일 수 있습니다:

  • * -- 모든 컬럼
  • 컬럼 이름: city
  • 정규화된 컬럼 이름: myTable.city
  • 표현식: price * quantity
  • 함수 호출: UPPER(city)
  • 집계 함수: COUNT(*), SUM(revenue)
  • CASE WHEN 표현식

MAP 요소 접근

컬럼이 MAP으로 선언되면 대괄호 문법으로 키로 값을 읽으세요:

mapColumn['key']

Pinot는 map 조회를 스칼라 표현식으로 취급하므로 SELECT, WHERE, GROUP BY, ORDER BY에서 사용할 수 있습니다:

SELECT attributes['country'] AS country, metrics['latencyMs'] AS latency
FROM events
WHERE metrics['latencyMs'] > 100
ORDER BY metrics['latencyMs']

SELECT attributes['country'] AS country, COUNT(*)
FROM events
GROUP BY attributes['country']

키가 없으면 Pinot는 값 필드의 구성된 defaultNullValue를 사용하거나, 구성이 없으면 타입별 기본값을 사용합니다. 예를 들어 누락된 STRING map 값은 기본적으로 "null"을, 누락된 INT map 값은 Integer.MIN_VALUE를 사용합니다. 이 동작은 키가 전체 세그먼트에 없어도 일관됩니다. null 처리(null handling)가 활성화되면 조회도 null로 표시되어 SQL NULL을 반환하고, IS NULL은 일치하며, 값 조건은 일치하지 않습니다. null 처리가 비활성화되면 프로젝션과 조건이 구성된 값이나 타입별 기본값을 봅니다.

스키마 문법은 Schema Configuration을 참고하세요.

별칭

AS로 어떤 select 표현식에도 별칭을 지정하세요:

SELECT city AS metro_area, COUNT(*) AS total_orders
FROM orders
GROUP BY city

DISTINCT

SELECT DISTINCT로 컬럼 값의 고유한 조합을 반환하세요:

SELECT DISTINCT city, state
FROM stores
LIMIT 100

{% hint style="warning" %} SSE에서 DISTINCT는 집계 함수로 구현됩니다. DISTINCT *는 지원되지 않으므로 특정 컬럼을 나열해야 합니다. GROUP BY와 함께 DISTINCT를 쓰는 것도 지원되지 않습니다. {% endhint %}


FROM 절

테이블 참조

가장 간단한 FROM 절은 단일 테이블을 참조합니다:

SELECT * FROM myTable

서브쿼리 (MSE 전용)

멀티 스테이지 엔진에서는 서브쿼리를 데이터 소스로 사용할 수 있습니다:

SET useMultistageEngine = true;
SELECT city, avg_revenue
FROM (
  SELECT city, AVG(revenue) AS avg_revenue
  FROM orders
  GROUP BY city
) AS sub
WHERE avg_revenue > 1000

JOIN (MSE 전용)

멀티 스테이지 엔진은 다음 조인 유형을 지원합니다:

조인 유형 설명
[INNER] JOIN 두 테이블 모두에서 일치하는 행
LEFT [OUTER] JOIN 왼쪽 테이블의 모든 행 + 오른쪽의 일치 행
RIGHT [OUTER] JOIN 오른쪽 테이블의 모든 행 + 왼쪽의 일치 행
FULL [OUTER] JOIN 두 테이블의 모든 행
CROSS JOIN 두 테이블의 카테시안 곱
SEMI JOIN 오른쪽 테이블에 일치가 있는 왼쪽 테이블의 행
ANTI JOIN 오른쪽 테이블에 일치가 없는 왼쪽 테이블의 행
ASOF JOIN 가장 가까운 값(예: 가장 가까운 타임스탬프)으로 일치된 행
LEFT ASOF JOIN ASOF JOIN과 같지만 모든 왼쪽 행 유지
SET useMultistageEngine = true;
SELECT o.order_id, c.name
FROM orders AS o
JOIN customers AS c ON o.customer_id = c.id
WHERE o.amount > 100

자세한 조인 문법·예제는 JOINs를 참고하세요.


WHERE 절

WHERE 절은 조건(predicate)으로 행을 필터링합니다. 여러 조건은 논리 연산자로 결합할 수 있습니다.

비교 연산자

연산자 설명 예시
= 같음 WHERE city = 'NYC'
<> 또는 != 같지 않음 WHERE status <> 'canceled'
< 작음 WHERE price < 100
> 큼 WHERE price > 50
<= 작거나 같음 WHERE quantity <= 10
>= 크거나 같음 WHERE rating >= 4.0

BETWEEN

값이 포함 범위 안에 있는지 검사합니다:

SELECT * FROM orders
WHERE amount BETWEEN 100 AND 500

NOT BETWEEN도 지원됩니다:

SELECT * FROM orders
WHERE amount NOT BETWEEN 100 AND 500

IN

값이 목록의 어떤 값과 일치하는지 검사합니다:

SELECT * FROM orders
WHERE city IN ('NYC', 'LA', 'Chicago')

NOT IN도 지원됩니다:

SELECT * FROM orders
WHERE status NOT IN ('canceled', 'refunded')

{% hint style="info" %} 큰 값 목록에는 더 나은 성능을 위해 Filtering with IdSet를 고려하세요. {% endhint %}

LIKE

와일드카드로 패턴 매칭. %는 모든 문자 시퀀스와 일치하고, _는 단일 문자와 일치합니다:

SELECT * FROM customers
WHERE name LIKE 'John%'

NOT LIKE도 지원됩니다.

IS NULL / IS NOT NULL

값이 null인지 검사합니다:

SELECT * FROM orders
WHERE discount IS NOT NULL

Pinot에서 null이 어떻게 동작하는지 자세한 내용은 NULL Semantics를 참고하세요.

REGEXP_LIKE

정규식 매칭으로 행을 필터링합니다:

SELECT * FROM airlines
WHERE REGEXP_LIKE(airlineName, '^U.*')

{% hint style="info" %} REGEXP_LIKE는 세 번째 파라미터로 대소문자 무시 매칭을 지원합니다: REGEXP_LIKE(col, pattern, 'i'). {% endhint %}

TEXT_MATCH

텍스트 인덱스가 있는 컬럼에 대한 전문(full-text) 검색:

SELECT * FROM logs
WHERE TEXT_MATCH(message, 'error AND timeout')

JSON_MATCH

JSON 인덱스가 있는 컬럼에 대한 조건 매칭:

SELECT * FROM events
WHERE JSON_MATCH(payload, '"$.type" = ''click''')

VECTOR_SIMILARITY

벡터 인덱스 컬럼에 대한 근사 최근접(nearest-neighbor) 검색:

SELECT * FROM embeddings
WHERE VECTOR_SIMILARITY(vector_col, ARRAY[0.1, 0.2, 0.3], 10)

GROUP BY

지정된 컬럼의 값을 공유하는 행을 그룹핑하며, 보통 집계 함수와 함께 사용합니다:

SELECT city, COUNT(*) AS order_count, SUM(amount) AS total
FROM orders
GROUP BY city

규칙:

  • SELECT 목록의 모든 비집계 컬럼은 GROUP BY 절에 나타나야 합니다.
  • GROUP BY 없이 SELECT 목록에서 집계 함수와 비집계 컬럼을 섞을 수 없습니다.
  • GROUP BY 절 안에는 집계 표현식을 쓸 수 없습니다.

GROUPING SETS, ROLLUP, CUBE

두 Pinot 쿼리 엔진 모두 그룹화된 소계(subtotal) 쿼리를 지원합니다. 멀티 스테이지 엔진에서 grouping-set 쿼리를 실행하려면 SET useMultistageEngine=true로 활성화하세요.

SELECT country, city, SUM(revenue) AS total_revenue
FROM sales
GROUP BY ROLLUP(country, city)

다음 형태를 사용하세요:

  • GROUP BY ROLLUP(a, b, c)는 GROUPING SETS ((a, b, c), (a, b), (a), ())로 확장됩니다.
  • GROUP BY CUBE(a, b, c)는 그 그룹핑 컬럼의 모든 조합으로 확장됩니다.
  • GROUP BY GROUPING SETS ((a, b), (a), ())는 나열한 grouping set을 정확히 사용합니다.
  • GROUP BY a, ROLLUP(b, c)는 유효합니다. Pinot는 일반 키를 그룹핑 구성과 교차 곱합니다.

()는 전체 합계(grand-total) grouping set을 나타냅니다. Pinot는 반복되는 grouping set을 중복 제거하므로 GROUPING SETS ((a), (a), ())는 (a) 소계 하나만 반환합니다.

SELECT country, city, SUM(revenue) AS total_revenue
FROM sales
GROUP BY GROUPING SETS ((country, city), (country), ())

중요한 제한·주의 사항:

  • Pinot는 쿼리 어딘가(SELECT, HAVING, 또는 ORDER BY)에 집계 함수가 하나 이상 있어야 합니다.
  • 쿼리는 최대 4096개의 grouping set으로 확장될 수 있습니다. 특히 CUBE는 최대 12개의 그룹핑 수준을 포함할 수 있습니다.
  • 그룹핑 컬럼 수는 무제한이지만 GROUPING() 또는 GROUPING_ID() 호출 하나는 최대 31개 인자를 받습니다.
  • MSE grouping-set 쿼리는 WITHIN GROUP이 있는 정렬된 집계 표현식이나 집계 힌트를 지원하지 않습니다.
  • usePhysicalOptimizer=true에서 MSE grouping-set 쿼리는 조인을 지원하지 않습니다.
  • 롤업된 컬럼은 null 처리(handling)가 비활성화되어 있어도 실제 NULL 값으로 반환됩니다.
  • 기존 비-grouping-set 쿼리는 롤링 업그레이드 중 와이어 호환을 유지하지만, 모든 서버가 업그레이드될 때까지 grouping-set 쿼리를 발행하지 마세요.

GROUPING()와 GROUPING_ID()

소계 행을 실제 데이터 NULL과 구분하려면 GROUPING()와 GROUPING_ID()를 사용하세요:

SELECT
  country,
  city,
  SUM(revenue) AS total_revenue,
  GROUPING(country) AS country_rolled_up,
  GROUPING(city) AS city_rolled_up,
  GROUPING_ID(country, city) AS grouping_id
FROM sales
GROUP BY ROLLUP(country, city)
ORDER BY GROUPING_ID(country, city)
  • GROUPING(col)는 현재 행에서 col이 롤업되면 1을, 그렇지 않으면 0을 반환합니다.
  • GROUPING_ID(c1, c2, ...)는 첫 번째 인자를 최상위 비트로 하는 그룹핑 비트마스크를 반환합니다.
  • 각 인자는 쿼리의 GROUP BY 컬럼에도 나타나야 합니다.
  • Pinot는 SELECT, HAVING, ORDER BY에서 이 함수들을 지원합니다.

HAVING

집계 후 그룹을 필터링합니다. 집계 값으로 필터링할 때는 WHERE 대신 HAVING을 사용하세요:

SELECT city, COUNT(*) AS order_count
FROM orders
GROUP BY city
HAVING COUNT(*) > 100

SSE에서 HAVING은 이전의 어떤 그룹 트리밍 이후 병합된 그룹 후보에 대해 실행됩니다. 트리밍이 HAVING과 일치했을 그룹을 이미 버렸다면 그 그룹은 다시 나타나지 않습니다. Grouping algorithm과 Querying Pinot을 참고하세요.

SSE는 GROUP BY 없이 집계에 대해서도 HAVING을 평가합니다: 전체 입력이 하나의 그룹을 형성하며, 조건이 false면 집계 행이 아니라 0개 행을 반환합니다. 예를 들어 SELECT COUNT(*) FROM orders HAVING COUNT(*) > 100은 count가 100 이하일 때 행을 반환하지 않습니다. HAVING(그리고 GROUP BY가 없을 때 SELECT 목록)의 표현식은 집계, 리터럴, 그룹핑된 컬럼이어야 합니다. 그룹핑되지 않은 컬럼은 조건을 조용히 무시하는 대신 검증 중 거부됩니다.

SSE에서 집계가 없는 GROUP BY에 HAVING 절이 있으면 DISTINCT로 다시 쓰고 필터를 버리는 대신 거부됩니다. 단일 값 그룹핑 컬럼은 그 조건을 WHERE로 옮길 수 있을 수 있지만, 다중 값 컬럼은 어떤 값이 출력되는지 바뀔 수 있으므로 그룹 수준 필터링이 필요하면 멀티 스테이지 엔진을 사용하세요. SELECT DISTINCT ... HAVING ...도 유효하지 않습니다. 롤링 업그레이드 중 이전 버전의 브로커가 이런 조건을 조용히 무시하거나 내부 오류를 반환할 수 있으므로, 배포 전에 HAVING을 사용하는 저장된 쿼리·대시보드·알림을 검토하세요. apache/pinot#19554 참고.

SELECT, HAVING, ORDER BY에서 사후 집계(post-aggregation) 표현식(집계와 그룹 키에 대한 산술이나 함수)을 사용할 수 있습니다:

SELECT city, SUM(amount) AS total, COUNT(*) AS cnt, SUM(amount) / COUNT(*) AS avg_amount
FROM orders
GROUP BY city
HAVING SUM(amount) / COUNT(*) > 25
ORDER BY avg_amount DESC
LIMIT 50

ORDER BY

결과 집합을 하나 이상의 표현식으로 정렬합니다:

SELECT city, SUM(amount) AS total
FROM orders
GROUP BY city
ORDER BY total DESC

ORDER BY가 없으면 Pinot는 행이나 그룹 순서를 보장하지 않습니다. ORDER BY 없는 SSE GROUP BY에서는 결과 테이블이 작은 LIMIT에 도달한 뒤 새 그룹 키 수용을 멈출 수도 있으므로 처리 순서에 따라 어떤 키가 살아남는지 달라질 수 있습니다. Querying Pinot 참고.

정렬 방향

  • ASC -- 오름차순(기본)
  • DESC -- 내림차순

NULL 정렬

  • NULLS FIRST -- null 값이 먼저 나타남
  • NULLS LAST -- null 값이 나중에 나타남
SELECT city, revenue
FROM stores
ORDER BY revenue DESC NULLS LAST

LIMIT / OFFSET

LIMIT

반환되는 행 수를 제한합니다:

SELECT * FROM orders LIMIT 50

단일 스테이지 엔진에서 LIMIT를 지정하지 않으면 브로커가 기본적으로 10행(pinot.broker.default.query.limit)을 반환하며, 이는 SSE GROUP BY 결과도 10개 그룹으로 제한합니다. 멀티 스테이지 엔진은 이 브로커 기본값을 적용하지 않습니다. 모든 프로덕션 쿼리에는 명시적 LIMIT를 선호하세요. Grouping algorithm과 Querying Pinot 참고.

OFFSET

결과를 반환하기 전에 행 수를 건너뜁니다. 일관된 페이지네이션을 위해 ORDER BY가 필요합니다:

SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 20 OFFSET 40

Pinot는 레거시 LIMIT offset, count 문법도 지원합니다:

SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 40, 20

논리 연산자

연산자 설명
AND 두 조건이 모두 true면 true
OR 어느 한 조건이 true면 true
NOT 조건을 부정

우선순위

높은 순에서 낮은 순으로:

  1. NOT
  2. AND
  3. OR

기본 우선순위를 재정의하려면 괄호를 사용하세요:

SELECT * FROM orders
WHERE (status = 'completed' OR status = 'shipped')
  AND amount > 100

산술 연산자

산술 표현식은 SELECT 표현식, WHERE 절, 다른 문맥에서 사용할 수 있습니다:

연산자 설명 예시
+ 덧셈 price + tax
- 뺄셈 total - discount
* 곱셈 price * quantity
/ 나눗셈 total / count
% 나머지(modulo) id % 10
SELECT order_id, price * quantity AS line_total
FROM line_items
WHERE (price * quantity) > 1000

단항 연산자

단항 -와 +는 숫자 표현식의 접두 연산자입니다. 스칼라 표현식이 유효한 곳이라면 어디든(SELECT 목록, WHERE, GROUP BY, HAVING, ORDER BY, JOIN 조건, CASE 분기, CAST 입력, IN 목록, 서브쿼리, 집계·윈도우 함수 내부) 사용할 수 있습니다.

연산자 설명 예시
- (단항) 부정(숫자) -price, ORDER BY -score
+ (단항) 항등(숫자) +price
SELECT order_id, -discount AS adjustment
FROM line_items
SELECT order_id
FROM line_items
WHERE -profit > 100
SELECT order_id, score
FROM line_items
ORDER BY -score

참고:

  • INT, LONG, FLOAT, DOUBLE, BIG_DECIMAL에서 지원됩니다. 결과는 입력 타입을 유지합니다.
  • NULL 입력은 NULL을 반환합니다.
  • -col은 negate(col)과 동등합니다.
  • 부정은 Math.negateExact 의미를 사용하므로 -INT_MIN과 -LONG_MIN은 런타임에 overflow됩니다.

타입 캐스팅

CAST로 값을 한 타입에서 다른 타입으로 변환하세요:

SELECT CAST(revenue AS BIGINT) FROM orders

지원되는 대상 타입

타입 설명
INT / INTEGER 32비트 부호 있는 정수
BIGINT / LONG 64비트 부호 있는 정수
FLOAT 32비트 부동 소수점
DOUBLE 64비트 부동 소수점
BOOLEAN 부울 값
TIMESTAMP 타임스탬프 값
VARCHAR / STRING 가변 길이 문자열
BYTES 바이트 배열
UUID 16바이트로 저장되는 논리 UUID
JSON JSON 값

Pinot는 캐스트 대상으로 TINYINT UNSIGNED, SMALLINT UNSIGNED, INTEGER UNSIGNED도 받아들입니다. 이들은 전체 범위를 보존하는 가장 작은 부호 있는 Pinot 타입을 반환합니다: TINYINT UNSIGNED와 SMALLINT UNSIGNED는 INTEGER처럼 동작하고, INTEGER UNSIGNED는 BIGINT / LONG처럼 동작합니다. BIGINT UNSIGNED는 지원되지 않습니다.

SELECT CAST(event_time AS TIMESTAMP), CAST(user_id AS VARCHAR)
FROM events

정규 UUID 문자열이나 16바이트 값을 논리 UUID 타입으로 캐스팅합니다. UUID 결과는 정규(소문자 대시) 문자열로 렌더링되며, UUID 표현식을 STRING으로 캐스팅해도 같은 표현을 씁니다. 잘못된 UUID 문자열과 16바이트가 아닌 바이트 배열은 거부됩니다. 이 변환은 단일 값과 다중 값 표현식을 지원합니다.

SELECT CAST('550e8400-e29b-41d4-a716-446655440000' AS UUID)
FROM events

UUID 표현식은 CASE, IN, 이진 비교를 지원합니다. 이 표현식에서 베어 UUID 리터럴은 저장된 16바이트의 고정 폭 32문자 16진 인코딩이어야 합니다. 정규 대시 텍스트는 UUID로 명시적으로 캐스팅하세요:

SELECT CASE
  WHEN user_id IN (CAST('550e8400-e29b-41d4-a716-446655440000' AS UUID))
  THEN user_id
  ELSE CAST('00000000-0000-0000-0000-000000000000' AS UUID)
END
FROM events

베어 정규 대시 문자열은 CASE나 IN 안에서 UUID로 암묵적으로 변환되지 않습니다.

UUID 컬럼에 대한 필터 조건은 정규 대시, 대시 없는, 대소문자 혼합 UUID 문자열을 받아들입니다. 이는 dictionary 인코딩과 원시 UUID 컬럼 모두에서 =, !=, IN, NOT IN, 범위 조건에 적용됩니다:

SELECT *
FROM events
WHERE user_id IN (
  '550e8400-e29b-41d4-a716-446655440000',
  '6BA7B8109DAD11D180B400C04FD430C8'
)

형식이 잘못된 UUID 조건 리터럴은 거부됩니다.


집합 연산 (MSE 전용)

멀티 스테이지 엔진은 여러 쿼리의 결과 결합을 지원합니다:

연산 설명
UNION ALL 두 쿼리의 모든 행 결합(중복 포함)
UNION 두 쿼리의 행 결합, 중복 제거
INTERSECT 두 쿼리 모두에 나타나는 행 반환
EXCEPT 첫 번째 쿼리에는 있고 두 번째에는 없는 행 반환
SET useMultistageEngine = true;

SELECT city FROM stores
UNION ALL
SELECT city FROM warehouses
SET useMultistageEngine = true;

SELECT customer_id FROM orders_2024
INTERSECT
SELECT customer_id FROM orders_2025

윈도우 함수 (MSE 전용)

윈도우 함수는 현재 행과 관련된 행 집합에 걸쳐 값을 계산하되, 그것들을 단일 출력 행으로 축소하지는 않습니다.

문법

function_name ( expression ) OVER (
  [ PARTITION BY partition_expression [, ...] ]
  [ ORDER BY order_expression [ ASC | DESC ] [, ...] ]
  [ frame_clause ]
)

프레임 절

{ ROWS | RANGE } BETWEEN frame_start AND frame_end

frame_start / frame_end:
  UNBOUNDED PRECEDING
  | offset PRECEDING
  | CURRENT ROW
  | offset FOLLOWING
  | UNBOUNDED FOLLOWING

예제

SET useMultistageEngine = true;

SELECT
  city,
  order_date,
  amount,
  SUM(amount) OVER (PARTITION BY city ORDER BY order_date) AS running_total,
  ROW_NUMBER() OVER (PARTITION BY city ORDER BY amount DESC) AS rank
FROM orders

지원되는 윈도우 함수 전체 목록과 자세한 문법은 Window Functions를 참고하세요.

QUALIFY (MSE 전용)

QUALIFY는 윈도우 함수가 평가된 후 행을 필터링합니다. 도시당 한 행 같은 쿼리에는 멀티 스테이지 엔진을 사용하세요:

SET useMultistageEngine = true;

SELECT city, category
FROM myTable
QUALIFY ROW_NUMBER() OVER (PARTITION BY city ORDER BY orderDate DESC) = 1

단일 스테이지 엔진은 QUALIFY를 포함한 쿼리를 컴파일 시간에 거부하며, 절을 무시하고 필터링되지 않은 행을 반환하지 않습니다. 조건이 윈도우 함수를 사용하지 않으면, 컬럼 필터에는 WHERE로, GROUP BY 쿼리의 집계 필터에는 HAVING으로 다시 쓰세요. 이전에 SSE에서 QUALIFY를 사용했던 저장된 쿼리·대시보드·알림은 이제 잘못된 결과를 반환하는 대신 실패합니다. 롤링 브로커 업그레이드 중에는 롤아웃이 끝날 때까지 구버전·신버전 브로커가 다르게 응답할 수 있습니다. 업그레이드 전에 기존 SSE 쿼리 로그에서 QUALIFY를 확인하세요.


OPTION 절

OPTION 절은 Pinot 특유의 쿼리 힌트를 제공합니다. 표준 SQL은 아니지만 엔진 동작을 제어할 수 있습니다:

SELECT * FROM orders
WHERE city = 'NYC'
OPTION(timeoutMs=5000)

권장 방식은 쿼리 전에 SET 문을 사용하는 것입니다:

SET timeoutMs = 5000;
SET useMultistageEngine = true;
SELECT * FROM orders WHERE city = 'NYC'

흔한 쿼리 옵션:

옵션 설명
timeoutMs 밀리초 단위 쿼리 타임아웃
useMultistageEngine 멀티 스테이지 엔진 사용 (true/false)
enableNullHandling 3값 null 로직 활성화
maxExecutionThreads 쿼리가 사용하는 CPU 스레드 제한
useStarTree star-tree 인덱스 사용 활성화·비활성화
skipUpsert upsert 테이블에서 삭제를 무시하고 모든 레코드 조회

쿼리 옵션 전체 목록은 Query Options를 참고하세요.


NULL 의미

기본 동작

기본적으로 Pinot는 null 값을 컬럼 타입의 기본값(숫자 타입은 0, 문자열은 빈 문자열 등)으로 취급합니다. 이는 null 추적 오버헤드를 피하고 하위 호환을 유지합니다.

Nullable 컬럼

전체 null 처리를 활성화하려면:

  1. 스키마에서 컬럼을 nullable로 표시합니다 (notNull: true를 설정하지 마세요).
  2. 쿼리 시간에 null 처리를 활성화합니다:
SET enableNullHandling = true;
SELECT * FROM orders WHERE discount IS NULL

3값 논리

null 처리가 활성화되면 Pinot는 표준 SQL 3값 논리를 따릅니다:

A B A AND B A OR B NOT A
TRUE TRUE TRUE TRUE FALSE
TRUE FALSE FALSE TRUE FALSE
TRUE NULL NULL TRUE NULL
FALSE FALSE FALSE FALSE TRUE
FALSE NULL FALSE NULL TRUE
NULL NULL NULL NULL NULL

null 처리 활성화 시 핵심 동작:

  • NULL과의 비교(예: col = NULL)는 NULL을 반환합니다(TRUE나 FALSE가 아님). IS NULL / IS NOT NULL을 사용하세요.
  • NULL IN (...)은 FALSE가 아니라 NULL을 반환합니다.
  • NULL NOT IN (...)은 TRUE가 아니라 NULL을 반환합니다.
  • SUM, AVG, MIN, MAX 같은 집계 함수는 NULL 값을 무시합니다.
  • COUNT(*)는 모든 행을 세고, COUNT(col)은 non-null 값만 셉니다.

자세한 내용은 Null value support를 참고하세요.


식별자와 리터럴 규칙

  • 큰따옴표(")는 식별자(컬럼 이름, 테이블 이름)를 구분합니다. 예약어나 특수 문자에는 큰따옴표를 사용하세요: SELECT "timestamp", "date" FROM myTable.
  • 작은따옴표(')는 문자열 리터럴을 구분합니다: WHERE city = 'NYC'. 포함된 작은따옴표는 두 번 연속으로 써서 이스케이프하세요: 'it''s'.
  • 십진 리터럴은 정밀도를 보존하려면 작은따옴표로 감싸야 합니다.
  • 이진 리터럴은 X'0102' 같은 16진 SQL 문법을 사용합니다. Pinot는 PostgreSQL hex bytea 리터럴도 '\x0102'::bytea 또는 CAST('\x0102' AS BYTEA)로 받아들입니다. 16진 숫자는 대소문자 구분이 없고, 바이트 쌍 사이의 공백은 허용되며, '\x'는 빈 값을 나타냅니다. ARRAY[X'00', X'0102', X'FF']로 BYTES_ARRAY를 구성하세요. Pinot는 두 쿼리 엔진 모두에서 프로젝션·중첩 표현식·조건에서 이 리터럴을 지원합니다. 응답 값은 16진 문자열이고 결과 메타데이터는 BYTES_ARRAY를 보고합니다. PostgreSQL 이스케이프 형식 값과 컬럼에서의 캐스트는 지원되지 않습니다. 일반 16진 텍스트를 포함한 컬럼에는 hexToBytes(columnName)을 사용하세요.

CASE WHEN

Pinot는 조건 로직을 위한 CASE WHEN 표현식을 지원합니다:

SELECT
  order_id,
  CASE
    WHEN amount > 1000 THEN 'high'
    WHEN amount > 100 THEN 'medium'
    ELSE 'low'
  END AS tier
FROM orders

CASE WHEN은 집계 함수 안에서 사용할 수 있습니다:

SELECT
  SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) AS completed_revenue
FROM orders

{% hint style="warning" %} ELSE 절 안의 집계 함수는 지원되지 않습니다. {% endhint %}


엔진 호환성 행렬

다음 표는 단일 스테이지 엔진(SSE)과 멀티 스테이지 엔진(MSE)에 걸친 기능 지원을 요약합니다:

기능 SSE MSE
SELECT, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT Yes Yes
GROUPING SETS / ROLLUP / CUBE Yes Yes
GROUPING() / GROUPING_ID() Yes Yes
DISTINCT Yes Yes
집계 함수 Yes Yes
CASE WHEN Yes Yes
BETWEEN, IN, LIKE, IS NULL Yes Yes
산술 연산자 (+, -, *, /, %) Yes Yes
CAST Yes Yes
OPTION / SET 쿼리 힌트 Yes Yes
EXPLAIN PLAN Yes Yes
OFFSET Yes Yes
JOIN (INNER, LEFT, RIGHT, FULL, CROSS) No Yes
Semi / Anti 조인 No Yes
ASOF / LEFT ASOF 조인 No Yes
서브쿼리 No Yes
집합 연산 (UNION, INTERSECT, EXCEPT) No Yes
윈도우 함수 (OVER, PARTITION BY) No Yes
QUALIFY No (컴파일 타임 오류) Yes
상관 서브쿼리 No No
INSERT INTO (파일에서) No Yes
Controller DDL (테이블·머티리얼라이즈드 뷰) No No
DISTINCT with * No No
DISTINCT with GROUP BY No No

이 행렬에서 Controller DDL (tables and materialized views)이 No로 남는 이유는 브로커 라우팅 SSE·MSE 쿼리가 컨트롤러 메타데이터 DDL을 실행하지 않기 때문입니다. Pinot는 컨트롤러 엔드포인트 POST /sql/ddl로 SQL DDL을 노출합니다. SQL DDL과 Materialized Views를 참고하세요.

더 알아보기 (Learn more)