EXPLAIN 문
EXPLAIN 문 (EXPLAIN Statements)
EXPLAIN 문은 쿼리나 INSERT 문의 논리적 및 최적화된 쿼리 계획을 설명하는 데 사용됩니다.
출처: 문서
본문
EXPLAIN 문 실행 (Run an EXPLAIN statement)
Java / Scala:
EXPLAIN 문은 TableEnvironment의 executeSql() 메서드로 실행할 수 있습니다. executeSql() 메서드는 성공적인 EXPLAIN 작업에 대해 explain 결과를 반환하고, 그렇지 않으면 예외를 던집니다. 다음 예제는 TableEnvironment에서 EXPLAIN 문을 실행하는 방법을 보여줍니다.
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
// "MyTable1" 테이블 등록
tEnv.executeSql("CREATE TABLE MyTable1 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')");
tEnv.executeSql("CREATE TABLE MyTable2 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')");
// TableEnvironment.explainSql()로 SELECT 문 설명
String explanation =
tEnv.explainSql(
"SELECT `count`, COUNT(word) FROM ("
+ "SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
+ "UNION ALL "
+ "SELECT `count`, word FROM MyTable2) tmp "
+ "GROUP BY `count`");
System.out.println(explanation);
// TableEnvironment.executeSql()로 SELECT 문 설명
TableResult tableResult =
tEnv.executeSql(
"EXPLAIN PLAN FOR "
+ "SELECT `count`, COUNT(word) FROM ("
+ "SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
+ "UNION ALL "
+ "SELECT `count`, word FROM MyTable2) tmp GROUP BY `count`");
tableResult.print();
TableResult tableResult2 =
tEnv.executeSql(
"EXPLAIN ESTIMATED_COST, CHANGELOG_MODE, PLAN_ADVICE, JSON_EXECUTION_PLAN "
+ "SELECT `count`, COUNT(word) FROM ( "
+ "SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
+ "UNION ALL "
+ "SELECT `count`, word FROM MyTable2) tmp GROUP BY `count`");
tableResult2.print();
Scala:
val env = StreamExecutionEnvironment.getExecutionEnvironment()
val tEnv = StreamTableEnvironment.create(env)
// "MyTable1" 테이블 등록
tEnv.executeSql("CREATE TABLE MyTable1 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')")
tEnv.executeSql("CREATE TABLE MyTable2 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')")
// TableEnvironment.explainSql()로 SELECT 문 설명
val explanation = tEnv.explainSql(
"""
|SELECT `count`, COUNT(word)
|FROM (
| SELECT `count`, word FROM MyTable1
| WHERE word LIKE 'F%'
| UNION ALL
| SELECT `count`, word FROM MyTable2 ) tmp
|GROUP BY `count`
|""".stripMargin)
println(explanation)
// TableEnvironment.executeSql()로 SELECT 문 설명
val tableResult = tEnv.executeSql(
"""
|EXPLAIN PLAN FOR
|SELECT `count`, COUNT(word)
|FROM (
| SELECT `count`, word FROM MyTable1
| WHERE word LIKE 'F%'
| UNION ALL
| SELECT `count`, word FROM MyTable2 ) tmp
|GROUP BY `count`
|""".stripMargin)
tableResult.print()
val tableResult2 = tEnv.executeSql(
"""
|EXPLAIN ESTIMATED_COST, CHANGELOG_MODE, PLAN_ADVICE, JSON_EXECUTION_PLAN
|SELECT `count`, COUNT(word)
|FROM (
| SELECT `count`, word FROM MyTable1
| WHERE word LIKE 'F%'
| UNION ALL
| SELECT `count`, word FROM MyTable2 ) tmp
|GROUP BY `count`
|""".stripMargin)
tableResult2.print()
Python:
table_env = StreamTableEnvironment.create(...)
t_env.execute_sql("CREATE TABLE MyTable1 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')")
t_env.execute_sql("CREATE TABLE MyTable2 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen')")
# TableEnvironment.explain_sql()로 SELECT 문 설명
explanation1 = t_env.explain_sql(
"SELECT `count`, COUNT(word) FROM ("
"SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
"UNION ALL "
"SELECT `count`, word FROM MyTable2) tmp GROUP BY `count`")
print(explanation1)
# TableEnvironment.execute_sql()로 SELECT 문 설명
table_result = t_env.execute_sql(
"EXPLAIN PLAN FOR "
"SELECT `count`, COUNT(word) FROM ("
"SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
"UNION ALL "
"SELECT `count`, word FROM MyTable2) tmp GROUP BY `count`")
table_result.print()
table_result2 = t_env.execute_sql(
"EXPLAIN ESTIMATED_COST, CHANGELOG_MODE, PLAN_ADVICE, JSON_EXECUTION_PLAN "
"SELECT `count`, COUNT(word) FROM ("
"SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%' "
"UNION ALL "
"SELECT `count`, word FROM MyTable2) tmp GROUP BY `count`")
table_result2.print()
SQL CLI:
EXPLAIN 문은 SQL CLI에서 실행할 수 있습니다.
Flink SQL> CREATE TABLE MyTable1 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen');
[INFO] Table has been created.
Flink SQL> CREATE TABLE MyTable2 (`count` bigint, word VARCHAR(256)) WITH ('connector' = 'datagen');
[INFO] Table has been created.
Flink SQL> EXPLAIN PLAN FOR SELECT `count`, COUNT(word) FROM
> ( SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%'
> UNION ALL
> SELECT `count`, word FROM MyTable2 ) tmp GROUP BY `count`;
Flink SQL> EXPLAIN ESTIMATED_COST, CHANGELOG_MODE, PLAN_ADVICE, JSON_EXECUTION_PLAN SELECT `count`, COUNT(word) FROM
> ( SELECT `count`, word FROM MyTable1 WHERE word LIKE 'F%'
> UNION ALL
> SELECT `count`, word FROM MyTable2 ) tmp GROUP BY `count`;
EXPLAIN 결과는 다음과 같습니다:
EXPLAIN PLAN:
== Abstract Syntax Tree ==
LogicalAggregate(group=[{0}], EXPR$1=[COUNT($1)])
+- LogicalUnion(all=[true])
:- LogicalProject(count=[$0], word=[$1])
: +- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
: +- LogicalTableScan(table=[[default_catalog, default_database, MyTable1]])
+- LogicalProject(count=[$0], word=[$1])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable2]])
== Optimized Physical Plan ==
GroupAggregate(groupBy=[count], select=[count, COUNT(word) AS EXPR$1])
+- Exchange(distribution=[hash[count]])
+- Union(all=[true], union=[count, word])
:- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable1]], fields=[count, word])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2]], fields=[count, word])
== Optimized Execution Plan ==
GroupAggregate(groupBy=[count], select=[count, COUNT(word) AS EXPR$1])
+- Exchange(distribution=[hash[count]])
+- Union(all=[true], union=[count, word])
:- Calc(select=[count, word], where=[LIKE(word, 'F%')])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable1]], fields=[count, word])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2]], fields=[count, word])
EXPLAIN PLAN WITH DETAILS:
== Abstract Syntax Tree ==
LogicalAggregate(group=[{0}], EXPR$1=[COUNT($1)])
+- LogicalUnion(all=[true])
:- LogicalProject(count=[$0], word=[$1])
: +- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
: +- LogicalTableScan(table=[[default_catalog, default_database, MyTable1]])
+- LogicalProject(count=[$0], word=[$1])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable2]])
== Optimized Physical Plan With Advice ==
GroupAggregate(advice=[1], groupBy=[count], select=[count, COUNT(word) AS EXPR$1], changelogMode=[I,UA]): rowcount = 1.05E8, cumulative cost = {5.2E8 rows, 1.805E10 cpu, 4.0E9 io, 2.1E9 network, 0.0 memory}
+- Exchange(distribution=[hash[count]], changelogMode=[I]): rowcount = 1.05E8, cumulative cost = {4.15E8 rows, 1.7945E10 cpu, 4.0E9 io, 2.1E9 network, 0.0 memory}
+- Union(all=[true], union=[count, word], changelogMode=[I]): rowcount = 1.05E8, cumulative cost = {3.1E8 rows, 3.05E8 cpu, 4.0E9 io, 0.0 network, 0.0 memory}
:- Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')], changelogMode=[I]): rowcount = 5000000.0, cumulative cost = {1.05E8 rows, 1.0E8 cpu, 2.0E9 io, 0.0 network, 0.0 memory}
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable1]], fields=[count, word], changelogMode=[I]): rowcount = 1.0E8, cumulative cost = {1.0E8 rows, 1.0E8 cpu, 2.0E9 io, 0.0 network, 0.0 memory}
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2]], fields=[count, word], changelogMode=[I]): rowcount = 1.0E8, cumulative cost = {1.0E8 rows, 1.0E8 cpu, 2.0E9 io, 0.0 network, 0.0 memory}
advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.exec.mini-batch.enabled' to 'true', 'table.exec.mini-batch.allow-latency' to a positive long value, 'table.exec.mini-batch.size' to a positive long value).
== Optimized Execution Plan ==
GroupAggregate(groupBy=[count], select=[count, COUNT(word) AS EXPR$1])
+- Exchange(distribution=[hash[count]])
+- Union(all=[true], union=[count, word])
:- Calc(select=[count, word], where=[LIKE(word, 'F%')])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable1]], fields=[count, word])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2]], fields=[count, word])
== Physical Execution Plan ==
{
"nodes" : [ {
"id" : 17,
"type" : "Source: MyTable1[15]",
"pact" : "Data Source",
"contents" : "[15]:TableSourceScan(table=[[default_catalog, default_database, MyTable1]], fields=[count, word])",
"parallelism" : 1
}, {
"id" : 18,
"type" : "Calc[16]",
"pact" : "Operator",
"contents" : "[16]:Calc(select=[count, word], where=[LIKE(word, 'F%')])",
"parallelism" : 1,
"predecessors" : [ {
"id" : 17,
"ship_strategy" : "FORWARD",
"side" : "second"
} ]
}, {
"id" : 19,
"type" : "Source: MyTable2[17]",
"pact" : "Data Source",
"contents" : "[17]:TableSourceScan(table=[[default_catalog, default_database, MyTable2]], fields=[count, word])",
"parallelism" : 1
}, {
"id" : 22,
"type" : "GroupAggregate[20]",
"pact" : "Operator",
"contents" : "[20]:GroupAggregate(groupBy=[count], select=[count, COUNT(word) AS EXPR$1])",
"parallelism" : 1,
"predecessors" : [ {
"id" : 18,
"ship_strategy" : "HASH",
"side" : "second"
}, {
"id" : 19,
"ship_strategy" : "HASH",
"side" : "second"
} ]
} ]
}
ExplainDetails
ExplainDetail은 explain 결과를 위한 세부 정보 유형을 정의합니다.
ESTIMATED_COST
ESTIMATED_COST를 지정하면 옵티마이저가 각 물리 rel 노드의 추정 최적 비용을 출력에 첨부하도록 지시합니다.
== Optimized Physical Plan ==
TableSourceScan(..., cumulative cost ={1.0E8 rows, 1.0E8 cpu, 2.4E9 io, 0.0 network, 0.0 memory})
CHANGELOG_MODE
CHANGELOG_MODE를 지정하면 옵티마이저가 각 물리 rel 노드의 changelog 모드(자세한 내용은 Dynamic Tables 참고)를 출력에 첨부하도록 지시합니다.
== Optimized Physical Plan ==
GroupAggregate(..., changelogMode=[I,UA,D])
PLAN_ADVICE
PLAN_ADVICE는 Flink 1.17 버전부터 지원됩니다.
PLAN_ADVICE를 지정하면 옵티마이저가 최적화된 물리 계획을 분석해 잠재적 위험 경고 및/또는 최적화 조언을 제공하도록 지시합니다. 동시에 "Optimized Physical Plan" 제목을 강조 표시로 "Optimized Physical Plan with Advice"로 변경합니다.
계획 조언은 Kind와 Scope에 따라 분류됩니다.
| Advice Kind | 설명 |
|---|---|
| WARNING | 잠재적 데이터 정확성 위험을 드러냅니다 |
| ADVICE | 잠재적 SQL 옵티마이저 튜닝 구성을 제안합니다 |
| Advice Scope | 설명 |
|---|---|
| QUERY_LEVEL | 전체 쿼리를 대상으로 전역 관점에서 조언을 제공합니다 |
| NODE_LEVEL | 특정 rel 노드에 조언을 제공합니다 |
Flink SQL은 다음 문제를 대상으로 하는 계획 조언을 제공합니다
- 그룹 집계(Group Aggregation)로 인한 데이터 편향(Data Skewness). 자세한 내용은 Group Aggregation과 Performance Tuning 참고
- 비결정적 업데이트(Non-deterministic Updates, 약칭 NDU). 자세한 내용은 Determinism In Continuous Queries 참고
GroupAggregate가 감지되고 local-global 집계로 최적화될 수 있다면 옵티마이저는 GroupAggregate rel 노드에 advice id를 태그하고, 사용자에게 구성을 업데이트하도록 제안합니다.
SQL1 (NODE_LEVEL ADVICE):
SET 'table.exec.mini-batch.enabled' = 'true';
SET 'table.exec.mini-batch.allow-latency' = '5s';
SET 'table.exec.mini-batch.size' = '200';
SET 'table.optimizer.agg-phase-strategy' = 'ONE_PHASE';
CREATE TABLE MyTable (
a BIGINT,
b INT NOT NULL,
c VARCHAR,
d BIGINT
) WITH (
'connector' = 'values',
'bounded' = 'false');
EXPLAIN PLAN_ADVICE
SELECT
AVG(a) AS avg_a,
COUNT(*) AS cnt,
COUNT(b) AS cnt_b,
MIN(b) AS min_b,
MAX(c) FILTER (WHERE a > 1) AS max_c
FROM MyTable;
== Optimized Physical Plan With Advice ==
Calc(select=[avg_a, cnt, cnt AS cnt_b, min_b, max_c])
+- GroupAggregate(advice=[1], select=[AVG(a) AS avg_a, COUNT(*) AS cnt, MIN(b) AS min_b, MAX(c) FILTER $f3 AS max_c])
+- Exchange(distribution=[single])
+- Calc(select=[a, b, c, IS TRUE(>(a, 1)) AS $f3])
+- MiniBatchAssigner(interval=[10000ms], mode=[ProcTime])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable, project=[a, b, c], metadata=[]]], fields=[a, b, c])
advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.optimizer.agg-phase-strategy' to 'AUTO').
NDU 문제가 감지되면 옵티마이저는 물리 계획 끝에 경고를 추가합니다.
SQL2 (QUERY_LEVEL WARNING):
CREATE TABLE MyTable (
a INT,
b BIGINT,
c STRING,
d INT,
`day` AS DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd'),
PRIMARY KEY (a, c) NOT ENFORCED
) WITH (
'connector' = 'values',
'changelog-mode' = 'I,UA,UB,D'
);
CREATE TABLE MySink (
a INT,
b BIGINT,
c STRING,
PRIMARY KEY (a) NOT ENFORCED
) WITH (
'connector' = 'values',
'sink-insert-only' = 'false'
);
EXPLAIN PLAN_ADVICE
INSERT INTO MySink
SELECT a, b, `day`
FROM MyTable
WHERE b > 100;
== Optimized Physical Plan With Advice ==
Sink(table=[default_catalog.default_database.MySink], fields=[a, b, day], upsertMaterialize=[true])
+- Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day], where=[>(b, 100)])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable, filter=[], project=[a, b], metadata=[]]], fields=[a, b])
advice[1]: [WARNING] The column(s): day(generated by non-deterministic function: CURRENT_TIMESTAMP ) can not satisfy the determinism requirement for correctly processing update message('UB'/'UA'/'D' in changelogMode, not 'I' only), this usually happens when input node has no upsertKey(upsertKeys=[{}]) or current node outputs non-deterministic update messages. Please consider removing these non-deterministic columns or making them deterministic by using deterministic functions.
related rel plan:
Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], where=[>(b, 100)], changelogMode=[I,UB,UA,D])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable, filter=[], project=[a, b], metadata=[]]], fields=[a, b], changelogMode=[I,UB,UA,D])
경고나 조언이 감지되지 않으면 옵티마이저는 물리 계획 끝에 "No available advice"라는 공지(notice)를 추가합니다.
SQL3 (NO_AVAILABLE ADVICE):
CREATE TABLE MyTable (
a INT,
b BIGINT,
c STRING,
d INT
) WITH (
'connector' = 'values',
'changelog-mode' = 'I'
);
EXPLAIN PLAN_ADVICE
SELECT * FROM MyTable WHERE b > 100;
== Optimized Physical Plan With Advice ==
Calc(select=[a, b, c, d], where=[>(b, 100)])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable]], fields=[a, b, c, d])
No available advice...
JSON_EXECUTION_PLAN
JSON_EXECUTION_PLAN을 지정하면 옵티마이저가 프로그램의 json 형식 실행 계획을 출력에 첨부하도록 지시합니다.
구문 (Syntax)
EXPLAIN [([ExplainDetail[, ExplainDetail]*]) | PLAN FOR] <query_statement_or_insert_statement_or_statement_set>
statement_set:
STATEMENT SET
BEGIN
insert_statement;
...
insert_statement;
END;
쿼리 구문은 Queries 페이지, insert 구문은 INSERT 페이지를 참고하세요.