Explain Plan

Explain Plan

Pinot 내의 쿼리 실행은 파이프라인 방식으로 실행되어 최종 결과를 만드는 연산자(operator) 시퀀스로 모델링돼요. EXPLAIN PLAN FOR 구문을 사용해 쿼리의 실행 계획을 얻을 수 있으며, 이는 쿼리를 더 최적화하는 데 유용해요.

경고: explain plan 출력 형식은 아직 개발 중이며 향후 릴리스에서 변경될 수 있어요. 이 개발 중이라는 표시는 explain plan 출력 형식에만 적용되며, 일반적으로 사용 가능한(GA) 핵심 multi-stage 엔진에는 적용되지 않아요. Pinot explain plan은 사람이 읽을 수 있으며 디버깅 및 최적화 목적으로 사용하기 위한 것이에요. 특히 explain plan을 자동화된 스크립트나 도구에서 사용할 때 중요해요. 테이블이나 JSON으로 반환되는 explain plan조차도 릴리스 간 안정적이라고 보장되지 않아요.

출처: 문서

본문

Pinot는 쿼리 엔진과 얻고자 하는 세분성(granularity) 또는 세부 정보에 따라 다른 유형의 explain plan을 지원해요.

graph LR
    EXPLAIN
    
    STAGE{"single or \n multi-stage?"}
   

    SSE_Q_VERBOSE{verbose?}
    SSE_SIMPLE[brief SSE]
    SSE_EXTENDED[verbose SSE]

    MSE_WORKERS[workers MSE]
    MSE_LOGICAL[logical MSE]
    MSE_Q_VERBOSE{verbose?}
    MSE_IMPL_SIMPLE["brief segment MSE"]
    MSE_IMPL_VERBOSE["verbose segment MSE"]
    
    EXPLAIN --> STAGE
    STAGE -- single --> SSE_Q_VERBOSE
    STAGE -- multi --> MSE_Q_DISTRIBUTION{workers?}
    
    SSE_Q_VERBOSE -- no --> SSE_SIMPLE
    SSE_Q_VERBOSE -- yes --> SSE_EXTENDED
    
    MSE_Q_DISTRIBUTION -- yes --> MSE_WORKERS
    MSE_Q_DISTRIBUTION -- no --> MSE_Q_LOGICAL{logical?}
    
    MSE_Q_LOGICAL -- yes --> MSE_LOGICAL
    MSE_Q_LOGICAL -- no --> MSE_Q_VERBOSE
    
    MSE_Q_VERBOSE -- yes --> MSE_IMPL_VERBOSE
    MSE_Q_VERBOSE -- no --> MSE_IMPL_SIMPLE

세그먼트에 따른 다른 계획

세그먼트는 Pinot에서 데이터 저장 및 처리의 기본 단위예요. 쿼리가 실행되면 각 세그먼트에서 실행되고 결과는 병합돼요. 모든 세그먼트가 동일한 데이터 분포, 인덱스 등을 가지는 것은 아니에요. 따라서 쿼리 엔진은 세그먼트에 따라 쿼리를 다르게 실행하기로 결정할 수 있어요. 여기에는 다음이 포함돼요.

  • 테이블 구성에 인덱스가 추가되거나 제거된 이후 새로고침되지 않은 세그먼트
  • 범위 인덱스 같은 일부 인덱스를 사용할 수 없는, 수집 중인 실시간 세그먼트
  • 쿼리 계획에 영향을 줄 수 있는 데이터 분포, 특히 컬럼의 최솟값과 최댓값

Pinot 쿼리는 수천 개의 세그먼트를 건드릴 수 있으므로, Pinot는 쿼리를 explain할 때 표시되는 [다른 쿼리][^1] 수를 최소화하려고 해요. 기본적으로 Pinot는 각 세그먼트에 대한 계획을 분석하고 단순화된 계획을 반환해요. 이 단순화가 어떻게 이루어지는지는 쿼리 엔진에 따라 달라지며, 아래에서 더 자세히 읽을 수 있어요.

각 세그먼트에 대한 계획을 보여주는 verbose 모드가 있어요. 이 모드는 explainPlanVerbose 쿼리 옵션을 true로 설정해 활성화하며, explain plan 문장 앞에 SET explainPlanVerbose=true;를 붙이면 돼요.

