쿼리 캐시

쿼리 캐시 (Query cache)

쿼리 캐시는 SELECT 쿼리를 한 번만 계산하고, 같은 쿼리의 이후 실행을 캐시에서 직접 서빙할 수 있게 합니다. 쿼리 유형에 따라 ClickHouse 서버의 지연과 리소스 소비를 극적으로 줄일 수 있어요.

출처: 문서

본문

쿼리 캐시는 SELECT 쿼리를 한 번만 계산하고, 같은 쿼리의 이후 실행을 캐시에서 직접 서빙할 수 있게 합니다. 쿼리 유형에 따라 ClickHouse 서버의 지연과 리소스 소비를 극적으로 줄일 수 있습니다.

배경, 설계와 한계

쿼리 캐시는 일반적으로 트랜잭션적으로 일관된 것(transactionally consistent) 또는 일관되지 않은 것으로 볼 수 있습니다.

  • 트랜잭션적으로 일관된 캐시에서 데이터베이스는 SELECT 쿼리의 결과가 바뀌거나 잠재적으로 바뀌면 캐시된 쿼리 결과를 무효화(버림)합니다. ClickHouse에서 데이터를 바꾸는 연산은 테이블의 삽입/업데이트/삭제 또는 콜랩싱 병합을 포함합니다. 트랜잭션적으로 일관된 캐싱은 OLTP 데이터베이스에 특히 적합합니다. 예: MySQL(v8.0 이후 쿼리 캐시 제거)과 Oracle.
  • 트랜잭션적으로 일관되지 않은 캐시에서 약간의 결과 부정확성은, 모든 캐시 엔트리에 만료 후 사라지는 유효 기간(예: 1분)이 할당되고 그 기간 동안 기본 데이터가 조금만 변한다는 가정 하에 수용됩니다. 이 접근 방식은 전반적으로 OLAP 데이터베이스에 더 적합합니다. 트랜잭션적으로 일관되지 않은 캐싱으로 충분한 예를 들자면, 여러 사용자가 동시에 접속하는 리포팅 도구의 시간별 판매 보고서를 고려해 보세요. 판매 데이터는 데이터베이스가 보고서를 한 번(첫 번째 SELECT 쿼리로 표시)만 계산하면 될 만큼 보통 충분히 느리게 바뀝니다. 이후 쿼리는 쿼리 캐시에서 직접 서빙됩니다. 이 예에서 합리적인 유효 기간은 30분일 수 있습니다.

트랜잭션적으로 일관되지 않은 캐싱은 전통적으로 데이터베이스와 상호작용하는 클라이언트 도구나 프록시 패키지(예: chproxy)에 의해 제공됩니다. 결과적으로 같은 캐싱 로직과 구성이 종종 중복됩니다. ClickHouse의 쿼리 캐시로 캐싱 로직이 서버 측으로 이동합니다. 이것은 유지보수 노력을 줄이고 중복을 피합니다.

구성 설정과 사용법

ClickHouse Cloud에서는 쿼리 수준 설정을 사용해 쿼리 캐시 설정을 편집해야 합니다. 구성 수준 설정편집은 현재 지원되지 않습니다.

clickhouse-local은 한 번에 단일 쿼리를 실행합니다. 쿼리 결과 캐싱이 의미가 없으므로 clickhouse-local에서는 쿼리 결과 캐시가 비활성화되어 있습니다.

use_query_cache 설정은 특정 쿼리 또는 현재 세션의 모든 쿼리가 쿼리 캐시를 활용해야 하는지를 제어할 수 있습니다. 예를 들어 다음 쿼리의 첫 실행:

SELECT some_expensive_calculation(column_1, column_2)
FROM table
SETTINGS use_query_cache = true;

은 쿼리 결과를 쿼리 캐시에 저장합니다. 같은 쿼리의 이후 실행(역시 use_query_cache = true 파라미터가 있는)은 계산된 결과를 캐시에서 읽어 즉시 반환합니다.

use_query_cache 설정과 다른 모든 쿼리-캐시 관련 설정은 독립형 SELECT 문에만 효과가 있습니다. 특히 CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true로 만들어진 뷰에 대한 SELECT 결과는, 그 SELECT 문이 SETTINGS use_query_cache = true로 실행되지 않는 한 캐시되지 않습니다.

캐시 활용 방식은 enable_writes_to_query_cacheenable_reads_from_query_cache 설정(둘 다 기본 true)으로 더 자세히 구성할 수 있습니다. 전자는 쿼리 결과가 캐시에 저장되는지 제어하고, 후자는 데이터베이스가 캐시에서 쿼리 결과를 가져오려 시도해야 하는지 결정합니다. 예를 들어 다음 쿼리는 캐시를 수동적으로만 사용합니다. 즉 읽기는 시도하지만 결과는 저장하지 않습니다:

SELECT some_expensive_calculation(column_1, column_2)
FROM table
SETTINGS use_query_cache = true, enable_writes_to_query_cache = false;

