EXPLAIN

EXPLAIN

문의 실행 계획을 보여줍니다.

Syntax:

EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ANALYZE | ESTIMATE | TABLE OVERRIDE | WHATIF] [setting = value, ...]
    [
      SELECT ... |
      tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [TTL ...]
    ]
    [FORMAT ...]

Example:

EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV;
Output: sum(number)

Union
├──Aggregating
│  │  Keys:
│  │  Aggregates: sum(number)
│  │  Skip merging: 0
│  └──ReadFromSystemNumbers
│        Output: number
└──Sorting (Sorting for ORDER BY)
   │  Sort description: sum(number) ASC
   └──Aggregating
      │  Keys:
      │  Aggregates: sum(number)
      │  Skip merging: 0
      └──ReadFromSystemNumbers
            Output: number

출처: 문서

본문

EXPLAIN Types

  • AST — 추상 구문 트리(Abstract syntax tree).
  • SYNTAX — AST 수준 최적화 후의 쿼리 텍스트.
  • QUERY TREE — Query Tree 수준 최적화 후의 쿼리 트리.
  • PLAN — 쿼리 실행 계획.
  • PIPELINE — 쿼리 실행 파이프라인.
  • ANALYZE — 쿼리를 실행하고 측정된 런타임 메트릭으로 실행 계획에 주석을 답니다.
  • ESTIMATE — 쿼리 처리 중 테이블에서 읽을 것으로 추정되는 행, 마크, 파트 수.
  • TABLE OVERRIDE — 테이블 함수 스키마에 대한 테이블 오버라이드의 검증된 결과.

EXPLAIN AST

쿼리 AST를 덤프합니다. SELECT뿐만 아니라 모든 유형의 쿼리를 지원합니다.

Settings:

  • graph – AST를 DOT 그래프 설명 언어로 설명된 그래프로 출력합니다. Default: 0.

Examples:

EXPLAIN AST SELECT 1;
SelectWithUnionQuery (children 1)
 ExpressionList (children 1)
  SelectQuery (children 1)
   ExpressionList (children 1)
    Literal UInt64_1
EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today();
  explain
  AlterQuery  t1 (children 1)
   ExpressionList (children 1)
    AlterCommand 27 (children 1)
     Function equals (children 1)
      ExpressionList (children 2)
       Identifier date
       Function today (children 1)
        ExpressionList

EXPLAIN SYNTAX

구문 분석 후의 쿼리 추상 구문 트리(AST)를 보여줍니다.

이는 쿼리를 파싱하고, 쿼리 AST와 쿼리 트리를 구성하고, 선택적으로 쿼리 분석기와 최적화 단계를 실행한 다음, 쿼리 트리를 다시 쿼리 AST로 변환하여 수행됩니다.

Settings:

  • oneline – 쿼리를 한 줄로 출력합니다. Default: 0.
  • run_query_tree_passes – 쿼리 트리를 덤프하기 전에 쿼리 트리 패스를 실행합니다. Default: 0.
  • query_tree_passesrun_query_tree_passes가 설정되면 실행할 패스 수를 지정합니다. query_tree_passes를 지정하지 않으면 모든 패스를 실행합니다.
  • single_record – 재포맷된 쿼리를 줄마다 한 레코드가 아닌 단일 다중 줄 레코드로 반환합니다. Default: 1(explain_syntax_single_record 설정으로 제어). 0으로 설정하면 역사적인 줄마다 한 레코드 출력을 복원하거나, explain_syntax_single_record = 0(전역 또는 쿼리별 SETTINGS)로 설정하거나, compatibility26.8보다 오래된 버전으로 설정할 수 있습니다.

Examples:

EXPLAIN SYNTAX SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;
SELECT *
FROM system.numbers AS a, system.numbers AS b, system.numbers AS c
WHERE (a.number = b.number) AND (b.number = c.number)

With run_query_tree_passes:

EXPLAIN SYNTAX run_query_tree_passes = 1 SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;
SELECT
    __table1.number AS `a.number`,
    __table2.number AS `b.number`,
    __table3.number AS `c.number`
FROM system.numbers AS __table1
ALL INNER JOIN system.numbers AS __table2 ON __table1.number = __table2.number
ALL INNER JOIN system.numbers AS __table3 ON __table2.number = __table3.number

EXPLAIN QUERY TREE

Settings:

  • run_passes — 쿼리 트리를 덤프하기 전에 모든 쿼리 트리 패스를 실행합니다. Default: 1.
  • dump_passes — 쿼리 트리를 덤프하기 전에 사용된 패스에 대한 정보를 덤프합니다. Default: 0.
  • passes — 실행할 패스 수를 지정합니다. -1로 설정하면 모든 패스를 실행합니다. Default: -1.
  • dump_tree — 쿼리 트리를 표시합니다. Default: 1.
  • dump_ast — 쿼리 트리에서 생성된 쿼리 AST를 표시합니다. Default: 0.

Example:

EXPLAIN QUERY TREE SELECT id, value FROM test_table;
QUERY id: 0
  PROJECTION COLUMNS
    id UInt64
    value String
  PROJECTION
    LIST id: 1, nodes: 2
      COLUMN id: 2, column_name: id, result_type: UInt64, source_id: 3
      COLUMN id: 4, column_name: value, result_type: String, source_id: 3
  JOIN TREE
    TABLE id: 3, table_name: default.test_table

EXPLAIN PLAN

쿼리 계획 단계를 덤프합니다.

Settings:

  • optimize — 계획을 표시하기 전에 쿼리 계획 최적화를 적용할지 제어합니다. Default: 1.
  • header — 단계에 대한 출력 헤더를 출력합니다. Default: 0.
  • description — 단계 설명을 출력합니다. Default: 1.
  • indexes — 사용된 인덱스, 필터링된 파트 수, 적용된 각 인덱스에 대해 필터링된 그레뉼 수를 보여줍니다. Default: 0. MergeTree 테이블에서 지원됩니다. ClickHouse >= v25.9부터, 이 문은 SETTINGS use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0과 함께 사용할 때만 합리적인 출력을 보여줍니다.
  • projections — 분석된 모든 projection과 프로젝션 기본 키 조건에 기반한 파트 수준 필터링에 대한 그 효과를 보여줍니다. 각 projection에 대해 이 섹션은 프로젝션 기본 키로 평가된 파트, 행, 마크, 범위 수 같은 통계를 포함합니다. 또한 이 필터링으로 인해 프로젝션 자체를 읽지 않고 건너뛴 데이터 파트 수를 보여줍니다. 프로젝션이 실제로 읽기에 사용되었는지 필터링에만 분석되었는지는 description 필드로 판단할 수 있습니다. Default: 0. MergeTree 테이블에서 지원됩니다.
  • actions — 단계 동작에 대한 상세 정보를 출력합니다. Default: 1.
  • sorting — 정렬된 출력을 생성하는 각 계획 단계에 대한 정렬 설명을 출력합니다. Default: 0.
  • keep_logical_steps — 조인을 물리적 조인 구현으로 변환하는 대신 논리적 계획 단계를 유지합니다. Default: 0.
  • json — 쿼리 계획 단계를 JSON 형식의 행으로 출력합니다. Default: 0. 불필요한 이스케이프를 피하려면 TabSeparatedRaw (TSVRaw) 형식을 사용하는 것이 권장됩니다.
  • input_headers — 단계에 대한 입력 헤더를 출력합니다. Default: 0. 대부분 입출력 헤더 불일치 관련 문제를 디버그하려는 개발자에게만 유용합니다.
  • column_structure — 이름과 타입 외에도 헤더의 컬럼 구조를 출력합니다. Default: 0. 대부분 입출력 헤더 불일치 관련 문제를 디버그하려는 개발자에게만 유용합니다.
  • distributed — 분산 테이블 또는 병렬 복제본에 대해 원격 노드에서 실행된 쿼리 계획을 보여줍니다. json과 함께 지원되지 않습니다. Default: 0.
  • compact — 활성화되면 계획에서 표현식 단계와 상세 동작 정보(입력, 함수, 별칭, 출력 위치)를 숨깁니다. actions = 1일 때만 효과가 있습니다. Default: 1.
  • pretty — 들여쓰기 대신 선 그리기 문자(├──, └──, │)를 사용해 계획 트리를 출력해 계층을 시각화합니다. 또한 조인 단계 속성을 인라인으로 포맷합니다. Default: 1.