multi-stage 쿼리 엔진에 대한 Explain

multi-stage 쿼리 엔진의 더 복잡한 특성에 따라, 그 explain plan은 쿼리 실행의 다양한 측면에 맞춘(특화된)[^2] 계획을 얻도록 커스터마이즈할 수 있어요.

multi-stage 쿼리 엔진에는 3가지 유형의 explain plan이 있어요.

모드 기본 구문 세그먼트 계획이 활성화된 경우의 구문 설명
Segment plan SET explainAskingServers=true; EXPLAIN PLAN FOR EXPLAIN PLAN FOR 세그먼트 특정 정보(예: 인덱스)를 포함해요.
Logical plan EXPLAIN PLAN FOR 또는 EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR 가장 단순한 multi-stage 계획. 인덱스나 데이터 셔플 정보가 없어요.
Workers plan EXPLAIN IMPLEMENTATION PLAN FOR EXPLAIN IMPLEMENTATION PLAN FOR 서버 간 데이터 셔플을 이해하는 데 사용해요. 참고: 이 모드의 이름은 논의 중이며 향후 변경될 수 있어요.

참고: 각 explain plan 모드를 선택하는 데 사용되는 구문은 혼란스러우며 향후 변경될 수 있어요.

Segment plan

세그먼트 계획은 데이터 분포, 인덱스 등 세그먼트 특정 정보를 포함하는 쿼리 실행 계획의 상세한 표현이에요.

이 모드는 Pinot 1.3.0에서 도입되었고 향후 릴리스에서 기본값이 될 예정이에요. 그동안 explainAskingServers 쿼리 옵션을 true로 설정하거나 explain plan 문장 앞에 SET explainAskingServers=true;를 붙여 사용할 수 있어요. 또는 broker 구성 pinot.query.multistage.explain.include.segment.plan을 true로 변경해 이 모드를 기본값으로 활성화할 수 있어요.

활성화 방식과 무관하게, 이 모드가 활성화되면 EXPLAIN PLAN FOR 구문에 세그먼트 정보가 포함돼요.

Verbose 및 brief 모드

Different plans for different segments에서 설명했듯이 기본적으로 Pinot는 쿼리를 explain할 때 표시되는 [다른 쿼리][^3] 수를 최소화하려고 해요. multi-stage에서 brief 모드는 고유한 세그먼트 계획을 Alternative(segments=[n]) 분기로 그룹화하며, 여기서 n은 해당 계획을 사용하는 세그먼트 수예요. 동등한 계획은 같은 분기로 집계되므로, 같은 계획이 100개 세그먼트에서 실행된다면 brief 모드는 한 번만 보여주고 문서 수 같은 가산 통계는 합산돼요.

일부 세그먼트가 다른 계획을 사용한다면 brief 모드는 세그먼트당 하나의 자식으로 되돌리는 대신 고유한 계획마다 하나의 Alternative 분기를 유지해요. 모든 세그먼트가 같은 계획을 공유할 때 Pinot는 병합 후 중복된 단일 Alternative 래퍼를 제거하므로, 일반적인 경우는 여전히 결합(combine) 노드 아래에 단일 자식으로 렌더링돼요.

verbose 모드에서는 각 세그먼트에 대해 세그먼트 이름과 모든 세그먼트 특정 정보를 포함해 하나의 계획이 표시돼요. 이는 어떤 세그먼트가 인덱스를 사용하지 않는지, 어떤 세그먼트가 다른 데이터 분포를 사용하는지 아는 데 유용할 수 있어요.

예시
-- SET explainAskingServer= true is required if 
-- pinot.query.multistage.explain.include.segment.plan is false, 
-- optional otherise
SET explainAskingServers=true;
EXPLAIN PLAN FOR
SELECT DISTINCT deviceOS, groupUUID
FROM userAttributes AS a
JOIN userGroups AS g
ON a.userUUID = g.userUUID
WHERE g.groupUUID = 'group-1'
LIMIT 100

반환:

