-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-25306][SQL] Avoid skewed filter trees to speed up createFilter in ORC
#22313
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 1 commit
ac06b0c
7720134
4acbaf8
5c46693
4a372a3
3cd4443
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 |
|---|---|---|
|
|
@@ -17,9 +17,13 @@ | |
|
|
||
| package org.apache.spark.sql.hive.orc | ||
|
|
||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| import com.google.common.cache.{CacheBuilder, CacheLoader} | ||
| import org.apache.hadoop.hive.ql.io.sarg.{SearchArgument, SearchArgumentFactory} | ||
| import org.apache.hadoop.hive.ql.io.sarg.SearchArgument.Builder | ||
|
|
||
| import org.apache.spark.SparkEnv | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.sql.sources._ | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -55,19 +59,52 @@ import org.apache.spark.sql.types._ | |
| * known to be convertible. | ||
| */ | ||
| private[orc] object OrcFilters extends Logging { | ||
| case class FilterWithTypeMap(filter: Filter, typeMap: Map[String, DataType]) | ||
|
|
||
| private lazy val cacheExpireTimeout = | ||
| org.apache.spark.sql.execution.datasources.orc.OrcFilters.cacheExpireTimeout | ||
|
|
||
| private lazy val searchArgumentCache = CacheBuilder.newBuilder() | ||
| .expireAfterAccess(cacheExpireTimeout, TimeUnit.SECONDS) | ||
| .build( | ||
| new CacheLoader[FilterWithTypeMap, Option[Builder]]() { | ||
| override def load(typeMapAndFilter: FilterWithTypeMap): Option[Builder] = { | ||
| buildSearchArgument( | ||
| typeMapAndFilter.typeMap, typeMapAndFilter.filter, SearchArgumentFactory.newBuilder()) | ||
| } | ||
| }) | ||
|
|
||
| private def getOrBuildSearchArgumentWithNewBuilder( | ||
|
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. Just a little question about is any possible to reuse code with https://github.com/apache/spark/pull/22313/files#diff-224b8cbedf286ecbfdd092d1e2e2f237R61?
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. @xuanyuanking . This already reuses For the cache value,
The only exception I made is |
||
| dataTypeMap: Map[String, DataType], | ||
| expression: Filter): Option[Builder] = { | ||
| // When `spark.sql.orc.cache.sarg.timeout` is 0, cache is disabled. | ||
| if (cacheExpireTimeout > 0) { | ||
| searchArgumentCache.get(FilterWithTypeMap(expression, dataTypeMap)) | ||
| } else { | ||
| buildSearchArgument(dataTypeMap, expression, SearchArgumentFactory.newBuilder()) | ||
|
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. When we set timeout to zero on the cache, the loaded element can be removed immediately. Maybe we don't need to check timeout like this and we can simplify the code.
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. Ya. It's possible. But, if we create a Guava loading cache and pass through all the cache management logic in Guava, it means a more overhead than this PR. In this PR, |
||
| } | ||
| } | ||
|
|
||
| def createFilter(schema: StructType, filters: Array[Filter]): Option[SearchArgument] = { | ||
| val dataTypeMap = schema.map(f => f.name -> f.dataType).toMap | ||
|
|
||
| // First, tries to convert each filter individually to see whether it's convertible, and then | ||
| // collect all convertible ones to build the final `SearchArgument`. | ||
| val convertibleFilters = for { | ||
| filter <- filters | ||
| _ <- buildSearchArgument(dataTypeMap, filter, SearchArgumentFactory.newBuilder()) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, filter) | ||
| } yield filter | ||
|
|
||
| for { | ||
| // Combines all convertible filters using `And` to produce a single conjunction | ||
| conjunction <- convertibleFilters.reduceOption(And) | ||
| conjunction <- convertibleFilters.reduceOption { (x, y) => | ||
| val newFilter = org.apache.spark.sql.sources.And(x, y) | ||
| if (cacheExpireTimeout > 0) { | ||
| // Build in a bottom-up manner | ||
| getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, newFilter) | ||
| } | ||
|
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. Why we need to cache all sub filters? Don't we just need to cache the final conjunction?
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. Final conjunction? All sub function results will be cached in the end. |
||
| newFilter | ||
| } | ||
| // Then tries to build a single ORC `SearchArgument` for the conjunction predicate | ||
| builder <- buildSearchArgument(dataTypeMap, conjunction, SearchArgumentFactory.newBuilder()) | ||
| } yield builder.build() | ||
|
|
@@ -77,8 +114,6 @@ private[orc] object OrcFilters extends Logging { | |
| dataTypeMap: Map[String, DataType], | ||
| expression: Filter, | ||
| builder: Builder): Option[Builder] = { | ||
| def newBuilder = SearchArgumentFactory.newBuilder() | ||
|
|
||
| def isSearchableType(dataType: DataType): Boolean = dataType match { | ||
| // Only the values in the Spark types below can be recognized by | ||
| // the `SearchArgumentImpl.BuilderImpl.boxLiteral()` method. | ||
|
|
@@ -98,23 +133,23 @@ private[orc] object OrcFilters extends Logging { | |
| // Pushing one side of AND down is only safe to do at the top level. | ||
| // You can see ParquetRelation's initializeLocalJobFunc method as an example. | ||
| for { | ||
| _ <- buildSearchArgument(dataTypeMap, left, newBuilder) | ||
| _ <- buildSearchArgument(dataTypeMap, right, newBuilder) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, left) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, right) | ||
| lhs <- buildSearchArgument(dataTypeMap, left, builder.startAnd()) | ||
| rhs <- buildSearchArgument(dataTypeMap, right, lhs) | ||
| } yield rhs.end() | ||
|
|
||
| case Or(left, right) => | ||
| for { | ||
| _ <- buildSearchArgument(dataTypeMap, left, newBuilder) | ||
| _ <- buildSearchArgument(dataTypeMap, right, newBuilder) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, left) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, right) | ||
| lhs <- buildSearchArgument(dataTypeMap, left, builder.startOr()) | ||
| rhs <- buildSearchArgument(dataTypeMap, right, lhs) | ||
| } yield rhs.end() | ||
|
|
||
| case Not(child) => | ||
| for { | ||
| _ <- buildSearchArgument(dataTypeMap, child, newBuilder) | ||
| _ <- getOrBuildSearchArgumentWithNewBuilder(dataTypeMap, child) | ||
| negate <- buildSearchArgument(dataTypeMap, child, builder.startNot()) | ||
| } yield negate.end() | ||
|
|
||
|
|
||
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.
This test looks tricky... It's a bad practice to assume some code will return in a certain time. Can we just add a microbenchmark for it?
Uh oh!
There was an error while loading. Please reload this page.
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.
ignore(...)instead oftest(...)here?FilterPushdownBenchmark?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'll choose (2), @cloud-fan .