기본적으로 explain_query_plan_default = 'pretty'이므로 actions, compact, pretty1로 초기화되고 계획은 압축되고(compact), 예쁘며(pretty), 동작 주석이 달린(action-annotated) 형태로 렌더링됩니다. EXPLAIN 문에서 이 옵션 중 하나를 명시적으로 지정하면(예: EXPLAIN actions = 0, compact = 0, pretty = 0 SELECT ...) 항상 기본값을 재정의합니다.

ClickHouse 26.7 이전에는 actions, compact, pretty의 기본값이 0이었습니다. explain_query_plan_default = 'legacy'(전역 또는 쿼리별 SETTINGS)를 설정하거나 compatibility26.7보다 오래된 버전으로 설정하면 여전히 그 출력을 얻을 수 있습니다.

jsondistributed 옵션은 explain_query_plan_default = 'pretty'일 때에도 pretty 기본값(actions, compact, pretty)을 활성화하지 않습니다. 해당 출력에 동작 세부 정보를 포함하려면 actions = 1을 수동으로 설정하세요.

Example:

EXPLAIN SELECT sum(number) FROM numbers(10) GROUP BY number % 4  LIMIT 1;
Output: sum(number)

Limit (preliminary LIMIT)
│  Limit 1
│  Offset 0
└──Aggregating
   │  Keys: number MOD 4
   │  Aggregates: sum(number)
   │  Skip merging: 0
   └──ReadFromSystemNumbers
         Output: number

단계 및 쿼리 비용 추정은 지원되지 않습니다.

json = 1이면 쿼리 계획이 JSON 형식으로 표현됩니다. 모든 노드는 항상 Node Type, Node Id, Plans 키를 가진 사전입니다. Node Type은 단계 이름이 있는 문자열이고, Node Id는 고유한 단계 식별자(숫자 접미사가 있는 단계 이름, 예: Union_10)입니다. Plans는 자식 단계 설명이 있는 배열입니다. 다른 선택적 키는 노드 타입과 설정에 따라 추가될 수 있습니다.

Example:

EXPLAIN json = 1, description = 0 SELECT 1 UNION ALL SELECT 2 FORMAT TSVRaw;
[
  {
    "Plan": {
      "Node Type": "Union",
      "Node Id": "Union_10",
      "Plans": [
        {
          "Node Type": "Expression",
          "Node Id": "Expression_13",
          "Plans": [
            {
              "Node Type": "ReadFromStorage",
              "Node Id": "ReadFromStorage_0"
            }
          ]
        },
        {
          "Node Type": "Expression",
          "Node Id": "Expression_16",
          "Plans": [
            {
              "Node Type": "ReadFromStorage",
              "Node Id": "ReadFromStorage_4"
            }
          ]
        }
      ]
    }
  }
]

description = 1이면 Description 키가 단계에 추가됩니다:

{
  "Node Type": "ReadFromStorage",
  "Description": "SystemOne"
}

header = 1이면 Header 키가 컬럼 배열로 단계에 추가됩니다.

Example:

EXPLAIN json = 1, description = 0, header = 1 SELECT 1, 2 + dummy;
[
  {
    "Plan": {
      "Node Type": "Expression",
      "Node Id": "Expression_5",
      "Header": [
        {
          "Name": "1",
          "Type": "UInt8"
        },
        {
          "Name": "plus(2, dummy)",
          "Type": "UInt16"
        }
      ],
      "Plans": [
        {
          "Node Type": "ReadFromStorage",
          "Node Id": "ReadFromStorage_0",
          "Header": [
            {
              "Name": "dummy",
              "Type": "UInt8"
            }
          ]
        }
      ]
    }
  }
]

indexes = 1이면 Indexes 키가 추가됩니다. 여기에는 사용된 인덱스 배열이 포함됩니다. 각 인덱스는 Type 키(문자열 Partition Min-Max, Partition, Statistics, PrimaryKey 또는 Skip)와 선택적 키로 JSON으로 설명됩니다:

Statistics 인덱스는 쿼리 필터와 일치할 수 없는 파트를 건너뛰기 위해 파트별 컬럼 통계(min/max 값, Nullable 컬럼의 NULL 값 수)를 사용합니다.

  • Name — 인덱스 이름(현재 Skip 인덱스에서만 사용).
  • Keys — 인덱스가 사용하는 컬럼 배열.
  • Condition — 사용된 조건.
  • Description — 인덱스 설명(현재 Skip 인덱스에서만 사용).
  • Parts — 인덱스가 적용된 후/전의 파트 수.
  • Granules — 인덱스가 적용된 후/전의 그레뉼 수.
  • Ranges — 인덱스가 적용된 후의 그레뉼 범위 수.

Example:

"Node Type": "ReadFromMergeTree",
"Indexes": [
  {
    "Type": "Partition Min-Max",
    "Keys": ["y"],
    "Condition": "(y in [1, +inf))",
    "Parts": 4/5,
    "Granules": 11/12
  },
  {
    "Type": "Partition",
    "Keys": ["y", "bitAnd(z, 3)"],
    "Condition": "and((bitAnd(z, 3) not in [1, 1]), and((y in [1, +inf)), (bitAnd(z, 3) not in [1, 1])))",
    "Parts": 3/4,
    "Granules": 10/11
  },
  {
    "Type": "PrimaryKey",
    "Keys": ["x", "y"],
    "Condition": "and((x in [11, +inf)), (y in [1, +inf)))",
    "Parts": 2/3,
    "Granules": 6/10,
    "Search Algorithm": "generic exclusion search"
  },
  {
    "Type": "Skip",
    "Name": "t_minmax",
    "Description": "minmax GRANULARITY 2",
    "Parts": 1/2,
    "Granules": 2/6
  },
  {
    "Type": "Skip",
    "Name": "t_set",
    "Description": "set GRANULARITY 2",
    "": 1/1,
    "Granules": 1/2
  }
]

projections = 1이면 Projections 키가 추가됩니다. 여기에는 분석된 프로젝션 배열이 포함됩니다. 각 프로젝션은 다음 키로 JSON으로 설명됩니다:

  • Name — 프로젝션 이름.
  • Condition — 사용된 프로젝션 기본 키 조건.
  • Description — 프로젝션이 사용되는 방식 설명(예: 파트 수준 필터링).
  • Selected Parts — 프로젝션이 선택한 파트 수.
  • Selected Marks — 선택된 마크 수.
  • Selected Ranges — 선택된 범위 수.
  • Selected Rows — 선택된 행 수.
  • Filtered Parts — 파트 수준 필터링으로 인해 건너뛴 파트 수.

Example:

"Node Type": "ReadFromMergeTree",
"Projections": [
  {
    "Name": "region_proj",
    "Description": "Projection has been analyzed and is used for part-level filtering",
    "Condition": "(region in ['us_west', 'us_west'])",
    "Search Algorithm": "binary search",
    "Selected Parts": 3,
    "Selected Marks": 3,
    "Selected Ranges": 3,
    "Selected Rows": 3,
    "Filtered Parts": 2
  },
  {
    "Name": "user_id_proj",
    "Description": "Projection has been analyzed and is used for part-level filtering",
    "Condition": "(user_id in [107, 107])",
    "Search Algorithm": "binary search",
    "Selected Parts": 1,
    "Selected Marks": 1,
    "Selected Ranges": 1,
    "Selected Rows": 1,
    "Filtered Parts": 2
  }
]

