SQL DDL
SQL DDL (SQL DDL)
컨트롤러가 관리하는 SQL DDL로 Pinot 테이블과 머티리얼라이즈드 뷰를 만들고, 조회하고, 목록화하고, 삭제하는 방법을 다룹니다.
출처: SQL DDL
본문
Pinot는 테이블과 머티리얼라이즈드 뷰 메타데이터 작업을 위한 컨트롤러 관리 SQL DDL 표면을 지원합니다. JSON 기반 /schemas, /tables 워크플로 대신 SQL 방식으로 하고 싶을 때 사용하세요.
컨트롤러는 요청당 하나의 DDL 문장을 받습니다:
CREATE TABLEDROP TABLESHOW TABLESSHOW CREATE TABLECREATE MATERIALIZED VIEWSHOW MATERIALIZED VIEWSSHOW CREATE MATERIALIZED VIEWDROP MATERIALIZED VIEW
{% hint style="info" %}
이 문장들은 브로커 쿼리 API가 아니라 컨트롤러 엔드포인트 POST /sql/ddl로 실행하세요. SSE와 MSE는 여전히 쿼리 문장을 실행하고, 컨트롤러가 테이블·머티리얼라이즈드 뷰 DDL을 소유합니다.
{% endhint %}
동작 방식
POST /sql/ddl은 SQL을 기존 컨트롤러 API가 쓰는 것과 동일한 Pinot Schema와 TableConfig 모델로 컴파일합니다. 즉:
- DDL로 만든 테이블은
POST /tables와 동일한 컨트롤러 검증 경로를 거칩니다. - DDL로 만든 머티리얼라이즈드 뷰는 일반
OFFLINE테이블 + JSON API가 쓰는 것과 동일한 MV 태스크 설정으로 영속화됩니다. SHOW CREATE TABLE은 저장된 스키마와 테이블 설정의 표준 SQL 형태를 렌더링하며, 검토·버전 관리·재실행이 가능합니다.SHOW CREATE MATERIALIZED VIEW는AS <query>절과 MV 전용 속성을 포함해 저장된 정의의 표준 MV DDL을 렌더링합니다.dryRun=true는 메타데이터를 영속화하지 않고 컴파일·검증만 수행합니다.- 기존 브로커 쿼리 API는 여전히
SELECT문장을 처리합니다. 컨트롤러 엔드포인트는 메타데이터 변경을 처리합니다.
지원되는 문장 형태
| 문장 | 참고 |
|---|---|
CREATE TABLE [IF NOT EXISTS] [db.]table (...) [PRIMARY KEY (...)] TABLE_TYPE = OFFLINE | REALTIME [PROPERTIES (...)] |
컬럼 목록 기반의 표준 형태 |
CREATE TABLE [IF NOT EXISTS] [db.]table WITH (key = value, ...) |
옵션 정의 테이블용 확장 형태. Pinot 배포판이 WITH 옵션에서 스키마·테이블 설정을 완전히 유도하는 핸들러를 설치할 수 있는데, Apache Pinot OSS는 그런 핸들러를 설치하지 않으므로 기본 동작은 이 형태를 거부하고 컬럼 목록 TABLE_TYPE = ... 형태를 쓰라고 안내합니다. |
DROP TABLE [IF EXISTS] [db.]table [TYPE OFFLINE | REALTIME] |
타입 지정 테이블 삭제 |
SHOW TABLES [FROM db] |
선택한 데이터베이스의 테이블 목록 |
SHOW CREATE TABLE [db.]table [TYPE OFFLINE | REALTIME] |
저장된 테이블 정의 표준 SQL |
CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]name [(...)] [REFRESH [INTERVAL] EVERY ...] PROPERTIES (...) AS <select> |
OFFLINE MV 생성. 컬럼 목록을 생략하면 SELECT 프로젝션에서 MV 스키마를 유도하고, 전체 컬럼 목록을 주면 유도된 타입이나 역할을 덮어쓸 수 있습니다. |
SHOW MATERIALIZED VIEWS [FROM db] |
_OFFLINE 접미사 없이 원래 이름으로 MV 목록 표시 |
SHOW CREATE MATERIALIZED VIEW [db.]name |
저장된 메타데이터의 표준 MV DDL 반환 |
DROP MATERIALIZED VIEW [IF EXISTS] [db.]name |
MV 삭제. MV는 항상 OFFLINE 테이블로 지원되므로 TYPE 절이 없습니다. |
db.table 한정자나 Database 헤더로 데이터베이스를 지정하세요. 둘 다 있으면 같은 데이터베이스를 가리켜야 하며, 그렇지 않으면 Pinot가 400 Bad Request를 반환합니다.
엔드포인트 계약
요청은 컨트롤러로 보냅니다:
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"SHOW TABLES"}'
HTTP 헤더로 데이터베이스를 지정하려면:
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-H "Database: analytics" \
-d '{"sql":"SHOW TABLES"}'
영속화 없이 검증만 하려면 dryRun=true를 사용하세요:
curl -X POST "http://localhost:9000/sql/ddl?dryRun=true" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"CREATE TABLE events (id INT DIMENSION) TABLE_TYPE = OFFLINE"}'
응답 동작 요약:
CREATE TABLE성공 시201 CreatedCREATE MATERIALIZED VIEW성공 시201 CreatedDROP TABLE,SHOW TABLES,SHOW CREATE TABLE,SHOW MATERIALIZED VIEWS,SHOW CREATE MATERIALIZED VIEW, dry run,IF EXISTS·IF NOT EXISTS멱등 케이스 시200 OK- 파싱 오류, 의미 검증 오류, 과대 SQL 시
400 Bad Request - 요청한 테이블이나 스키마가 없으면
404 Not Found IF NOT EXISTS없는 중복CREATE TABLE, 드롭을 막는 논리 테이블 참조, 다른 작성자와의 경합 시409 Conflict
응답 본문에는 실행된 작업에 해당하는 필드만 포함됩니다.
| 필드 | 적용 대상 | 참고 |
|---|---|---|
operation |
모든 응답 | SHOW_TABLES, SHOW_MATERIALIZED_VIEWS, CREATE_TABLE, SHOW_CREATE_TABLE, DROP_TABLE, CREATE_MATERIALIZED_VIEW, SHOW_CREATE_MATERIALIZED_VIEW, DROP_MATERIALIZED_VIEW 중 하나 |
databaseName |
create, drop, show create | 사용 가능할 때 작업에 사용된 데이터베이스 범위 |
tableName |
create, drop, show create | 해석된 Pinot 테이블 이름(대개 _OFFLINE·_REALTIME 접미사 포함). MV 작업도 내부적으로는 OFFLINE 저장소를 대상으로 합니다. |
tableType |
테이블 create, 타입 지정 테이블 drop, 테이블 show create | OFFLINE 또는 REALTIME. MV 전용 문장은 TYPE 절을 쓰지 않습니다. |
schema |
create | 컴파일된 Pinot 스키마 JSON. dry run과 영속화된 create(MV create 포함)에 반환됩니다. |
tableConfig |
create | 테이블 설정 튜너 처리 후 컴파일된 Pinot 테이블 설정 JSON. dry run과 영속화된 create(MV create 포함)에 반환됩니다. |
warnings |
create | 무시된 DECIMAL(p,s) 정밀도 세부 사항 같은 치명적이지 않은 컴파일 경고 |
dryRun |
create, drop | 요청이 메타데이터 영속화·삭제 없이 검증만 했는지 여부 |
ifNotExists |
create | 문장이 IF NOT EXISTS를 사용했는지 |
ifExists |
drop | 문장이 IF EXISTS를 사용했는지 |
deletedTables |
drop | drop이 제거한 타입 지정 테이블 이름들 |
tableNames |
카탈로그 목록 | 문장에 따라 선택한 데이터베이스에서 보이는 테이블 또는 MV |
ddl |
show create | 저장된 메타데이터의 표준 CREATE TABLE 또는 CREATE MATERIALIZED VIEW SQL |
message |
대부분의 응답 | 사람이 읽을 수 있는 작업 요약 |
컬럼, 타입, 기본값
모든 컬럼은 Pinot 데이터 타입과 선택적 역할을 가집니다. 역할을 생략하면 Pinot는 그 컬럼을 단일 값 차원으로 취급합니다.
| 컬럼 형태 | 결과 |
|---|---|
name STRING |
단일 값 차원 |
name STRING DIMENSION |
단일 값 차원 |
tags STRING DIMENSION ARRAY |
다중 값 차원 |
score DOUBLE METRIC |
메트릭 컬럼 |
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS' |
날짜-시간 컬럼 |
지원되는 데이터 타입 이름:
| SQL 타입 이름 | Pinot 타입 |
|---|---|
INT, INTEGER |
INT |
BIGINT, LONG |
LONG |
FLOAT, REAL |
FLOAT |
DOUBLE |
DOUBLE |
DECIMAL, NUMERIC, BIG_DECIMAL |
BIG_DECIMAL |
BOOLEAN |
BOOLEAN |
TIMESTAMP |
TIMESTAMP |
VARCHAR, CHAR, STRING |
STRING |
VARBINARY, BINARY, BYTES |
BYTES |
JSON |
JSON |
SMALLINT와 TINYINT는 의도적으로 거부됩니다. Pinot가 더 좁은 정수 타입을 노출할 때까지 INT를 사용하세요.
비널(non-nullable) 필드를 설정하려면 NOT NULL을, Pinot의 기본 null 값을 설정하려면 DEFAULT를 사용하세요:
CREATE TABLE users (
id INT NOT NULL DIMENSION,
name STRING NOT NULL DEFAULT 'unknown' DIMENSION,
score DOUBLE DEFAULT 0.0 METRIC,
active BOOLEAN DEFAULT TRUE DIMENSION
)
TABLE_TYPE = OFFLINE;
DEFAULT NULL은 거부됩니다. 기본값 리터럴은 선언된 컬럼 타입과 호환되어야 합니다. TIMESTAMP 기본값은 SHOW CREATE TABLE에서 UTC ISO-8601 형태로, BYTES 기본값은 따옴표로 감싼 16진 문자열로 출력됩니다.
속성 매핑
컬럼 목록이나 TABLE_TYPE 절에 없는 테이블 설정 값에는 PROPERTIES (...) 절을 사용하세요. 속성 키와 값은 문자열 리터럴입니다.
PROPERTIES (
'timeColumnName' = 'ts',
'replication' = '3',
'brokerTenant' = 'DefaultTenant',
'serverTenant' = 'DefaultTenant'
)
Pinot는 다음 규칙으로 속성을 라우팅합니다:
| 속성 형태 | 목적지 | 예시 |
|---|---|---|
| 승격된 스칼라 테이블 설정 키 | 전용 TableConfig 필드 |
replication, brokerTenant, serverTenant, timeColumnName, retentionTimeUnit, retentionTimeValue, loadMode, sortedColumn, nullHandlingEnabled, aggregateMetrics, segmentVersion, tags, invertedIndexColumns, noDictionaryColumns, bloomFilterColumns, rangeIndexColumns, jsonIndexColumns |
| JSON blob 테이블 설정 키 | 중첩 TableConfig 객체 |
ingestionConfig, upsertConfig, dedupConfig, routingConfig, queryConfig, quotaConfig, tierConfigs, tunerConfigs, fieldConfigs, instanceAssignmentConfigMap, tagOverrideConfig, starTreeIndexConfigs, segmentPartitionConfig, jsonIndexConfigs |
streamType, stream.*, realtime.* |
실시간 스트림 설정 | stream.kafka.topic.name, stream.kafka.decoder.class.name, stream.kafka.consumer.factory.class.name, realtime.segment.flush.threshold.rows |
task.<taskType>.<key> |
Minion 태스크 설정 | task.RealtimeToOfflineSegmentsTask.bucketTimePeriod |
| 기타 모든 키 | 테이블 커스텀 설정 | owner, team.pipeline, custom.flag |
목록 값 승격 속성은 쉼표로 구분된 문자열을 사용합니다. 예: 'invertedIndexColumns' = 'userId,country'. 스트림·실시간 속성은 REALTIME 테이블에서만 유효합니다.
확장 형태: 옵션 정의 CREATE TABLE
일부 Pinot 배포판은 컨트롤러가 컬럼 목록 대신 WITH 옵션 맵에서 스키마와 테이블 설정을 유도하는 확장 형태를 노출합니다:
CREATE TABLE trips_analytics WITH (
type = 'iceberg',
catalog_type = 'rest',
catalog_uri = 'https://unity-catalog.company.com/api/2.1/unity-catalog/iceberg',
schema_name = 'transportation',
table_name = 'nyc_taxi_trips',
storage.region = 'us-west-2',
refresh_interval = '5m',
enable_schema_evolution = true
);
이 형태에서:
- 키는 따옴표로 감싼 문자열 리터럴이거나 따옴표 없는 식별자일 수 있으며
storage.region같은 점 표기 이름도 포함됩니다. - 값은 따옴표 문자열, 부울, 부호 없는 숫자 리터럴일 수 있습니다.
- Pinot는 설치된 테이블 핸들러에 넘기기 전에 파싱된 옵션을 정렬된 문자열 키/값 쌍으로 정규화합니다.
Apache Pinot OSS는 내장 옵션 정의 테이블 핸들러를 제공하지 않으므로, 기본 컨트롤러 동작은 이 형태를 거부하고 컬럼 목록 CREATE TABLE ... TABLE_TYPE = OFFLINE | REALTIME 문법을 쓰도록 안내합니다.
예제: 오프라인 테이블 생성
CREATE TABLE events (
id INT NOT NULL DIMENSION,
city STRING DIMENSION,
amount DOUBLE METRIC,
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'
)
TABLE_TYPE = OFFLINE
PROPERTIES (
'timeColumnName' = 'ts',
'replication' = '3',
'brokerTenant' = 'DefaultTenant',
'serverTenant' = 'DefaultTenant'
);
그 문장을 POST /sql/ddl로 제출하세요:
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{"sql":"CREATE TABLE events (id INT NOT NULL DIMENSION, city STRING DIMENSION, amount DOUBLE METRIC, ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS') TABLE_TYPE = OFFLINE PROPERTIES ('timeColumnName' = 'ts', 'replication' = '3', 'brokerTenant' = 'DefaultTenant', 'serverTenant' = 'DefaultTenant')"}
EOF
예제: 실시간 Kafka 테이블 생성
CREATE TABLE clicks (
user_id STRING DIMENSION,
url STRING DIMENSION,
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'
)
TABLE_TYPE = REALTIME
PROPERTIES (
'timeColumnName' = 'ts',
'replication' = '2',
'streamType' = 'kafka',
'stream.kafka.topic.name' = 'click_events',
'stream.kafka.decoder.class.name' = 'org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder',
'stream.kafka.consumer.factory.class.name' = 'org.apache.pinot.plugin.stream.kafka30.KafkaConsumerFactory',
'stream.kafka.broker.list' = 'kafka-broker:9092',
'stream.kafka.consumer.prop.auto.offset.reset' = 'smallest',
'realtime.segment.flush.threshold.rows' = '500000'
);
예제: upsert 테이블 생성
실시간 테이블에 PRIMARY KEY를 사용하고 upsert 설정을 JSON 속성으로 전달하세요:
CREATE TABLE upsertOrders (
orderId INT NOT NULL DIMENSION,
userId STRING NOT NULL DIMENSION,
amount DOUBLE METRIC,
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'
)
PRIMARY KEY (orderId)
TABLE_TYPE = REALTIME
PROPERTIES (
'timeColumnName' = 'ts',
'replication' = '2',
'streamType' = 'kafka',
'stream.kafka.topic.name' = 'orders',
'stream.kafka.decoder.class.name' = 'org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder',
'stream.kafka.consumer.factory.class.name' = 'org.apache.pinot.plugin.stream.kafka30.KafkaConsumerFactory',
'stream.kafka.broker.list' = 'kafka-broker:9092',
'stream.kafka.consumer.prop.auto.offset.reset' = 'smallest',
'upsertConfig' = '{"mode":"FULL"}'
);
예제: 다중 값 차원
다중 값 차원에는 DIMENSION ARRAY를 사용하세요:
CREATE TABLE products (
id INT DIMENSION,
tags STRING DIMENSION ARRAY
)
TABLE_TYPE = OFFLINE;
예제: 인덱스와 수집 설정
승격된 목록 속성은 쉼표로 구분된 문자열을, 중첩 테이블 설정은 JSON 문자열을 사용합니다.
CREATE TABLE pageviews (
userId STRING DIMENSION,
country STRING DIMENSION,
payload JSON DIMENSION,
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'
)
TABLE_TYPE = OFFLINE
PROPERTIES (
'timeColumnName' = 'ts',
'invertedIndexColumns' = 'userId,country',
'jsonIndexColumns' = 'payload',
'ingestionConfig' = '{"batchIngestionConfig":{"segmentIngestionType":"APPEND","segmentIngestionFrequency":"DAILY"}}'
);
예제: 태스크 설정
minion 태스크 설정에는 task.<taskType>.<key> 속성을 사용하세요:
CREATE TABLE events (
id INT DIMENSION,
ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'
)
TABLE_TYPE = OFFLINE
PROPERTIES (
'timeColumnName' = 'ts',
'task.RealtimeToOfflineSegmentsTask.bucketTimePeriod' = '1d',
'task.RealtimeToOfflineSegmentsTask.maxNumRecordsPerSegment' = '5000000',
'task.SegmentRefreshTask.tableMaxNumTasks' = '5'
);
예제: 머티리얼라이즈드 뷰 생성·조회
컨트롤러가 MV를 OFFLINE 테이블 + MaterializedViewTask 메타데이터로 영속화하길 원하면 CREATE MATERIALIZED VIEW를 사용하세요. 컬럼 목록은 선택 사항입니다: 생략하면 SELECT 프로젝션에서 스키마를 유도하고, 유도된 타입이나 역할을 덮어쓰려면 전체 컬럼 목록을 주면 됩니다.
CREATE MATERIALIZED VIEW salesByHourMv
REFRESH EVERY 1 HOUR
PROPERTIES (
'timeColumnName' = 'bucket_start_ts',
'bucketTimePeriod' = '1h',
'stalenessThresholdMs' = '900000',
'replication' = '1'
)
AS
SELECT DATETRUNC('HOUR', event_ts) AS bucket_start_ts,
region,
SUM(revenue) AS sum_revenue,
COUNT(*) AS row_count
FROM sales
GROUP BY DATETRUNC('HOUR', event_ts), region;
같은 엔드포인트로 MV를 목록화·조회·삭제하세요:
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"SHOW MATERIALIZED VIEWS"}'
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"SHOW CREATE MATERIALIZED VIEW salesByHourMv"}'
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"DROP MATERIALIZED VIEW IF EXISTS salesByHourMv"}'
SHOW CREATE MATERIALIZED VIEW는 원래 create 문장이 유도 컬럼에 의존했더라도 명시적 컬럼 목록을 포함해 저장된 MV 정의의 표준 DDL을 출력합니다.
예제: 저장된 테이블 정의 보기
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"SHOW CREATE TABLE events TYPE OFFLINE"}'
응답 예시:
{
"operation": "SHOW_CREATE_TABLE",
"tableName": "events_OFFLINE",
"tableType": "OFFLINE",
"ddl": "CREATE TABLE events (\n id INT NOT NULL DIMENSION,\n city STRING DIMENSION,\n amount DOUBLE METRIC,\n ts TIMESTAMP DATETIME FORMAT 'TIMESTAMP' GRANULARITY '1:MILLISECONDS'\n)\nTABLE_TYPE = OFFLINE\nPROPERTIES (\n 'brokerTenant' = 'DefaultTenant',\n 'replication' = '3',\n 'serverTenant' = 'DefaultTenant',\n 'timeColumnName' = 'ts'\n)",
"message": "Rendered canonical CREATE TABLE for events_OFFLINE."
}
표준 DDL은 속성에 결정적 순서를 사용하고 Pinot가 표준 형태로 저장하는 값을 정규화합니다. 예를 들어 TIMESTAMP 날짜-시간 컬럼은 FORMAT 'TIMESTAMP'를 출력하고, 부울 기본값은 TRUE·FALSE를 출력하며, 따옴표가 필요한 식별자는 큰따옴표로 감싸집니다.
예제: 테이블 목록화와 삭제
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-H "Database: analytics" \
-d '{"sql":"SHOW TABLES"}'
curl -X POST "http://localhost:9000/sql/ddl" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{"sql":"DROP TABLE events TYPE OFFLINE"}'
TYPE 없이 DROP TABLE events를 쓰면 오프라인·실시간 변형이 모두 존재할 때 둘 다 삭제합니다. 테이블이 없어서 성공적인 no-op이 되길 원하면 DROP TABLE IF EXISTS events를 사용하세요.
JSON API를 계속 쓸 때
이미 POST /schemas, POST /tables, PUT /tables/{tableName}으로 테이블 메타데이터를 관리하고 있다면 그 API는 계속 동작합니다. SQL DDL은 기존 컨트롤러 메타데이터 API를 대체하는 게 아니라 추가 인터페이스입니다. 머티리얼라이즈드 뷰의 경우 REFRESH EVERY <N> MINUTES|HOURS|DAYS 또는 '<N>m|h|d'로 표현할 수 없는 MaterializedViewTask 스케줄이 필요하면 원시 JSON 메타데이터를 계속 사용하세요.