Execution Plan
LogicalSort(offset=[0], fetch=[100])
  PinotLogicalSortExchange(distribution=[hash], collation=[[]], isSortOnSender=[false], isSortOnReceiver=[false])
    LogicalSort(fetch=[100])
      PinotLogicalAggregate(group=[{0, 1}])
        PinotLogicalExchange(distribution=[hash[0, 1]])
          PinotLogicalAggregate(group=[{0, 2}])
            LogicalJoin(condition=[=($1, $3)], joinType=[inner])
              PinotLogicalExchange(distribution=[hash[1]])
                LeafStageCombineOperator(table=[userAttributes])
                  StreamingInstanceResponse
                    StreamingCombineSelect
                      SelectStreaming(table=[userAttributes], totalDocs=[10000])
                        Project(columns=[[deviceOS, userUUID]])
                          DocIdSet(maxDocs=[40000])
                            FilterMatchEntireSegment(numDocs=[10000])
              PinotLogicalExchange(distribution=[hash[1]])
                LeafStageCombineOperator(table=[userGroups])
                  StreamingInstanceResponse
                    StreamingCombineSelect
                      SelectStreaming(table=[userGroups], totalDocs=[2478])
                        Project(columns=[[groupUUID, userUUID]])
                          DocIdSet(maxDocs=[50000])
                            FilterInvertedIndex(predicate=[groupUUID = 'group-1'], indexLookUp=[inverted_index], operator=[EQ])
                      SelectStreaming(segment=[userGroups_OFFLINE_4], table=[userGroups], totalDocs=[4])
                        Project(columns=[[groupUUID, userUUID]])
                          DocIdSet(maxDocs=[10000])
                            FilterEmpty
                      SelectStreaming(segment=[userGroups_OFFLINE_6], table=[userGroups], totalDocs=[4])
                        Project(columns=[[groupUUID, userUUID]])
                          DocIdSet(maxDocs=[10000])
                            FilterMatchEntireSegment(numDocs=[4])

Logical Plan

논리 계획은 쿼리 실행 계획의 고수준 표현이에요. 이 계획은 서버에 세그먼트 특정 계획을 묻지 않고 broker에서 계산돼요. 즉 논리 계획에는 데이터 분포, 인덱스 등 세그먼트 특정 정보가 포함되지 않아요.

Pinot 1.3.0에서 논리 계획은 기본적으로 활성화되어 있으며 EXPLAIN PLAN FOR 구문을 사용해 얻을 수 있어요. 선택적으로 세그먼트 계획을 기본값으로 활성화할 수 있으며, 그 경우 EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR 구문을 사용해 논리 계획을 얻을 수 있어요.

EXPLAIN PLAN FOR는 기본적으로 논리 계획을 렌더링하지만, Pinot는 또한 쿼리가 실행 가능한 계획으로 변환될 수 있는지 검증해요. 따라서 현재 라우팅 상태에 의존하는 오류를 포함해 쿼리 실행이 직면하게 될 계획 오류를 반환해요. 구현 계획 없이 논리 계획만을 특별히 필요로 할 때, 현재는 실행할 수 없는 쿼리를 포함해 EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR를 사용해요.

참고: 논리 계획을 요청하는 권장 방법은 EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR를 사용하는 것이에요. 이 구문은 구성과 무관하게 모든 Pinot 버전에서 사용할 수 있기 때문이에요.

예시:
-- WITHOUT IMPLENTATION qualifier can be used to ensure logical plan is used
-- It can be used in any version of Pinot even when segment plan is enabled by default
EXPLAIN PLAN WITHOUT IMPLEMENTATION FOR 
SELECT DISTINCT deviceOS, groupUUID
FROM userAttributes AS a
JOIN userGroups AS g
ON a.userUUID = g.userUUID
WHERE g.groupUUID = 'group-1'
LIMIT 100

반환:

Execution Plan
LogicalSort(offset=[0], fetch=[100])
  PinotLogicalSortExchange(distribution=[hash], collation=[[]], isSortOnSender=[false], isSortOnReceiver=[false])
    LogicalSort(fetch=[100])
      PinotLogicalAggregate(group=[{0, 1}])
        PinotLogicalExchange(distribution=[hash[0, 1]])
          PinotLogicalAggregate(group=[{0, 2}])
            LogicalJoin(condition=[=($1, $3)], joinType=[inner])
              PinotLogicalExchange(distribution=[hash[1]])
                LogicalProject(deviceOS=[$4], userUUID=[$6])
                  LogicalTableScan(table=[[default, userAttributes]])
              PinotLogicalExchange(distribution=[hash[1]])
                LogicalProject(groupUUID=[$3], userUUID=[$4])
                  LogicalFilter(condition=[=($3, _UTF-8'group-1')])
                    LogicalTableScan(table=[[default, userGroups]])

