Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,27 @@ object PropagateEmptyRelation extends Rule[LogicalPlan] with PredicateHelper wit
case Generate(_: Explode, _, _, _, _, _) => empty(p)
case _ => p
}

// Nodes below GlobalLimit or LocalLimit can be pruned if the limit value is zero (0).
// Any subtree in the logical plan that has GlobalLimit 0 or LocalLimit 0 as its root is
// semantically equivalent to an empty relation.
//
// In such cases, the effects of Limit 0 can be propagated through the Logical Plan by replacing
// the (Global/Local) Limit subtree with an empty LocalRelation, thereby pruning the subtree
// below and triggering other optimization rules of PropagateEmptyRelation to propagate the
// changes up the Logical Plan.
//
// Replace Global Limit 0 nodes with empty Local Relation
case p @ GlobalLimit(IntegerLiteral(limit), _) if limit == 0 =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to do a new rule, to make it more modular.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, we can also create a new Batch for the new rule, since it requires only one-time transformation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rxin @gengliangwang Hi!
We are relying on the other case rules in PropagateEmptyRelation to propagate the effects of substituting Limit 0 with empty LocalRelation through the Logical Plan - such as in case of joins, project, etc.
So, should we handle this case also part of this rule, and then propagation of empty relation happens only as part of this rule? Or do you recommend doing it as a separate rule?

@gengliangwang gengliangwang Apr 3, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimization in this PR is not directly related to "propagate". We can create a separate rule and batch , e.g.

    Batch("OptimizeLimitZero", Once, OptimizeLimitZero) :+
    Batch("LocalRelation", fixedPoint,
      ConvertToLocalRelation,
      PropagateEmptyRelation
   )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rxin @gengliangwang @srowen I have made the recommended changes. Please have a look. Thanks!

empty(p)

// Note: For all SQL queries, if a LocalLimit 0 node exists in the Logical Plan, then a
// GlobalLimit 0 node would also exist. Thus, the above case would be sufficient to handle
// almost all cases. However, if a user explicitly creates a Logical Plan with LocalLimit 0 node
// then the following rule will handle that case as well.
//
// Replace Local Limit 0 nodes with empty Local Relation
case p @ LocalLimit(IntegerLiteral(limit), _) if limit == 0 =>
empty(p)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.dsl.expressions._
import org.apache.spark.sql.catalyst.dsl.plans._
import org.apache.spark.sql.catalyst.expressions.Literal
import org.apache.spark.sql.catalyst.plans._
import org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, LogicalPlan, Project}
import org.apache.spark.sql.catalyst.plans.logical.{Distinct, GlobalLimit, LocalLimit, LocalRelation, LogicalPlan, Project}
import org.apache.spark.sql.catalyst.rules.RuleExecutor
import org.apache.spark.sql.types.{IntegerType, StructType}

Expand Down Expand Up @@ -221,4 +221,79 @@ class PropagateEmptyRelationSuite extends PlanTest {
val optimized = Optimize.execute(query.analyze)
assert(optimized.resolved)
}

test("Limit 0: return empty local relation") {
val query = testRelation1.limit(0)

val optimized = Optimize.execute(query.analyze)
val correctAnswer = LocalRelation('a.int)

comparePlans(optimized, correctAnswer)
}

test("Limit 0: individual LocalLimit 0 node") {
val query = LocalLimit(0, testRelation1)

val optimized = Optimize.execute(query.analyze)
val correctAnswer = LocalRelation('a.int)

comparePlans(optimized, correctAnswer)
}

test("Limit 0: individual GlobalLimit 0 node") {
val query = GlobalLimit(0, testRelation1)

val optimized = Optimize.execute(query.analyze)
val correctAnswer = LocalRelation('a.int)

comparePlans(optimized, correctAnswer)
}

test("Limit 0: Joins") {
val testcases = Seq(
(Inner, Some(LocalRelation('a.int, 'b.int))),
(LeftOuter,
Some(Project(Seq('a, Literal(null).cast(IntegerType).as('b)), testRelation1).analyze)),
(RightOuter, Some(LocalRelation('a.int, 'b.int))),
(FullOuter,
Some(Project(Seq('a, Literal(null).cast(IntegerType).as('b)), testRelation1).analyze))
)

testcases.foreach { case (jt, answer) =>
val query = testRelation1
.join(testRelation2.limit(0), joinType = jt, condition = Some('a.attr == 'b.attr))

val optimized = Optimize.execute(query.analyze)
val correctAnswer =
answer.getOrElse(OptimizeWithoutPropagateEmptyRelation.execute(query.analyze))

comparePlans(optimized, correctAnswer)
}
}

test("Limit 0: 3-way join") {
val testRelation3 = LocalRelation.fromExternalRows(Seq('c.int), data = Seq(Row(1)))

val subJoinQuery = testRelation1
.join(testRelation2, joinType = Inner, condition = Some('a.attr == 'b.attr))
val query = subJoinQuery
.join(testRelation3.limit(0), joinType = Inner, condition = Some('a.attr == 'c.attr))

val optimized = Optimize.execute(query.analyze)
val correctAnswer = Some(LocalRelation('a.int, 'b.int, 'c.int))
.getOrElse(OptimizeWithoutPropagateEmptyRelation.execute(query.analyze))

comparePlans(optimized, correctAnswer)
}

test("Limit 0: intersect") {
val query = testRelation1
.intersect(testRelation1.limit(0), isAll = false)

val optimized = Optimize.execute(query.analyze)
val correctAnswer = Distinct(Some(LocalRelation('a.int))
.getOrElse(OptimizeWithoutPropagateEmptyRelation.execute(query.analyze)))

comparePlans(optimized, correctAnswer)
}
}