스키마
스키마 (Schema) — 성능 관점 (schema)
스키마를 어떻게 설계하느냐에 따라 같은 쿼리라도 처리 속도와 저장 공간이 크게 달라질 수 있어요. 이 페이지에서는 타입 선택과 제약 조건(constraint)이 성능에 어떤 영향을 주는지, 그리고 실제 마이크로벤치마크 결과로 그 차이를 확인해 볼게요.
타입 (Types)
컬럼을 인코딩할 때 올바른 타입을 사용하는 것이 중요해요(예: BIGINT, DATE, DATETIME). 더 구체적인 값을 문자열 타입(VARCHAR 등)으로 인코딩하는 것이 항상 가능하긴 하지만, 권장되지는 않아요. 문자열은 더 많은 공간을 차지하고 필터링(filtering), 조인(join), 집계(aggregation) 같은 연산에서 처리 속도도 더 느립니다. CSV 파일을 로드할 때는 CSV 리더의 자동 감지(autodetection) 메커니즘을 활용해 CSV 입력에 맞는 올바른 타입을 결정할 수 있어요.
메모리가 제한된 환경에서 실행한다면, 더 작은 데이터 타입(예: TINYINT)을 사용하면 쿼리를 완료하는 데 필요한 메모리와 디스크 공간을 줄일 수 있어요. DuckDB의 비트패킹 압축(bitpacking compression) 덕분에 더 큰 데이터 타입에 저장된 작은 값은 디스크에서 더 큰 공간을 차지하지 않지만, 처리 중에는 더 많은 메모리를 차지해요. 그래서 컬럼을 만들 때 가능한 한 가장 제한적인 타입을 사용하는 것이 메모리 소비를 줄이는 데 필요해요.
마이크로벤치마크: 타임스탬프 사용 (Using Timestamps)
스케일 팩터 300의 LDBC Comment 테이블 creationDate 컬럼을 사용해 집계 속도 차이를 보여드릴게요. 이 테이블은 약 5억 5,400만 개의 순서 없는 타임스탬프 값을 갖고 있어요. 두 가지 설정에서 타임스탬프로부터 월(day-of-the-month)의 평균을 반환하는 간단한 집계 쿼리를 실행합니다.
먼저 DATETIME으로 값을 인코딩하고 extract 날짜 함수를 사용해 쿼리를 실행할게요:
SELECT avg(extract('day' FROM creationDate)) FROM Comment;
둘째로 VARCHAR 타입을 사용하고 문자열 연산을 적용합니다:
SELECT avg(CAST(creationDate[9:10] AS INTEGER)) FROM Comment;
마이크로벤치마크 결과는 다음과 같아요:
| Column type | Storage size | Query time |
|---|---|---|
DATETIME |
3.3 GB | 0.9 s |
VARCHAR |
5.2 GB | 3.9 s |
결과를 보면 DATETIME 값을 쓰는 것이 더 작은 저장 공간과 더 빠른 처리를 가져다줘요.
마이크로벤치마크: 문자열 조인 (Joining on Strings)
스케일 팩터 100의 LDBC Comment 테이블에서 셀프 조인(self-join)을 계산해 서로 다른 타입으로 조인할 때의 차이를 보여드릴게요. 이 테이블은 각 행의 id 속성으로 사용되는 64비트 정수 식별자를 갖고 있어요. 다음 조인 연산을 수행합니다:
SELECT count(*) AS count
FROM Comment c1
JOIN Comment c2 ON c1.ParentCommentId = c2.id;
첫 번째 실험에서는 올바른(가장 제한적인) 타입을 사용해요. 즉 id와 ParentCommentId 컬럼 모두 BIGINT로 정의합니다. 두 번째 실험에서는 모든 컬럼을 VARCHAR 타입으로 정의해요. 두 실험의 쿼리 결과는 같지만 실행 시간은 크게 다릅니다. 아래 결과를 보면 BIGINT 컬럼으로 조인하는 것이 같은 값을 인코딩한 VARCHAR 타입 컬럼으로 조인하는 것보다 약 1.8배 빠르다는 걸 알 수 있어요.
| Join column payload type | Join column schema type | Example value | Query time |
|---|---|---|---|
BIGINT |
BIGINT |
70368755640078 |
1.2 s |
BIGINT |
VARCHAR |
'70368755640078' |
2.1 s |
모범 사례(Best practice) — 특히 조인 연산을 수행하려 한다면 숫자 값을 문자열로 표현하지 마세요.
제약 조건 (Constraints)
DuckDB는 UNIQUE, PRIMARY KEY, FOREIGN KEY 같은 제약 조건을 정의할 수 있게 해줘요. 이런 제약 조건은 데이터 무결성(integrity)을 보장하는 데 유용하지만, 인덱스를 만들고 검사(check)를 수행해야 하므로 로드 성능에는 부정적인 영향을 줘요. 게다가 이 인덱스들은 조인과 집계 연산자에 사용되지 않기 때문에, 제약 조건이 쿼리 성능을 향상시키는 경우는 매우 드물어요 (인덱싱 페이지 참고).
모범 사례(Best practice) — 목표가 데이터 무결성 보장이 아니라면 제약 조건을 정의하지 마세요.
마이크로벤치마크: 기본 키의 효과 (The Effect of Primary Keys)
스케일 팩터 300의 LDBC Comment 테이블로 기본 키 사용의 효과를 보여드릴게요. 이 테이블은 약 5억 5,400만 개의 항목을 갖고 있어요. 첫 번째 실험에서는 기본 키 있이 스키마를 만든 뒤 데이터를 로드합니다. 두 번째 실험에서는 기본 키 없이 스키마를 만든 뒤 데이터를 로드해요. 세 번째 실험에서는 기본 키 없이 스키마를 만들고, 데이터를 로드한 뒤 기본 키 제약을 추가합니다. 모든 경우에 데이터는 .csv.gz 파일에서 가져오고, 로드에 걸린 시간을 측정했어요.
| Operation | Execution time |
|---|---|
| Load with primary key | 461.6 s |
| Load without primary key | 121.0 s |
| Load without primary key then add primary key | 242.0 s |
이 데이터셋에서 기본 키는 단일 식별자로 필터링하는 것 같은 선택도(selectivity)가 매우 높은 쿼리에만 (작은) 긍정적인 효과가 있어요. 기본 키(또는 인덱스)를 정의해도 조인과 집계 연산자에는 영향을 주지 않습니다.
모범 사례(Best practice) — 벌크 로드(bulk load) 성능을 최대로 하려면 기본 키 제약을 피하세요. 기본 키가 필요하다면 벌크 로드 단계 이후에 정의하세요.