쿼리 성능 이해하기
일반적인 고려 사항
- 쿼리 파싱 및 분석
- 쿼리 최적화
- 쿼리 파이프라인 실행
- 최종 처리
데이터셋
느린 쿼리 찾기
쿼리 로그
system.query_log 테이블에 저장됩니다.
ClickHouse는 실행된 각 쿼리에 대해 쿼리 실행 시간, 읽은 행 수, CPU, 메모리 사용량, 파일 시스템 캐시 적중 횟수와 같은 리소스 사용량 등의 통계를 기록합니다.
따라서 느린 쿼리를 조사할 때 쿼리 로그는 가장 먼저 살펴보기 좋은 곳입니다. 실행 시간이 오래 걸리는 쿼리를 쉽게 찾아내고 각 쿼리의 리소스 사용량 정보도 확인할 수 있습니다.
이제 NYC taxi 데이터셋에서 실행 시간이 가장 긴 상위 5개의 쿼리를 찾아보겠습니다.
query_duration_ms 필드는 해당 쿼리의 실행에 걸린 시간을 나타냅니다. 쿼리 로그의 결과를 보면 첫 번째 쿼리의 실행에 2967ms가 걸리므로, 개선의 여지가 있음을 알 수 있습니다.
메모리나 CPU를 가장 많이 사용하는 쿼리를 살펴보면 어떤 쿼리가 시스템에 부하를 주고 있는지도 파악할 수 있습니다.
enable_filesystem_cache 설정을 0으로 지정해 파일 시스템 캐시를 비활성화하는 것이 매우 중요합니다.
각 쿼리가 무엇을 하는지 조금 더 자세히 살펴보겠습니다.
- 쿼리 1은 시속 평균 30마일을 초과하는 운행의 거리 분포를 계산합니다.
- 쿼리 2는 주별 운행 건수와 평균 비용을 구합니다.
- 쿼리 3은 데이터셋에서 각 이동의 평균 시간을 계산합니다.
Explain 구문
nyc_taxi.trips_small_inferred 테이블에서 데이터를 읽습니다. 이후 WHERE 절이 적용되어 계산된 값을 기준으로 행을 필터링합니다. 필터링된 데이터는 집계를 위해 준비되고, 분위수가 계산됩니다. 마지막으로 결과가 정렬되어 출력됩니다.
여기서 프라이머리 키(primary key)가 사용되지 않은 것을 확인할 수 있습니다. 테이블 생성 시 프라이머리 키를 정의하지 않았으므로 당연한 결과입니다. 이로 인해 ClickHouse는 해당 쿼리에 대해 테이블 전체를 풀 스캔하고 있습니다.
파이프라인 설명(Explain Pipeline)
EXPLAIN Pipeline은 쿼리의 구체적인 실행 전략을 보여줍니다. 여기서 ClickHouse가 앞서 살펴본 일반적인 쿼리 플랜(query plan)을 실제로 어떻게 실행하는지 확인할 수 있습니다.
방법론
system.query_logs의 user, tables, databases 필드를 사용해 검색 범위를 좁힐 수 있습니다.
최적화할 쿼리를 식별했다면 해당 쿼리 개선 작업을 시작할 수 있습니다. 이 단계에서 개발자가 흔히 저지르는 실수 중 하나는 여러 사항을 동시에 변경하고, ad-hoc 실험을 수행한 뒤, 대개 일관성 없는 결과만 얻게 되는 것입니다. 더 중요한 문제는 무엇이 쿼리를 더 빠르게 만들었는지 제대로 이해하지 못한다는 점입니다.
쿼리 최적화에는 체계적인 접근이 필요합니다. 고급 벤치마크를 하자는 뜻은 아니지만, 변경 사항이 쿼리 성능에 어떤 영향을 미치는지 파악할 수 있도록 간단한 절차를 마련해 두는 것만으로도 큰 도움이 됩니다.
먼저 쿼리 로그에서 느린 쿼리를 식별한 다음, 가능한 개선 사항을 개별적으로 검토하십시오. 쿼리를 테스트할 때는 파일 시스템 캐시를 반드시 비활성화하세요.
ClickHouse는 여러 단계에서 캐싱을 활용해 쿼리 성능을 높입니다. 이는 쿼리 성능에는 유리하지만, 트러블슈팅 중에는 잠재적인 I/O 병목이나 비효율적인 테이블 스키마를 가릴 수 있습니다. 이런 이유로 테스트 중에는 파일 시스템 캐시를 끄는 것을 권장합니다. 프로덕션 환경에서는 반드시 다시 활성화해 두십시오.가능한 최적화 방안을 식별했다면, 성능에 미치는 영향을 더 잘 추적할 수 있도록 이를 하나씩 적용하는 것이 좋습니다. 아래는 일반적인 접근 방식을 설명하는 다이어그램입니다. 마지막으로, 이상치에 주의하십시오. 사용자가 ad-hoc으로 비용이 큰 쿼리를 실행했거나 다른 이유로 시스템에 부하가 걸려 쿼리가 느리게 실행되는 경우는 매우 흔합니다.
normalized_query_hash 필드로 그룹화하면 정기적으로 실행되는 고비용 쿼리를 식별할 수 있습니다. 이런 쿼리를 우선 조사하는 것이 좋습니다.
기본 최적화
널 허용
mta_tax와 payment_type 두 개뿐입니다. 나머지 필드에는 Nullable 컬럼을 사용하지 않아야 합니다.
낮은 카디널리티
ratecode_id, pickup_location_id, dropoff_location_id, vendor_id 4개의 컬럼은 LowCardinality 필드 유형에 적합한 후보입니다.
데이터 타입 최적화
최적화를 적용합니다
쿼리 시간과 메모리 사용량 모두에서 일부 개선을 확인할 수 있습니다. 데이터 스키마 최적화 덕분에 데이터를 표현하는 전체 데이터 양이 줄어들어 메모리 사용량이 감소하고 처리 시간도 단축됩니다.
차이를 확인하기 위해 테이블 크기를 살펴보겠습니다.
기본 키의 중요성
ClickHouse의 그래뉼은 쿼리 실행 중 데이터를 읽는 가장 작은 단위입니다. 그래뉼에는 index_granularity에 따라 결정되는 일정 개수 이하의 행이 포함되며, 기본값은 8192행입니다. 그래뉼은 연속적으로 저장되며 기본 키 순서로 정렬됩니다.
적절한 기본 키 집합을 선택하는 것은 성능에 매우 중요합니다. 실제로는 특정 쿼리 집합의 속도를 높이기 위해 동일한 데이터를 서로 다른 테이블에 저장하고, 각 테이블에 서로 다른 기본 키 집합을 사용하는 경우도 흔합니다.
Projection이나 구체화된 뷰(Materialized View)와 같이 ClickHouse가 지원하는 다른 옵션을 사용하면 동일한 데이터에 서로 다른 기본 키 집합을 적용할 수 있습니다. 이 블로그 시리즈의 두 번째 파트에서 이를 더 자세히 다룰 예정입니다.
기본 키 선택
- 대부분의 쿼리에서 필터링에 사용되는 필드를 사용합니다
- 카디널리티가 낮은 컬럼을 먼저 선택합니다
- 타임스탬프 데이터셋에서는 시간 기준 필터링이 매우 흔하므로, 기본 키에 시간 기반 요소를 포함하는 것을 고려합니다.
passenger_count, pickup_datetime, dropoff_datetime를 기본 키로 실험해 보겠습니다.
passenger_count의 카디널리티는 낮고(고유값 24개) 느린 쿼리에서 사용됩니다. 또한 자주 필터링될 수 있으므로 타임스탬프 필드(pickup_datetime 및 dropoff_datetime)도 추가합니다.
기본 키를 포함한 새 테이블을 만들고 데이터를 다시 수집합니다.
| 쿼리 1 | |||
|---|---|---|---|
| 실행 1 | 실행 2 | 실행 3 | |
| 경과 시간 | 1.699 sec | 1.353 sec | 0.765 sec |
| 처리된 행 수 | 329.04 million | 329.04 million | 329.04 million |
| 최대 메모리 사용량 | 440.24 MiB | 337.12 MiB | 444.19 MiB |
| 쿼리 2 | |||
|---|---|---|---|
| 실행 1 | 실행 2 | 실행 3 | |
| 경과 시간 | 1.419 sec | 1.171 sec | 0.248 sec |
| 처리된 행 수 | 329.04 million | 329.04 million | 41.46 million |
| 최대 메모리 사용량 | 546.75 MiB | 531.09 MiB | 173.50 MiB |
| 쿼리 3 | |||
|---|---|---|---|
| 실행 1 | 실행 2 | 실행 3 | |
| 경과 시간 | 1.414 sec | 1.188 sec | 0.431 sec |
| 처리된 행 | 329.04 million | 329.04 million | 276.99 million |
| 최대 메모리 | 451.53 MiB | 265.05 MiB | 197.38 MiB |