최대 제어를 위해 일반적으로 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에 표시됩니다. 데이터베이스 시작 이후의 쿼리 캐시 히트와 미스 수는 시스템 테이블 system.events에서 "QueryCacheHits"와 "QueryCacheMisses" 이벤트로 표시됩니다. 두 카운터 모두 use_query_cache = true 설정으로 실행되는 SELECT 쿼리에 대해서만 갱신되며, 다른 쿼리는 "QueryCacheMisses"에 영향을 주지 않습니다. 시스템 테이블 system.query_logquery_cache_usage 필드는 각 실행된 쿼리에 대해 쿼리 결과가 쿼리 캐시에 쓰여졌는지 또는 읽혔는지 보여 줍니다. 시스템 테이블 system.metricsQueryCacheEntriesQueryCacheBytes 메트릭은 쿼리 캐시가 현재 포함하는 엔트리 수/바이트 수를 보여 줍니다.

쿼리 캐시는 ClickHouse 서버 프로세스당 한 번 존재합니다. 그러나 캐시 결과는 기본적으로 사용자 간에 공유되지 않습니다. 이것은 변경할 수 있지만(아래 참고), 보안상의 이유로 권장되지 않습니다.

쿼리 결과는 쿼리 캐시에서 해당 쿼리의 Abstract Syntax Tree(AST)로 참조됩니다. 이것은 캐싱이 대소문자에 무관함을 의미합니다. 예를 들어 SELECT 1select 1은 같은 쿼리로 취급됩니다. 매칭을 더 자연스럽게 만들기 위해 쿼리 캐시 관련 모든 쿼리 수준 설정과 출력 형식이 AST에서 제거됩니다.

예외나 사용자 취소로 쿼리가 중단되면 쿼리 캐시에 엔트리가 기록되지 않습니다.

바이트 단위의 쿼리 캐시 크기, 최대 캐시 엔트리 수, 개별 캐시 엔트리의 최대 크기(바이트 및 레코드)는 다양한 서버 구성 옵션으로 구성할 수 있습니다.

<query_cache>
    <max_size_in_bytes>1073741824</max_size_in_bytes>
    <max_entries>1024</max_entries>
    <max_entry_size_in_bytes>1048576</max_entry_size_in_bytes>
    <max_entry_size_in_rows>30000000</max_entry_size_in_rows>
</query_cache>

또한 개별 사용자의 캐시 사용을 설정 프로필과 설정 제약으로 제한할 수 있습니다. 더 구체적으로, 사용자가 쿼리 캐시에 할당할 수 있는 최대 메모리 양(바이트)과 저장된 쿼리 결과의 최대 수를 제한할 수 있습니다. 그러려면 먼저 users.xml의 사용자 프로필에 query_cache_max_size_in_bytesquery_cache_max_entries 구성을 제공한 다음, 두 설정 모두 읽기 전용으로 만듭니다:

<profiles>
    <default>
        <!-- The maximum cache size in bytes for user/profile 'default' -->
        <query_cache_max_size_in_bytes>10000</query_cache_max_size_in_bytes>
        <!-- The maximum number of SELECT query results stored in the cache for user/profile 'default' -->
        <query_cache_max_entries>100</query_cache_max_entries>
        <!-- Make both settings read-only so the user cannot change them -->
        <constraints>
            <query_cache_max_size_in_bytes>
                <readonly/>
            </query_cache_max_size_in_bytes>
            <query_cache_max_entries>
                <readonly/>
            </query_cache_max_entries>
        </constraints>
    </default>
</profiles>

쿼리가 최소한 얼마 동안 실행되어야 결과가 캐시될 수 있는지 정의하려면 query_cache_min_query_duration 설정을 사용할 수 있습니다. 예를 들어 다음 쿼리의 결과:

SELECT some_expensive_calculation(column_1, column_2)
FROM table
SETTINGS use_query_cache = true, query_cache_min_query_duration = 5000;

는 쿼리가 5초보다 오래 실행될 때만 캐시됩니다. 쿼리가 결과가 캐시될 때까지 몇 번 실행되어야 하는지 지정하는 것도 가능합니다 — 이를 위해 query_cache_min_query_runs 설정을 사용하세요.

쿼리 캐시의 엔트리는 일정 시간(수명, time-to-live)이 지나면 오래됩니다. 기본적으로 이 기간은 60초이지만 세션, 프로필 또는 쿼리 수준에서 query_cache_ttl 설정으로 다른 값을 지정할 수 있습니다. 쿼리 캐시는 엔트리를 "느리게" 축출합니다. 즉 엔트리가 오래되어도 즉시 캐시에서 제거되지 않습니다. 대신 새 엔트리를 쿼리 캐시에 삽입하려 할 때 데이터베이스는 캐시에 새 엔트리를 위한 충분한 여유 공간이 있는지 확인합니다. 그렇지 않으면 데이터베이스는 모든 오래된 엔트리를 제거하려 시도합니다. 여전히 충분한 여유 공간이 없으면 새 엔트리는 삽입되지 않습니다.

