Skip to main content

Query Performance

Append-table queries can avoid work at several layers. Start with the predicates your queries use, then choose a layout or index that helps eliminate irrelevant data.

Query patternOptimizationWhat it can skip
Filters on partition columnsPartition pruningOther partitions.
Equality or IN on all bucket-key columnsBucket pruningOther buckets in a bucketed table.
Selective filters on data columnsFile statistics, improved by clusteringFiles whose value ranges cannot match.
Predicates supported by a file indexBloom filter, bitmap, or range bitmap indexesIrrelevant data identified by the index.
Supported aggregates with sufficient metadataAggregate pushdownReading data rows to compute the aggregate.

These optimizations can be combined. Their effectiveness depends on the engine, the predicate, and the distribution of the stored values. Use the engine's EXPLAIN output and scan metrics to check which optimizations a query actually uses.

File Statistics and Clustering

Paimon stores column statistics in file metadata, subject to the table's statistics configuration. Min/max values let a reader reject files whose ranges do not overlap a query predicate. For example:

SELECT * FROM my_table
WHERE dt = '2026-09-10' AND product_id BETWEEN 100 AND 200;

The partition filter first limits the scan to one date. Within that partition, file statistics can exclude files whose product_id range falls outside the requested interval. If every file contains a broad mix of product IDs, min/max pruning will be less effective.

The same nine product IDs are spread across three files before clustering. After clustering, only one file overlaps product IDs 100 through 200.

In this example, every unsorted file overlaps the predicate, so all three must be read. After clustering, only the middle file overlaps it. File pruning selects candidate files; the reader still evaluates the predicate on their rows.

Incremental clustering sorts selected files by frequently filtered columns, which can make their value ranges more selective without rewriting every file on each run. Use sort compaction for an explicit sort rewrite. Clustering improves the physical layout; SQL result ordering still requires ORDER BY.

File Indexes

File indexes provide filtering beyond min/max statistics. Configure the relevant columns before writing indexed data:

IndexTable optionTypical use
Bloom filterfile-index.bloom-filter.columnsEquality lookups; false positives may still require reading data.
Bitmapfile-index.bitmap.columnsEquality and set-membership filtering.
Range bitmapfile-index.range-bitmap.columnsRange filtering.

Each indexed data file has associated index data. Small indexes can be embedded in the manifest; larger indexes are stored alongside the data files. Indexes add storage and write work, so select columns used by relevant queries.

Index Existing Files

Changing the table options affects subsequently written files; it does not add indexes to existing files. After setting the options, run rewrite_file_index to build indexes for existing data without rewriting the data files. The procedure still reads the data needed to construct the indexes.

For example, in Flink SQL, with the table in the default database:

ALTER TABLE my_table SET ('file-index.bloom-filter.columns' = 'product_id');
CALL sys.rewrite_file_index(`table` => 'default.my_table');

The equivalent Spark SQL is:

ALTER TABLE my_table SET TBLPROPERTIES ('file-index.bloom-filter.columns' = 'product_id');
CALL sys.rewrite_file_index(table => 'default.my_table');

Use the actual database name for your table. To limit the rewrite, Flink accepts a partitions argument and Spark accepts a partition predicate in where. See Flink procedures and Spark procedures for those engine-specific arguments.

Check Index Coverage

Query the file indexes system table to see which data files have the configured indexes. For example, in Spark SQL:

SELECT column_name, index_type, storage_type,
COUNT(DISTINCT file_path) AS indexed_file_count
FROM `my_table$file_indexes`
GROUP BY column_name, index_type, storage_type
ORDER BY column_name, index_type, storage_type;

Each row in the system table describes one column and index type in one data file. storage_type is EMBEDDED for index data stored in metadata and FILE for an external index file. Compare the indexed files with my_table$files to check coverage; the presence of an index alone does not show that a query used it. Check the query's predicates, plan, and scan metrics as well.

Aggregate Pushdown

Supported aggregate queries can use table metadata. Using the partitioned table from the overview:

SELECT COUNT(*) FROM my_table WHERE dt = '2026-09-10';

Spark can also use column statistics for supported MIN and MAX queries:

SELECT MIN(price), MAX(sales) FROM my_table WHERE dt = '2026-09-10';

Keep the required statistics available through metadata.stats-mode and any column-specific statistics settings. Pushdown depends on the query and available metadata: a filter that needs row-by-row evaluation can prevent a metadata-only aggregate. These examples use a partition predicate so that complete files can be selected.

Spark can also use statistics to reduce the files scanned for a top-N query:

SELECT * FROM my_table ORDER BY price LIMIT 1;

Top-N pruning does not imply that the full result can be produced from metadata alone. Check the query plan rather than assuming every aggregate or ORDER BY ... LIMIT query avoids reading data files.