인덱싱

인덱싱 (Indexing - indexing)

DuckDB에는 zonemapART 인덱스라는 두 종류의 인덱스가 있어요. 다만 일반 RDBMS처럼 "무조건 인덱스를 많이 만들수록 좋다"는 생각과는 결이 달라요. 각 인덱스가 언제, 어떻게 작동하는지 이해해야 제대로 활용할 수 있답니다.

출처: DuckDB 공식 문서 — indexing

개요 (Overview)

DuckDB에는 두 종류의 인덱스가 있어요. zonemapART 인덱스죠. 둘의 역할과 특성이 다르니 하나씩 살펴볼게요.

Zonemaps

DuckDB는 모든 범용 데이터 타입 컬럼에 대해 zonemap(일명 min-max 인덱스)을 자동으로 생성해요. 스캔 연산자로의 프레디킷 푸시다운(predicate pushdown)이나 집계 계산 같은 연산이 zonemap을 사용해요.

WHERE column1 = 123 같은 필터 기준이 사용되면, DuckDB는 min-max 범위가 그 필터 값을 포함하지 않는 row group을 건너뛸 수 있어요. 예를 들어 min-max 범위가 1000~2000인 블록은 = 123이나 < 400 비교 시 생략할 수 있죠.

Zonemaps에서 정렬의 효과 (The Effect of Ordering on Zonemaps)

컬럼 내 데이터가 정렬될수록 zonemap 인덱스의 가치가 커져요. 최악의 경우 컬럼이 매 행마다 난수를 담고 있을 수 있는데, 그러면 DuckDB는 어떤 row group도 건너뛰지 못할 가능성이 높아요.

선택적(selective) 필터로 특정 컬럼을 쿼리한다면, 데이터를 삽입할 때 그 컬럼별로 미리 정렬하는 게 좋아요. 완벽하지 않은 정렬조차도 여전히 도움이 돼요. 정렬된 데이터의 가장 좋은 사례는 흔히 DATETIME 컬럼에서 나와요.

마이크로벤치마크: 정렬의 효과 (Microbenchmark)

타임스탬프 마이크로벤치마크를, 오름차순으로 정렬된 타임스탬프 컬럼과 정렬되지 않은 컬럼으로 반복해 볼게요.

Column type Ordered Storage size Query time
DATETIME yes 1.3 GB 0.6 s
DATETIME no 3.3 GB 0.9 s

결과를 보면 단순히 컬럼 순서를 유지하는 것만으로도 압축이 개선되어 저장 크기가 2.5배 작아지고, 계산도 1.5배 빨라져요.

정렬된 정수 (Ordered Integers)

정렬을 활용하는 또 다른 실용적인 방법은, 선택적 필터로 쿼리되는 컬럼에 UUID 대신 자동 증가하는 INTEGER 타입을 쓰는 거예요. 테이블이 순서가 뒤섞인 UUID를 담고 있다면, 특정 UUID 값을 찾기 위해 많은 row group을 스캔해야 해요. 반면 정렬된 INTEGER 컬럼은 그 값을 담은 row group을 제외한 나머지를 모두 건너뛸 수 있어요.

ART 인덱스 (ART Indexes)

DuckDB는 Adaptive Radix Tree (ART) 인덱스를 두 가지 방식으로 정의할 수 있어요.

  • 첫째, PRIMARY KEY, FOREIGN KEY, UNIQUE 제약 조건이 있는 컬럼에 대해 암시적으로 생성돼요.
  • 둘째, CREATE INDEX 문을 명시적으로 실행하면 대상 컬럼에 ART 인덱스를 생성해요.

컬럼에 ART 인덱스를 두는 것의 트레이드오프는 다음과 같아요.

  1. ART 인덱스는 변경(insert, update, delete) 시 제약 조건 검사를 가능하게 해줘요.
  2. 인덱스된 테이블의 변경은 인덱스가 없는 테이블보다 성능이 나빠요. 인덱스 유지보수 때문이죠.
  3. 일부 사용 사례에서 단일 컬럼 ART 인덱스는 인덱스된 컬럼을 쓰는 고도로 선택적인 쿼리의 성능을 개선해요.