Workers plan

참고: 이 explain 모드의 이름을 어떻게 지을 것인지에 대한 논의가 있었으며 향후 버전에서 변경될 수 있어요. worker라는 용어는 사용자 문서의 다른 곳에서는 설명되지 않는 구현 세부 정보를 드러내요.

workers plan은 쿼리가 서로 다른 서버와 그 안의 workers에게 어떻게 분산되는지에 대한 정보를 포함하는 쿼리 실행 계획의 상세한 표현이에요. 이 계획에는 데이터 분포, 인덱스 등 세그먼트 특정 정보가 포함되지 않으며, 일반적인 사용 사례에서는 계획 중 가장[^4] 덜 유용할 수 있어요.

주요 사용 사례는 예를 들어 조인이 colocated 방식으로 실행되는지 검증함으로써 workers 간 데이터 셔플링을 줄이려는 것이에요.

예시
EXPLAIN IMPLEMENTATION PLAN FOR
SELECT DISTINCT deviceOS, groupUUID
FROM userAttributes AS a
JOIN userGroups AS g
ON a.userUUID = g.userUUID
WHERE g.groupUUID = 'group-1'
LIMIT 100

반환:

0]@192.168.0.98:54196|[0] MAIL_RECEIVE(BROADCAST_DISTRIBUTED)
├── [1]@192.168.0.98:54227|[3] MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@192.168.0.98:54196|[0]} (Subtree Omitted)
├── [1]@192.168.0.98:54220|[2] MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@192.168.0.98:54196|[0]} (Subtree Omitted)
├── [1]@192.168.0.98:54214|[1] MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@192.168.0.98:54196|[0]} (Subtree Omitted)
└── [1]@192.168.0.98:54206|[0] MAIL_SEND(BROADCAST_DISTRIBUTED)->{[0]@192.168.0.98:54196|[0]}
    └── [1]@192.168.0.98:54206|[0] SORT LIMIT 100
        └── [1]@192.168.0.98:54206|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)
            ├── [2]@192.168.0.98:54227|[3] MAIL_SEND(HASH_DISTRIBUTED)->{[1]@192.168.0.98:54207|[0],[1]@192.168.0.98:54215|[1],[1]@192.168.0.98:54221|[2],[1]@192.168.0.98:54228|[3]} (Subtree Omitted)
            ├── [2]@192.168.0.98:54220|[2] MAIL_SEND(HASH_DISTRIBUTED)->{[1]@192.168.0.98:54207|[0],[1]@192.168.0.98:54215|[1],[1]@192.168.0.98:54221|[2],[1]@192.168.0.98:54228|[3]} (Subtree Omitted)
            ├── [2]@192.168.0.98:54214|[1] MAIL_SEND(HASH_DISTRIBUTED)->{[1]@192.168.0.98:54207|[0],[1]@192.168.0.98:54215|[1],[1]@192.168.0.98:54221|[2],[1]@192.168.0.98:54228|[3]} (Subtree Omitted)
            └── [2]@192.168.0.98:54206|[0] MAIL_SEND(HASH_DISTRIBUTED)->{[1]@192.168.0.98:54207|[0],[1]@192.168.0.98:54215|[1],[1]@192.168.0.98:54221|[2],[1]@192.168.0.98:54228|[3]}
                └── [2]@192.168.0.98:54206|[0] SORT LIMIT 100
                    └── [2]@192.168.0.98:54206|[0] AGGREGATE_FINAL
                        └── [2]@192.168.0.98:54206|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)
                            ├── [3]@192.168.0.98:54227|[3] MAIL_SEND(HASH_DISTRIBUTED)->{[2]@192.168.0.98:54207|[0],[2]@192.168.0.98:54215|[1],[2]@192.168.0.98:54221|[2],[2]@192.168.0.98:54228|[3]} (Subtree Omitted)
                            ├── [3]@192.168.0.98:54220|[2] MAIL_SEND(HASH_DISTRIBUTED)->{[2]@192.168.0.98:54207|[0],[2]@192.168.0.98:54215|[1],[2]@192.168.0.98:54221|[2],[2]@192.168.0.98:54228|[3]} (Subtree Omitted)
                            ├── [3]@192.168.0.98:54214|[1] MAIL_SEND(HASH_DISTRIBUTED)->{[2]@192.168.0.98:54207|[0],[2]@192.168.0.98:54215|[1],[2]@192.168.0.98:54221|[2],[2]@192.168.0.98:54228|[3]} (Subtree Omitted)
                            └── [3]@192.168.0.98:54206|[0] MAIL_SEND(HASH_DISTRIBUTED)->{[2]@192.168.0.98:54207|[0],[2]@192.168.0.98:54215|[1],[2]@192.168.0.98:54221|[2],[2]@192.168.0.98:54228|[3]}
                                └── [3]@192.168.0.98:54206|[0] AGGREGATE_LEAF
                                    └── [3]@192.168.0.98:54206|[0] JOIN
                                        ├── [3]@192.168.0.98:54206|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)
                                        │   ├── [4]@192.168.0.98:54227|[1] MAIL_SEND(HASH_DISTRIBUTED)->{[3]@192.168.0.98:54207|[0],[3]@192.168.0.98:54215|[1],[3]@192.168.0.98:54221|[2],[3]@192.168.0.98:54228|[3]} (Subtree Omitted)
                                        │   └── [4]@192.168.0.98:54214|[0] MAIL_SEND(HASH_DISTRIBUTED)->{[3]@192.168.0.98:54207|[0],[3]@192.168.0.98:54215|[1],[3]@192.168.0.98:54221|[2],[3]@192.168.0.98:54228|[3]}
                                        │       └── [4]@192.168.0.98:54214|[0] PROJECT
                                        │           └── [4]@192.168.0.98:54214|[0] TABLE SCAN (userAttributes) null
                                        └── [3]@192.168.0.98:54206|[0] MAIL_RECEIVE(HASH_DISTRIBUTED)
                                            ├── [5]@192.168.0.98:54227|[1] MAIL_SEND(HASH_DISTRIBUTED)->{[3]@192.168.0.98:54207|[0],[3]@192.168.0.98:54215|[1],[3]@192.168.0.98:54221|[2],[3]@192.168.0.98:54228|[3]} (Subtree Omitted)
                                            └── [5]@192.168.0.98:54214|[0] MAIL_SEND(HASH_DISTRIBUTED)->{[3]@192.168.0.98:54207|[0],[3]@192.168.0.98:54215|[1],[3]@192.168.0.98:54221|[2],[3]@192.168.0.98:54228|[3]}
                                                └── [5]@192.168.0.98:54214|[0] PROJECT
                                                    └── [5]@192.168.0.98:54214|[0] FILTER
                                                        └── [5]@192.168.0.98:54214|[0] TABLE SCAN (userGroups) null

