쿼리 실행

쿼리 실행 (Query execution)

Druid가 쿼리를 실행하는 방식을 datasource 타입별로 설명하는 문서예요. 어떤 datasource를 조회하느냐에 따라 실행 방식이 달라져요.

출처: 문서

본문

이 문서는 Druid가 native 쿼리 를 어떻게 실행하는지 설명해요. 그런데 Druid SQL 쿼리는 native 쿼리로 번역되므로, 이 문서는 SQL 런타임에도 그대로 적용돼요. SQL 쿼리가 native 쿼리로 어떻게 번역되는지에 대해서는 SQL Query translation 페이지 를 참고하세요.

Druid의 쿼리 실행 방식은 조회하는 datasource의 종류에 따라 달라져요.

Datasource 타입 (Datasource type)

table

table datasource에서 직접 동작하는 쿼리는 Broker 프로세스가 주도하는 scatter-gather 방식으로 실행돼요. 과정은 다음과 같아요:

  1. Broker는 intervals 파라미터를 기준으로 쿼리와 관련된 세그먼트를 식별해요. 세그먼트는 항상 시간으로 파티셔닝되므로, interval이 쿼리 interval과 겹치는 세그먼트는 잠재적으로 관련이 있어요.
  2. Broker는 입력 데이터가 single_dim partitionsSpec 으로 범위 파티셔닝되어 있고, 필터가 파티셔닝에 사용된 차원과 일치한다면 filter 를 기준으로 세그먼트 목록을 추가로 정리(prune)할 수 있어요.
  3. 세그먼트 목록을 정리한 Broker는 그 세그먼트들을 현재 서빙하고 있는 데이터 서버(Historical과 Middle Manager에서 실행되는 태스크 같은)로 쿼리를 전달해요.
  4. Scan 을 제외한 모든 쿼리 타입에서 데이터 서버는 각 세그먼트를 병렬로 처리하고 각 세그먼트에 대한 부분 결과(partial results)를 생성해요. 수행되는 구체적 처리는 쿼리 타입에 따라 달라져요. 쿼리 캐싱이 활성화되어 있다면 이 부분 결과들은 캐시될 수 있어요. Scan 쿼리의 경우 세그먼트들은 단일 스레드로 순서대로 처리돼요.
  5. Broker는 각 데이터 서버의 부분 결과를 받아 최종 결과 집합으로 병합하고 호출자에게 반환해요. Timeseries · Scan 쿼리와, 정렬이 없는 GroupBy 쿼리에서는 Broker가 스트리밍 방식으로 이 작업을 수행할 수 있어요. 그 외에는 Broker가 아무것도 반환하기 전에 결과 집합을 완전히 계산해요.

lookup

lookup datasource에서 (조인 없이) 직접 동작하는 쿼리는 쿼리를 받은 Broker에서, lookup의 로컬 복사본을 사용해 실행돼요. 등록된 모든 lookup 테이블은 Broker에 미리 메모리로 로드돼요. 쿼리는 단일 스레드로 실행돼요.

lookup을 조인의 오른쪽 입력으로 사용하는 쿼리의 실행은 아래의 조인 섹션에서 설명하는 것처럼 해당 쿼리의 "base"(가장 왼쪽-아래) datasource에 따라 달라져요.

union

union datasource에서 직접 동작하는 쿼리는 Broker에서 union의 일부인 각 테이블에 대한 별도 쿼리로 분할돼요. 각 쿼리는 별도로 실행되고, Broker가 그 결과들을 병합해요.

inline

inline datasource에서 직접 동작하는 쿼리는 쿼리를 받은 Broker에서 실행돼요. 쿼리는 단일 스레드로 실행돼요.

inline datasource를 조인의 오른쪽 입력으로 사용하는 쿼리의 실행은 아래의 조인 섹션에서 설명하는 것처럼 해당 쿼리의 "base"(가장 왼쪽-아래) datasource에 따라 달라져요.

query

Query datasource는 서브쿼리(subquery)예요. 각 서브쿼리는 마치 자기 자신이 하나의 쿼리인 것처럼 실행되고, 결과는 Broker로 가져와져요. 그다음 Broker는 서브쿼리가 inline datasource로 대체된 것처럼 나머지 쿼리를 계속 진행해요.

대부분의 경우 Druid는 나머지 쿼리가 진행되기 전에 서브쿼리 결과를 Broker의 메모리에 버퍼링해요. 따라서 서브쿼리는 순차적으로 실행돼요. 특정 쿼리의 모든 서브쿼리에 걸쳐 버퍼링되는 총 행 수는 기본값이 100000 행인 druid.server.http.maxSubqueryRows를 초과할 수 없고, 설정된 경우 druid.server.http.maxSubqueryBytes를 초과할 수 없어요. 그렇지 않으면 Druid는 resource limit exceeded 예외를 던져요.

한 가지 예외가 있어요: 외부 쿼리가 groupBy 타입이고, 타입이 query인 dataSource가 그 자체로 또 다른 groupBy인 경우, 서브쿼리 결과를 스트리밍 방식으로 처리할 수 있어요. 이 경우 druid.server.http.maxSubqueryRows와 druid.server.http.maxSubqueryBytes 제한은 적용되지 않아요.

join

Join datasource는 broadcast hash-join 방식으로 처리돼요.

  1. Broker는 조인의 입력인 서브쿼리들을 위의 query 섹션에서 설명한 대로 실행하고, 그것들을 inline datasource로 대체해요.
  2. Broker는 조인 트리가 있다면 그것을 "base" datasource(가장 왼쪽-아래)와 나머지 leaf datasource(그 외 나머지)로 펼쳐요(flatten).
  3. 쿼리 실행은 base datasource가 단독으로 사용할 구조와 동일한 구조로 진행돼요. base datasource가 table 이면 평소처럼 intervals를 기준으로 세그먼트가 정리되고, 쿼리는 관련된 모든 데이터 서버에 병렬로 전달되어 클러스터에서 실행돼요. base datasource가 lookup 또는 inline datasource라면(서브쿼리를 인라인한 결과인 inline datasource 포함) 쿼리는 Broker 자체에서 실행돼요. base 쿼리는 union일 수 없어요. union은 현재 조인의 입력으로 지원되지 않기 때문이에요.
  4. base datasource 처리를 시작하기 전에, 쿼리를 실행할 서버는 먼저 모든 non-base leaf datasource를 검사해서 다가올 hash join을 위해 새 해시 테이블을 만들어야 하는지 결정해요. 현재 lookup은 (미리 로드되어 있기 때문에) 새 해시 테이블을 만들 필요가 없지만, inline datasource는 필요해요.
  5. 쿼리 실행은 다시 base datasource가 단독으로 사용할 구조와 동일하게 진행되는데, 한 가지가 추가돼요: base datasource를 처리하는 동안 Druid 서버는 다른 조인 입력들로 만든 해시 테이블을 사용해 조인 결과를 행 단위로 생성하고, 쿼리 엔진은 base 행이 아니라 조인된 행을 대상으로 동작해요.

더 알아보기 (Learn more)