ART 인덱스는 조인, 집계, 정렬 쿼리의 성능에는 영향을 주지 않아요.

ART 인덱스 스캔 (ART Index Scans)

ART 인덱스 스캔은 테이블을 순차 스캔하는 대신 요청된 데이터에 대해 단일 컬럼 ART 인덱스를 프로브해요. 프로빙은 일부 쿼리의 성능을 개선할 수 있어요. DuckDB는 동등(=) 조건과 IN(...) 조건에 대해 인덱스 스캔을 사용하려고 해요. 또 해시 조인 같은 데서 나온 동적 필터를 스캔으로 푸시해서, 그 필터에 대해 동적 인덱스 스캔을 가능하게 해요.

인덱스 스캔에 적합한 인덱스는 표현식 없이 단일 컬럼을 인덱스하는 경우뿐이에요. 예를 들어 다음 인덱스는 인덱스 스캔에 적합해요.

CREATE INDEX idx ON tbl (col1);

반면 다음 두 인덱스는 인덱스 스캔에 적합하지 않아요.

CREATE INDEX idx_multi_column ON tbl (col1, col2);
CREATE INDEX idx_expr ON tbl (col1 + 1);

인덱스 스캔의 기본 임계값은 MAX(2048, 0.001 * table_cardinality)예요. 이 임계값은 index_scan_percentageindex_scan_max_count로 설정하거나, 이 값을 0으로 설정해 비활성화할 수 있어요. 확실하지 않으면 EXPLAIN ANALYZE로 쿼리 플랜이 인덱스 스캔을 사용하는지 확인해 보세요.

인덱스와 메모리 (Indexes and Memory)

DuckDB는 버퍼 매니저를 통해 인덱스 메모리를 등록해요. 하지만 이 인덱스 버퍼는 아직 버퍼 관리되지 않아요. 즉 DuckDB는 메모리를 축출해야 할 때 인덱스 버퍼를 파괴하지 않아요. 따라서 인덱스는 DuckDB의 사용 가능한 메모리 중 상당한 부분을 차지할 수 있고, 메모리 집약적 쿼리의 성능에 영향을 줄 수 있어요. 인덱스를 포함한 데이터베이스를 다시 연결(DETACH + ATTACH)하면 인덱스 메모리를 지연(lazily) 역직렬화하므로 이 영향을 완화할 수 있어요. 변경 후 인덱스 스캔을 비활성화하고 다시 연결하면 인덱스가 DuckDB 사용 가능 메모리에 미치는 영향을 더 줄일 수 있어요.

인덱스와 데이터베이스 열기 (Indexes and Opening Databases)

인덱스는 디스크에 직렬화되고 지연 역직렬화돼요. 즉 데이터베이스를 다시 열 때 역직렬화되죠. 인덱스를 사용하는 연산은 인덱스의 필요한 부분만 로드해요. 따라서 인덱스가 있다고 해서 기존 데이터베이스를 열 때 느려지지는 않아요.

Best practice 다음 지침을 권장해요.

  • 제약 조건을 데이터에 적용하기 위해 꼭 필요한 경우에만 기본 키(primary key), 외래 키(foreign key), 유니크(unique) 제약 조건을 사용하세요.
  • 고도로 선택적인 쿼리가 있고 사용 가능한 메모리가 충분하지 않다면 명시적 인덱스를 정의하지 마세요.
  • ART 인덱스를 정의한다면, 데이터를 테이블에 벌크 로드한 후에 하세요. 로드 전에 (명시적으로든 기본/외래 키를 통해서든) 인덱스를 추가하는 것은 로드 성능에 해로워요.

더 알아보기 (Learn more)