multi-stage explain 계획 해석

multi-stage 계획은 single-stage 계획보다 더 복잡해요. 이 섹션에서는 이를 해석하는 방법을 설명해요.

EXPLAIN PLAN 구문을 사용해 쿼리의 논리 계획을 얻을 수 있어요. 출력 형식은 다양하지만 모두 쿼리의 논리 계획을 나타내요.

쿼리:

explain plan for
select customer.c_address, orders.o_shippriority
from customer
join orders
    on customer.c_custkey = orders.o_custkey
limit 10

다음 출력을 만들 수 있어요:

LogicalSort(offset=[0], fetch=[10])
  PinotLogicalSortExchange(distribution=[hash], collation=[[]], isSortOnSender=[false], isSortOnReceiver=[false])
    LogicalSort(fetch=[10])
      LogicalProject(c_address=[$0], o_shippriority=[$3])
        LogicalJoin(condition=[=($1, $2)], joinType=[inner])
          PinotLogicalExchange(distribution=[hash[1]])
            LogicalProject(c_address=[$4], c_custkey=[$6])
              LogicalTableScan(table=[[default, customer]])
          PinotLogicalExchange(distribution=[hash[0]])
            LogicalProject(o_custkey=[$5], o_shippriority=[$10])
              LogicalTableScan(table=[[default, orders]])

트리의 각 노드는 하나의 연산을 나타내며, 각 연산자는 속성(attribute)을 가져요. 예를 들어 LogicalJoin 연산자는 조인 조건을 지정하는 condition 속성과 joinType을 가져요.

인덱스 참조 이해하기

