Skip to main content
쿼리 캐시를 사용하면 SELECT 쿼리를 한 번만 실행하고, 동일한 쿼리의 이후 실행 결과는 캐시에서 직접 제공할 수 있습니다. 쿼리 유형에 따라 ClickHouse 서버의 지연 시간과 리소스 사용량을 크게 줄일 수 있습니다.

배경, 설계 및 제한 사항

쿼리 캐시는 일반적으로 트랜잭션 일관성을 보장하는 방식과 그렇지 않은 방식으로 나눌 수 있습니다.
  • 트랜잭션 일관성을 보장하는 캐시에서는 SELECT 쿼리의 결과가 변경되었거나 변경되었을 가능성이 있는 경우 데이터베이스가 캐시된 쿼리 결과를 무효화(폐기)합니다. ClickHouse에서 데이터를 변경하는 작업에는 테이블에 대한/에서의 삽입/업데이트/삭제 또는 축약 머지가 포함됩니다. 트랜잭션 일관성을 보장하는 캐싱은 특히 OLTP 데이터베이스에 적합하며, 예를 들어 MySQL (v8.0 이후 쿼리 캐시가 제거됨)와 Oracle이 있습니다.
  • 트랜잭션 일관성을 보장하지 않는 캐시에서는 모든 엔트리에 유효 기간이 할당되어 해당 기간이 지나면 만료되고(예: 1분), 그 기간 동안 기반 데이터가 거의 변경되지 않는다는 가정하에 쿼리 결과의 약간의 부정확성을 허용합니다. 이러한 방식은 전반적으로 OLAP 데이터베이스에 더 적합합니다. 트랜잭션 일관성을 보장하지 않는 캐싱으로도 충분한 예로, 여러 사용자가 동시에 접근하는 리포팅 도구의 시간별 매출 보고서를 생각해 볼 수 있습니다. 일반적으로 매출 데이터는 변화 속도가 충분히 느리므로 데이터베이스는 보고서를 한 번만 컴퓨트하면 됩니다(첫 번째 SELECT 쿼리로 표현됨). 이후의 쿼리는 쿼리 캐시에서 직접 제공할 수 있습니다. 이 예시에서 합리적인 유효 기간은 30분일 수 있습니다.
트랜잭션 일관성을 보장하지 않는 캐싱은 전통적으로 데이터베이스와 상호작용하는 클라이언트 도구나 프록시 패키지(예: chproxy)에서 제공되었습니다. 그 결과 동일한 캐싱 로직과 구성이 중복되는 경우가 많습니다. ClickHouse의 쿼리 캐시를 사용하면 캐싱 로직이 서버 측으로 이동합니다. 이를 통해 유지보수 부담을 줄이고 중복을 피할 수 있습니다.

구성 설정 및 사용