actions = 1이면 추가 키가 단계 타입에 따라 달라집니다.

Example:

EXPLAIN json = 1, actions = 1, description = 0 SELECT 1 FORMAT TSVRaw;
[
  {
    "Plan": {
      "Node Type": "Expression",
      "Node Id": "Expression_5",
      "Expression": {
        "Inputs": [
          {
            "Name": "dummy",
            "Type": "UInt8"
          }
        ],
        "Actions": [
          {
            "Node Type": "INPUT",
            "Result Type": "UInt8",
            "Result Name": "dummy",
            "Arguments": [0],
            "Removed Arguments": [0],
            "Result": 0
          },
          {
            "Node Type": "COLUMN",
            "Result Type": "UInt8",
            "Result Name": "1",
            "Column": "Const(UInt8)",
            "Arguments": [],
            "Removed Arguments": [],
            "Result": 1
          }
        ],
        "Outputs": [
          {
            "Name": "1",
            "Type": "UInt8"
          }
        ],
        "Positions": [1]
      },
      "Plans": [
        {
          "Node Type": "ReadFromStorage",
          "Node Id": "ReadFromStorage_0"
        }
      ]
    }
  }
]

compact = 0actions = 1이면 Expression 단계를 표현식에 대한 상세 정보와 함께 볼 수 있습니다:

EXPLAIN actions = 1, compact = 0 SELECT sum(number) FROM numbers(10) GROUP BY number % 4;
Output: sum(number)

Expression ((Project names + Projection))
│  Actions: INPUT : 0 -> sum(__table1.number) UInt64 : 0
│           INPUT :: 1 -> modulo(__table1.number, 4_UInt8) UInt8 : 1
│           ALIAS sum(__table1.number) :: 0 -> sum(number) UInt64 : 2
│  Positions: 2
└──Aggregating
   │  Keys: number MOD 4
   │  Aggregates: sum(number)
   │  Skip merging: 0
   └──Expression ((Before GROUP BY + Change column names to column identifiers))
      │  Actions: INPUT : 0 -> number UInt64 : 0
      │           COLUMN Const(UInt8) -> 4_UInt8 UInt8 : 1
      │           ALIAS number :: 0 -> __table1.number UInt64 : 2
      │           FUNCTION modulo(__table1.number : 2, 4_UInt8 :: 1) -> modulo(__table1.number, 4_UInt8) UInt8 : 0
      │  Positions: 0 2
      └──ReadFromSystemNumbers
            Output: number

distributed = 1이면 출력에는 로컬 쿼리 계획뿐만 아니라 원격 노드에서 실행될 쿼리 계획도 포함됩니다. 이는 분산 쿼리를 분석하고 디버그하는 데 유용합니다.

distributedpretty 출력이 원격 샤드 계획을 계획 트리에 통합하지 않으므로 레거시(non-pretty) 형식으로만 렌더링됩니다. 이러한 이유로 distributed를 활성화하면 explain_query_plan_default와 무관하게 pretty 기본값(actions, compact, pretty)이 자동으로 비활성화됩니다. 여전히 actions=1을 수동으로 설정할 수 있습니다. distributed 옵션은 json과도 함께 지원되지 않습니다.

분산 테이블 예시:

EXPLAIN distributed=1 SELECT * FROM remote('127.0.0.{1,2}', numbers(2)) WHERE number = 1;
Union
  Expression ((Project names + (Projection + (Change column names to column identifiers + (Project names + Projection)))))
    Filter ((WHERE + Change column names to column identifiers))
      ReadFromSystemNumbers
  Expression ((Project names + (Projection + Change column names to column identifiers)))
    ReadFromRemote (Read from remote replica)
      Expression ((Project names + Projection))
        Filter ((WHERE + Change column names to column identifiers))
          ReadFromSystemNumbers

병렬 복제본 예시:

SET enable_parallel_replicas = 2, max_parallel_replicas = 2, cluster_for_parallel_replicas = 'default';

EXPLAIN distributed=1 SELECT sum(number) FROM test_table GROUP BY number % 4;
Expression ((Project names + Projection))
  MergingAggregated
    Union
      Aggregating
        Expression ((Before GROUP BY + Change column names to column identifiers))
          ReadFromMergeTree (default.test_table)
      ReadFromRemoteParallelReplicas
        BlocksMarshalling
          Aggregating
            Expression ((Before GROUP BY + Change column names to column identifiers))
              ReadFromMergeTree (default.test_table)

두 예시 모두에서 쿼리 계획은 로컬 및 원격 단계를 포함한 완전한 실행 흐름을 보여줍니다.

pretty = 1이면 계획 트리는 들여쓰기 대신 선 그리기 문자를 사용해 표시되고, 주요 단계에 추가 정보가 표시됩니다:

  • 쿼리 출력 컬럼이 계획의 맨 위에 출력됩니다.
  • 필터, 집계 키, 정렬 설명, 창 함수의 표현식은 인간이 읽을 수 있는 SQL 같은 표기로 표시됩니다(예: greater(plus(a, 1), 5) 대신 a + 1 > 5). 내부 컬럼 식별자 접두사(예: __table1.)는 명료함을 위해 제거됩니다.
  • 소스 단계(예: ReadFromMergeTree)는 출력 컬럼을 표시합니다.
  • 필터 단계는 조건을 SQL 표기로 표시합니다. 런타임 조인 필터가 있으면 별도로 표시됩니다.
  • 집계 단계는 키와 집계 함수를 인자와 함께 표시합니다(예: sum(c), count()).
  • 튜플 리터럴의 IN 집합은 값을 표시합니다(큰 집합은 잘림), 서브쿼리 기반 집합은 subquery1, subquery2 등으로 표시되고, Set 엔진 테이블의 집합은 테이블 이름을 표시합니다.
  • 조인 단계는 조인 관계를 수학적 표기로 표시하고, 조인 순서 최적화 도구가 단계에 대해 생성한 추정치(비용, 선택성, 출력 행, 각 측의 행)와 각 측의 입력 컬럼을 표시합니다. 다음 기호는 서로 다른 조인 타입을 나타내는 데 사용됩니다:
Symbol Join Type
Inner Join
Left Join
Right Join
Full Join
Left Semi Join
Right Semi Join
with strikethrough Left Anti Join
with strikethrough Right Anti Join
× Cross Join

예를 들어 t1 ⟕ t2는 테이블 t1t2 사이의 left join을 의미합니다. 테이블 이름 뒤 괄호 안의 숫자(예: t1[100])는 테이블 통계가 있을 때 추정 행 수를 나타냅니다.

조인 관계 아래, 각 조인 단계는 조인 순서 최적화 도구가 생성한 추정치를 출력합니다:

Cost: estimated <cost>
Selectivity: estimated (NDV) <selectivity>
Output rows: estimated <rows>
Left: rows estimated <left_rows>
Right: rows estimated <right_rows>
  • Cost — 이 단계 아래 전체 조인 서브트리의 비용으로, 최적화 도구가 후보 조인 순서를 비교할 때 최소화하는 값입니다. 조인의 비용은 추정된 일치 행 쌍 수, <selectivity> * <left_rows> * <right_rows>에 입력의 비용을 더한 것입니다.
  • Selectivity — 조인 조건을 살아남는 두 측 데카르트 곱의 추정 분율입니다. 조인 키의 고유 값 수(NDV)에서 파생됩니다: 키 동등성은 쌍의 약 1 / max(NDV_left, NDV_right)를 유지하고, 조인 조건 중 가장 작은 분율이 사용됩니다.
  • Output rows — 조인이 생성하는 추정 행 수: inner join의 경우 <selectivity> * <left_rows> * <right_rows>, LEFT의 경우 <left_rows>, RIGHT의 경우 <right_rows>, FULL의 경우 <left_rows> + <right_rows>로 바닥이 정해집니다. 외부 조인이 보존 측의 모든 행을 유지하기 때문입니다. SEMI 또는 ANTI 조인이 재정렬에 참여하면 보존 측의 분율로 추정되며, SEMI의 경우 <preserved_rows> * min(1, <selectivity> * <other_rows>), ANTI의 경우 나머지 행으로 추정됩니다; 그렇지 않으면 조인 종류의 공식을 사용합니다.
  • Left / Right — 각 측에서 조인으로 들어오는 추정 행 수.

