-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
[HUDI-2788] Fixing issues w/ Z-order Layout Optimization #4026
Changes from all commits
2ac677a
19afab3
7508f81
6891171
4d52e23
0373aed
4627e14
c2a7b62
9e84598
b9e7c0c
f578a4a
7211f8b
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 |
---|---|---|
|
@@ -160,41 +160,92 @@ case class HoodieFileIndex( | |
spark.sessionState.conf.getConfString(DataSourceReadOptions.ENABLE_DATA_SKIPPING.key(), "false")).toBoolean | ||
} | ||
|
||
private def filterFilesByDataSkippingIndex(dataFilters: Seq[Expression]): Set[String] = { | ||
var allFiles: Set[String] = Set.empty | ||
var candidateFiles: Set[String] = Set.empty | ||
/** | ||
* Computes pruned list of candidate base-files' names based on provided list of {@link dataFilters} | ||
* conditions, by leveraging custom Z-order index (Z-index) bearing "min", "max", "num_nulls" statistic | ||
* for all clustered columns | ||
* | ||
* NOTE: This method has to return complete set of candidate files, since only provided candidates will | ||
* ultimately be scanned as part of query execution. Hence, this method has to maintain the | ||
* invariant of conservatively including every base-file's name, that is NOT referenced in its index. | ||
* | ||
* @param dataFilters list of original data filters passed down from querying engine | ||
* @return list of pruned (data-skipped) candidate base-files' names | ||
*/ | ||
private def lookupCandidateFilesNamesInZIndex(dataFilters: Seq[Expression]): Option[Set[String]] = { | ||
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. rename method 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. Are you suggesting to drop Z-index in its name? Here we actually read from Z-index (folder name of which is also |
||
val indexPath = metaClient.getZindexPath | ||
val fs = metaClient.getFs | ||
if (fs.exists(new Path(indexPath)) && dataFilters.nonEmpty) { | ||
// try to load latest index table from index path | ||
val candidateIndexTables = fs.listStatus(new Path(indexPath)).filter(_.isDirectory) | ||
.map(_.getPath.getName).filter(f => completedCommits.contains(f)).sortBy(x => x) | ||
if (candidateIndexTables.nonEmpty) { | ||
val dataFrameOpt = try { | ||
Some(spark.read.load(new Path(indexPath, candidateIndexTables.last).toString)) | ||
} catch { | ||
case _: Throwable => | ||
logError("missing index skip data-skipping") | ||
None | ||
} | ||
|
||
if (dataFrameOpt.isDefined) { | ||
val indexSchema = dataFrameOpt.get.schema | ||
val indexFiles = DataSkippingUtils.getIndexFiles(spark.sparkContext.hadoopConfiguration, new Path(indexPath, candidateIndexTables.last).toString) | ||
val indexFilter = dataFilters.map(DataSkippingUtils.createZindexFilter(_, indexSchema)).reduce(And) | ||
logInfo(s"index filter condition: $indexFilter") | ||
dataFrameOpt.get.persist() | ||
if (indexFiles.size <= 4) { | ||
allFiles = DataSkippingUtils.readParquetFile(spark, indexFiles) | ||
} else { | ||
allFiles = dataFrameOpt.get.select("file").collect().map(_.getString(0)).toSet | ||
} | ||
candidateFiles = dataFrameOpt.get.filter(new Column(indexFilter)).select("file").collect().map(_.getString(0)).toSet | ||
dataFrameOpt.get.unpersist() | ||
} | ||
} | ||
if (!enableDataSkipping() || !fs.exists(new Path(indexPath)) || dataFilters.isEmpty) { | ||
// scalastyle:off return | ||
return Option.empty | ||
// scalastyle:on return | ||
} | ||
|
||
// Collect all index tables present in `.zindex` folder | ||
val candidateIndexTables = | ||
fs.listStatus(new Path(indexPath)) | ||
.filter(_.isDirectory) | ||
.map(_.getPath.getName) | ||
.filter(f => completedCommits.contains(f)) | ||
.sortBy(x => x) | ||
|
||
if (candidateIndexTables.isEmpty) { | ||
// scalastyle:off return | ||
return Option.empty | ||
// scalastyle:on return | ||
} | ||
|
||
val dataFrameOpt = try { | ||
Some(spark.read.load(new Path(indexPath, candidateIndexTables.last).toString)) | ||
} catch { | ||
case t: Throwable => | ||
logError("Failed to read Z-index; skipping", t) | ||
None | ||
} | ||
allFiles -- candidateFiles | ||
|
||
dataFrameOpt.map(df => { | ||
val indexSchema = df.schema | ||
val indexFilter = | ||
dataFilters.map(DataSkippingUtils.createZIndexLookupFilter(_, indexSchema)) | ||
.reduce(And) | ||
|
||
logInfo(s"Index filter condition: $indexFilter") | ||
|
||
df.persist() | ||
|
||
val allIndexedFileNames = | ||
df.select("file") | ||
.collect() | ||
.map(_.getString(0)) | ||
.toSet | ||
|
||
val prunedCandidateFileNames = | ||
df.filter(new Column(indexFilter)) | ||
.select("file") | ||
.collect() | ||
.map(_.getString(0)) | ||
.toSet | ||
|
||
df.unpersist() | ||
|
||
// NOTE: Z-index isn't guaranteed to have complete set of statistics for every | ||
// base-file: since it's bound to clustering, which could occur asynchronously | ||
// at arbitrary point in time, and is not likely to touching all of the base files. | ||
// | ||
// To close that gap, we manually compute the difference b/w all indexed (Z-index) | ||
// files and all outstanding base-files, and make sure that all base files not | ||
// represented w/in Z-index are included in the output of this method | ||
val notIndexedFileNames = | ||
lookupFileNamesMissingFromIndex(allIndexedFileNames) | ||
|
||
prunedCandidateFileNames ++ notIndexedFileNames | ||
}) | ||
} | ||
|
||
private def lookupFileNamesMissingFromIndex(allIndexedFileNames: Set[String]) = { | ||
val allBaseFileNames = allFiles.map(f => f.getPath.getName).toSet | ||
allBaseFileNames -- allIndexedFileNames | ||
} | ||
|
||
/** | ||
|
@@ -206,18 +257,22 @@ case class HoodieFileIndex( | |
*/ | ||
override def listFiles(partitionFilters: Seq[Expression], | ||
dataFilters: Seq[Expression]): Seq[PartitionDirectory] = { | ||
// try to load filterFiles from index | ||
val filterFiles: Set[String] = if (enableDataSkipping()) { | ||
filterFilesByDataSkippingIndex(dataFilters) | ||
} else { | ||
Set.empty | ||
} | ||
// Look up candidate files names in the Z-index, if all of the following conditions are true | ||
// - Data-skipping is enabled | ||
// - Z-index is present | ||
// - List of predicates (filters) is present | ||
val candidateFilesNamesOpt: Option[Set[String]] = lookupCandidateFilesNamesInZIndex(dataFilters) | ||
|
||
logDebug(s"Overlapping candidate files (from Z-index): ${candidateFilesNamesOpt.getOrElse(Set.empty)}") | ||
|
||
if (queryAsNonePartitionedTable) { // Read as Non-Partitioned table. | ||
val candidateFiles = if (!filterFiles.isEmpty) { | ||
allFiles.filterNot(fileStatus => filterFiles.contains(fileStatus.getPath.getName)) | ||
} else { | ||
allFiles | ||
} | ||
// Filter in candidate files based on the Z-index lookup | ||
val candidateFiles = | ||
allFiles.filter(fileStatus => | ||
// NOTE: This predicate is true when {@code Option} is empty | ||
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 by default - data skipping being off, this is how none of the files in 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. Correct |
||
candidateFilesNamesOpt.forall(_.contains(fileStatus.getPath.getName)) | ||
) | ||
|
||
logInfo(s"Total files : ${allFiles.size}," + | ||
s" candidate files after data skipping: ${candidateFiles.size} " + | ||
s" skipping percent ${if (allFiles.length != 0) (allFiles.size - candidateFiles.size) / allFiles.size.toDouble else 0}") | ||
|
@@ -236,11 +291,13 @@ case class HoodieFileIndex( | |
null | ||
} | ||
}).filterNot(_ == null) | ||
val candidateFiles = if (!filterFiles.isEmpty) { | ||
baseFileStatuses.filterNot(fileStatus => filterFiles.contains(fileStatus.getPath.getName)) | ||
} else { | ||
baseFileStatuses | ||
} | ||
|
||
// Filter in candidate files based on the Z-index lookup | ||
val candidateFiles = | ||
baseFileStatuses.filter(fileStatus => | ||
// NOTE: This predicate is true when {@code Option} is empty | ||
candidateFilesNamesOpt.forall(_.contains(fileStatus.getPath.getName))) | ||
|
||
totalFileSize += baseFileStatuses.size | ||
candidateFileSize += candidateFiles.size | ||
PartitionDirectory(partition.values, candidateFiles) | ||
|
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.
add error message to the exception to get some specific context?
understand that this code existed prior.
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 will actually tackle this on as part of #4060