ClickHouse Cloud에서는 쿼리 캐시 설정을 변경하려면 쿼리 수준 설정을 사용해야 합니다. 현재는 구성 수준 설정 변경은 지원되지 않습니다.
clickhouse-local은 한 번에 하나의 쿼리만 실행합니다. 단일 프로세스에서는 쿼리 결과를 메모리에 캐싱해도 의미가 없으므로 clickhouse-local에서는 인메모리 쿼리 캐시가 비활성화되어 있습니다. 대신 디스크 기반 쿼리 캐시는 사용할 수 있습니다. 해당 엔트리는 디스크의 파일 시스템 캐시에 저장되므로, 한 clickhouse-local 프로세스가 캐시한 결과를 동일한 파일 시스템 캐시 디렉터리를 사용하는 이후 프로세스에서 그대로 활용할 수 있습니다.
설정 use_query_cache를 사용하면 특정 쿼리 또는 현재 세션의 모든 쿼리에서 쿼리 캐시를 사용할지 제어할 수 있습니다. 예를 들어, 다음 쿼리를 처음 실행할 때
쿼리 결과를 쿼리 캐시에 저장합니다. 이후 동일한 쿼리를 다시 실행하면(use_query_cache = true 매개변수도 함께 사용한 경우), 계산된 결과를 캐시에서 읽어 즉시 반환합니다.
use_query_cache 설정과 그 밖의 모든 쿼리 캐시 관련 설정은 독립 실행형 SELECT SQL 문에만 적용됩니다. 특히, CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true로 생성된 뷰에 대해 실행한 SELECT의 결과는 해당 SELECT SQL 문을 SETTINGS use_query_cache = true로 실행하지 않는 한 캐시되지 않습니다.
캐시 활용 방식은 enable_writes_to_query_cache 및 enable_reads_from_query_cache 설정(두 설정의 기본값은 모두 true)으로 더 세부적으로 구성할 수 있습니다. 앞의 설정은 쿼리 결과를 캐시에 저장할지 제어하고, 뒤의 설정은 데이터베이스가 쿼리 결과를 캐시에서 가져오도록 시도할지 결정합니다. 예를 들어, 다음 쿼리는 캐시를 수동적으로만 사용합니다. 즉, 캐시에서 읽기만 시도하고 그 결과를 캐시에 저장하지는 않습니다:
최대한 세밀하게 제어하려면 일반적으로 설정 use_query_cache, enable_writes_to_query_cache 및 enable_reads_from_query_cache는 특정 쿼리에만 지정하는 것이 좋습니다. 사용자 또는 프로필 수준에서 캐싱을 활성화하는 것도 가능하지만(예: SET use_query_cache = true), 이 경우 모든 SELECT 쿼리가 캐시된 결과를 반환할 수 있다는 점을 염두에 두어야 합니다. 쿼리 캐시는 SYSTEM CLEAR QUERY CACHE 문을 사용해 비울 수 있습니다. 쿼리 캐시의 내용은 시스템 테이블 system.query_cache에 표시됩니다. 데이터베이스 시작 이후 발생한 쿼리 캐시 hits 및 미스 횟수는 시스템 테이블 system.events의 이벤트 “QueryCacheHits” 및 “QueryCacheMisses”로 표시됩니다. 두 카운터는 설정 use_query_cache = true로 실행된 SELECT 쿼리에 대해서만 갱신되며, 다른 쿼리는 “QueryCacheMisses”에 영향을 주지 않습니다. 이 두 카운터는 쿼리 캐시 전체를 기준으로 집계됩니다. 디스크 기반 쿼리 캐시가 활성화된 경우(아래 참고), 메모리에서 미스가 발생한 후 디스크에서 제공된 쿼리는 “QueryCacheHits” 1회로 집계되며, 두 백엔드 모두에서 미스가 발생한 쿼리는 “QueryCacheMisses” 1회로 집계됩니다. 시스템 테이블 system.query_log의 필드 query_cache_usage는 실행된 각 쿼리에 대해 쿼리 결과가 쿼리 캐시에 기록되었는지, 또는 쿼리 캐시에서 읽혔는지를 보여줍니다. 시스템 테이블 system.metrics의 메트릭 QueryCacheEntries 및 QueryCacheBytes는 현재 인메모리 쿼리 캐시에 포함된 엔트리 수 / 바이트 수를 보여줍니다. 이 메트릭에는 디스크 기반 쿼리 캐시의 엔트리가 포함되지 않으며, 디스크 기반 쿼리 캐시에는 현재 점유량을 나타내는 메트릭이 없습니다. 쿼리 캐시는 각 ClickHouse 서버 프로세스마다 하나씩 존재합니다. 그러나 기본적으로 캐시된 결과는 사용자 간에 공유되지 않습니다. 이는 변경할 수 있지만(아래 참고), 보안상의 이유로 권장되지 않습니다. 쿼리 결과는 쿼리 캐시에서 해당 쿼리의 추상 구문 트리(Abstract Syntax Tree, AST)를 기준으로 참조됩니다. 즉, 캐싱은 대소문자를 구분하지 않으므로 예를 들어 SELECT 1과 select 1은 동일한 쿼리로 처리됩니다. 보다 자연스럽게 매칭되도록 쿼리 캐시 및 출력 형식 지정)과 관련된 모든 쿼리 수준 설정은 AST에서 제거됩니다. 쿼리가 예외 또는 사용자 취소로 인해 중단되면 쿼리 캐시에 엔트리가 기록되지 않습니다. 쿼리 캐시의 크기(바이트), 캐시 엔트리의 최대 개수, 개별 캐시 엔트리의 최대 크기(바이트 및 레코드 수)는 여러 서버 구성 옵션으로 구성할 수 있습니다.
개별 사용자의 캐시 사용량도 SETTINGS PROFILE 및 설정 제약 조건을 사용해 제한할 수 있습니다. 구체적으로는 사용자가 인메모리 쿼리 캐시에서 할당할 수 있는 최대 메모리 양(바이트)과 저장할 수 있는 쿼리 결과의 최대 개수를 제한할 수 있습니다(이러한 제한은 디스크 기반 쿼리 캐시에는 적용되지 않습니다). 이를 위해 먼저 users.xml의 사용자 프로필에 query_cache_max_size_in_bytes 및 query_cache_max_entries 구성을 지정한 다음, 두 설정을 모두 읽기 전용으로 설정합니다:
쿼리 결과를 캐시할 수 있도록 하려면 쿼리가 최소 얼마 동안 실행되어야 하는지 query_cache_min_query_duration 설정으로 지정할 수 있습니다. 예를 들어, 다음 쿼리의 결과는
쿼리가 5초를 초과해 실행되는 경우에만 캐시됩니다. 또한 쿼리 결과가 캐시되기까지 쿼리가 몇 번 실행되어야 하는지도 지정할 수 있습니다. 이를 위해 query_cache_min_query_runs 설정을 사용하십시오. 엔트리는 일정 시간이 지나면 오래된 상태가 됩니다(time-to-live). 기본적으로 이 기간은 60초이지만, query_cache_ttl 설정을 사용하여 세션, 프로필 또는 쿼리 수준에서 다른 값을 지정할 수 있습니다. 쿼리 캐시는 엔트리를 “지연 방식으로” 제거합니다. 즉, 엔트리가 오래된 상태가 되어도 캐시에서 즉시 제거되지는 않습니다. 대신 새 엔트리를 쿼리 캐시에 삽입하려고 할 때 데이터베이스는 새 엔트리를 위한 충분한 여유 공간이 캐시에 있는지 확인합니다. 여유 공간이 충분하지 않은 경우 데이터베이스는 오래된 엔트리를 모두 제거하려고 시도합니다. 그래도 캐시에 여유 공간이 충분하지 않으면 새 엔트리는 삽입되지 않습니다. 쿼리가 HTTP를 통해 실행되면 ClickHouse는 캐시된 엔트리의 경과 시간(초 단위)과 만료 timestamp를 나타내는 Age 및 Expires 헤더를 설정합니다. 엔트리는 기본적으로 압축됩니다. 이렇게 하면 쿼리 캐시에 대한 쓰기/읽기 속도는 느려지지만 전체 메모리 사용량은 줄어듭니다. 압축을 비활성화하려면 query_cache_compress_entries 설정을 사용하십시오. 때로는 동일한 쿼리에 대해 여러 결과를 캐시된 상태로 유지하는 것이 유용합니다. 이는 엔트리의 레이블(또는 네임스페이스) 역할을 하는 query_cache_tag 설정을 사용해 구현할 수 있습니다. 쿼리 캐시는 서로 다른 태그가 있는 동일한 쿼리의 결과를 서로 다른 것으로 간주합니다. 동일한 쿼리에 대해 서로 다른 3개의 쿼리 캐시 엔트리를 생성하는 예시:
쿼리 캐시에서 태그 tag가 있는 엔트리만 제거하려면 SYSTEM CLEAR QUERY CACHE TAG 'tag' 문을 사용할 수 있습니다.