최적화 도구가 추정할 수 없는 값은 no stats로 보고됩니다. 이는 조인 순서 최적화가 실행되지 않았을 때 발생합니다 — 예를 들어 query_plan_optimize_join_order_limit0일 때 — 또는 추정의 근거가 없을 때입니다.

Input (left):Input (right): 줄은 각 측이 조인에 공급하는 컬럼을 나열합니다.

pretty 옵션은 Expression 단계와 상세 동작 정보를 숨겨 계획을 더 읽기 쉽게 만드는 compact = 1과 잘 작동합니다.

조인이 있는 상세 예시. join_runtime_filter_min_probe_rows 설정은 이렇게 작은 테이블도 런타임 조인 필터를 구축하도록 낮추었을 뿐입니다:

SET join_runtime_filter_min_probe_rows = 10;

CREATE TABLE t1 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
CREATE TABLE t2 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
INSERT INTO t1 SELECT number, toString(number) FROM numbers(100);
INSERT INTO t2 SELECT number, toString(number) FROM numbers(100);

EXPLAIN actions = 1, compact = 1, pretty = 1
SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id FORMAT Raw;
Output: id, value, id, value

Join (JOIN FillRightFirst)
│  t1[100] ⋈ t2[100]
│  Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│  Cost: estimated 100.00
│  Selectivity: estimated (NDV) 0.01
│  Output rows: estimated 100.00
│  Left: rows estimated 100.00
│  Right: rows estimated 100.00
│  Join conditions: id = id
│  Input (left): id, value
│  Input (right): id, value
├──ReadFromMergeTree (default.t1)
│     Read type: Default
│     Parts: 1 | Granules: 1
│     Output: id, value
│     Runtime filters: RF1(id, id from default.t2)
└──BuildRuntimeFilter (Build runtime join filter on id)
   │  Filter id: RF1
   │  Source table: default.t2
   └──ReadFromMergeTree (default.t2)
         Read type: Default
         Parts: 1 | Granules: 1
         Output: id, value

EXPLAIN PIPELINE

Settings:

  • header — 각 출력 포트에 대한 헤더를 출력합니다. Default: 0.
  • graphDOT 그래프 설명 언어로 설명된 그래프를 출력합니다. Default: 0.
  • compactgraph 설정이 활성화되면 압축 모드로 그래프를 출력합니다. Default: 1.
  • compact_repeated_processor_chains — 텍스트 출력에서 인접한 반복 프로세서 체인을 반복 횟수와 함께 한 사본으로 표시해 압축합니다. 이는 같은 체인이 여러 번 나타날 때(예: 조인에서) 병렬 파이프라인을 읽기 쉽게 만들 수 있습니다. 그래프 출력에는 영향을 주지 않습니다. Default: 0.
Resize 16 → 1
  FillingRightJoinSide          │
    SimpleSquashingTransform    │ × 16
      Resize 1 → 16

compact=0graph=1일 때 프로세서 이름에는 고유한 프로세서 식별자가 있는 추가 접미사가 포함됩니다.

Example:

EXPLAIN PIPELINE SELECT sum(number) FROM numbers_mt(100000) GROUP BY number % 4;
(Union)
(Expression)
ExpressionTransform
  (Expression)
  ExpressionTransform
    (Aggregating)
    Resize 2 → 1
      AggregatingTransform × 2
        (Expression)
        ExpressionTransform × 2
          (SettingQuotaAndLimits)
            (ReadFromStorage)
            NumbersRange × 2 0 → 1

EXPLAIN ANALYZE

EXPLAIN ANALYZE는 실제로 쿼리를 실행하고 결과 행을 버린 다음, 각 단계에 런타임에 실제로 일어난 일이 주석으로 달린 EXPLAIN PLAN과 같은 계획 트리를 출력합니다.

Settings:

EXPLAIN ANALYZEEXPLAIN PLAN( EXPLAIN PLAN 섹션에 문서화)과 같은 표시 옵션을 받습니다.

  • headerEXPLAIN PLAN 섹션 참고.
  • descriptionEXPLAIN PLAN 섹션 참고.
  • projectionsEXPLAIN PLAN 섹션 참고.
  • sortingEXPLAIN PLAN 섹션 참고.
  • input_headersEXPLAIN PLAN 섹션 참고.
  • column_structureEXPLAIN PLAN 섹션 참고.
  • actionsEXPLAIN PLAN 섹션 참고. Default: 1.
  • indexesEXPLAIN PLAN 섹션 참고. Default: 1.
  • compactEXPLAIN PLAN 섹션 참고. Default: 1.
  • prettyEXPLAIN PLAN 섹션 참고. Default: 1.
  • processorsEXPLAIN ANALYZE의 경우, 각 단계마다 프로세서별 경과 시간 분포(min, median, max, sum)가 있는 추가 줄을 출력합니다. 병렬 프로세서 전반의 부하 편향을 찾는 데 유용합니다. Default: 0.
  • matchesEXPLAIN ANALYZE의 경우, 조인 단계가 그 숫자를 조인이 어쨌든 생성하는 것에서 파생할 수 없는 경우에 matched, match rate, fanout 메트릭에 필요한 추가 부기(bookkeeping)를 하게 합니다. 파생할 수 있는 곳에서는 이 옵션 없이 보고됩니다. Join steps 참고. Default: 0.

EXPLAIN ANALYZE는 실제로 감싸진 쿼리를 실행하므로, 실행하지 않는 EXPLAIN 형태와 달리 몇 가지 방식에서 그 쿼리처럼 동작합니다:

  • 할당량 및 제한. 쿼리를 직접 실행하는 것과 같은 quotas로 계산되고 같은 limits(예: query_selects, read_rows)이 적용됩니다. 계획 중에 할당량이 면제되는 소스(예: system.one)는 계산되지 않습니다.
  • 실패한 트랜잭션. 이미 실패한(ROLLED_BACK) transaction 안에서, 일반 SELECT처럼 INVALID_TRANSACTION으로 거부됩니다 — 먼저 ROLLBACK을 실행하세요.
  • 스트리밍 읽기. 스트리밍(FROM ... STREAM) 읽기에서는 그러한 읽기가 결코 완료되지 않으므로 NOT_IMPLEMENTED로 거부됩니다.
  • 분산 쿼리. distributed 모드로 실행된 쿼리에는 지원되지 않습니다.

Example:

EXPLAIN ANALYZE SELECT number % 10 AS k, count() FROM numbers_mt(1000000) GROUP BY k;
Query summary:
  Time:        10.72 ms (planning 6.45 ms · execution 4.26 ms)
  Read:        1.00 million rows, 8.00 MB (234.49 million rows/s., 1.88 GB/s.)
  Peak memory: 28.98 KiB

Output: number MOD 10, count()

