Materialization·Index·통계
CTAS, persistent table, zonemap, ART index, ANALYZE를 DuckDB 성능 판단으로 연결합니다.
핵심 요약
- materialization은 속도용 복사가 아니라 통계와 반복 실행을 위해 상태를 남길지 정하는 일이며, 같은 파일을 반복해서 읽거나 join 계획이 불안정하거나 CSV/JSON을 반복 읽거나 결과가 downstream 계약이면 적재 신호입니다.
- zonemap은 일반 type 컬럼에 자동 생성되어 min/max로 row group을 건너뛰므로, 자주 필터링하는 날짜·tenant·region 기준으로 ORDER BY 정렬해두면 이득이 큽니다.
- ART index는 선택도가 높은 단일 컬럼 equality·IN과 constraint check에 유리합니다. join·집계·정렬용 만능 인덱스가 아니며, scan 단계의 동적 필터 활용은 실행 계획으로 판단합니다.
- VIEW는 항상 다시 계산되므로 반복 비용이 크거나 특정 시점 snapshot이 필요하면 CTAS로 table에 materialize합니다.
DuckDB에서 materialization은 "속도를 위해 복사할지"가 아니라 "통계와 반복 실행을 위해 상태를 남길지"를 정하는 일입니다. 파일을 바로 읽는 쪽이 단순하지만, 반복 join이나 복잡한 view가 많고 decompression 비용이 높거나 join order가 어긋나면 적재가 낫습니다.
Materialization 기준
| 신호 | 판단 |
|---|---|
| 같은 Parquet를 여러 번 읽는다 | DuckDB table 또는 중간 Parquet 생성 |
| join이 많고 계획이 흔들린다 | DuckDB table로 적재 후 통계 활용 |
| CSV/JSON을 반복 읽는다 | Parquet 또는 DuckDB table로 변환 |
| 결과가 downstream 계약이다 | curated Parquet로 publish |
| 탐색성 ad hoc 쿼리다 | 직접 scan 유지 |
CREATE OR REPLACE TABLE orders AS
SELECT *
FROM read_parquet('raw/orders/**/*.parquet', hive_partitioning = true);
ANALYZE orders;Zonemap
DuckDB는 일반-purpose type 컬럼에 zonemap을 자동 생성합니다. min/max 범위로 row group을 건너뛰므로, 자주 필터링하는 컬럼 기준으로 데이터가 어느 정도 정렬돼 있으면 이득이 큽니다.
CREATE OR REPLACE TABLE events_ordered AS
SELECT *
FROM events
ORDER BY event_date, tenant_id;날짜, 증가 integer, tenant, region처럼 selective filter가 반복되는 컬럼은 정렬 후보입니다.
ART index
ART index는 primary key나 unique constraint에서 암묵적으로 생기거나 CREATE INDEX로 직접 만듭니다. 다만 모든 분석 쿼리를 빠르게 해주는 만능 도구는 아닙니다.
| ART index가 유리한 경우 | 피해야 할 경우 |
|---|---|
| 단일 컬럼 equality lookup | 대규모 scan, aggregate |
IN (...) 조건 | join/aggregation/sort 가속 기대 |
| constraint check가 필요 | write-heavy table |
ART를 join·aggregation·sort 자체를 가속하는 인덱스로 보지 않습니다. 다만 hash join에서 내려온 dynamic filter가 scan의 ART 탐색을 유도할 수 있습니다. 단일 컬럼·비표현식 인덱스인지, 실제 계획에서 index scan이 선택됐는지 확인합니다. ART index scans.
CREATE INDEX idx_customers_id ON customers(customer_id);View와 table
CREATE OR REPLACE VIEW active_customers AS
SELECT *
FROM customers
WHERE status = 'active';
CREATE OR REPLACE TABLE active_customers_snapshot AS
SELECT *
FROM active_customers;view는 호출할 때마다 다시 계산됩니다. 반복 비용이 크거나 특정 시점 snapshot이 필요하면 table로 materialize합니다.
2.0 트리거: 변경 내역과 증분 유지
CREATE TRIGGER로 테이블 변경에 반응하는 SQL을 등록합니다. 배치 처리에는 statement 단위와 transition table이 유용합니다. 다음 예제는 변경 전후 값을 남기며, 조인 키 id가 바뀌지 않는다는 전제를 둡니다.
CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER);
CREATE TABLE balance_audit (id INTEGER, before_balance INTEGER, after_balance INTEGER);
CREATE TRIGGER audit_balance
AFTER UPDATE ON accounts
REFERENCING OLD TABLE AS before_rows NEW TABLE AS after_rows
FOR EACH STATEMENT
INSERT INTO balance_audit
SELECT a.id, b.balance, a.balance
FROM before_rows b JOIN after_rows a USING (id);
INSERT INTO accounts VALUES (1, 100), (2, 200);
UPDATE accounts SET balance = balance + 10;
SELECT * FROM balance_audit ORDER BY id; -- (1,100,110), (2,200,210)트리거로 집계 테이블을 유지할 수 있지만 자동 materialized view 관리와는 다릅니다. 초기 적재, UPDATE/DELETE 보정, 재처리 중복, 전체 재계산과의 일치를 직접 설계합니다.
2.0에서는 BEFORE ... FOR EACH ROW, row 단위 UPDATE, view의 트리거, 임시 트리거를 지원하지 않습니다. 트리거가 있는 테이블의 MERGE INTO와 row 트리거의 RETURNING에도 제한이 있습니다. ON CONFLICT 계열 적재와 함께 쓰기 전에 지원 조합을 확인합니다. CREATE TRIGGER와 제한.