SQL DDL

SQL DDL (SQL DDL)

컨트롤러가 관리하는 SQL DDL로 Pinot 테이블과 머티리얼라이즈드 뷰를 만들고, 조회하고, 목록화하고, 삭제하는 방법을 다룹니다.

출처: SQL DDL

본문

Pinot는 테이블과 머티리얼라이즈드 뷰 메타데이터 작업을 위한 컨트롤러 관리 SQL DDL 표면을 지원합니다. JSON 기반 /schemas, /tables 워크플로 대신 SQL 방식으로 하고 싶을 때 사용하세요.

컨트롤러는 요청당 하나의 DDL 문장을 받습니다:

  • CREATE TABLE
  • DROP TABLE
  • SHOW TABLES
  • SHOW CREATE TABLE
  • CREATE MATERIALIZED VIEW
  • SHOW MATERIALIZED VIEWS
  • SHOW CREATE MATERIALIZED VIEW
  • DROP 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 Created
  • CREATE MATERIALIZED VIEW 성공 시 201 Created
  • DROP 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 메타데이터를 계속 사용하세요.

더 알아보기 (Learn more)