Expression ((Project names + Projection))
│  I/O: rows 10 → 10 · 90 B → 90 B
│    time 21.82 us (0.5%) · parallelism 0.98/1
└──Aggregating
   │  Keys: number MOD 10
   │  Aggregates: count()
   │  Skip merging: 0
   │  I/O: rows 1.00 million → 10 (0.00%) · 1.00 MB → 90 B
   │    Stage (partial aggregation): time 868.45 us (20.4%) · parallelism 3.80/15
   │    Stage (final aggregation): time 445.27 us (10.4%) · parallelism 1.11/16
   └──Expression ((Before GROUP BY + Change column names to column identifiers))
      │  I/O: rows 1.00 million → 1.00 million · 8.00 MB → 1.00 MB
      │    time 677.07 us (15.9%) · parallelism 4.31/15
      └──ReadFromSystemNumbers
            Output: number
            I/O: rows 0 → 1.00 million · 0 B → 8.00 MB
              time 993.94 us (23.3%) · parallelism 7.52/15

출력을 살펴보겠습니다. 먼저 헤더를 봅시다.

   Query summary:
     Time:        <total> (planning <planning> · execution <execution>)
     Read:        <rows> rows, <bytes> (<rows/s>, <bytes/s>)
     Peak memory: <peak>
  • Time — 총 시간을 계획(즉 계획 생성 + 계획 최적화 + 파이프라인 구축)과 실행(파이프라인 실행) 단계로 나눕니다.
  • Read — 테이블에서 읽은 행과 압축되지 않은 바이트, 처리량 포함 — 일반 쿼리 푸터가 "Processed"로 보고하는 것과 같은 숫자입니다.
  • Peak memory — 쿼리가 사용한 최대 메모리.

이제 쿼리 계획에 나타나는 새 줄들을 살펴보겠습니다.

I/O: rows <in> → <out> (<selectivity>%) · <bytes_in> → <bytes_out>
  [Stage (<stage>): ]time <t> (<share>%) · parallelism <avg>/<max>

행과 바이트는 전체 단계에 대해 한 번 보고됩니다(I/O 줄). 시간과 병렬성은 다음 들여쓰기 줄에서 단계의 각 stage별로 보고됩니다.

  • rows <in> → <out> — 단계에 들어가고 나간 행; (<selectivity>%)는 단계가 데이터를 얼마나 필터링(out/in)하거나 확장했는지 보여주며, 입력 행이 출력 행과 같고 입력 행이 0일 때는 숨겨집니다.
  • <bytes_in> → <bytes_out> — 단계를 흐르는 압축되지 않은 메모리 내 바이트(둘 다 0이면 생략).
  • time <t> (<share>%) — 단계(stage)가 활성 상태였던 벽시계 시간과 쿼리 실행 시간에서의 비중(즉 빌드 시간 제외). stage와 step이 동시에 실행되므로 비중의 합이 100%를 넘을 수 있습니다.
  • parallelism <avg>/<max> — 이 stage 내에서 동시에 작업한 CPU 스레드의 평균 수, 사용할 수 있는 최대 수 대비. max에 가까운 값은 stage가 잘 병렬화되었음을, 1에 가까운 값은 대부분 직렬로 실행되었음을 의미합니다.
  • Stage (<stage>) — stage의 이름. 단일 stage가 있는 step은 Stage (...) 라벨 없이 시간 줄을 직접 출력합니다. 여러 stage가 있는 step은 stage마다 라벨이 있는 줄을 출력합니다, 예: AggregatingStage (partial aggregation)Stage (final aggregation)을, 해시 조인은 Stage (build)Stage (probe)를 보여줍니다.

ClickHouse는 계획 단계 내 작업의 실행뿐만 아니라 계획 단계의 실행도 병렬화합니다. parallelism 메트릭은 이 단계의 작업만 반영합니다. 다른 단계는 동시에 실행될 수 있으므로, 이 숫자는 단계의 병렬성이 전체 쿼리와 비교해 얼마인지 보여주지 않습니다.

parallelism의 최대 수는 다음 사이의 최소값으로 계산됩니다:

  1. 계획 단계 내 총 작업 수;
  2. max_threads에 설정된 최대 쿼리 처리 스레드 수.

Join steps

조인 단계에서 EXPLAIN ANALYZE는 조인 순서 최적화 도구의 추정치를 실제 일어난 것과 비교하는 줄(Estimated vs. actual join metrics 참고)과 측별 참여 줄 — LeftRight — 그리고 조인 구현에 특화된 줄을 출력합니다. join_algorithm의 모든 값(hash, parallel_hash, grace_hash, partial_merge, full_sorting_merge, parallel_full_sorting_merge, direct)과 설정이 선택할 수 없는 두 구현(CROSS 또는 COMMA 조인, 키 동등성 없는 ON 섹션, Join 테이블 엔진)도 다룹니다. 대부분 양쪽을 보고합니다; 일부는 물질화하는 측만 보고합니다(예: directLeft:만 출력).

측별 줄은 같은 형태를 공유합니다:

Left:  rows estimated <estimated_left_rows>  · rows <left_rows>  · matched <matched_left_rows>  · match rate <match_rate>% · fanout <fanout>
Right: rows estimated <estimated_right_rows> · rows <right_rows> · matched <matched_right_rows> · match rate <match_rate>% · fanout <fanout>

각 측에 대해 EXPLAIN ANALYZE는 다음을 보고합니다:

  • rows estimated <estimated_rows> — 조인 순서 최적화 도구의 해당 측 행 추정치로, 옆의 실제 rows와 비교하기 위해 출력됩니다; 최적화 도구가 추정치를 생성하지 않으면 no stats(Estimated vs. actual join metrics 참고).
  • rows <rows> — 조인을 통과한 해당 측의 총 행 수.
  • matched <matched_rows> — 다른 측에서 적어도 하나의 조인 파트너를 찾은 해당 측의 행 수. 이것은 키가 아니라 행을 셉니다: 키가 오른쪽에 세 번 나타나고 일치하면, 세 오른쪽 행 모두 일치로 계산됩니다.
  • match rate <match_rate>% — 일치한 해당 측 행의 백분율, 100 * <matched_rows> / <rows>로 계산.
  • fanout <fanout> — 해당 측의 평균 일치 행이 생성한 출력 행 수.

정확히 파생할 수 없는 숫자는 0이 아니라 not collected로 보고됩니다. match ratefanoutmatched에서 파생되므로, 그것이 없는 측은 세 가지 모두 not collected로 보고합니다.

Estimated vs. actual join metrics

조인 단계는 조인 순서 최적화 도구의 추정치 — EXPLAIN PLAN이 보여주는 것과 같은 (EXPLAIN PLAN 섹션 참고) — 를 지니고 있으며, EXPLAIN ANALYZE는 각각을 측정된 값 옆에 출력합니다:

Cost: estimated <cost> · actual <cost>
Selectivity: estimated (NDV) <selectivity> · actual (cartesian) <selectivity>
Output rows: estimated <rows> · actual <rows> · q-error <ratio>
  • Cost — 두 값 모두 일치 출력 행을 셉니다. 추정치는 최적화 도구의 조인 서브트리 비용: <selectivity> * <left_rows> * <right_rows>에 입력 비용을 더한 것입니다. 실제 값도 같은 방식으로 측정됩니다 — 이 조인의 일치 출력 행에 같은 재정렬 클러스터에 속하는 아래 모든 조인의 실제 비용을 더한 것입니다.
  • Selectivity — 추정치는 조인 키의 고유 값 수에서 파생됩니다; 실제 값은 출력에 들어간 데카르트 곱의 측정된 분율: <matched output rows> / (<left rows> * <right rows>)입니다. 최적화 도구가 추정하려는 것이 이것이기 때문입니다.
  • Output rows — 조인이 생성한 추정 및 실제 행 수. 둘 다 0이 아닐 때 q-errormax(estimated / actual, actual / estimated)를 보고하는데, 이는 카디널리티 추정 품질의 표준 측도입니다: 1.00은 완벽한 추정을, 큰 값은 최적화 도구가 심하게 잘못된 카디널리티로 조인 순서를 선택했음을 의미합니다.

