-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-12616] [SQL] Making Logical Operator Union Support Arbitrary Number of Children
#10577
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 4 commits
01e4cdf
6835704
9180687
b38a21e
d2b84af
fda8025
ac0dccd
6e0018b
0546772
b37a64f
73270c8
d9811c7
5d031a7
c1f66f7
c1dcd02
51ad5b2
c2a872c
5681ca8
7a54c8f
95e2349
6a6003e
15ec058
5e06647
ab6dbd7
b821af0
2229932
b3327b1
4276356
2dab708
723c0da
42b81a8
4e0387f
b03d813
ab732c1
0458770
1debdfa
7320e21
741371a
031a5d8
f3d23dc
a56e595
abfcf93
763706d
e8e19a1
3b13ddf
b88bdeb
3041864
6259fd9
4de6ec1
f112026
4f71741
9422a4f
59b5895
c63f237
2e8562d
a571998
52bdf48
c18381e
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 |
|---|---|---|
|
|
@@ -189,6 +189,16 @@ trait CheckAnalysis { | |
| s"but the left table has ${left.output.length} columns and the right has " + | ||
| s"${right.output.length}") | ||
|
|
||
| case s: Unions if s.children.exists(_.output.length != s.children.head.output.length) => | ||
| s.children.filter(_.output.length != s.children.head.output.length).foreach { child => | ||
| failAnalysis( | ||
| s""" | ||
| |Unions can only be performed on tables with the same number of columns, | ||
| | but the table '${child.simpleString}' has '${child.output.length}' columns | ||
|
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. Is the simple string really a helpful part of this error message?
Member
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. Ok, let me remove it. |
||
| | and the first table '${s.children.head.simpleString}' has | ||
| | '${s.children.head.output.length}' columns""".stripMargin) | ||
| } | ||
|
|
||
| case _ => // Fallbacks to the following checks | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,7 +196,7 @@ object HiveTypeCoercion { | |
| * - LongType to DoubleType | ||
| * - DecimalType to Double | ||
| * | ||
| * This rule is only applied to Union/Except/Intersect | ||
| * This rule is only applied to Unions/Except/Intersect | ||
| */ | ||
| object WidenSetOperationTypes extends Rule[LogicalPlan] { | ||
|
|
||
|
|
@@ -212,29 +212,59 @@ object HiveTypeCoercion { | |
| case other => None | ||
| } | ||
|
|
||
| def castOutput(plan: LogicalPlan): LogicalPlan = { | ||
| val casted = plan.output.zip(castedTypes).map { | ||
| case (e, Some(dt)) if e.dataType != dt => | ||
| Alias(Cast(e, dt), e.name)() | ||
| case (e, _) => e | ||
| } | ||
| Project(casted, plan) | ||
| if (castedTypes.exists(_.isDefined)) { | ||
| (castOutput(left, castedTypes), castOutput(right, castedTypes)) | ||
| } else { | ||
| (left, right) | ||
| } | ||
| } | ||
|
|
||
| private[this] def widenOutputTypes( | ||
| planName: String, | ||
|
Member
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. Where do we use this? If not, can we remove it?
Member
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. Yeah, this is useless. Let me remove it. |
||
| children: Seq[LogicalPlan]): Seq[LogicalPlan] = { | ||
| require(children.forall(_.output.length == children.head.output.length)) | ||
|
|
||
| val castedTypes: Seq[Option[DataType]] = | ||
| children.tail.foldLeft(children.head.output.map(a => Option(a.dataType))) { | ||
|
Member
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. There is a bug in this function. Will fix it tonight. Thanks! |
||
| case (currentOutputDataTypes, child) => { | ||
| currentOutputDataTypes.zip(child.output).map { | ||
| case (Some(dt), a2) if dt != a2.dataType => | ||
| findWiderTypeForTwo(dt, a2.dataType) | ||
| case other => None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (castedTypes.exists(_.isDefined)) { | ||
| (castOutput(left), castOutput(right)) | ||
| children.map(castOutput(_, castedTypes)) | ||
| } else { | ||
| (left, right) | ||
| children | ||
| } | ||
| } | ||
|
|
||
| private[this] def castOutput( | ||
| plan: LogicalPlan, | ||
| castedTypes: Seq[Option[DataType]]): LogicalPlan = { | ||
| val casted = plan.output.zip(castedTypes).map { | ||
| case (e, Some(dt)) if e.dataType != dt => | ||
| Alias(Cast(e, dt), e.name)() | ||
| case (e, _) => e | ||
| } | ||
| Project(casted, plan) | ||
| } | ||
|
|
||
| def apply(plan: LogicalPlan): LogicalPlan = plan resolveOperators { | ||
| case p if p.analyzed => p | ||
|
|
||
| case s @ SetOperation(left, right) if s.childrenResolved | ||
| && left.output.length == right.output.length && !s.resolved => | ||
| val (newLeft, newRight) = widenOutputTypes(s.nodeName, left, right) | ||
| s.makeCopy(Array(newLeft, newRight)) | ||
|
|
||
| case s: Unions if s.childrenResolved && | ||
| s.children.forall(_.output.length == s.children.head.output.length) && !s.resolved => | ||
|
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. maybe indent 4 space here rather than 2 to make it clear this is not the same block as the next line
Member
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. ok, will do it. |
||
| val newChildren: Seq[LogicalPlan] = widenOutputTypes(s.nodeName, s.children) | ||
| s.makeCopy(Array(newChildren)) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,8 @@ abstract class Optimizer extends RuleExecutor[LogicalPlan] { | |
| Batch("Aggregate", FixedPoint(100), | ||
| ReplaceDistinctWithAggregate, | ||
| RemoveLiteralFromGroupExpressions) :: | ||
| Batch("Unions", FixedPoint(100), | ||
| CombineUnions) :: | ||
|
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. you should explain in comments why CombineUnions appear twice.
Member
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. Sure, will do.
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. and maybe move this before aggregate?
Member
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. Sure. |
||
| Batch("Operator Optimizations", FixedPoint(100), | ||
| // Operator push down | ||
| SetOperationPushDown, | ||
|
|
@@ -54,6 +56,7 @@ abstract class Optimizer extends RuleExecutor[LogicalPlan] { | |
| ProjectCollapsing, | ||
| CombineFilters, | ||
| CombineLimits, | ||
| CombineUnions, | ||
| // Constant folding | ||
| NullPropagation, | ||
| OptimizeIn, | ||
|
|
@@ -95,8 +98,8 @@ object SamplePushDown extends Rule[LogicalPlan] { | |
| /** | ||
| * Pushes certain operations to both sides of a Union, Intersect or Except operator. | ||
| * Operations that are safe to pushdown are listed as follows. | ||
| * Union: | ||
| * Right now, Union means UNION ALL, which does not de-duplicate rows. So, it is | ||
| * Unions: | ||
| * Right now, Unions means UNION ALL, which does not de-duplicate rows. So, it is | ||
| * safe to pushdown Filters and Projections through it. Once we add UNION DISTINCT, | ||
| * we will not be able to pushdown Projections. | ||
| * | ||
|
|
@@ -116,15 +119,15 @@ object SetOperationPushDown extends Rule[LogicalPlan] with PredicateHelper { | |
| * Maps Attributes from the left side to the corresponding Attribute on the right side. | ||
| */ | ||
| private def buildRewrites(bn: BinaryNode): AttributeMap[Attribute] = { | ||
| assert(bn.isInstanceOf[Union] || bn.isInstanceOf[Intersect] || bn.isInstanceOf[Except]) | ||
| assert(bn.isInstanceOf[Intersect] || bn.isInstanceOf[Except]) | ||
| assert(bn.left.output.size == bn.right.output.size) | ||
|
|
||
| AttributeMap(bn.left.output.zip(bn.right.output)) | ||
| } | ||
|
|
||
| /** | ||
| * Rewrites an expression so that it can be pushed to the right side of a | ||
| * Union, Intersect or Except operator. This method relies on the fact that the output attributes | ||
| * Unions, Intersect or Except operator. This method relies on the fact that the output attributes | ||
| * of a union/intersect/except are always equal to the left child's output. | ||
| */ | ||
| private def pushToRight[A <: Expression](e: A, rewrites: AttributeMap[Attribute]) = { | ||
|
|
@@ -153,27 +156,6 @@ object SetOperationPushDown extends Rule[LogicalPlan] with PredicateHelper { | |
| } | ||
|
|
||
| def apply(plan: LogicalPlan): LogicalPlan = plan transform { | ||
| // Push down filter into union | ||
| case Filter(condition, u @ Union(left, right)) => | ||
| val (deterministic, nondeterministic) = partitionByDeterministic(condition) | ||
| val rewrites = buildRewrites(u) | ||
| Filter(nondeterministic, | ||
| Union( | ||
| Filter(deterministic, left), | ||
| Filter(pushToRight(deterministic, rewrites), right) | ||
| ) | ||
| ) | ||
|
|
||
| // Push down deterministic projection through UNION ALL | ||
| case p @ Project(projectList, u @ Union(left, right)) => | ||
| if (projectList.forall(_.deterministic)) { | ||
| val rewrites = buildRewrites(u) | ||
| Union( | ||
| Project(projectList, left), | ||
| Project(projectList.map(pushToRight(_, rewrites)), right)) | ||
| } else { | ||
| p | ||
| } | ||
|
|
||
| // Push down filter through INTERSECT | ||
| case Filter(condition, i @ Intersect(left, right)) => | ||
|
|
@@ -594,6 +576,22 @@ object BooleanSimplification extends Rule[LogicalPlan] with PredicateHelper { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Combines all adjacent [[Unions]] operators into a single [[Unions]]. | ||
| */ | ||
| object CombineUnions extends Rule[LogicalPlan] { | ||
| private def buildUnionChildren(children: Seq[LogicalPlan]): Seq[LogicalPlan] = | ||
| children.foldLeft(Seq.empty[LogicalPlan]) { (newChildren, child) => child match { | ||
| case Unions(grandchildren) => newChildren ++ grandchildren | ||
| case other => newChildren ++ Seq(other) | ||
| } | ||
| } | ||
|
|
||
| def apply(plan: LogicalPlan): LogicalPlan = plan transformUp { | ||
| case u @ Unions(children) => Unions(buildUnionChildren(children)) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Combines two adjacent [[Filter]] operators into one, merging the | ||
| * conditions into one conjunctive predicate. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,21 +107,42 @@ private[sql] object SetOperation { | |
| def unapply(p: SetOperation): Option[(LogicalPlan, LogicalPlan)] = Some((p.left, p.right)) | ||
| } | ||
|
|
||
| case class Union(left: LogicalPlan, right: LogicalPlan) extends SetOperation(left, right) { | ||
|
|
||
| override def statistics: Statistics = { | ||
| val sizeInBytes = left.statistics.sizeInBytes + right.statistics.sizeInBytes | ||
| Statistics(sizeInBytes = sizeInBytes) | ||
| } | ||
| } | ||
|
|
||
| case class Intersect(left: LogicalPlan, right: LogicalPlan) extends SetOperation(left, right) | ||
|
|
||
| case class Except(left: LogicalPlan, right: LogicalPlan) extends SetOperation(left, right) { | ||
| /** We don't use right.output because those rows get excluded from the set. */ | ||
| override def output: Seq[Attribute] = left.output | ||
| } | ||
|
|
||
| /** Factory for constructing new `Unions` nodes. */ | ||
|
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. Unions -> Union |
||
| object Union { | ||
| def apply(left: LogicalPlan, right: LogicalPlan): Unions = { | ||
| Unions (left :: right :: Nil) | ||
| } | ||
| } | ||
|
|
||
| case class Unions(children: Seq[LogicalPlan]) extends LogicalPlan { | ||
|
|
||
| override def output: Seq[Attribute] = { | ||
| children.tail.foldLeft(children.head.output) { case (currentOutput, child) => | ||
|
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. I'd also add some comment here explaining what's going on. at a high level, you are just updating nullability right?
Member
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. yeah |
||
| currentOutput.zip(child.output).map { case (a1, a2) => | ||
| a1.withNullability(a1.nullable || a2.nullable) | ||
| } | ||
|
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. how about
Member
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. Thank you! |
||
| } | ||
| } | ||
|
|
||
| override lazy val resolved: Boolean = | ||
| childrenResolved && | ||
| children.forall(_.output.length == children.head.output.length) && | ||
| children.forall(_.output.zip(children.head.output).forall { | ||
|
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. this nested forall is pretty confusing. I'd restructure this.
Member
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. Ok, will do. |
||
| case (l, r) => l.dataType == r.dataType }) | ||
|
|
||
| override def statistics: Statistics = { | ||
| val sizeInBytes = children.map(_.statistics.sizeInBytes).sum | ||
| Statistics(sizeInBytes = sizeInBytes) | ||
| } | ||
| } | ||
|
|
||
| case class Join( | ||
| left: LogicalPlan, | ||
| right: LogicalPlan, | ||
|
|
||
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.
I think we don't need
foreachhere, as the first exception will interrupt the analysis phase. We can just throw exception for the first error.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.
Thanks! Will do the change