쿼리가 HTTP로 실행되면 ClickHouse는 캐시된 엔트리의 수명(초)과 만료 타임스탬프와 함께 AgeExpires 헤더를 설정합니다.

쿼리 캐시의 엔트리는 기본적으로 압축됩니다. 이것은 쿼리 캐시에서/로의 느린 쓰기와 읽기를 대가로 전체 메모리 소비를 줄입니다. 압축을 비활성화하려면 query_cache_compress_entries 설정을 사용하세요.

때로는 같은 쿼리에 대해 여러 결과를 캐시해 두는 것이 유용합니다. 이는 쿼리 캐시 엔트리의 라벨(또는 네임스페이스) 역할을 하는 query_cache_tag 설정으로 달성할 수 있습니다. 쿼리 캐시는 같은 쿼리의 서로 다른 태그를 가진 결과를 다르게 간주합니다.

같은 쿼리에 대해 세 개의 서로 다른 쿼리 캐시 엔트리를 만드는 예제:

SELECT 1 SETTINGS use_query_cache = true; -- query_cache_tag is implicitly '' (empty string)
SELECT 1 SETTINGS use_query_cache = true, query_cache_tag = 'tag 1';
SELECT 1 SETTINGS use_query_cache = true, query_cache_tag = 'tag 2';

쿼리 캐시에서 tag 태그가 있는 엔트리만 제거하려면 SYSTEM CLEAR QUERY CACHE TAG 'tag' 문을 사용할 수 있습니다.

서브쿼리 캐싱

기본적으로 바깥 쿼리의 use_query_cache는 서브쿼리로 전파되지 않습니다. 각 서브쿼리가 캐싱을 명시적으로 선택해야 합니다:

SELECT *
FROM (SELECT number FROM system.numbers LIMIT 1000 SETTINGS use_query_cache = true)
WHERE number > 500;

이 예제에서는 안쪽 서브쿼리 결과만 캐시됩니다. 바깥 쿼리는 캐시되지 않습니다.

모든 서브쿼리에 대해 한 번에 캐싱을 활성화하려면 query_cache_for_subqueries 설정을 사용하세요:

SELECT *
FROM (SELECT number FROM system.numbers LIMIT 1000)
WHERE number > 500
SETTINGS use_query_cache = true, query_cache_for_subqueries = true;

대량 전파가 활성화되었을 때 특정 서브쿼리에서 캐싱을 명시적으로 비활성화하려면 해당 서브쿼리에 use_query_cache = false를 설정하세요:

SELECT *
FROM (SELECT number FROM system.numbers LIMIT 1000 SETTINGS use_query_cache = false)
WHERE number > 500
SETTINGS use_query_cache = true, query_cache_for_subqueries = true;

서브쿼리 캐시 엔트리는 is_subquery = 1system.query_cache에서 볼 수 있습니다. query_cache_ttl 설정도 서브쿼리 캐시 엔트리에 적용되며 서브쿼리별로 설정할 수 있습니다.

ClickHouse는 max_block_size 행의 블록으로 테이블 데이터를 읽습니다. 필터링, 집계 등으로 인해 결과 블록은 보통 max_block_size보다 훨씬 작지만, 훨씬 큰 경우도 있습니다. query_cache_squash_partial_results(기본 활성) 설정은 쿼리 결과에 삽입하기 전에 결과 블록을 'max_block_size' 크기의 블록으로 뭉갤지(작으면) 또는 쪼갤지(크면) 제어합니다. 이것은 쿼리 캐시로의 쓰기 성능을 낮추지만 캐시 엔트리의 압축률을 개선하고, 쿼리 결과가 나중에 쿼리 캐시에서 서빙될 때 더 자연스러운 블록 입자를 제공합니다.

결과적으로 쿼리 캐시는 각 쿼리에 대해 여러 (부분) 결과 블록을 저장합니다. 이 동작은 좋은 기본값이지만 query_cache_squash_partial_results 설정으로 억제할 수 있습니다.

또한 비결정적 함수가 있는 쿼리의 결과는 기본적으로 캐시되지 않습니다. 그러한 함수에는 다음이 포함됩니다:

비결정적 함수가 있는 쿼리의 결과를 무조건 캐싱하려면 query_cache_nondeterministic_function_handling 설정을 사용하세요.

시스템 테이블(예: system.processes 또는 information_schema.tables)을 포함하는 쿼리의 결과는 기본적으로 캐시되지 않습니다. 시스템 테이블이 있는 쿼리의 결과를 무조건 캐싱하려면 query_cache_system_table_handling 설정을 사용하세요.

마지막으로, 쿼리 캐시의 엔트리는 보안상의 이유로 사용자 간에 공유되지 않습니다. 예를 들어 사용자 A는 존재하지 않는 행 정책을 가진 사용자 B와 같은 쿼리를 실행해 행 정책을 우회할 수 없어야 합니다. 그러나 필요한 경우 query_cache_share_between_users 설정을 제공해 캐시 엔트리를 다른 사용자가 접근할 수 있도록(즉 공유) 표시할 수 있습니다.

관련 콘텐츠

더 알아보기 (Learn more)