결코 이루어지지 않은 추정은 no stats로 보고됩니다 — 예를 들어 query_plan_optimize_join_order_limit0이어서 조인 순서 최적화가 실행되지 않았을 때. 이는 실행이 측정할 수 없는 실제 값을 표시하는 not collected와는 구별됩니다. 미리 채워진 오른쪽 측이 있는 조인(Join 테이블 엔진, direct 조인)은 조인 순서 최적화 도구를 거치지 않고 참여 줄만 출력합니다.

조인 단계는 각 측이 조인에 공급하는 컬럼이 있는 Input (left):Input (right): 줄도 출력합니다.

EXPLAIN PLAN 조인 예시의 테이블에 대해 EXPLAIN ANALYZE SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id의 조인 단계는 다음과 같습니다:

Join (JOIN FillRightFirst)
│  t1[100] ⋈ t2[100]
│  Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│  Join conditions: id = id
│  Cost: estimated 100.00 · actual 100.00
│  Selectivity: estimated (NDV) 0.01 · actual (cartesian) 0.01
│  Output rows: estimated 100.00 · actual 100.00 · q-error 1.00
│  Left: rows estimated 100.00 · rows 100.00 · matched 100.00 · match rate 100.00% · fanout 1.00
│  Right: rows estimated 100.00 · rows 100.00 · matched not collected · match rate not collected · fanout not collected
│  Hash table: unique keys 100.00 · memory 6.27 KB
│  Input (left): id, value
│  Input (right): id, value
│  I/O: rows 200 → 100 (50.00%) · 3.58 KB → 3.58 KB
│    Stage (build): time 127.45 us (1.2%) · parallelism 0.99/1
│    Stage (probe): time 124.99 us (1.2%) · parallelism 0.99/1

Fanout

fanout은 행 곱셈을 측정합니다:

matched output rows = <output_rows> - <NULL-padded rows of both sides>
fanout              = <matched output rows> / <matched_rows of that side>

외부 조인은 파트너를 찾지 못한 보존 측의 각 행에 대해 하나의 NULL로 채워진 출력 행을 생성합니다. 그 행들은 비율을 희석시키지 않도록 빼집니다. 보존 측만이 그것을 가집니다 — RIGHTFULL의 오른쪽 측, LEFTFULL의 왼쪽 측:

  • fanout = 0 — 일치 행이 출력 행을 전혀 생성하지 않았으며, 이는 ANTI 조인이 하는 일입니다: 파트너를 찾지 못한 행만 생성합니다.
  • fanout = 1 — 깨끗한 1:1 조인; 모든 일치 행이 정확히 하나의 출력 행을 생성했습니다.
  • fanout > 1 — 1:N 조인; 다른 측의 중복 키가 행을 곱했습니다. 양쪽이 동시에 큰 값은 의도하지 않은 데카르트 폭발의 특징입니다.

When the numbers require matches = 1

이 숫자들의 대부분은 조인이 어쨌든 구축하는 데이터에서 나오며 일반 EXPLAIN ANALYZE로 보고됩니다. 나머지는 조인이 그렇지 않으면 하지 않을 부기가 필요하므로 EXPLAIN ANALYZE matches = 1로만 보고됩니다. 어느 것인지는 알고리즘에 따라 다릅니다; 해시 계열에서는 두 경우입니다:

  • ALL INNERALL LEFT의 오른쪽 측, 모든 일치 오른쪽 행을 표시해야 하므로;
  • ALL LEFTALL FULL의 왼쪽 측, 그러나 쿼리가 오른쪽 테이블에서 아무것도 선택하지 않고 ON 섹션이 평범한 키 동등성일 때만. 그렇지 않으면 probe가 이미 어느 왼쪽 행이 일치했는지 기록합니다(오른쪽 컬럼을 물질화하거나 잔여 조건을 평가하기 위해) — 오른쪽 컬럼을 물질화하거나 잔여 조건을 평가하기 위해 — 그래서 그 개수는 옵션 없이 정확합니다.

partial_merge는 같은 이유로 네 가지 ALL 종류의 오른쪽 측에 그것이 필요합니다. full_sorting_mergeparallel_full_sorting_mergeANY 종류의 양쪽에 필요합니다. ALL 종류는 아무것도 필요하지 않습니다.

이 옵션은 추가 부기가 무료가 아니고, 그 비용이 측정 충실도이므로 기본적으로 꺼져 있습니다. 작업은 probe 루프 안에서 수행되며 출력 행 수에 따라 커집니다. 왼쪽과 오른쪽 행이 다른 측에서 찾은 정확한 일치를 알아야 할 때 matches = 1을 사용하세요.

matches = 1이 모든 조합을 수집 가능하게 만들지는 않습니다. 조인이 보고할 수 있는 측은 그 조인이 어쨌든 해야 하는 일에 따르므로, 알고리즘뿐 아니라 종류와 strictness에도 의존합니다.

해시 계열. hash, parallel_hash, grace_hash는 항상 서로 일치합니다:

Join matched left matched right
ALL INNER, ALL LEFT, ALL RIGHT, ALL FULL yes yes
SEMI LEFT, ANTI LEFT yes no
ANY RIGHT, ANTI RIGHT no yes
ASOF (inner) yes no
SEMI RIGHT no no
ANY INNER, ANY LEFT, ASOF LEFT no no

오른쪽 측은 조인이 해시 테이블에 키당 한 행만 유지할 때 사용할 수 없으며, ANY, SEMI, ANTI 조인이 그러합니다: 중복 오른쪽 행은 결코 저장되지 않으므로 셀 수 없습니다. 왼쪽 측은 조인이 이미 다른 왼쪽 행이 차지한 파트너를 가진 왼쪽 행의 출력을 억제할 때 사용할 수 없으며, 이로 인해 생성된 행이 일치한 것의 과소 계수가 됩니다.

any_join_distinct_right_table_keys를 활성화하면 ANY를 더 오래된 RightAny 의미론으로 전환하며, 이는 왼쪽 행마다 한 행을 생성하므로 두 개수를 모두 유지합니다. 그러면 ANY RIGHTANY FULL은 양쪽을 보고하고, ANY INNERSEMI LEFT로 재작성됩니다.

Join 테이블 엔진은 엔진에 선언된 종류와 strictness를 사용해 같은 표를 따릅니다: Join(ALL, INNER, …)은 양쪽을 보고하고, Join(ANY, LEFT, …)은 어느 쪽도 보고하지 않습니다.

병합 알고리즘. full_sorting_mergeparallel_full_sorting_merge는 네 가지 ALL 종류, ANY INNER, ANY LEFT, ANY RIGHT, ASOF, ASOF LEFT를 받습니다. 오른쪽 측이 not collectedASOFASOF LEFT를 제외한 모든 종류에 대해 양쪽을 보고하며, matches = 1 없이도 — 정렬된 두 입력을 걷고, 소비하면서 같은 범위의 모든 행을 보므로 나중에 아무것도 재구성할 필요가 없습니다.

partial_mergeALL INNER, ALL LEFT, ALL RIGHT, ALL FULL, ANY INNER, ANY LEFT, SEMI LEFT를 받습니다. 네 가지 ALL 종류에 대해 양쪽을 보고하며, 오른쪽은 matches = 1로; ANY INNER, ANY LEFT, SEMI LEFT의 경우 오른쪽 측은 not collected입니다.

direct. 왼쪽 측만. 오른쪽 측은 결코 행으로 물질화되지 않는 키-값 저장소이므로 Right: 줄이 전혀 없습니다.

CROSS, COMMA 및 상수 ON. 에서 설명한 대로 어느 쪽도 아닙니다.