$2 같은 표현식은 각 연산자에 대한 입력 행으로의 인덱스 참조예요. 이를 이해하려면 연산자의 자식들을 보고 어떤 속성이 참조되는지 확인해요. 보통 잎 연산자에서 시작해요.

예를 들어 LogicalTableScan은 항상 테이블의 전체 행을 반환하므로 그 속성은 테이블의 컬럼들이에요:

         PinotLogicalExchange(distribution=[hash[0]])
            LogicalProject(o_custkey=[$5], o_shippriority=[$10])
              LogicalTableScan(table=[[default, orders]])

LogicalProject 연산자는 컬럼 o_custkey와 o_shippriority(테이블 행의 $5와 $10 위치)를 선택하고 두 컬럼으로 된 행을 생성해요. PinotLogicalExchange는 hash[0]을 사용해 행을 분산하는데, 이는 LogicalProject의 첫 번째 컬럼인 o_custkey의 해시를 의미해요.

조인의 가상 행

LogicalJoin 연산자는 두 업스트림 스테이지에서 행을 받아요. 조인이 보는 가상 행은 왼쪽 + 오른쪽의 연결(concatenation)이에요.

위 예시에서 왼쪽 스테이지는 [c_address, c_custkey]를 보내고 오른쪽 스테이지는 [o_custkey, o_shippriority]를 보내요. 조인은 [c_address, c_custkey, o_custkey, o_shippriority] 컬럼의 행을 봐요. 조건 =($1, $2)는 c_custkey와 o_custkey로 조인해요. 조인은 모든 컬럼을 변경하지 않고 통과시키므로, 아래의 LogicalProject가 $0과 $3을 선택해 [c_address, o_shippriority]를 만들어요.

ORDER BY 없는 LogicalSort

SQL 쿼리에 ORDER BY가 없어도 LogicalSort 연산자가 나타날 수 있어요. 관계 대수에서 정렬 노드는 LIMIT을 표현하는 데 사용돼요. 정렬 조건이 지정되지 않으면 실제 정렬은 수행되지 않고 행 제한만 적용돼요.

single stage 쿼리 엔진에 대한 Explain

참고: single stage 쿼리 엔진에 대한 explain plan은 explain-plan.md에 자세히 설명되어 있어요.

single stage 쿼리 엔진에 대한 explain plan은 더 단순하고 덜 커스터마이즈되지만, 정보를 표 형식으로 반환해요. 예를 들어 쿼리 EXPLAIN PLAN FOR SELECT playerID, playerName FROM baseballStats.

다음 표를 반환해요:

+---------------------------------------------|------------|---------|
| Operator                                    | Operator_Id|Parent_Id|
+---------------------------------------------|------------|---------|
|BROKER_REDUCE(limit:10)                      | 1          | 0       |
|COMBINE_SELECT                               | 2          | 1       |
|PLAN_START(numSegmentsForThisPlan:1)         | -1         | -1      |
|SELECT(selectList:playerID, playerName)      | 3          | 2       |
|TRANSFORM_PASSTHROUGH(playerID, playerName)  | 4          | 3       |
|PROJECT(playerName, playerID)                | 5          | 4       |
|DOC_ID_SET                                   | 6          | 5       |
|FILTER_MATCH_ENTIRE_SEGMENT(docs:97889)      | 7          | 6       |
+---------------------------------------------|------------|---------|

Operator 컬럼은 Pinot가 실행할 연산자를 설명하는 반면, Operator_Id와 Parent_Id 컬럼은 연산자 간 부모-자식 관계를 보여주며, 이는 실행 트리를 형성해요. 예를 들어 위 계획은 다음과 같이 이해해야 해요:

BROKER_REDUCE(limit:10)
└── COMBINE_SELECT
    └── PLAN_START(numSegmentsForThisPlan:1)
        └── SELECT(selectList:playerID, playerName)
            └── TRANSFORM_PASSTHROUGH(playerID, playerName)
                └── PROJECT(playerName, playerID)
                    └── DOC_ID_SET
                        └── FILTER_MATCH_ENTIRE_SEGMENT(docs:97889)

[^1]: '다른 쿼리 계획' ? [^2]: 집중된(focused)? [^3]: 다른 계획들? [^4]: 가장 적게(least)?

더 알아보기 (Learn more)