-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-35455][SQL] Unify empty relation optimization between normal and AQE optimizer #32602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
4514d27
5097247
8df68d9
165077b
2f5fa20
7b80db0
05e074c
48dd92a
8f4dc80
1220087
e26df96
f7a14cf
2c0dfb0
0e151d9
c086f72
9b78ac0
767dd92
47e0c3a
d9ca6da
2fead86
0754936
a6213ea
624e45e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,23 +20,20 @@ package org.apache.spark.sql.catalyst.optimizer | |
| import org.apache.spark.sql.catalyst.analysis.CastSupport | ||
| import org.apache.spark.sql.catalyst.expressions._ | ||
| import org.apache.spark.sql.catalyst.expressions.Literal.FalseLiteral | ||
| import org.apache.spark.sql.catalyst.planning.ExtractSingleColumnNullAwareAntiJoin | ||
| import org.apache.spark.sql.catalyst.plans._ | ||
| import org.apache.spark.sql.catalyst.plans.logical._ | ||
| import org.apache.spark.sql.catalyst.rules._ | ||
| import org.apache.spark.sql.catalyst.trees.TreePattern.{LOCAL_RELATION, TRUE_OR_FALSE_LITERAL} | ||
| import org.apache.spark.sql.catalyst.trees.TreePattern.LOCAL_RELATION | ||
|
|
||
| /** | ||
| * Collapse plans consisting empty local relations generated by [[PruneFilters]]. | ||
| * 1. Binary(or Higher)-node Logical Plans | ||
| * 1. Higher-node Logical Plans | ||
| * - Union with all empty children. | ||
| * - Join with one or two empty children (including Intersect/Except). | ||
| * 2. Unary-node Logical Plans | ||
| * - Project/Filter/Sample/Join/Limit/Repartition with all empty children. | ||
| * - Join with false condition. | ||
| * - Aggregate with all empty children and at least one grouping expression. | ||
| * - Generate(Explode) with all empty children. Others like Hive UDTF may return results. | ||
| * - Project/Filter/Sample with all empty children. | ||
| */ | ||
| object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper with CastSupport { | ||
| object PropagateEmptyRelationBasic extends Rule[LogicalPlan] { | ||
| private def isEmptyLocalRelation(plan: LogicalPlan): Boolean = plan match { | ||
| case p: LocalRelation => p.data.isEmpty | ||
| case _ => false | ||
|
|
@@ -45,12 +42,8 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper wit | |
| private def empty(plan: LogicalPlan) = | ||
| LocalRelation(plan.output, data = Seq.empty, isStreaming = plan.isStreaming) | ||
|
|
||
| // Construct a project list from plan's output, while the value is always NULL. | ||
| private def nullValueProjectList(plan: LogicalPlan): Seq[NamedExpression] = | ||
| plan.output.map{ a => Alias(cast(Literal(null), a.dataType), a.name)(a.exprId) } | ||
|
|
||
| def apply(plan: LogicalPlan): LogicalPlan = plan.transformUpWithPruning( | ||
| _.containsAnyPattern(LOCAL_RELATION, TRUE_OR_FALSE_LITERAL), ruleId) { | ||
| override def apply(plan: LogicalPlan): LogicalPlan = plan.transformUpWithPruning( | ||
| _.containsAnyPattern(LOCAL_RELATION), ruleId) { | ||
| case p: Union if p.children.exists(isEmptyLocalRelation) => | ||
| val newChildren = p.children.filterNot(isEmptyLocalRelation) | ||
| if (newChildren.isEmpty) { | ||
|
|
@@ -72,11 +65,72 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper wit | |
| } | ||
| } | ||
|
|
||
| case p: UnaryNode if p.children.nonEmpty && p.children.forall(isEmptyLocalRelation) => p match { | ||
| case _: Project => empty(p) | ||
| case _: Filter => empty(p) | ||
| case _: Sample => empty(p) | ||
| case _ => p | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The rule used by both normal Optimizer and AQE Optimizer for: | ||
| * 1. Binary-node Logical Plans | ||
| * - Join with one or two empty children (including Intersect/Except). | ||
| * - Join is single column NULL-aware anti join (NAAJ) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should remove this now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved it to |
||
| * Broadcasted [[HashedRelation]] is [[HashedRelationWithAllNullKeys]]. Eliminate join to an | ||
| * empty [[LocalRelation]]. | ||
| * - Left semi Join | ||
| * Right side is non-empty and condition is empty. Eliminate join to its left side. | ||
| * - Left anti join | ||
| * Right side is non-empty and condition is empty. Eliminate join to an empty | ||
| * [[LocalRelation]]. | ||
| * 2. Unary-node Logical Plans | ||
| * - Limit/Repartition with all empty children. | ||
| * - Aggregate with all empty children and at least one grouping expression. | ||
| * - Generate(Explode) with all empty children. Others like Hive UDTF may return results. | ||
| * | ||
| * @param checkRowCount At AQE side, we use the query stage stats to check the check. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| * @param isRelationWithAllNullKeys At AQE side, we use the broadcast query stage to do the check. | ||
| */ | ||
| case class PropagateEmptyRelationAdvanced( | ||
| checkRowCount: Option[(LogicalPlan, Boolean) => Boolean] = None, | ||
| isRelationWithAllNullKeys: Option[LogicalPlan => Boolean] = None) | ||
| extends Rule[LogicalPlan] with CastSupport { | ||
|
|
||
| private def isEmptyLocalRelation(plan: LogicalPlan): Boolean = { | ||
| val defaultEmptyRelation: Boolean = plan match { | ||
| case p: LocalRelation => p.data.isEmpty | ||
| case _ => false | ||
| } | ||
|
|
||
| if (checkRowCount.isDefined) { | ||
| checkRowCount.get.apply(plan, false) || defaultEmptyRelation | ||
| } else { | ||
| defaultEmptyRelation | ||
| } | ||
| } | ||
|
|
||
| private def empty(plan: LogicalPlan) = | ||
| LocalRelation(plan.output, data = Seq.empty, isStreaming = plan.isStreaming) | ||
|
|
||
| // Construct a project list from plan's output, while the value is always NULL. | ||
| private def nullValueProjectList(plan: LogicalPlan): Seq[NamedExpression] = | ||
| plan.output.map{ a => Alias(cast(Literal(null), a.dataType), a.name)(a.exprId) } | ||
|
|
||
| // We can not use transformUpWithPruning here since this rule is used by both normal Optimizer | ||
| // and AQE Optimizer. And this may only effective at AQE side. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah good point. I think there is a way to overcome it:
Then these two rules can define their transformation prunning separatedly. |
||
| def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp { | ||
| case j @ ExtractSingleColumnNullAwareAntiJoin(_, _) | ||
| if isRelationWithAllNullKeys.isDefined && isRelationWithAllNullKeys.get(j.right) => | ||
| empty(j) | ||
|
|
||
| // Joins on empty LocalRelations generated from streaming sources are not eliminated | ||
| // as stateful streaming joins need to perform other state management operations other than | ||
| // just processing the input data. | ||
| case p @ Join(_, _, joinType, conditionOpt, _) | ||
| if !p.children.exists(_.isStreaming) => | ||
|
cloud-fan marked this conversation as resolved.
|
||
| if !p.children.exists(_.isStreaming) => | ||
| val isLeftEmpty = isEmptyLocalRelation(p.left) | ||
| val isRightEmpty = isEmptyLocalRelation(p.right) | ||
| val isFalseCondition = conditionOpt match { | ||
|
|
@@ -103,14 +157,17 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper wit | |
| Project(nullValueProjectList(p.left) ++ p.right.output, p.right) | ||
| case _ => p | ||
| } | ||
| } else if (joinType == LeftSemi && conditionOpt.isEmpty && | ||
| checkRowCount.isDefined && checkRowCount.get.apply(p.right, true)) { | ||
| p.left | ||
| } else if (joinType == LeftAnti && conditionOpt.isEmpty && | ||
| checkRowCount.isDefined && checkRowCount.get.apply(p.right, true)) { | ||
| empty(p) | ||
| } else { | ||
| p | ||
| } | ||
|
|
||
| case p: UnaryNode if p.children.nonEmpty && p.children.forall(isEmptyLocalRelation) => p match { | ||
| case _: Project => empty(p) | ||
| case _: Filter => empty(p) | ||
| case _: Sample => empty(p) | ||
| case _: Sort => empty(p) | ||
| case _: GlobalLimit if !p.isStreaming => empty(p) | ||
| case _: LocalLimit if !p.isStreaming => empty(p) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| package org.apache.spark.sql.execution.adaptive | ||
|
|
||
| import org.apache.spark.sql.catalyst.analysis.UpdateAttributeNullability | ||
| import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, LogicalPlanIntegrity, PlanHelper} | ||
| import org.apache.spark.sql.catalyst.rules.RuleExecutor | ||
| import org.apache.spark.sql.internal.SQLConf | ||
|
|
@@ -27,7 +28,9 @@ import org.apache.spark.util.Utils | |
| */ | ||
| class AQEOptimizer(conf: SQLConf) extends RuleExecutor[LogicalPlan] { | ||
| private val defaultBatches = Seq( | ||
| Batch("Eliminate Unnecessary Join", Once, EliminateUnnecessaryJoin), | ||
| Batch("Propagate Empty LocalRelation", Once, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| PropagateEmptyRelationAdvancedHelper.propagateEmptyRelationAdvanced, | ||
| UpdateAttributeNullability), | ||
|
ulysses-you marked this conversation as resolved.
|
||
| Batch("Demote BroadcastHashJoin", Once, DemoteBroadcastHashJoin) | ||
| ) | ||
|
|
||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark.sql.execution.adaptive | ||
|
|
||
| import org.apache.spark.sql.catalyst.optimizer.PropagateEmptyRelationAdvanced | ||
| import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan | ||
| import org.apache.spark.sql.execution.joins.HashedRelationWithAllNullKeys | ||
|
|
||
| /** | ||
| * A helper class to provide a AQE side `PropagateEmptyRelationAdvanced` rule. | ||
| */ | ||
| object PropagateEmptyRelationAdvancedHelper { | ||
|
|
||
| private def isRelationWithAllNullKeys(plan: LogicalPlan) = plan match { | ||
| case LogicalQueryStage(_, stage: BroadcastQueryStageExec) | ||
| if stage.resultOption.get().isDefined => | ||
| stage.broadcast.relationFuture.get().value == HashedRelationWithAllNullKeys | ||
| case _ => false | ||
| } | ||
|
|
||
| private def checkRowCount(plan: LogicalPlan, hasRow: Boolean): Boolean = plan match { | ||
| case LogicalQueryStage(_, stage: QueryStageExec) if stage.resultOption.get().isDefined => | ||
| stage.getRuntimeStatistics.rowCount match { | ||
| case Some(count) => hasRow == (count > 0) | ||
| case _ => false | ||
| } | ||
| case _ => false | ||
| } | ||
|
|
||
| lazy val propagateEmptyRelationAdvanced: PropagateEmptyRelationAdvanced = { | ||
| PropagateEmptyRelationAdvanced(Some(checkRowCount), Some(isRelationWithAllNullKeys)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.