두 알고리즘이 모두 숫자를 보고하면 숫자는 일치합니다. 병합 알고리즘은 단지 더 많은 정보를 가질 뿐입니다; 일치가 무엇인지에 대해 의견이 다르지 않습니다.

Algorithm-specific lines

각 조인 구현이 그 위에 추가하는 줄들을 살펴보겠습니다.

hashparallel_hash 조인과 Join 테이블 엔진의 경우 Hash table: 줄이 오른쪽 테이블에서 구축된 해시 테이블을 설명합니다:

Hash table: unique keys <unique_keys> · memory <peak_memory>
  • unique keys <unique_keys> — 빌드 단계 동안 해시 테이블에 저장된 고유 키 수.
  • memory <peak_memory> — 빌드 단계 동안 해시 테이블이 사용한 최대 메모리.

grace_hash 조인의 경우 Hash table: 줄이 조인이 메모리 제한에 어떻게 적응했는지 추가로 보고하고, Spill: 줄이 데이터가 디스크로 유출되었는지 보고합니다:

Hash table: unique keys <unique_keys> · memory <peak_memory> · buckets <buckets> · rehashes <rehashes>
Spill: yes · left spilled <left_spilled_bytes> · right spilled <right_spilled_bytes>
  • buckets <buckets> — 실행이 끝날 때 grace hash join이 갖게 된 버킷 수. 항상 2의 거듭제곱입니다.
  • rehashes <rehashes> — 메모리 제한에 맞추기 위해 버킷 수를 두 배로 늘려야 했던 횟수.
  • Spill: — 디스크로 유출이 발생했는지 알려주는 yes/no 플래그. 발생했다면 left spilled <left_spilled_bytes>right spilled <right_spilled_bytes>가 왼쪽(probe)과 오른쪽(build) 측에서 유출된 압축 바이트를 보고합니다; 아무것도 유출되지 않았으면 줄은 단순히 Spill: no입니다.

partial_merge 조인의 경우 Right: 줄이 오른쪽 테이블이 어떻게 버퍼링되고 정렬되었는지에 대한 추가 정보를 지니고, 정렬 시간은 Stage (build)Stage (probe) 줄에 표시됩니다:

Right: rows estimated <estimated_right_rows> · rows <right_rows> · matched <matched_right_rows> · size <right_size> · blocks <right_blocks> · storage <in-memory|external> · match rate <match_rate>% · fanout <fanout>
  Stage (build): time <t> (<share>%) · parallelism <avg>/<max> · sort time <build_sort_time> · sort share <build_sort_share>%
  Stage (probe): time <t> (<share>%) · parallelism <avg>/<max> · sort time <probe_sort_time> · sort share <probe_sort_share>%
  • size <right_size> — 오른쪽 테이블 블록의 메모리 크기.
  • blocks <right_blocks> — 오른쪽 테이블이 버퍼링된 블록 수.
  • storage <in-memory|external> — 오른쪽 테이블이 메모리에 맞았는지(in-memory) 디스크로 유출되어야 했는지(external). external이면 spilled <spilled_bytes>가 디스크에 기록된 압축 바이트를 보고합니다.
  • sort time <sort_time> — 오른쪽 테이블(build 단계에서)과 각 들어오는 왼쪽 블록(probe 단계에서)을 정렬하는 데 걸린 시간.
  • sort share <sort_share>%sort time을 그 stage 자체의 바쁜 시간(프로세서들의 경과 시간 합)의 비중으로 표시한 것으로, stage time 백분율(전체 쿼리 실행 시간의 비중)과는 다릅니다.

full_sorting_merge 조인의 경우 공통 Left:Right: 줄만 출력됩니다.

direct 조인의 경우 Left: 줄만 출력됩니다. 오른쪽 측이 행으로 물질화되지 않고 직접 조회되는 키-값 저장소이기 때문입니다.

CROSS 또는 COMMA 조인과 키 동등성이 없는 ON 섹션의 경우 Buffer: 줄이 오른쪽 테이블이 메모리에 어떻게 유지되었는지 설명하고 Spill: 줄이 디스크로 갔는지 보고합니다:

Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>
  • memory <peak_memory> — 버퍼링된 오른쪽 테이블이 차지한 최대 메모리.
  • compressed <yes|no> — 버퍼링된 블록 중 적어도 하나가 압축되었는지; 그러면 읽는 측이 저장된 모든 블록을 압축 해제합니다.
  • Spill:grace_hash와 같은 yes/no 플래그, right spilled <right_spilled_bytes>가 디스크에 기록된 압축 바이트를 보고합니다.

여기서 양쪽 모두 matched not collected를 보고합니다: 상수 조건자는 모든 왼쪽 행을 모든 오른쪽 행과 쌍을 이루거나 아무것도 아니므로, 어떤 개별 행이 일치했는지 묻는 것은 답이 없습니다.

Join 테이블 엔진에 대한 조인의 경우 미리 구축된 테이블을 설명하는 Hash table: 줄과 함께 양쪽이 보고됩니다. 오른쪽 측은 엔진에 저장된 행을 세며, 쿼리별 빌드의 행을 세지 않습니다.

Per-processor times

processors = 1이면 각 stage 아래에 추가 줄이 출력되어 stage의 프로세서 전반의 경과 시간 분포를 보여줍니다:

Time per processor (<n>): min <t> · median <t> · max <t> · sum <t>

<n>은 stage의 프로세서 수입니다. medianmax 사이의 큰 격차는 병렬 프로세서 사이의 부하 편향을 가리킵니다.

EXPLAIN ESTIMATE

쿼리 처리 중 테이블에서 읽을 것으로 추정되는 행, 마크, 파트 수를 보여줍니다. MergeTree 계열의 테이블에서 작동합니다.

Example

테이블 생성:

CREATE TABLE ttt (i Int64) ENGINE = MergeTree() ORDER BY i SETTINGS index_granularity = 16, write_final_mark = 0;
INSERT INTO ttt SELECT number FROM numbers(128);
OPTIMIZE TABLE ttt;
EXPLAIN ESTIMATE SELECT * FROM ttt;
┌─database─┬─table─┬─parts─┬─rows─┬─marks─┐
│ default  │ ttt   │     1 │  128 │     8 │
└──────────┴───────┴───────┴──────┴───────┘

EXPLAIN WHATIF

가상의 skip index가 인덱스를 디스크에 물질화하지 않고 SELECT 쿼리에 가지는 이점을 추정합니다. CREATE HYPOTHETICAL INDEX로 하나 이상의 후보를 정의한 다음, EXPLAIN WHATIF SELECT ...를 실행해 각 후보에 대해 적용 가능성, 추정 마크 읽기, 추정 바이트, skip 비율을 확인하세요.

CREATE HYPOTHETICAL PROJECTION으로 정의된 가상 프로젝션도 후보로 나열되지만, 그 이점은 아직 추정되지 않습니다: 각각 status: not_applicable로 보고됩니다. 정의가 더 이상 테이블에 적용되지 않는 프로젝션 — 삭제된 컬럼, 또는 필요한 기능을 비활성화하는 설정 변경 — 은 그 이유와 함께 보고됩니다.

Syntax

EXPLAIN WHATIF [empirical = 0] SELECT ...

Settings

  • empirical1(기본값)은 baseline으로 가지친 그레뉼 위에서 메모리로 인덱스를 실행해 skip 비율(상한)을 측정합니다. 0은 그 경로를 건너뜁니다. 어느 쪽이든, empirical이 결과를 만들지 못하면(비활성화 또는 인덱스를 메모리에서 평가할 수 없음) 추정기는 컬럼 statistics로 대체하고, 둘 다 없으면 마지막으로 적용 가능성만 요약합니다.

Output

