|
| 1 | +<!-- |
| 2 | +Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +or more contributor license agreements. See the NOTICE file |
| 4 | +distributed with this work for additional information |
| 5 | +regarding copyright ownership. The ASF licenses this file |
| 6 | +to you under the Apache License, Version 2.0 (the |
| 7 | +"License"); you may not use this file except in compliance |
| 8 | +with the License. You may obtain a copy of the License at |
| 9 | +
|
| 10 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +
|
| 12 | +Unless required by applicable law or agreed to in writing, |
| 13 | +software distributed under the License is distributed on an |
| 14 | +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +KIND, either express or implied. See the License for the |
| 16 | +specific language governing permissions and limitations |
| 17 | +under the License. |
| 18 | +--> |
| 19 | + |
| 20 | +# Paimon Rust at Native Speed |
| 21 | + |
| 22 | +## TPC-DS SF100 read performance with DataFusion |
| 23 | + |
| 24 | +!!! warning "Benchmark disclosure" |
| 25 | + |
| 26 | + This is a TPC-DS-derived non-TPC benchmark. It is not an official TPC |
| 27 | + result and must not be compared with official TPC results. |
| 28 | + |
| 29 | +This experiment compares Paimon Rust with DataFusion's native Parquet reader |
| 30 | +using the same DataFusion 54 SQL, planner, execution operators, query files, |
| 31 | +runtime limits, and local SSD. It measures 24 unpartitioned, append-only |
| 32 | +Paimon tables imported from the SF100 Parquet source. |
| 33 | + |
| 34 | +## Result |
| 35 | + |
| 36 | +Each query received one warmup followed by three measured iterations. The |
| 37 | +table reports the median of three workload totals, where each workload total |
| 38 | +is the sum of `execution_ms` for the same measured-iteration index across the |
| 39 | +96 queries that completed in every measured iteration for both sources. |
| 40 | + |
| 41 | +| Source | Median workload execution time | Three-run range | |
| 42 | +| --- | ---: | ---: | |
| 43 | +| Paimon Rust | 317.61 s | 314.64–317.76 s | |
| 44 | +| Native Parquet | 290.53 s | 289.69–290.75 s | |
| 45 | + |
| 46 | +Paimon Rust completes the workload within 9.3% of DataFusion's native Parquet |
| 47 | +reader. For a table-format reader that also interprets Paimon metadata and |
| 48 | +preserves Paimon's correctness boundaries, this is near-native performance. |
| 49 | + |
| 50 | +The two layouts do not use the same compression codec. The source Parquet |
| 51 | +files use SNAPPY, while the Paimon rewrite uses the higher-compression ZSTD |
| 52 | +codec. Paimon's data files occupy 27.98 GB instead of 36.45 GB, a 23.25% |
| 53 | +reduction in physical storage. ZSTD saves storage and read I/O at the cost of |
| 54 | +more CPU-intensive decompression than SNAPPY, so the runtime comparison |
| 55 | +includes that tradeoff; it does not isolate Paimon metadata overhead from |
| 56 | +codec cost. |
| 57 | + |
| 58 | +At the query level, Paimon has the lower median execution time on |
| 59 | +23 of the 96 comparable queries. For the 14 queries whose native-Parquet |
| 60 | +median is at least five seconds, the sum of per-query medians is 184.82 s for |
| 61 | +Paimon and 179.29 s for Parquet, a gap of 3.1%. The smaller gap on the long |
| 62 | +queries shows closer alignment as query runtime grows. |
| 63 | + |
| 64 | +The detailed per-iteration JSON reports are not committed with this article |
| 65 | +to keep the documentation lightweight. During validation, all 288 measured |
| 66 | +output-row counts matched between the two sources, and the 96 normal queries |
| 67 | +completed without warmup failures, execution errors, or spill events on |
| 68 | +either source. The table above summarizes the validated reports using the |
| 69 | +aggregation method described in this section. |
| 70 | + |
| 71 | +### Resource-limit queries |
| 72 | + |
| 73 | +Q67, Q78, and Q97 are excluded from the workload aggregate because they hit |
| 74 | +the same 32 GiB memory-pool limit on both sources. Repeating an out-of-memory |
| 75 | +query in the same DataFusion session can leave the benchmark runtime unable |
| 76 | +to make progress, so these three queries were run once in separate focused |
| 77 | +processes; the other 96 queries use the full 1+3 protocol. |
| 78 | + |
| 79 | +## Scope |
| 80 | + |
| 81 | +The result applies to the append-only SF100 read path tested here. The loader |
| 82 | +creates unpartitioned append-only Paimon tables, so this experiment does |
| 83 | +**not** measure the runtime cost of primary-key merging, schema evolution, |
| 84 | +deletion vectors, row lineage, or non-Parquet file formats. |
| 85 | + |
| 86 | +Paimon Rust supports or is developing those broader table-format capabilities, |
| 87 | +and its read path includes correctness safeguards for them. They should be |
| 88 | +evaluated with dedicated workloads before making performance claims about |
| 89 | +their overhead. |
| 90 | + |
| 91 | +This is also an end-to-end source comparison rather than a catalog-overhead |
| 92 | +microbenchmark. Importing the source into Paimon rewrites the physical file |
| 93 | +layout: the original dataset contains 24 Parquet files and the Paimon copy |
| 94 | +contains 125 Parquet data files. It also changes compression from SNAPPY to |
| 95 | +ZSTD, so this is intentionally not a codec-matched microbenchmark. |
| 96 | + |
| 97 | +## Engineering behind the read path |
| 98 | + |
| 99 | +The SF100 result follows a set of changes that align Paimon scans with |
| 100 | +DataFusion's execution model: |
| 101 | + |
| 102 | +- Paimon string and binary columns map to Arrow `Utf8View` and `BinaryView` at |
| 103 | + the DataFusion boundary. |
| 104 | +- `read.batch-size` reaches raw readers, primary-key readers, and |
| 105 | + data-evolution readers. |
| 106 | +- row counts, null counts, column bounds, and compressed sizes are exposed |
| 107 | + with explicit precision rather than being overstated as exact. |
| 108 | +- Paimon predicates provide conservative partition, file, and row-group |
| 109 | + pruning where they can represent the expression. |
| 110 | +- a format-neutral Arrow row-filter interface carries supported DataFusion |
| 111 | + expressions into the Parquet decoder for late materialization; the parent |
| 112 | + DataFusion filter is retained as the exact correctness filter. |
| 113 | +- primary-key, data-evolution, and `_ROW_ID` paths use conservative boundaries |
| 114 | + where pushdown could change merge results or physical row positions. |
| 115 | + |
| 116 | +Only the append-only Parquet-backed read path is benchmarked by this report; |
| 117 | +the last point describes a correctness capability, not a measured result. |
| 118 | + |
| 119 | +## Multi-engine direction |
| 120 | + |
| 121 | +### DataFusion |
| 122 | + |
| 123 | +DataFusion is the first complete SQL integration, from `SQLContext` and |
| 124 | +`TableProvider` through statistics and physical runtime-filter pruning. |
| 125 | + |
| 126 | +### StarRocks |
| 127 | + |
| 128 | +Work continues on the Paimon connector, row lineage, and vector-search path. |
| 129 | +Paimon Rust's native reader and FFI provide a foundation for deeper |
| 130 | +integration. |
| 131 | + |
| 132 | +### Apache Doris |
| 133 | + |
| 134 | +The community Paimon write architecture reserves a Rust FFI backend, providing |
| 135 | +a possible path toward removing the JVM bridge and reducing data-exchange |
| 136 | +costs. |
| 137 | + |
| 138 | +### Milvus |
| 139 | + |
| 140 | +Integration work is also underway for Milvus. Paimon Rust already exposes a |
| 141 | +materialized vector-search API through its C FFI: callers can select a vector |
| 142 | +column, provide a query vector, filter and limit the search, pass index |
| 143 | +options, and consume the result as streaming Arrow record batches. This gives |
| 144 | +Milvus a native path to Paimon-managed vector data without requiring a JVM |
| 145 | +bridge. The integration is an ecosystem direction and is not measured by this |
| 146 | +TPC-DS report. |
| 147 | + |
| 148 | +## Reproduction |
| 149 | + |
| 150 | +### Revisions and software |
| 151 | + |
| 152 | +| Component | Version or revision | |
| 153 | +| --- | --- | |
| 154 | +| paimon-rust benchmark binary | `7afff74a20c7dee2fe7dc1f862ce7b8b74bf6cd2` | |
| 155 | +| Release binary SHA-256 | `1ebda8878fd2b7f4880a3fa2afaa14520e4cbc40a16352315b6a63b8aee643e7` | |
| 156 | +| DataFusion | `54.0.0` | |
| 157 | +| paimon-rust crate | `0.3.0` development build | |
| 158 | +| Query files | `delta-io/delta-rs` at `0f68868d1dbbe77fa4e99c96df49ae121f8974e4` | |
| 159 | +| Data generator | DuckDB `v1.5.0` (`3a3967aa81`) TPC-DS extension | |
| 160 | + |
| 161 | +The benchmark binary was rebuilt from a clean Cargo target directory: |
| 162 | + |
| 163 | +```bash |
| 164 | +cargo clean |
| 165 | +cargo build --release -p paimon-tpcds-bench |
| 166 | +``` |
| 167 | + |
| 168 | +### Dataset and physical layout |
| 169 | + |
| 170 | +| Item | Value | |
| 171 | +| --- | ---: | |
| 172 | +| Scale factor | 100 | |
| 173 | +| Source Parquet files | 24 | |
| 174 | +| Source Parquet compression | SNAPPY | |
| 175 | +| Source Parquet physical bytes | 36,449,466,908 | |
| 176 | +| Paimon data files | 125 | |
| 177 | +| Paimon data-file compression | ZSTD | |
| 178 | +| Paimon data-file physical bytes | 27,975,615,232 | |
| 179 | +| Paimon physical data-file reduction | 8,473,851,676 bytes / 23.25% | |
| 180 | +| All files in Paimon warehouse | 269 files / 27,975,785,742 bytes | |
| 181 | +| Paimon table mode | unpartitioned, append-only | |
| 182 | + |
| 183 | +The source data was generated and exported with this command: |
| 184 | + |
| 185 | +```bash |
| 186 | +TPCDS_ROOT=/path/to/tpcds-benchmark/sf100 |
| 187 | +DUCKDB_SPILL_ROOT=/path/to/tpcds-benchmark/spill/duckdb-sf100 |
| 188 | + |
| 189 | +./duckdb "${TPCDS_ROOT}/tpcds.duckdb" -c \ |
| 190 | + "SET threads=12; \ |
| 191 | + SET memory_limit='32GB'; \ |
| 192 | + SET temp_directory='${DUCKDB_SPILL_ROOT}'; \ |
| 193 | + CALL dsdgen(sf=100); \ |
| 194 | + EXPORT DATABASE '${TPCDS_ROOT}/parquet' \ |
| 195 | + (FORMAT PARQUET);" |
| 196 | +``` |
| 197 | + |
| 198 | +The Paimon copy was loaded with one writer partition. Query execution still |
| 199 | +used 12 target partitions. |
| 200 | + |
| 201 | +```bash |
| 202 | +target/release/paimon-tpcds-bench load \ |
| 203 | + --data "${TPCDS_ROOT}/parquet" \ |
| 204 | + --warehouse "${TPCDS_ROOT}/paimon-layout-target1-full" \ |
| 205 | + --database tpcds \ |
| 206 | + --if-exists error \ |
| 207 | + --target-partitions 1 \ |
| 208 | + --memory-limit-gib 32 |
| 209 | +``` |
| 210 | + |
| 211 | +### Machine and runtime |
| 212 | + |
| 213 | +| Item | Value | |
| 214 | +| --- | --- | |
| 215 | +| Operating system | macOS 26.3.1, build 25D2128 | |
| 216 | +| CPU | Apple M4 Pro, 12 physical cores | |
| 217 | +| Memory | 48 GiB physical / 32 GiB DataFusion memory pool | |
| 218 | +| Storage | Apple SSD AP1024Z, local APFS/NVMe | |
| 219 | +| DataFusion target partitions | 12 | |
| 220 | +| Spill directory | none for query runs | |
| 221 | +| Cache protocol | one warmup per query, then three measured iterations | |
| 222 | +| Source order | Alternated by query batch; see below | |
| 223 | + |
| 224 | +The OS page cache was not evicted, so these are warm-cache measurements. To |
| 225 | +reduce systematic source-order bias, the first source alternates by query |
| 226 | +batch: |
| 227 | + |
| 228 | +| Batch | Queries | Execution order | |
| 229 | +| --- | --- | --- | |
| 230 | +| 01 | Q1–Q10 | Paimon, Parquet | |
| 231 | +| 02 | Q11–Q20 | Parquet, Paimon | |
| 232 | +| 03 | Q21–Q30 | Paimon, Parquet | |
| 233 | +| 04 | Q31–Q40 | Parquet, Paimon | |
| 234 | +| 05 | Q41–Q50 | Paimon, Parquet | |
| 235 | +| 06 | Q51–Q60 | Parquet, Paimon | |
| 236 | +| 07 | Q61–Q66 | Paimon, Parquet | |
| 237 | +| 08 | Q68–Q77 | Parquet, Paimon | |
| 238 | +| 09 | Q79–Q88 | Paimon, Parquet | |
| 239 | +| 10 | Q89–Q96 | Parquet, Paimon | |
| 240 | +| 11 | Q98–Q99 | Paimon, Parquet | |
| 241 | + |
| 242 | +### Measured commands |
| 243 | + |
| 244 | +The following functions reproduce the arguments used for every normal batch; |
| 245 | +set the two root paths for the local environment: |
| 246 | + |
| 247 | +```bash |
| 248 | +TPCDS_ROOT=/path/to/tpcds-benchmark/sf100 |
| 249 | +QUERY_ROOT=/path/to/delta-rs/crates/benchmarks/queries/tpcds |
| 250 | + |
| 251 | +run_paimon() { |
| 252 | + target/release/paimon-tpcds-bench run \ |
| 253 | + --source paimon \ |
| 254 | + --warehouse "${TPCDS_ROOT}/paimon-layout-target1-full" \ |
| 255 | + --database tpcds \ |
| 256 | + --queries "${QUERY_ROOT}" \ |
| 257 | + --output "paimon-publication-batch-$1.json" \ |
| 258 | + --query "$2" \ |
| 259 | + --warmup 1 \ |
| 260 | + --iterations 3 \ |
| 261 | + --target-partitions 12 \ |
| 262 | + --memory-limit-gib 32 |
| 263 | +} |
| 264 | + |
| 265 | +run_parquet() { |
| 266 | + target/release/paimon-tpcds-bench run \ |
| 267 | + --source parquet \ |
| 268 | + --data "${TPCDS_ROOT}/parquet" \ |
| 269 | + --warehouse "${TPCDS_ROOT}/parquet-catalog-review" \ |
| 270 | + --database tpcds \ |
| 271 | + --queries "${QUERY_ROOT}" \ |
| 272 | + --output "parquet-publication-batch-$1.json" \ |
| 273 | + --query "$2" \ |
| 274 | + --warmup 1 \ |
| 275 | + --iterations 3 \ |
| 276 | + --target-partitions 12 \ |
| 277 | + --parquet-pushdown-filters \ |
| 278 | + --memory-limit-gib 32 |
| 279 | +} |
| 280 | + |
| 281 | +run_paimon 01 1-10; run_parquet 01 1-10 |
| 282 | +run_parquet 02 11-20; run_paimon 02 11-20 |
| 283 | +run_paimon 03 21-30; run_parquet 03 21-30 |
| 284 | +run_parquet 04 31-40; run_paimon 04 31-40 |
| 285 | +run_paimon 05 41-50; run_parquet 05 41-50 |
| 286 | +run_parquet 06 51-60; run_paimon 06 51-60 |
| 287 | +run_paimon 07 61-66; run_parquet 07 61-66 |
| 288 | +run_parquet 08 68-77; run_paimon 08 68-77 |
| 289 | +run_paimon 09 79-88; run_parquet 09 79-88 |
| 290 | +run_parquet 10 89-96; run_paimon 10 89-96 |
| 291 | +run_paimon 11 98-99; run_parquet 11 98-99 |
| 292 | +``` |
| 293 | + |
| 294 | +Q67, Q78, and Q97 were each run in a fresh process with the same source |
| 295 | +arguments and runtime settings, replacing the batch arguments with |
| 296 | +`--query <query> --warmup 0 --iterations 1`. Both sources hit the 32 GiB |
| 297 | +memory-pool limit for all three queries. |
| 298 | + |
| 299 | +## Interpretation limits |
| 300 | + |
| 301 | +This result is one machine, one data layout, and a warm-cache protocol. It is |
| 302 | +useful as a reproducible engineering checkpoint, not a universal performance |
| 303 | +ranking. A broader study should repeat complete benchmark processes, separate |
| 304 | +cold and warm cache states, and include object storage and multi-node |
| 305 | +environments. |
0 commit comments