서브쿼리 캐싱

기본적으로 외부 쿼리의 use_query_cache는 서브쿼리에 전파되지 않습니다. 즉, 각 서브쿼리에서 캐시 사용을 명시적으로 설정해야 합니다:
이 예시에서는 내부 서브쿼리의 결과만 캐시됩니다. 외부 쿼리는 캐시되지 않습니다. 모든 서브쿼리에 대한 캐싱을 한 번에 활성화하려면 query_cache_for_subqueries 설정을 사용하십시오:
벌크 전파가 활성화된 상태에서 특정 서브쿼리의 캐싱을 명시적으로 비활성화하려면, 해당 서브쿼리에서 use_query_cache = false를 설정하십시오:
인메모리 쿼리 캐시의 서브쿼리 캐시 엔트리는 is_subquery = 1인 경우 system.query_cache에서 확인할 수 있습니다. 디스크 기반 쿼리 캐시가 활성화되어 있으면 서브쿼리 결과가 디스크에도 캐시되지만, 이러한 엔트리는 system.query_cache에 표시되지 않습니다. query_cache_ttl 설정도 서브쿼리 캐시 엔트리에 적용되며, 서브쿼리별로 설정할 수 있습니다. ClickHouse는 테이블 데이터를 max_block_size행 단위의 블록으로 읽습니다. 필터링, 집계 등으로 인해 결과 블록은 일반적으로 ‘max_block_size’보다 훨씬 작지만, 그보다 훨씬 커지는 경우도 있습니다. 설정 query_cache_squash_partial_results (기본적으로 활성화됨)는 결과 블록을 쿼리 결과 캐시에 삽입하기 전에 ‘max_block_size’ 크기의 블록으로 합치거나(너무 작은 경우) 분할할지(큰 경우) 제어합니다. 이 설정을 사용하면 쿼리 캐시에 쓰는 성능은 다소 떨어지지만, 캐시 엔트리의 압축률이 향상되고 이후 쿼리 결과를 쿼리 캐시에서 제공할 때 더 자연스러운 블록 세분화 수준을 얻을 수 있습니다. 그 결과, 쿼리 캐시는 각 쿼리마다 여러 개의 (partial) 결과 블록을 저장합니다. 이 동작은 기본값으로 적절하지만, 설정 query_cache_squash_partial_results를 사용해 비활성화할 수 있습니다. 또한 비결정적 함수를 포함하는 쿼리 결과는 기본적으로 캐시되지 않습니다. 이러한 함수에는 다음이 포함됩니다. 그럼에도 비결정적 함수를 포함하는 쿼리 결과를 강제로 캐시하려면 설정 query_cache_nondeterministic_function_handling을 사용하십시오. 시스템 테이블이 포함된 쿼리(예: system.processes` 또는 information_schema.tables)의 결과는 기본적으로 캐시되지 않습니다. 그럼에도 시스템 테이블이 포함된 쿼리의 결과를 강제로 캐시하려면 query_cache_system_table_handling 설정을 사용하십시오. 마지막으로, 보안상의 이유로 쿼리 캐시의 엔트리는 사용자 간에 공유되지 않습니다. 예를 들어, 사용자 A가 그러한 정책이 없는 다른 사용자 B와 동일한 쿼리를 실행하는 방식으로 테이블의 ROW POLICY를 우회할 수는 없어야 합니다. 그러나 필요한 경우에는 설정 query_cache_share_between_users을 지정하여 캐시 엔트리를 다른 사용자가 접근할 수 있도록(즉, 공유되도록) 표시할 수 있습니다.

디스크 기반 쿼리 캐시

기본적으로 쿼리 캐시는 엔트리를 메모리에 보관합니다. 또한 쿼리 결과를 파일 시스템 캐시를 기반으로 디스크에 캐시할 수도 있습니다. 디스크 기반 쿼리 캐시는 인메모리 쿼리 캐시보다 더 많은 공간을 제공하며, 서버를 재시작해도 유지됩니다. 엔트리는 해당 엔트리를 기록한 서버 빌드에서만 제공됩니다. 따라서 업그레이드나 다운그레이드 후에는 이전 빌드가 기록한 엔트리를 조회하면 캐시 미스가 발생하며, 이러한 엔트리는 결국 덮어쓰이거나 축출됩니다. 디스크 기반 쿼리 캐시를 사용하려면 먼저 서버 구성의 filesystem_caches 섹션에서 파일 시스템 캐시를 구성하십시오.
그런 다음 query_cache_on_disk_cache_name 설정에 해당 캐시 이름을 지정합니다:
디스크 기반 쿼리 캐시는 인메모리 쿼리 캐시와 독립적으로 동작하며, enable_reads_from_query_cache_on_disk 및 enable_writes_to_query_cache_on_disk 설정(둘 다 기본값 true)을 사용하여 읽기와 쓰기를 각각 활성화할 수 있습니다. 두 캐시 모두 읽기가 활성화되어 있으면 먼저 메모리에서 조회하고, 캐시 미스가 발생한 경우에만 디스크에서 조회합니다. 두 캐시 모두 쓰기가 활성화되어 있으면 결과는 양쪽에 모두 저장됩니다. 결과를 저장할 수 있는 캐시가 없는 경우(예: 인메모리 쿼리 캐시가 없는 clickhouse-local에서 enable_writes_to_query_cache_on_disk = false로 설정한 경우)에는 비결정적 함수, 시스템 테이블 또는 throw가 아닌 오버플로우 모드 때문에 쿼리가 거부되지 않습니다. 이러한 검사는 잘못된 결과가 저장되는 것을 막기 위한 것일 뿐이기 때문입니다. 디스크의 엔트리는 query_cache_on_disk_codec 설정(기본값 ZSTD(3))으로 지정한 코덱으로 압축됩니다. 쿼리 캐시의 서버 설정인 max_entry_size_in_bytes와 max_entry_size_in_rows는 두 백엔드의 엔트리를 모두 제한합니다. 각 백엔드는 엔트리를 저장하는 형태를 기준으로 크기를 측정하므로, 디스크에서는 바이트 제한이 직렬화 및 압축된 엔트리에 적용됩니다. 둘 중 하나라도 0으로 설정하면 두 백엔드 모두 쓰기가 비활성화됩니다. 그 밖의 모든 쿼리 캐시 설정(예: query_cache_ttl, query_cache_min_query_duration)은 아래 두 가지 예외를 제외하고 인메모리 쿼리 캐시와 동일한 방식으로 디스크 기반 쿼리 캐시에 적용됩니다. query_cache_max_size_in_bytes 및 query_cache_max_entries 설정은 인메모리 쿼리 캐시에만 적용되는 사용자별 제한입니다. 이 설정은 디스크 기반 쿼리 캐시에는 적용되지 않습니다. 디스크의 엔트리는 구성된 파일 시스템 캐시의 크기로 제한되며 해당 캐시의 규칙에 따라 축출되므로(아래 참조), 제한적인 SETTINGS PROFILE이 적용된 사용자라도 이 설정이 메모리에서 허용하는 양보다 더 많은 결과를 디스크에 저장할 수 있습니다. 이는 의도된 동작입니다. 파일 시스템 캐시는 크기가 제한된 공유 리소스이며, 디스크 기반 쿼리 캐시는 그 안에 공간을 따로 예약하지 않습니다. query_cache_share_between_users 설정도 예외입니다. 이 설정은 두 백엔드에서 동일한 의미를 가지지만(이 설정을 사용해 기록된 엔트리는 모든 사용자에게 제공됨), 공유되지 않는 엔트리를 격리하는 방식은 서로 다릅니다. 인메모리 쿼리 캐시는 쿼리당 하나의 엔트리만 유지하며, 읽기 시 엔트리가 다른 사용자의 것이면 이를 거부합니다. 따라서 사용자 A의 공유되지 않는 엔트리가 만료되거나 축출될 때까지 사용자 B의 쓰기는 가려집니다. 반면 디스크 기반 쿼리 캐시는 사용자와 해당 사용자의 현재 역할 집합을 공유되지 않는 엔트리 키에 포함하므로, 사용자 B는 사용자 A의 엔트리에 대해 캐시 미스가 발생하고 결과의 사본을 따로 저장합니다. 한 사용자가 서로 다른 현재 역할 집합을 사용하는 경우(예: SET ROLE 실행 후)에도 마찬가지입니다. 역할에는 권한과 row policy가 포함되므로, 서로 다른 역할로 계산된 결과는 별도로 캐시됩니다. 디스크 기반 쿼리 캐시의 엔트리는 파일 시스템 캐시의 일반 엔트리이므로, 동일한 파일 시스템 캐시에 캐시된 다른 데이터와 구별되지 않습니다. 즉, 삭제 대상에서 보호되지 않으며 파일 시스템 캐시의 공간이 부족해지면 동일한 규칙(예: LRU(least-recently-used))에 따라 축출됩니다. 이 엔트리의 전체 크기나 개수에 대한 별도의 제한은 없으며, 파일 시스템 캐시 자체의 제한만 적용됩니다. SYSTEM CLEAR QUERY CACHE SQL 문(및 SYSTEM CLEAR QUERY CACHE TAG 'tag')은 세션의 query_cache_on_disk_cache_name 설정으로 선택된 파일 시스템 캐시(예: SET query_cache_on_disk_cache_name = 'query_results' 실행 후)에서 디스크 기반 쿼리 캐시의 엔트리(태그를 지정한 경우 해당 태그를 가진 엔트리이며, 손상된 엔트리와 호환되지 않는 포맷의 엔트리는 태그와 관계없이 제거됨)도 제거하며, 파일 시스템 캐시의 다른 데이터는 그대로 유지합니다. 엔트리를 찾기 위해 파일 시스템 캐시에 있는 모든 파일 세그먼트의 메타데이터를 디스크에서 읽지 않고 메모리상에서 순회하며, 쿼리 캐시 엔트리로 표시된 모든 키의 헤더를 읽습니다. 순회 시간은 파일 시스템 캐시의 파일 세그먼트 수에 비례하므로, 쿼리 결과 전용 파일 시스템 캐시를 사용하면 SYSTEM CLEAR QUERY CACHE의 비용을 낮게 유지할 수 있습니다. 파일 시스템 캐시가 엔트리의 앞부분을 이미 축출한 경우, 엔트리의 나머지 부분은 파일 시스템 캐시가 이를 축출하거나 동일한 쿼리 결과가 다시 기록될 때까지 남아 있지만, 결과 반환에는 사용되지 않습니다. 아직 기록 중인 엔트리도 그대로 유지됩니다. SYSTEM DROP FILESYSTEM CACHE '<name>' SQL 문은 이러한 엔트리를 파일 시스템 캐시의 다른 모든 데이터와 함께 제거합니다. 시스템 테이블 system.query_cache와 메트릭 QueryCacheEntries 및 QueryCacheBytes에는 인메모리 쿼리 캐시의 엔트리만 표시됩니다. 디스크 기반 쿼리 캐시의 hits 및 미스 횟수는 시스템 테이블 system.events의 이벤트 “QueryCacheOnDiskHits” 및 “QueryCacheOnDiskMisses”로 확인할 수 있습니다. 이 값은 디스크 기반 백엔드만 따로 집계한 수치이며, “QueryCacheHits”와 “QueryCacheMisses”는 여전히 쿼리 캐시 전체에 대한 카운터이고, “QueryCacheAgeSeconds”는 두 백엔드의 hits를 모두 집계합니다. 인메모리 캐시를 먼저 조회하므로, 인메모리 hits 수는 “QueryCacheHits”에서 “QueryCacheOnDiskHits”를 뺀 값입니다.