-
Notifications
You must be signed in to change notification settings - Fork 28.2k
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
[SPARK-26626][SQL] Maximum size for repeatedly substituted aliases in SQL expressions #23556
Changes from all commits
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 |
---|---|---|
|
@@ -691,7 +691,8 @@ object CollapseProject extends Rule[LogicalPlan] { | |
|
||
def apply(plan: LogicalPlan): LogicalPlan = plan transformUp { | ||
case p1 @ Project(_, p2: Project) => | ||
if (haveCommonNonDeterministicOutput(p1.projectList, p2.projectList)) { | ||
if (haveCommonNonDeterministicOutput(p1.projectList, p2.projectList) || | ||
hasOversizedRepeatedAliases(p1.projectList, p2.projectList)) { | ||
p1 | ||
} else { | ||
p2.copy(projectList = buildCleanedProjectList(p1.projectList, p2.projectList)) | ||
|
@@ -735,6 +736,28 @@ object CollapseProject extends Rule[LogicalPlan] { | |
}.exists(!_.deterministic)) | ||
} | ||
|
||
private def hasOversizedRepeatedAliases( | ||
upper: Seq[NamedExpression], lower: Seq[NamedExpression]): Boolean = { | ||
val aliases = collectAliases(lower) | ||
|
||
// Count how many times each alias is used in the upper Project. | ||
// If an alias is only used once, we can safely substitute it without increasing the overall | ||
// tree size | ||
val referenceCounts = AttributeMap( | ||
upper | ||
.flatMap(_.collect { case a: Attribute => a }) | ||
.groupBy(identity) | ||
.mapValues(_.size).toSeq | ||
) | ||
|
||
// Check for any aliases that are used more than once, and are larger than the configured | ||
// maximum size | ||
aliases.exists({ case (attribute, expression) => | ||
referenceCounts.getOrElse(attribute, 0) > 1 && | ||
expression.treeSize > SQLConf.get.maxRepeatedAliasSize | ||
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'm not sure about using How about we simplify it with a blacklist? e.g. UDF is expensive and we shouldn't collapse projects if udf is repeated. 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 isn't trying to determine the cost of the expression - the cost of the expression is irrelevant here, we're just trying to determine the size of the expression itself (using tree size as a proxy for memory size). That way, if the expression is too large (takes up too much memory) we can prevent OOMs by not de-aliasing it multiple times (and thus greatly increasing the amount of heap the expression tree takes up). 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. so your fix only care about memory usage of the expressions, instead of execution time? 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. @cloud-fan That's right, the primary concern is memory usage, since the exponential increase in memory usage currently causes crashes (due to OOMs), time outs, and performance issues. |
||
}) | ||
} | ||
|
||
private def buildCleanedProjectList( | ||
upper: Seq[NamedExpression], | ||
lower: Seq[NamedExpression]): Seq[NamedExpression] = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1717,6 +1717,15 @@ object SQLConf { | |
"and java.sql.Date are used for the same purpose.") | ||
.booleanConf | ||
.createWithDefault(false) | ||
|
||
val MAX_REPEATED_ALIAS_SIZE = | ||
buildConf("spark.sql.maxRepeatedAliasSize") | ||
.internal() | ||
.doc("The maximum size of alias expression that will be substituted multiple times " + | ||
"(size defined by the number of nodes in the expression tree). " + | ||
"Used by the CollapseProject optimizer, and PhysicalOperation.") | ||
.intConf | ||
.createWithDefault(100) | ||
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. It does add something automatic stuff but I don't think this is so much worth since we already have a general mechanism. Note that you can also increases the driver side's memory. How does it relate with this configuration? 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. @HyukjinKwon increasing the driver memory unfortunately isn't an option, because due to the exponential tree size explosion, the necessary memory would be much larger than that available on most servers. Also, users wouldn't know that they needed a very large driver memory size, because they can be running small queries over small data. |
||
} | ||
|
||
/** | ||
|
@@ -2162,6 +2171,8 @@ class SQLConf extends Serializable with Logging { | |
def setCommandRejectsSparkCoreConfs: Boolean = | ||
getConf(SQLConf.SET_COMMAND_REJECTS_SPARK_CORE_CONFS) | ||
|
||
def maxRepeatedAliasSize: Int = getConf(SQLConf.MAX_REPEATED_ALIAS_SIZE) | ||
|
||
/** ********************** SQLConf functionality methods ************ */ | ||
|
||
/** Set Spark SQL configuration properties. */ | ||
|
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.
Is the purpose to give up this rule basically? Why don't we consider using
spark.sql.optimizer.excludedRules
? I think it's more general way to resolve such issues.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.
@HyukjinKwon The purpose is to improve the rule so that it only applies when it will yield a performance improvement (and not apply when it could cause memory issues). This was the preferred solution, since if we excluded the rule entirely we wouldn't benefit from it in the instances where it would be beneficial.
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.
So, basically what you want to do is to give up a rule given a condition because processing huge tree causes OOM issue only in the driver. Am I correct?
What's the diff if we set the threshold
spark.sql.maxRepeatedAliasSize
to set the specific number based upon the rough estimation vs explicitly excluding the rule byspark.sql.optimizer.excludedRules
based on user's rough estimation?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.
@HyukjinKwon it's not that processing a huge tree causes an OOM, it's that the user can write a small tree, that seems very reasonable to execute, but under the hood the optimiser turns it into a huge tree that OOMs. The user doesn't know beforehand that the optimiser issue is going to happen, in order to disable the rule. It takes a lot of debugging, looking through stack traces, etc, to identify that the OOM is caused by CollapseProject and that you can disable it. Also, we typically run many different queries within a spark session, and wouldn't want to disable CollapseProject for all of them.
This change means that we can still run CollapseProject, we just don't substitute overly large aliases. In the types of query we had problems with, this means that it will collapse the query until the aliases get too large, and then stop. So we still do apply CollapseProject to every query, we just stop substituting any alias the gets too large.
spark.sql.maxRepeatedAliasSize
just determines the size of alias tree that is determined to be too large to efficiently substitute multiple times. The default value of100
was determined by some basic testing to find the best perf balance (see charts at top), but happy to tweak this if you don't htink it's appropriate?