-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-31365][SQL] Enable nested predicate pushdown per data sources #28366
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
9659699
6feaaa4
e555a1c
84bc8dd
a49b73c
17d1094
aa32dcc
00b9d47
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 |
|---|---|---|
|
|
@@ -2063,16 +2063,17 @@ object SQLConf { | |
| .booleanConf | ||
| .createWithDefault(true) | ||
|
|
||
| val NESTED_PREDICATE_PUSHDOWN_ENABLED = | ||
| buildConf("spark.sql.optimizer.nestedPredicatePushdown.enabled") | ||
| .internal() | ||
| .doc("When true, Spark tries to push down predicates for nested columns and or names " + | ||
| "containing `dots` to data sources. Currently, Parquet implements both optimizations " + | ||
| "while ORC only supports predicates for names containing `dots`. The other data sources" + | ||
| "don't support this feature yet.") | ||
| val NESTED_PREDICATE_PUSHDOWN_V1_SOURCE_LIST = | ||
| buildConf("spark.sql.optimizer.nestedPredicatePushdown.v1sourceList") | ||
| .internal() | ||
| .doc("A comma-separated list of data source short names or fully qualified data source " + | ||
| "implementation class names for which Spark tries to push down predicates for nested " + | ||
| "columns and or names containing `dots` to data sources. Currently, Parquet implements " + | ||
|
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. nit:
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. Actually I coped the wordings. I think it means |
||
| "both optimizations while ORC only supports predicates for names containing `dots`. The " + | ||
| "other data sources don't support this feature yet.") | ||
|
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. How about listing up a valid set of sources like |
||
| .version("3.0.0") | ||
| .booleanConf | ||
| .createWithDefault(true) | ||
| .stringConf | ||
|
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. We need
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. We compare this list with Currently I think it is safer to assume custom data sources don't support this feature. I actually also think if custom data source wants to support it, it is better to adapt data source v2. We don't have a common API for v1 data sources that tells if it supports nested predicate pushdown. If we really want to allow custom v1 data sources have that, we can consider adding one common v1 API for the purpose. But, again, seems to me that we will encourage adapting v2 instead adding new things to v1.
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.
Yea, +1 on your thought.
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.
Looks fine. @dbtsai are you good with it? Do you have use cases that need nested predicate pushdown for non-file-source?
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. In v1, we don't have any use-case for supporting it in custom data source. I'm good with it. |
||
| .createWithDefault("parquet,orc") | ||
|
|
||
| val SERIALIZER_NESTED_SCHEMA_PRUNING_ENABLED = | ||
| buildConf("spark.sql.optimizer.serializer.nestedSchemaPruning.enabled") | ||
|
|
@@ -3098,7 +3099,8 @@ class SQLConf extends Serializable with Logging { | |
|
|
||
| def nestedSchemaPruningEnabled: Boolean = getConf(NESTED_SCHEMA_PRUNING_ENABLED) | ||
|
|
||
| def nestedPredicatePushdownEnabled: Boolean = getConf(NESTED_PREDICATE_PUSHDOWN_ENABLED) | ||
| def nestedPredicatePushdownv1SourceList: String = | ||
| getConf(NESTED_PREDICATE_PUSHDOWN_V1_SOURCE_LIST) | ||
|
|
||
| def serializerNestedSchemaPruningEnabled: Boolean = | ||
| getConf(SERIALIZER_NESTED_SCHEMA_PRUNING_ENABLED) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -448,60 +448,62 @@ object DataSourceStrategy { | |
| } | ||
| } | ||
|
|
||
| private def translateLeafNodeFilter(predicate: Expression): Option[Filter] = predicate match { | ||
| case expressions.EqualTo(PushableColumn(name), Literal(v, t)) => | ||
| private def translateLeafNodeFilter( | ||
| predicate: Expression, | ||
| pushableColumn: PushableColumnBase): Option[Filter] = predicate match { | ||
| case expressions.EqualTo(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.EqualTo(name, convertToScala(v, t))) | ||
| case expressions.EqualTo(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.EqualTo(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.EqualTo(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.EqualNullSafe(PushableColumn(name), Literal(v, t)) => | ||
| case expressions.EqualNullSafe(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.EqualNullSafe(name, convertToScala(v, t))) | ||
| case expressions.EqualNullSafe(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.EqualNullSafe(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.EqualNullSafe(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.GreaterThan(PushableColumn(name), Literal(v, t)) => | ||
| case expressions.GreaterThan(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.GreaterThan(name, convertToScala(v, t))) | ||
| case expressions.GreaterThan(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.GreaterThan(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.LessThan(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.LessThan(PushableColumn(name), Literal(v, t)) => | ||
| case expressions.LessThan(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.LessThan(name, convertToScala(v, t))) | ||
| case expressions.LessThan(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.LessThan(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.GreaterThan(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.GreaterThanOrEqual(PushableColumn(name), Literal(v, t)) => | ||
| case expressions.GreaterThanOrEqual(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.GreaterThanOrEqual(name, convertToScala(v, t))) | ||
| case expressions.GreaterThanOrEqual(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.GreaterThanOrEqual(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.LessThanOrEqual(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.LessThanOrEqual(PushableColumn(name), Literal(v, t)) => | ||
| case expressions.LessThanOrEqual(pushableColumn(name), Literal(v, t)) => | ||
| Some(sources.LessThanOrEqual(name, convertToScala(v, t))) | ||
| case expressions.LessThanOrEqual(Literal(v, t), PushableColumn(name)) => | ||
| case expressions.LessThanOrEqual(Literal(v, t), pushableColumn(name)) => | ||
| Some(sources.GreaterThanOrEqual(name, convertToScala(v, t))) | ||
|
|
||
| case expressions.InSet(e @ PushableColumn(name), set) => | ||
| case expressions.InSet(e @ pushableColumn(name), set) => | ||
| val toScala = CatalystTypeConverters.createToScalaConverter(e.dataType) | ||
| Some(sources.In(name, set.toArray.map(toScala))) | ||
|
|
||
| // Because we only convert In to InSet in Optimizer when there are more than certain | ||
| // items. So it is possible we still get an In expression here that needs to be pushed | ||
| // down. | ||
| case expressions.In(e @ PushableColumn(name), list) if list.forall(_.isInstanceOf[Literal]) => | ||
| case expressions.In(e @ pushableColumn(name), list) if list.forall(_.isInstanceOf[Literal]) => | ||
| val hSet = list.map(_.eval(EmptyRow)) | ||
| val toScala = CatalystTypeConverters.createToScalaConverter(e.dataType) | ||
| Some(sources.In(name, hSet.toArray.map(toScala))) | ||
|
|
||
| case expressions.IsNull(PushableColumn(name)) => | ||
| case expressions.IsNull(pushableColumn(name)) => | ||
| Some(sources.IsNull(name)) | ||
| case expressions.IsNotNull(PushableColumn(name)) => | ||
| case expressions.IsNotNull(pushableColumn(name)) => | ||
| Some(sources.IsNotNull(name)) | ||
| case expressions.StartsWith(PushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| case expressions.StartsWith(pushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| Some(sources.StringStartsWith(name, v.toString)) | ||
|
|
||
| case expressions.EndsWith(PushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| case expressions.EndsWith(pushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| Some(sources.StringEndsWith(name, v.toString)) | ||
|
|
||
| case expressions.Contains(PushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| case expressions.Contains(pushableColumn(name), Literal(v: UTF8String, StringType)) => | ||
| Some(sources.StringContains(name, v.toString)) | ||
|
|
||
| case expressions.Literal(true, BooleanType) => | ||
|
|
@@ -518,8 +520,9 @@ object DataSourceStrategy { | |
| * | ||
| * @return a `Some[Filter]` if the input [[Expression]] is convertible, otherwise a `None`. | ||
| */ | ||
| protected[sql] def translateFilter(predicate: Expression): Option[Filter] = { | ||
| translateFilterWithMapping(predicate, None) | ||
| protected[sql] def translateFilter( | ||
| predicate: Expression, supportNestedPredicatePushdown: Boolean): Option[Filter] = { | ||
| translateFilterWithMapping(predicate, None, supportNestedPredicatePushdown) | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -529,11 +532,14 @@ object DataSourceStrategy { | |
| * @param translatedFilterToExpr An optional map from leaf node filter expressions to its | ||
| * translated [[Filter]]. The map is used for rebuilding | ||
| * [[Expression]] from [[Filter]]. | ||
| * @param nestedPredicatePushdownEnabled Whether nested predicate pushdown is enabled. Default is | ||
| * disabled. | ||
|
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. What does
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. Oh, forgot to change it. I was adding default value but removed it later. |
||
| * @return a `Some[Filter]` if the input [[Expression]] is convertible, otherwise a `None`. | ||
| */ | ||
| protected[sql] def translateFilterWithMapping( | ||
| predicate: Expression, | ||
| translatedFilterToExpr: Option[mutable.HashMap[sources.Filter, Expression]]) | ||
| translatedFilterToExpr: Option[mutable.HashMap[sources.Filter, Expression]], | ||
| nestedPredicatePushdownEnabled: Boolean) | ||
| : Option[Filter] = { | ||
| predicate match { | ||
| case expressions.And(left, right) => | ||
|
|
@@ -547,21 +553,31 @@ object DataSourceStrategy { | |
| // Pushing one leg of AND down is only safe to do at the top level. | ||
| // You can see ParquetFilters' createFilter for more details. | ||
| for { | ||
| leftFilter <- translateFilterWithMapping(left, translatedFilterToExpr) | ||
| rightFilter <- translateFilterWithMapping(right, translatedFilterToExpr) | ||
| leftFilter <- translateFilterWithMapping( | ||
| left, translatedFilterToExpr, nestedPredicatePushdownEnabled) | ||
| rightFilter <- translateFilterWithMapping( | ||
| right, translatedFilterToExpr, nestedPredicatePushdownEnabled) | ||
| } yield sources.And(leftFilter, rightFilter) | ||
|
|
||
| case expressions.Or(left, right) => | ||
| for { | ||
| leftFilter <- translateFilterWithMapping(left, translatedFilterToExpr) | ||
| rightFilter <- translateFilterWithMapping(right, translatedFilterToExpr) | ||
| leftFilter <- translateFilterWithMapping( | ||
| left, translatedFilterToExpr, nestedPredicatePushdownEnabled) | ||
| rightFilter <- translateFilterWithMapping( | ||
| right, translatedFilterToExpr, nestedPredicatePushdownEnabled) | ||
| } yield sources.Or(leftFilter, rightFilter) | ||
|
|
||
| case expressions.Not(child) => | ||
| translateFilterWithMapping(child, translatedFilterToExpr).map(sources.Not) | ||
| translateFilterWithMapping(child, translatedFilterToExpr, nestedPredicatePushdownEnabled) | ||
| .map(sources.Not) | ||
|
|
||
| case other => | ||
| val filter = translateLeafNodeFilter(other) | ||
| val pushableColumn = if (nestedPredicatePushdownEnabled) { | ||
| PushableColumnAndNestedColumn | ||
| } else { | ||
| PushableColumnWithoutNestedColumn | ||
| } | ||
|
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. How about moving this check to the
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 |
||
| val filter = translateLeafNodeFilter(other, pushableColumn) | ||
| if (filter.isDefined && translatedFilterToExpr.isDefined) { | ||
| translatedFilterToExpr.get(filter.get) = predicate | ||
| } | ||
|
|
@@ -608,8 +624,9 @@ object DataSourceStrategy { | |
|
|
||
| // A map from original Catalyst expressions to corresponding translated data source filters. | ||
| // If a predicate is not in this map, it means it cannot be pushed down. | ||
| val supportNestedPredicatePushdown = DataSourceUtils.supportNestedPredicatePushdown(relation) | ||
| val translatedMap: Map[Expression, Filter] = predicates.flatMap { p => | ||
| translateFilter(p).map(f => p -> f) | ||
| translateFilter(p, supportNestedPredicatePushdown).map(f => p -> f) | ||
| }.toMap | ||
|
|
||
| val pushedFilters: Seq[Filter] = translatedMap.values.toSeq | ||
|
|
@@ -650,9 +667,10 @@ object DataSourceStrategy { | |
| /** | ||
| * Find the column name of an expression that can be pushed down. | ||
| */ | ||
| object PushableColumn { | ||
| abstract class PushableColumnBase { | ||
| val nestedPredicatePushdownEnabled: Boolean | ||
|
|
||
| def unapply(e: Expression): Option[String] = { | ||
| val nestedPredicatePushdownEnabled = SQLConf.get.nestedPredicatePushdownEnabled | ||
| import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper | ||
| def helper(e: Expression): Option[Seq[String]] = e match { | ||
| case a: Attribute => | ||
|
|
@@ -668,3 +686,11 @@ object PushableColumn { | |
| helper(e).map(_.quoted) | ||
| } | ||
| } | ||
|
|
||
| object PushableColumnAndNestedColumn extends PushableColumnBase { | ||
| override val nestedPredicatePushdownEnabled = true | ||
| } | ||
|
|
||
| object PushableColumnWithoutNestedColumn extends PushableColumnBase { | ||
| override val nestedPredicatePushdownEnabled = false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,13 +17,16 @@ | |
|
|
||
| package org.apache.spark.sql.execution.datasources | ||
|
|
||
| import java.util.Locale | ||
|
|
||
| import org.apache.hadoop.fs.Path | ||
| import org.json4s.NoTypeHints | ||
| import org.json4s.jackson.Serialization | ||
|
|
||
| import org.apache.spark.sql.{SPARK_LEGACY_DATETIME, SPARK_VERSION_METADATA_KEY} | ||
| import org.apache.spark.sql.AnalysisException | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.sources.BaseRelation | ||
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.util.Utils | ||
|
|
||
|
|
@@ -68,6 +71,20 @@ object DataSourceUtils { | |
| private[sql] def isDataFile(fileName: String) = | ||
| !(fileName.startsWith("_") || fileName.startsWith(".")) | ||
|
|
||
| /** | ||
| * Returns if the given relation's V1 datasource provider supports nested predicate pushdown. | ||
| */ | ||
| private[sql] def supportNestedPredicatePushdown(relation: BaseRelation): Boolean = | ||
| relation match { | ||
| case hs: HadoopFsRelation => | ||
| val supportedDatasources = | ||
| SQLConf.get.getConf(SQLConf.NESTED_PREDICATE_PUSHDOWN_V1_SOURCE_LIST) | ||
| .toLowerCase(Locale.ROOT) | ||
| .split(",").map(_.trim) | ||
|
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. Could we use
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. |
||
| supportedDatasources.contains(hs.toString) | ||
| case _ => false | ||
| } | ||
|
|
||
| def needRebaseDateTime(lookupFileMeta: String => String): Option[Boolean] = { | ||
| if (Utils.isTesting && SQLConf.get.getConfString("spark.test.forceNoRebase", "") == "true") { | ||
| return Some(false) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -178,8 +178,11 @@ object FileSourceStrategy extends Strategy with Logging { | |
| // Partition keys are not available in the statistics of the files. | ||
| val dataFilters = | ||
| normalizedFiltersWithoutSubqueries.filter(_.references.intersect(partitionSet).isEmpty) | ||
| logInfo(s"Pushed Filters: " + | ||
| s"${dataFilters.flatMap(DataSourceStrategy.translateFilter).mkString(",")}") | ||
| val supportNestedPredicatePushdown = | ||
| DataSourceUtils.supportNestedPredicatePushdown(fsRelation) | ||
| val pushedFilters = dataFilters | ||
| .flatMap(DataSourceStrategy.translateFilter(_, supportNestedPredicatePushdown)) | ||
| logInfo(s"Pushed Filters: " + s"${pushedFilters.mkString(",")}") | ||
|
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. nit: |
||
|
|
||
| // Predicates with both partition keys and attributes need to be evaluated after the scan. | ||
| val afterScanFilters = filterSet -- partitionKeyFilters.filter(_.references.nonEmpty) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -179,15 +179,22 @@ class DataSourceV2Strategy(session: SparkSession) extends Strategy with Predicat | |
|
|
||
| case OverwriteByExpression(r: DataSourceV2Relation, deleteExpr, query, writeOptions, _) => | ||
| // fail if any filter cannot be converted. correctness depends on removing all matching data. | ||
| val filters = splitConjunctivePredicates(deleteExpr).map { | ||
| filter => DataSourceStrategy.translateFilter(deleteExpr).getOrElse( | ||
| throw new AnalysisException(s"Cannot translate expression to source filter: $filter")) | ||
| }.toArray | ||
| val filters = splitConjunctivePredicates(deleteExpr) | ||
| def transferFilters = | ||
| (filters: Seq[Expression], supportNestedPredicatePushdown: Boolean) => { | ||
|
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. Do we need the |
||
| filters.map { filter => | ||
| DataSourceStrategy.translateFilter(deleteExpr, supportNestedPredicatePushdown) | ||
| .getOrElse(throw new AnalysisException( | ||
| s"Cannot translate expression to source filter: $filter")) | ||
| }.toArray | ||
| } | ||
| r.table.asWritable match { | ||
| case v1 if v1.supports(TableCapability.V1_BATCH_WRITE) => | ||
| OverwriteByExpressionExecV1(v1, filters, writeOptions.asOptions, query) :: Nil | ||
| OverwriteByExpressionExecV1( | ||
| v1, transferFilters(filters, false), writeOptions.asOptions, query) :: Nil | ||
|
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 is v1 fallback API, which is new in DS v2. I think we can always support nested filter pushdown.
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. got it. thanks. |
||
| case v2 => | ||
| OverwriteByExpressionExec(v2, filters, writeOptions.asOptions, planLater(query)) :: Nil | ||
| OverwriteByExpressionExec( | ||
| v2, transferFilters(filters, true), writeOptions.asOptions, planLater(query)) :: Nil | ||
| } | ||
|
|
||
| case OverwritePartitionsDynamic(r: DataSourceV2Relation, query, writeOptions, _) => | ||
|
|
@@ -205,7 +212,7 @@ class DataSourceV2Strategy(session: SparkSession) extends Strategy with Predicat | |
| // correctness depends on removing all matching data. | ||
| val filters = DataSourceStrategy.normalizeExprs(condition.toSeq, output) | ||
| .flatMap(splitConjunctivePredicates(_).map { | ||
| f => DataSourceStrategy.translateFilter(f).getOrElse( | ||
| f => DataSourceStrategy.translateFilter(f, true).getOrElse( | ||
| throw new AnalysisException(s"Exec update failed:" + | ||
| s" cannot translate expression to source filter: $f")) | ||
| }).toArray | ||
|
|
||
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.
nit: How about
v1sourceList->supportedV1Sources?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.
sure.