-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-25121][SQL] Supports multi-part relation names for join strategy hint resolution #27935
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 5 commits
52089fc
ac19ea1
70c994a
fbbe824
2444a20
f07613d
6528dad
c517c1f
61e4a95
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 |
|---|---|---|
|
|
@@ -64,31 +64,59 @@ object ResolveHints { | |
| _.toUpperCase(Locale.ROOT)).contains(hintName.toUpperCase(Locale.ROOT)))) | ||
| } | ||
|
|
||
| // This method checks if given multi-part identifiers are matched with each other. | ||
| // The [[ResolveJoinStrategyHints]] rule is applied before the resolution batch | ||
| // in the analyzer and we cannot semantically compare them at this stage. | ||
| // Therefore, we follow a simple rule; they match if an identifier in a hint | ||
| // is a tail of an identifier in a relation. This process is independent of a session | ||
| // catalog (`currentDb` in [[SessionCatalog]]) and it just compares them literally. | ||
| // | ||
| // For example, | ||
| // * in a query `SELECT /*+ BROADCAST(t) */ * FROM db1.t JOIN t`, | ||
| // the broadcast hint will match both tables, `db1.t` and `t`. | ||
| // * in a query `SELECT /*+ BROADCAST(default.t) */ * FROM default.t JOIN t`, | ||
| // the broadcast hint will match the left-side table only, `default.t`. | ||
| private def matchedIdentifier(identInHint: Seq[String], identInQuery: Seq[String]): Boolean = { | ||
| if (identInHint.length <= identInQuery.length) { | ||
| identInHint.zip(identInQuery.takeRight(identInHint.length)) | ||
| .forall { case (i1, i2) => resolver(i1, i2) } | ||
| } else { | ||
| false | ||
| } | ||
| } | ||
|
|
||
| private def applyJoinStrategyHint( | ||
| plan: LogicalPlan, | ||
| relations: mutable.HashSet[String], | ||
| relationsInHint: Set[Seq[String]], | ||
| relationsInHintWithMatch: mutable.HashSet[Seq[String]], | ||
| hintName: String): LogicalPlan = { | ||
| // Whether to continue recursing down the tree | ||
| var recurse = true | ||
|
|
||
| def extractIdentifier(r: SubqueryAlias): Seq[String] = { | ||
| r.identifier.qualifier :+ r.identifier.name | ||
| } | ||
|
|
||
| val newNode = CurrentOrigin.withOrigin(plan.origin) { | ||
| plan match { | ||
| case ResolvedHint(u @ UnresolvedRelation(ident), hint) | ||
| if relations.exists(resolver(_, ident.last)) => | ||
| relations.remove(ident.last) | ||
|
cloud-fan marked this conversation as resolved.
|
||
| if relationsInHint.exists(matchedIdentifier(_, ident)) => | ||
| relationsInHintWithMatch += ident | ||
|
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. The problem here is,
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. Urr, I see. I'll fix that.
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. Since your suggested code above broke some existing tests, I modified it a little based on that. Updated in the latest commit. |
||
| ResolvedHint(u, createHintInfo(hintName).merge(hint, hintErrorHandler)) | ||
|
|
||
| case ResolvedHint(r: SubqueryAlias, hint) | ||
| if relations.exists(resolver(_, r.alias)) => | ||
| relations.remove(r.alias) | ||
| if relationsInHint.exists(matchedIdentifier(_, extractIdentifier(r))) => | ||
| relationsInHintWithMatch += extractIdentifier(r) | ||
| ResolvedHint(r, createHintInfo(hintName).merge(hint, hintErrorHandler)) | ||
|
|
||
| case u @ UnresolvedRelation(ident) if relations.exists(resolver(_, ident.last)) => | ||
| relations.remove(ident.last) | ||
| case UnresolvedRelation(ident) | ||
| if relationsInHint.exists(matchedIdentifier(_, ident)) => | ||
| relationsInHintWithMatch += ident | ||
| ResolvedHint(plan, createHintInfo(hintName)) | ||
|
|
||
| case r: SubqueryAlias if relations.exists(resolver(_, r.alias)) => | ||
| relations.remove(r.alias) | ||
| case r: SubqueryAlias | ||
| if relationsInHint.exists(matchedIdentifier(_, extractIdentifier(r))) => | ||
| relationsInHintWithMatch += extractIdentifier(r) | ||
| ResolvedHint(plan, createHintInfo(hintName)) | ||
|
|
||
| case _: ResolvedHint | _: View | _: With | _: SubqueryAlias => | ||
|
|
@@ -107,7 +135,9 @@ object ResolveHints { | |
| } | ||
|
|
||
| if ((plan fastEquals newNode) && recurse) { | ||
| newNode.mapChildren(child => applyJoinStrategyHint(child, relations, hintName)) | ||
| newNode.mapChildren { child => | ||
| applyJoinStrategyHint(child, relationsInHint, relationsInHintWithMatch, hintName) | ||
| } | ||
| } else { | ||
| newNode | ||
| } | ||
|
|
@@ -120,17 +150,24 @@ object ResolveHints { | |
| ResolvedHint(h.child, createHintInfo(h.name)) | ||
| } else { | ||
| // Otherwise, find within the subtree query plans to apply the hint. | ||
| val relationNames = h.parameters.map { | ||
| case tableName: String => tableName | ||
| case tableId: UnresolvedAttribute => tableId.name | ||
| val relationNamesInHint = h.parameters.map { | ||
| case tableName: String => UnresolvedAttribute.parseAttributeName(tableName) | ||
| case tableId: UnresolvedAttribute => tableId.nameParts | ||
| case unsupported => throw new AnalysisException("Join strategy hint parameter " + | ||
| s"should be an identifier or string but was $unsupported (${unsupported.getClass}") | ||
| }.toSet | ||
| val relationsInHintWithMatch = new mutable.HashSet[Seq[String]] | ||
| val applied = applyJoinStrategyHint( | ||
| h.child, relationNamesInHint, relationsInHintWithMatch, h.name) | ||
|
|
||
| // Filters unmatched relation identifiers in the hint | ||
| val unmatchedIdents = relationNamesInHint.filterNot { relationIdent => | ||
| relationsInHintWithMatch.exists { ident => | ||
| relationIdent.size == ident.size && | ||
| relationIdent.zip(ident).forall { case (a, b) => resolver(a, b) } | ||
| } | ||
| } | ||
| val relationNameSet = new mutable.HashSet[String] | ||
| relationNames.foreach(relationNameSet.add) | ||
|
|
||
| val applied = applyJoinStrategyHint(h.child, relationNameSet, h.name) | ||
| hintErrorHandler.hintRelationsNotFound(h.name, h.parameters, relationNameSet.toSet) | ||
| hintErrorHandler.hintRelationsNotFound(h.name, h.parameters, unmatchedIdents) | ||
| applied | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -241,4 +241,52 @@ class ResolveHintsSuite extends AnalysisTest { | |
| Project(testRelation.output, testRelation), | ||
| caseSensitive = false) | ||
| } | ||
|
|
||
| test("Supports multi-part table names for broadcast hint resolution") { | ||
| // local temp table (single-part identifier case) | ||
| checkAnalysis( | ||
| UnresolvedHint("MAPJOIN", Seq("table", "table2"), | ||
| table("table").join(table("table2"))), | ||
|
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. The following will be better because this is a - table("table").join(table("table2"))),
+ table("TaBlE").join(table("TaBlE2"))),
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. Yea, yes! |
||
| Join( | ||
| ResolvedHint(testRelation, HintInfo(strategy = Some(BROADCAST))), | ||
| ResolvedHint(testRelation2, HintInfo(strategy = Some(BROADCAST))), | ||
| Inner, | ||
| None, | ||
| JoinHint.NONE), | ||
| caseSensitive = false) | ||
|
|
||
| checkAnalysis( | ||
| UnresolvedHint("MAPJOIN", Seq("TaBlE", "table2"), | ||
| table("TaBlE").join(table("TaBlE2"))), | ||
| Join( | ||
| ResolvedHint(testRelation, HintInfo(strategy = Some(BROADCAST))), | ||
| testRelation2, | ||
| Inner, | ||
| None, | ||
| JoinHint.NONE), | ||
| caseSensitive = true) | ||
|
|
||
| // global temp table (multi-part identifier case) | ||
| checkAnalysis( | ||
| UnresolvedHint("MAPJOIN", Seq("global_temp.table4", "GlOBal_TeMP.table5"), | ||
|
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. shall we test
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.
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. updated. |
||
| table("global_temp", "table4").join(table("global_temp", "table5"))), | ||
| Join( | ||
| ResolvedHint(testRelation4, HintInfo(strategy = Some(BROADCAST))), | ||
| ResolvedHint(testRelation5, HintInfo(strategy = Some(BROADCAST))), | ||
| Inner, | ||
| None, | ||
| JoinHint.NONE), | ||
| caseSensitive = false) | ||
|
|
||
| checkAnalysis( | ||
| UnresolvedHint("MAPJOIN", Seq("global_temp.TaBlE4", "table5"), | ||
| table("global_temp", "TaBlE4").join(table("global_temp", "TaBlE5"))), | ||
| Join( | ||
| ResolvedHint(testRelation4, HintInfo(strategy = Some(BROADCAST))), | ||
| testRelation5, | ||
| Inner, | ||
| None, | ||
| JoinHint.NONE), | ||
| caseSensitive = true) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,10 +17,13 @@ | |
|
|
||
| package org.apache.spark.sql | ||
|
|
||
| import org.apache.spark.sql.catalyst.TableIdentifier | ||
| import org.apache.spark.sql.catalyst.plans.{Inner, InnerLike, LeftOuter, RightOuter} | ||
| import org.apache.spark.sql.catalyst.plans.logical.{Filter, Join, LogicalPlan, Project} | ||
| import org.apache.spark.sql.catalyst.plans.logical.{BROADCAST, Filter, HintInfo, Join, JoinHint, LogicalPlan, Project} | ||
| import org.apache.spark.sql.execution.FileSourceScanExec | ||
| import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper | ||
| import org.apache.spark.sql.execution.datasources.LogicalRelation | ||
| import org.apache.spark.sql.execution.exchange.BroadcastExchangeExec | ||
| import org.apache.spark.sql.execution.joins.BroadcastHashJoinExec | ||
| import org.apache.spark.sql.functions._ | ||
| import org.apache.spark.sql.internal.SQLConf | ||
|
|
@@ -322,4 +325,81 @@ class DataFrameJoinSuite extends QueryTest | |
| } | ||
| } | ||
| } | ||
|
|
||
| test("Supports multi-part names for broadcast hint resolution") { | ||
| val (table1Name, table2Name) = ("t1", "t2") | ||
|
|
||
| withTempDatabase { dbName => | ||
| withTable(table1Name, table2Name) { | ||
| withSQLConf(SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") { | ||
| spark.range(50).write.saveAsTable(s"$dbName.$table1Name") | ||
| spark.range(100).write.saveAsTable(s"$dbName.$table2Name") | ||
|
|
||
| // First, makes sure a join is not broadcastable | ||
| val plan = sql(s"SELECT * FROM $dbName.$table1Name, $dbName.$table2Name " + | ||
| s"WHERE $table1Name.id = $table2Name.id") | ||
| .queryExecution.executedPlan | ||
| assert(plan.collect { case p: BroadcastHashJoinExec => p }.isEmpty) | ||
|
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.
- .queryExecution.executedPlan
- assert(plan.collect { case p: BroadcastHashJoinExec => p }.isEmpty)
+ checkIfHintNotApplied(plan)
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. Ah... I forgot to remove the old tests... I can remove it. Thanks! |
||
|
|
||
| def checkIfHintApplied(tableName: String, hintTableName: String): Unit = { | ||
|
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. cann we test DataFrame hint API as well?
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. updated. |
||
| val p = sql(s"SELECT /*+ BROADCASTJOIN($hintTableName) */ * " + | ||
| s"FROM $tableName, $dbName.$table2Name " + | ||
| s"WHERE $tableName.id = $table2Name.id") | ||
| .queryExecution.executedPlan | ||
| val broadcastHashJoins = p.collect { case p: BroadcastHashJoinExec => p } | ||
| assert(broadcastHashJoins.size == 1) | ||
| val broadcastExchanges = broadcastHashJoins.head.collect { | ||
| case p: BroadcastExchangeExec => p | ||
| } | ||
| assert(broadcastExchanges.size == 1) | ||
| val tables = broadcastExchanges.head.collect { | ||
| case FileSourceScanExec(_, _, _, _, _, _, Some(tableIdent)) => tableIdent | ||
| } | ||
| assert(tables.size == 1) | ||
| assert(tables.head === TableIdentifier(table1Name, Some(dbName))) | ||
| } | ||
|
|
||
| def checkIfHintNotApplied(tableName: String, hintTableName: String): Unit = { | ||
| val p = sql(s"SELECT /*+ BROADCASTJOIN($hintTableName) */ * " + | ||
| s"FROM $tableName, $dbName.$table2Name " + | ||
| s"WHERE $tableName.id = $table2Name.id") | ||
| .queryExecution.executedPlan | ||
| val broadcastHashJoins = p.collect { case p: BroadcastHashJoinExec => p } | ||
| assert(broadcastHashJoins.isEmpty) | ||
| } | ||
|
|
||
| sql(s"USE $dbName") | ||
| checkIfHintApplied(table1Name, table1Name) | ||
| checkIfHintApplied(s"$dbName.$table1Name", s"$dbName.$table1Name") | ||
| checkIfHintApplied(s"$dbName.$table1Name", table1Name) | ||
| checkIfHintNotApplied(table1Name, s"$dbName.$table1Name") | ||
| checkIfHintNotApplied(s"$dbName.$table1Name", s"$dbName.$table1Name.id") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("The same table name exists in two databases for broadcast hint resolution") { | ||
| val (db1Name, db2Name) = ("db1", "db2") | ||
|
|
||
| withDatabase(db1Name, db2Name) { | ||
| withTable("t") { | ||
| withSQLConf(SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") { | ||
| sql(s"CREATE DATABASE $db1Name") | ||
| sql(s"CREATE DATABASE $db2Name") | ||
| spark.range(1).write.saveAsTable(s"$db1Name.t") | ||
| spark.range(1).write.saveAsTable(s"$db2Name.t") | ||
|
|
||
| // Checks if a broadcast hint applied in both sides | ||
| val statement = s"SELECT /*+ BROADCASTJOIN(t) */ * FROM $db1Name.t, $db2Name.t " + | ||
| s"WHERE $db1Name.t.id = $db2Name.t.id" | ||
| sql(statement).queryExecution.optimizedPlan match { | ||
| case Join(_, _, _, _, JoinHint(Some(HintInfo(Some(BROADCAST))), | ||
| Some(HintInfo(Some(BROADCAST))))) => | ||
| case _ => fail("broadcast hint not found in both tables") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
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.