Baseline (after PK + partition + existing indexes):
  table:       db.t
  parts:       1
  marks:       100
  est_bytes:   1.50 MiB             (only when the query reads rows)

With idx_b (minmax, hypothetical):
  status:       applicable
  marks:        1
  est_bytes:    15.00 KiB           (only when baseline bytes are known)
  skip_ratio:   99.0%

Estimation:
  source:           empirical | statistical | applicability_only
  empirical_status: ok | unsupported | disabled
  empirical_reason: <reason>        (only when empirical_status = unsupported)
  sampled_parts:    50 / 100        (only when source = empirical)
  sampled_marks:    50 / 100        (only when source = empirical)
  elapsed_us:       631             (only when source = empirical)
  • source — 추정치가 어떻게 생성되었는지.

empirical: baseline으로 가지친 그레뉼 위에서 메모리로 인덱스를 구축하고 인덱스가 건너뛸 그레뉼을 셉니다. 이는 상한입니다 — CREATE HYPOTHETICAL INDEX의 제한 사항 참고. statistical: 컬럼 통계에서 파생. empirical이 비활성화되었거나(empirical = 0) 결과를 만들지 못했고, 관련 컬럼에 컬럼 통계가 정의된 경우에 사용됩니다. applicability_only: 인덱스는 조건자에 적용 가능하지만 empirical과 statistical 추정 모두 결과를 만들지 못했습니다(예: empirical = 0이고 컬럼 통계 없음). 보수적 상한으로 skip_ratio: 0.0%를 보고합니다.

  • empirical_reason — empirical 추정이 실행될 수 없었던 이유. empirical_status: unsupported에서만 표시됩니다. 예를 들어 0이 아닌 merge_tree_min_rows_for_seek 또는 merge_tree_min_bytes_for_seek는 실제 읽기가 마크 범위를 병합하게 만들며, per-granule 개수는 이를 모델링하지 않으므로 추정은 statistical 또는 applicability_only로 대체됩니다.
  • sampled_parts / sampled_marks<baseline-pruned> / <total in the table>. 표의 얼마가 PK, 파티션, 기존 인덱스 가지치기를 살아남았는지, 즉 가상 인덱스의 입력을 보여줍니다.
  • est_bytes — 테이블의 평균 행 크기에서 파생된 읽기 바이트 추정치로, 근사치이며 저장소와 압축에 따라 달라집니다. baseline 줄은 쿼리가 행을 읽을 때만 나타나고, 후보별 줄은 baseline 바이트 추정치가 알려질 때만 나타납니다.

설정은 WHATIFSELECT 사이에 인라인으로 작성됩니다 — SETTINGS 키워드는 없습니다(이것은 다른 EXPLAIN 변형이 옵션을 받는 방식과 일치합니다).

테이블에 가상 인덱스도 가상 프로젝션도 정의되지 않으면 EXPLAIN WHATIF는 하나를 만들라는 힌트와 함께 status: not_applicable을 보고합니다.

결합 행(Combined row, 여러 후보)

둘 이상의 후보가 empirical로 평가될 때 EXPLAIN WHATIF는 후보별 행 뒤에 (combined: idx_a, idx_b, ...)라는 이름의 추가 블록을 덧붙입니다. 모든 인덱스를 한 번에 가지는 공동 이점을 보고합니다: 실제 읽기는 모든 skip 인덱스를 살아남는 경우에만 그레뉼을 유지하므로, 결합 추정치는 후보들의 생존 그레뉼의 교집합입니다. 따라서 그 skip_ratio는 최소한 최고 단일 후보만큼 높습니다 — 보완 인덱스는 함께 더 많이 가지치고, 중복 인덱스는 그것을 바꾸지 않습니다.

결합 블록은 per-granule 생존 집합을 교차하여 구축되므로 source: empirical인 후보만 기여합니다. statistical 또는 applicability_only로 추정된 후보는 per-granule 데이터가 없어 제외됩니다; 따라서 결합 블록은 적어도 두 후보가 empirical 추정치를 만들었을 때만 나타나며, 그렇지 않으면 생략됩니다(예: empirical = 0 하에서). 그 추정 필드는 per-candidate empirical 블록과 같게 읽히지만, elapsed_us0입니다 — 결합 추정치는 새 스캔이 아니라 per-candidate 스캔에서 파생됩니다. 합성 (combined: ...) 이름은 보고 라벨일 뿐이며 force_data_skipping_indices와 함께 사용할 수 없습니다.

Empirical 예시

CREATE TABLE t (a UInt64, b UInt64) ENGINE = MergeTree ORDER BY a
SETTINGS index_granularity = 100;

INSERT INTO t SELECT number, number FROM numbers(10000);

CREATE HYPOTHETICAL INDEX idx_b ON t (b) TYPE minmax GRANULARITY 1;

EXPLAIN WHATIF SELECT * FROM t WHERE b = 42;
Baseline (after PK + partition + existing indexes):
  table:       default.t
  parts:       1
  marks:       100
  est_bytes:   85.52 KiB

With idx_b (minmax, hypothetical):
  status:       applicable
  marks:        1
  est_bytes:    875.00 B
  skip_ratio:   99.0%

Estimation:
  source:           empirical
  empirical_status: ok
  sampled_parts:    1 / 1
  sampled_marks:    100 / 100

가상 minmax는 100 마크를 1로 가지치었을 것입니다 — skip_ratio: 99.0%. (est_bytes는 평균 행 크기에서의 추정치이므로 정확한 수치는 달라집니다.)

Statistical 예시

컬럼 statistics은 기본적으로 꺼져 있습니다. statistical 경로를 사용하려면 먼저 관련 컬럼에 정의하고 materialize mutation이 끝날 때까지 기다리세요:

ALTER TABLE t ADD STATISTICS b TYPE tdigest;
ALTER TABLE t MATERIALIZE STATISTICS b SETTINGS mutations_sync = 1;

그런 다음 empirical 경로를 비활성화해 추정기가 컬럼 통계로 대체되게 하세요:

EXPLAIN WHATIF empirical = 0 SELECT * FROM t WHERE b < 10;
With idx_b (minmax, hypothetical):
  status:       applicable
  marks:        1
  est_bytes:    1.66 KiB
  skip_ratio:   99.9%

Estimation:
  source:           statistical
  empirical_status: disabled

그 숫자는 b < 10의 컬럼 통계 선택성(10000 중 약 10행)에서 나오며 skip_ratio의 상한으로 보고됩니다. sampled_parts / sampled_marks는 없습니다 — 읽힌 데이터가 없기 때문입니다.

두 경로가 모두 없으면(예: empirical = 0이고 컬럼 통계 정의 없음) 추정기는 source: applicability_only와 보수적인 skip_ratio: 0.0%를 보고합니다.

EXPLAIN TABLE OVERRIDE

테이블 함수를 통해 접근되는 테이블 스키마에 대한 테이블 오버라이드의 결과를 보여줍니다. 또한 일부 검증을 수행하며, 오버라이드가 어떤 종류의 실패를 일으켰다면 예외를 던집니다.

Example

다음과 같은 원격 MySQL 테이블이 있다고 가정합니다:

CREATE TABLE db.tbl (
    id INT PRIMARY KEY,
    created DATETIME DEFAULT now()
)
EXPLAIN TABLE OVERRIDE mysql('127.0.0.1:3306', 'db', 'tbl', 'root', 'clickhouse')
PARTITION BY toYYYYMM(assumeNotNull(created))
┌─explain─────────────────────────────────────────────────┐
│ PARTITION BY uses columns: `created` Nullable(DateTime) │
└─────────────────────────────────────────────────────────┘

검증은 완전하지 않으므로, 성공적인 쿼리가 오버라이드가 문제를 일으키지 않을 것임을 보장하지는 않습니다.

더 알아보기 (Learn more)