Skip to content

[lake/paimon] Introduce scan-based lake table lookuper - #4124

Open
zuston wants to merge 7 commits into
apache:mainfrom
zuston:scanBasedLookuper
Open

[lake/paimon] Introduce scan-based lake table lookuper#4124
zuston wants to merge 7 commits into
apache:mainfrom
zuston:scanBasedLookuper

Conversation

@zuston

@zuston zuston commented Aug 27, 2026

Copy link
Copy Markdown
Member

Purpose

this is the sub-task for #3631

The SST-based Paimon lake table lookuper introduced in #3632 caches Paimon SST files on TabletServer local disks. This approach can also benefit from future Fluss-level optimizations, such as a global index.

However, for highly sparse lookup workloads, the cache hit ratio can be very low, resulting in frequent file downloads and additional I/O overhead. Scan-based lookup can reduce this overhead by using predicate pushdown to read only the relevant Parquet row groups instead of entire files. This is particularly beneficial when row groups are small relative to the overall Parquet file size.

Brief change log

  1. introduce the scan-based lake table lookuper

Tests

API and Format

Documentation

@zuston
zuston force-pushed the scanBasedLookuper branch from b412192 to 0f3bdb9 Compare August 30, 2026 15:11

@fresh-borzoni fresh-borzoni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zuston Thank you for the PR, left a couple of comments, PTAL

Also this and #4113 both rework the lookuper SPI in ways that conflict I believe

partitionKeyExtractor::partition);
ReadBuilder readBuilder =
table.newReadBuilder()
.withFilter(predicates)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think withFilter defeats the withBucket below it as Paimon derives the bucket itself when the
predicate covers the bucket-key, and if it disagrees with context.bucketId() we return null instead
of the row.

Wrote a few keys into one bucket and looked them up there: SCAN misses some, SST finds all.
Am I reading this right?

try {
FileStoreTable table = table();
// Paimon tables contain mutable lazy store state; isolate it per lookup.
return scanLookup(table.copy(table.schema()), key, context);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-plans the snapshot on every key, and HistoricalLakeLookupManager calls lookup() per key.
Noticeably slower than SST. Worth hoisting the setup out?

@zuston zuston Sep 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out.

I don't think an additional plan cache would help here. The catalog and table are initialized only once, and Paimon's CachingCatalog already caches snapshot and manifest metadata; table.copy() preserves those caches. The remaining plan is key-specific because each lookup has a different primary-key predicate, so it cannot be reused safely and caching it could also pin an old snapshot. The per-key planning is the expected trade-off of SCAN mode for avoiding local SST downloads.

Future historical-partition writes should not depend on the SST-based lookuper, so making the lookup implementation pluggable is appropriate. That said, the SST mode and its local cache remain important and deserve further investment. Potential optimizations include remote SST lookup files and global indexes.

To share our initial production benchmark results for scan-based lookuper:

  1. With the default SST-based lookuper under a highly sparse lookup workload, building local SST files requires reading an entire 128 MB Parquet file. On our relatively slow HDFS cluster, this became the main bottleneck, with P50 lookup latency reaching approximately 5 seconds. This assumes the concurrent lookup issue has already been addressed (however this haven't. and it also will result in the tablet potential OOM).
  2. After reducing the Parquet row-group size to approximately 2 MB while keeping the file size at 128 MB, scan-based lookup can push down the primary-key predicate and read only the relevant row group. This reduced P50 lookup latency to approximately 2 seconds.
    In this workload, the reduction in remote I/O outweighs the additional per-key planning overhead.

@zuston zuston Sep 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-plans the snapshot on every key, and HistoricalLakeLookupManager calls lookup() per key.

Nice catch, this is necessary to make it parallelize. let me follow up in another PR.

And one possible way to use the multi filter that could cover multi lookup key by using the OR-filer scan instead of using concurrency in the invoking side.


/** Lookup strategy for historical partitions stored in lake storage. */
public static final ConfigOption<LookupMode> TABLE_DATALAKE_HISTORICAL_PARTITION_LOOKUP_MODE =
key("table.datalake.historical-partition.lookup-mode")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding this to options.md/ddl.md/lookups.md too? historical-partition.enabled is in all three.

lookupContext(schema, "20240101", 1, SCHEMA_ID)))
.isNull();
assertThat(lookuper.lookup(compactedKey(schema, 1, "20240101"), context)).isNull();
if (lookupMode == LookupMode.SST) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why this one is SST-only, what does SCAN return for a compacted-encoded key?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the KV format v1 with compacted format, this encoding style is illegal for the paimon side. anyway, the V2 is the compatible way, let me remove this assert logic.

}

/** Mode used to look up historical data in lake storage. */
enum LookupMode {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this live in org.apache.fluss.metadata, next to DataLakeFormat and KvFormat?

@zuston

zuston commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Also this and #4113 both rework the lookuper SPI in ways that conflict I believe

Yes, and I think we should prioritize this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants