[Experiment] Antalya 26.6: Parquet v3 constant column skip - #2181
[Experiment] Antalya 26.6: Parquet v3 constant column skip#2181UnamedRus wants to merge 65 commits into
Conversation
When a Parquet column chunk provably holds a single value in every row - its min/max statistics have `min_value == max_value`, no nulls, and the value is exact - the reader no longer fetches or decodes that chunk's data pages. Instead `detectConstantColumn` records the value and `decodePrimitiveColumn`/`formOutputColumn` materialize it directly. This skips the offset index, column index, dictionary page and data page reads for such chunks (the row group already passed the key condition via its `min == max` hyperrectangle), which is a byte-level I/O win for wide constant columns, plus the decode/decompression CPU. Restricted to flat, top-level primitive columns with no element nulls. For `BYTE_ARRAY`/`FIXED_LEN_BYTE_ARRAY` the writer may truncate min/max, so `min == max` is trusted only when `is_min_value_exact` and `is_max_value_exact` are both set; fixed-width numeric types are never truncated. The value is taken from `PageDecoderInfo::decodeField`, which yields it in the final output (post-cast) domain - e.g. `DateTime` written as `TIMESTAMP_MILLIS` decodes to seconds, not the raw millisecond `decoded_type`. So the constant is materialized directly in the output type, bypassing the `decoded_type` column and `castColumn`. Gated by the new setting `input_format_parquet_use_constant_column_optimization` (default on). A new `ParquetConstantColumnChunks` ProfileEvent counts materialized chunks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two independent scheduling fixes for the v3 reader on large remote files. A. Split the per-stage memory budget from the thread budget. Previously `Stage::memory_target_fraction` drove both a stage's memory watermark and its thread count (`getLimitsPerReader`), and every active stage defaulted to an equal 0.2 share. So `ColumnData` - the one stage that decodes large row groups - was capped at 0.2 of both memory and threads, and on a single large cross-region file only ~2 row groups were read/decoded ahead, leaving the link idle. `Stage` now has a separate `thread_target_fraction`; `getLimitsPerReader` takes both. `ColumnData` gets the lion's share of memory and a larger thread share, while the small, latency-bound index/bloom reads keep enough threads for parallel small reads. The split is static and wants a perf run to tune; it still couples prefetch depth to decode concurrency (decoupling those is a separate, larger change). B. Charge decoded output by its actual footprint. The memory reserved for a subchunk before decoding was an estimate (`estimateColumnMemoryBytesPerRow`) that undershoots for long strings and skewed data, so real RAM overshot the watermark and the overshoot grew with decode-ahead depth. After `decodePrimitiveColumn`, reconcile the `MemoryUsageToken` up to the real `allocatedBytes` of the decoded column, offsets and null maps (grow-only, fail-closed), so the stage counter is honest and the scheduler stops decoding ahead before RAM exceeds the cap. Both are internal scheduling/accounting changes with no query-result change. NOT YET BUILT OR PERF-TESTED; the stage split numbers are a starting point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the placement of the honest-accounting reconciliation from the previous commit. It ran in `runTask` after `decodePrimitiveColumn` returned, but for the common single-primitive column the function's tail already `std::move`s `subchunk.column` into the output via `formOutputColumn`, so the measurement saw a null column and was a no-op. Thread `MemoryUsageDiff &` into `decodePrimitiveColumn` and reconcile the `MemoryUsageToken` up to the real `allocatedBytes` of `subchunk.column` (plus array offsets and the group null map) at the end of decoding, just before the bookkeeping that may move the column out. Grow-only, fail-closed. Matches the earlier `parquet/honest-memory-cap` prototype. Still not built or perf-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two gate bugs made detectConstantColumn reject every column, so the optimization never triggered on real files. Found by building and testing against CH-written Parquet. 1. Null gate. The CH writer omits `null_count` for physically non-nullable (`REQUIRED`) columns, but the gate required `null_count` to be present and zero, rejecting every non-nullable column. A `REQUIRED` column can have no nulls regardless of the statistic, so only require zero `null_count` when the column is physically nullable (max definition level > 0). 2. Structural gate. `levels[0]` is a synthetic root sentinel with `is_array = true`, so a flat `REQUIRED` column's `levels.back()` IS that root (`size() == 1`, `is_array == true`) and a `Nullable` column has `size() == 2`. The old `levels.size() == 1 && !levels.back().is_array` test therefore rejected exactly the flat columns it meant to accept. Replace with the correct flatness signals (`levels.back().rep == 0` and `max_array_def == 0`), keep the `group_nullable` exclusion, and require the output column to be primitive (top-level, not a Tuple/Map/Array leaf) via `output_columns`. Verified on a CH-written file (1M rows, constant Int64/String/DateTime/ Nullable columns): the optimization now fires (`ParquetConstantColumnChunks` = columns x row groups), results are correct (including the TIMESTAMP_MILLIS -> DateTime seconds path), reading the constant columns drops `ParquetPrefetcherReadRandomRead` 10 -> 1 and `ParquetDecodingTasks` 96 -> 64, and a varying column is correctly not treated as constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tch stage) Adds a ColumnDataPrefetch pipeline stage between OffsetIndex and ColumnData. It runs `determinePagesToPrefetch` and issues the compressed data-page reads (`startPrefetch`) but does not decode; ColumnData then only decodes from the already-in-flight buffers. The two stages have separate memory budgets: ColumnDataPrefetch gets a large share (compressed row groups are cheap, ~tens of MB) so many row groups can have their reads outstanding, while ColumnData gets a bounded share (decoded row groups are large, ~hundreds of MB) that caps how many are decoded/resident at once. Because row groups are independent and their reads run in the Prefetcher's own io pool (not the parsing threads), fetch depth is now decoupled from decode-ahead depth - the fetch-deep / decode-shallow mode that finding #5 called for. Within a row group subgroups stay sequential, so `determinePagesToPrefetch`'s in-order requirement is preserved. The compressed-read memory is charged to the ColumnDataPrefetch stage via startPrefetch and released when ColumnData resets the prefetch handles (the handle records its allocating stage, so the release credits the right budget regardless of which stage's diff performs it). Verified on the Debug build: reads are correct with and without PREWHERE (3M-row multi-row-group file), the constant-column optimization still fires, and there is no deadlock. Perf/prefetch-depth gains need a high-RTT remote (S3) run to observe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds read back-pressure on the ColumnDataPrefetch stage: once the compressed data already in flight covers more than `input_format_parquet_prefetch_bandwidth_hide_seconds` of the measured read throughput, stop prefetching further ahead. Beyond that point the storage link is already fed, so extra compressed buffering only wastes memory without improving throughput (the "+21 GB RAM for +3.6 s" case in finding #4). The Prefetcher now tracks completed-read throughput (bytes since init / elapsed, `averageThroughputBytesPerSec`). The scheduler compares it against the ColumnDataPrefetch stage's in-flight compressed bytes (that stage's memory usage) and stops admitting more prefetch tasks when the in-flight bytes exceed throughput x hide_seconds. The privileged-task escape (lowest incomplete row group is always schedulable) still applies, so back-pressure can never deadlock. Defaults to 0 (disabled): the throughput heuristic and the hide-seconds target can only be validated on a high-RTT remote (S3) run, and on local fast IO the estimate is meaningless, so it is opt-in for cluster tuning. With C's ColumnDataPrefetch memory budget already providing a static cap on compressed buffering, this setting makes that cap adaptive. Verified on the Debug build: results are correct with the setting off (default) and with an aggressively small value (0.001s) that forces heavy throttling - no deadlock, identical results. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port adaptation: antalya-26.6 predates the Nullable(Tuple) support on master, so PrimitiveColumnInfo::group_nullable and ColumnSubchunk::group_null_map do not exist there. Remove those references from the constant-column detection gate and the decoded-memory reconciliation. Both are safe: nested-in-nullable-struct leaves are already excluded from the constant-column optimization by the `output_columns[...].is_primitive` guard, and the group null map (when it would exist) is negligible in the memory reconciliation. Also resolved during the rebase onto antalya-26.6: - ProfileEvents/Reader: keep only the new ParquetConstantColumnChunks event (antalya has no ParquetPrunedPages). - Column index uses antalya's singular `column_index_condition`. - Dropped the master-only `pruningMemoryReservation` (antalya's applyBloomAndDictionaryFilters takes no reservation). NOT YET COMPILED against antalya-26.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: c6b70b3 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 34816a3 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: f260506 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 2a20ab9 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 114640e I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: a019cd7 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: a3ee936 Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Commit c6b70b3 accidentally added a `unique_key_probe_implementation` entry to `SettingsChangesHistory.cpp` under version 26.8. That setting is not registered anywhere in `Settings.cpp`, so `02324_compatibility_setting` failed with `UNKNOWN_SETTING` when applying `compatibility` to old versions. The entry is unrelated to the parquet constant-column work; remove it. CI: https://github.com/Altinity/ClickHouse/actions/runs/31099889198/job/92610989464 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Extends the constant-column optimization to the all-null case. When a Parquet column chunk provably holds only nulls - its `null_count` statistic equals `num_values` on a physically nullable leaf - the reader no longer fetches or decodes that chunk's dictionary or data pages. `detectConstantColumn` marks the chunk constant (and `is_all_null`), and `formOutputColumn` materializes the result directly: `Null` for a Nullable output, or the output default when `input_format_null_as_default` substitutes nulls for a non-nullable output. A non-nullable output without null substitution cannot represent the result, so such a chunk is left to the normal decode path. Unlike the single-value case this needs no value decode, so it sidesteps the min/max exactness and `BYTE_ARRAY` truncation checks entirely; it only reads the `null_count` count. `formOutputColumn` records every row of an all-null chunk in `block_missing_values` (the single-value case has no nulls and records nothing), matching the normal decode path's null-map bookkeeping so `input_format_null_as_default` stays correct. All-null wide chunks are common in schema-evolved files (a column added later is all-null in older row groups), so this skips dictionary, data page, offset index and column index reads for a frequent real-world shape. Reuses the existing `input_format_parquet_use_constant_column_optimization` setting and the `ParquetConstantColumnChunks` ProfileEvent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Commit c6b70b3 fabricated two entire version blocks (26.8 and 26.7) in SettingsChangesHistory.cpp holding ~80 setting entries pulled in by a bad merge. 63 of them reference settings that are not registered on this branch and 10 duplicate entries recorded elsewhere, so applying `compatibility` to an older version threw `UNKNOWN_SETTING` (e.g. `s3_base`, `unique_key_probe_implementation`), failing 02324_compatibility_setting. The only entry that belongs to this parquet commit is `input_format_parquet_use_constant_column_optimization`. Remove both fabricated blocks and keep just that setting, moved into the pre-existing 26.6 block. The resulting history equals the commits parent plus that single line. Follow-up to 77c2c72 which removed only the first offending entry. CI: https://github.com/Altinity/ClickHouse/actions/runs/31105816361/job/92630866726 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
…ds in history The setting was added by a019cd7 but never given a SettingsChangesHistory entry, so 02995_new_settings_history reported it as an undocumented new setting (the failure surfaced once 02324_compatibility_setting stopped failing first). Add it to the 26.6 block next to input_format_parquet_use_constant_column_optimization; default 0 disables the back-pressure and matches the pre-existing behavior. Reproduced the full 02995 check locally (registered settings minus both baseline TSVs minus recent-version history): this was the only missing setting. CI: https://github.com/Altinity/ClickHouse/actions/runs/31110878761/job/92648476492 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
The previous run failed in Config Workflow / Set up job with "Failed to resolve action download info. Error: Service Unavailable" - a transient GitHub Actions outage, not a code failure. Empty commit to re-run CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Adds the DataLakeCatalog `namespaces` setting: a comma-separated list of allowed namespaces for `rest`, `glue` and `unity` catalog types, so a DataLake database exposes only the selected namespaces. `rest` supports nested rules (`foo`, `foo.bar`, `foo.*`) via `RestCatalog::AllowedNamespaces`; `glue`/`unity` use a flat allow-set. Default `*` allows everything (pre-existing behavior). Ported from the merged antalya-25.8 PR onto antalya-26.6. Cross-version adaptations: - Catalog construction moved into `DatabaseDataLake` in 26.6, so the namespaces are threaded through `CatalogSettings` / the catalog constructors there; the 25.8 inline construction in `DataLakeConfiguration::getCatalog` (and its `catalog_namespaces` plumbing) is obsolete and dropped, leaving `DataLakeConfiguration.h` unchanged. - `CATALOG_NAMESPACE_DISABLED` error code renumbered 757 -> 779 (757..778 are already taken on this branch). - The namespace filter checks were merged into 26.6-refactored code paths (threadpool-based `RestCatalog::getTables`, extracted Glue credentials provider, 26.6 `resolveMetadataPathFromTableLocation`). - The delegating 6-arg `RestCatalog` constructor (used by OneLake/BigLake, which do not support the filter) defaults `allowed_namespaces` to `*`. - Added explicit `<boost/algorithm/string.hpp>` and `<unordered_map>`/ `<unordered_set>` includes (the upstream PR relied on transitive includes). PR: #1337 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
The ported AllowedNamespaces block opened a public: section for the nested class and closed with private:, which downgraded every RestCatalog member after it (loadConfig, getAuthHeaders, retrieveAccessToken, ...) from protected to private. Subclasses (OneLake/BigLake/Paimon REST catalogs) call those, so Build failed with "is a private member of DataLake::RestCatalog". Restore the trailing access specifier to protected; AllowedNamespaces stays public (the gtest references it). CI: https://github.com/Altinity/ClickHouse/actions/runs/31171012685/job/92846936566 PR: #2181 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Adding the namespaces_ parameter to the primary RestCatalog constructor broke the pre-existing unit test gtest_rest_catalog.cpp, which constructed a RestCatalog without it (no matching constructor). Pass namespaces = "*" (allow all) in the new argument slot, before the context argument. CI: https://github.com/Altinity/ClickHouse/actions/runs/31176356154/job/92863225108 PR: #2181 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
formOutputColumn previously expanded a constant column chunk with insertMany(Field, num_rows) - an O(rows) per-row Field-dispatch fill that showed up as a consistent ~20% UserTime increase in profiling (the baseline decoded the same near-constant chunk cheaply via RLE/dictionary). Emit a ColumnConst instead: O(1) to build, and the const-ness propagates through the pipeline. A PREWHERE/WHERE predicate computes its result from the value without expanding the stored column, and GROUP BY / aggregation over the column get a const key. The value is already in the output (post-cast) domain. The all-null block_missing_values bookkeeping is unchanged. The reader path preserves the const: getOrFormOutputColumn returns it as-is, ColumnConst::size() == rows_pass satisfies the delivery checks, and ColumnConst::filter keeps it const through multistage PREWHERE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Extends the constant-column optimization from whole-chunk (tier 1, footer statistics) to per-row-subgroup granularity using the Column Index per-page min/max/null_pages, which the reader already loads for predicate push-down. Catches columns constant over a run of pages without the whole row group being constant (common in sorted/clustered data), with no change to subgroup/chunk sizing. - constColumnMaterializationEligible: shared flat-top-level-primitive gate, factored out of detectConstantColumn. - applyColumnIndex: retain per-page constant info (value / is_const / all_null) in ColumnChunk::page_const_info. Fixed-width numeric/date/time only for value constants (the Column Index has no per-page exactness flag, so a truncated BYTE_ARRAY min==max is untrustworthy); all_null is a plain flag, always safe. - detectConstantSubchunk: a column is constant for a subgroup iff every page overlapping the subgroup row range is all-null, or all hold the same value. - Wired into intersectColumnIndexResultsAndInitSubgroups; sets the subchunk constant fields so decodePrimitiveColumn skips decode and formOutputColumn materializes a ColumnConst. New ParquetConstantColumnSubchunks ProfileEvent. Tier 1 stays the always-on baseline and the only detector for BYTE_ARRAY. Tier 2 is opportunistic: only where the Column Index is already loaded (predicate push-down columns), never force-fetched. Deferred (follow-up): skipping the prefetch of constant subgroups pages (the I/O win). Currently those pages are still fetched and skipped forward by the next subgroups skipToRowOrNextPage; correct but does not yet save the read. Design: docs/design/parquet-v3-page-level-constant-column.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Completes tier-2: a per-subgroup constant column (detectConstantSubchunk) now also skips fetching its data pages, not just decoding them. In determinePagesToPrefetch, a constant subchunk claims no pages; a page fully inside it is released (never fetched), while a page shared with a neighbouring non-constant subgroup is left for that subgroup to claim. Safe because tier 2 only exists when the Column Index (hence Offset Index) is loaded: a constant subgroup never calls skipToRowOrNextPage, and a non-constant subgroup jumps directly via the offset index to its own claimed pages, so a released constant page between them is never accessed. The whole-chunk data_pages_prefetch is still split (likely_to_be_used=false), so only claimed pages are actually read. This turns the tier-2 CPU/memory win into an I/O win as well (fewer S3 GETs / ParquetPrefetcherReadRandomRead), matching tier 1 but at page granularity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Adds input_format_parquet_use_column_index_for_constant_columns (default off). By default tier-2 per-subgroup constant detection only runs where the Column Index is already loaded (columns with a predicate). This setting force-loads the Column Index (and Offset Index) for eligible read columns without a predicate, so tier-2 can skip single-valued page runs on them too - worthwhile for sorted / low-cardinality columns, at the cost of a small extra (tail-contiguous, coalesced) index read. A force-loaded column takes the same load path as a predicate column (use_column_index = true); applyColumnIndex records per-page constant info but skips page-level predicate pruning when the column has no condition (prev_row_idx stays 0 -> whole chunk selected -> no restriction). Setting is declared in FormatFactorySettings, plumbed through FormatSettings / FormatFactory, and recorded in SettingsChangesHistory (26.6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
|
RowGroup level + push ColumnConst to transforms |
…off] Approach B: when only PART of a row subgroup is single-valued (a constant run shorter than, or straddling, a subgroup), fill those pages from the per-page Column Index while decoding only the varying pages - producing a full column where whole-subgroup tier-2 cannot make a ColumnConst. Gated behind input_format_parquet_fill_constant_pages (default off). Additional gates keep it correct and simple: - output column must not need a post-decode cast (we fill the decoded_type column with the Column Index value, valid only when decoded == output value type); - no predicate on the column (so no page pruning; data_pages == all pages); - no prewhere filtering in the subgroup (avoids filter/range intersection); - at least one single-value page and no all-null page in the subgroup (all-null pages fall back to the standard decode). fillConstantPagesAndDecodeRest walks the subgroup page by page: a single-value page is filled via insertMany (+ null-map zeros) without reading it; a varying page is decoded with the existing skipToRowOrNextPage + readRowsInPage, which jumps over the filled pages via the offset index without loading them. First cut is decode-fill only: constant pages are still prefetched (correct, some wasted I/O). Skipping their prefetch needs cross-subgroup coordination and is a follow-up. Design: docs/design/parquet-v3-page-level-constant-column.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Completes the mixed-topology fill (input_format_parquet_fill_constant_pages, default off): a subgroup that fills its single-value pages from the Column Index no longer prefetches them. determinePagesToPrefetch consults willFillConstantPages (the same deterministic predicate the decode path uses) and, for a fill subgroup, does not claim its is_const pages - so a page overlapped only by fill subgroups is released and never read, while a page also decoded normally by another subgroup is still fetched. Also adds a no-prewhere / no-row-level-filter gate to willFillConstantPages so rows_pass == rows_total holds identically at prefetch time and decode time, keeping the two decisions consistent. This turns the mixed-topology fill from a decode-CPU-only win into an I/O win too (fewer page reads), matching tier-2 at sub-subgroup granularity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
min_value == max_value provably means a single exact value for every physical type, including BYTE_ARRAY / FIXED_LEN_BYTE_ARRAY. Statistics and Column Index bounds are always valid (min_value <= every value <= max_value) and truncation only ever widens them, so a truncated value - or any page/chunk with two distinct values - yields min_value < max_value. Equality therefore requires a single value short enough to be stored exactly; the is_*_value_exact flags are implied and, for the Column Index (which has no per-page exact flag), never needed. Remove the BYTE_ARRAY/FIXED_LEN exclusion from tier 1 (detectConstantColumn) and the per-page recording in tier 2 (applyColumnIndex), so constant string columns and string constant page runs get the optimization too. Also fixes tier 1 for writers that omit is_*_value_exact (previously any such BYTE_ARRAY constant was skipped even when short and exact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Previously a mixed-topology subgroup (input_format_parquet_fill_constant_pages) bailed to normal decode if any overlapping page was all-null. Now fill them too: an all-null page appends no non-null values and marks its rows null in the null map; the existing expand() + Nullable-wrap / null_as_default tail finalizes them exactly as the normal decode does (the column holds compact non-null values, the null map covers all rows). determinePagesToPrefetch also skips prefetching all-null pages, matching the fill. willFillConstantPages now accepts all-null pages as fillable, but still bails when an all-null page is present and the output can represent neither null (non-Nullable output) nor a default (no null_as_default) - the standard decode raises the usual not-null error there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Drop the column_index_condition gate from willFillConstantPages. A page-pruning predicate on the column (column_index_condition) is safe for the fill: subgroups are built only over contiguous surviving row ranges, so a pruned page never overlaps a subgroup and the fill never walks it; the prefetch-skip indexes page_const_info by each page global position (page.meta into page_locations), which is correct regardless of the data_pages subset. A column carrying an actual prewhere/row-level filter is still blocked by the no-prewhere gate. Gate 2 (output needs_cast) is intentionally left in place: whether decodeField yields the decoded or the output value domain is ambiguous from static reading (SchemaConverter/convertField vs the tier-1 is_constant branch), and guessing wrong is silent wrong data. It needs a build + a needs_cast test to resolve (and that test would also confirm tier-1/tier-2 on hint-cast columns). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
The mixed-topology fill now walks the rows that pass the filter (row_subgroup.filter) instead of the whole subgroup range, so it works under PREWHERE / row-level filters: it iterates passing ranges (like the standard row-range decode) and, within each, fills constant / all-null pages for their overlapping passing rows and decodes varying pages for the contiguous passing sub-range. It therefore produces exactly rows_pass values for any filter, and willFillConstantPages no longer depends on rows_pass - so the decision is identical at prefetch time and decode time and the prefetch-skip can never drop a page the decode needs. Removes the no-prewhere and rows_pass == rows_total gates from willFillConstantPages. The only remaining gate is needs_cast (build-gated; see the decodeField value-domain question). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Object identity cacheClickHouse have multiple caches for Parquet/Iceberg/Data, but some of them are cached on: This does make sense for certain use cases with s3, (not to use expired cache if object is changed)
But, this protection (against expired object) doesn't make sense for iceberg tables model, because in their consistency model, they do not store etag value in manifest lists. So, even if object is replaced we will not know/detect that. So, idea was to introduce very small cache just for those engines, which does not fix in contract value of etag. And use it, in order to probe those caches, which use object_path + etag as key. |
Parquet footerWhen reading Parquet file:
So, total latency was (GET 1) + (GET 2) So, we either can be bit more generous and just read more data for GET 1 request (in hope that whole footer will land in it) Another option, in case of big estimates, do 2 requests in parallel:
|
Guard for *_min_bytes_for_seekClickHouse glue reads in one GET request, if there is less than 4MB gap between them Which in general, was tradeoff we were willing to accept, but as always there should be some limit to that. So, idea was to allow creation of more GET requests, for some pathological cases. |
Hedged requestsSend duplicate GET request, if original is not answering (TTFB? total latency?) for longer than X Parallelize iceberg manifest calculationDone in #1753 |
Constant Column optimizationIcebergIceberg store metadata/statistics for some columns.
Done in #1069 ParquetParquet in footer themself store statistics for all columns in file for RG. is safe to use., then again we can avoid read this column and push it as const in chunk. Another case, For non numerics, if min_values/max_values are truncated, max_value should be truncated-and-increment. And again, we can do the same optimization (not read page/range of pages), create const column in chunk |
ianton-ru
left a comment
There was a problem hiding this comment.
I count at least 5 different independent features in this PR
- Cache for S3 object metadata
- Host resolving with weights based on latency
- Hint for parquet footer size
- Parallel parquet file loading
- Constant columns
Plus namespace filter from #1019
This is not PR for merging, only as a proof of concept.
| auto fetch = [&](const S3::Client & c) -> S3::ObjectInfo | ||
| { | ||
| if (identity_only) | ||
| return S3::getObjectIdentity(c, uri.bucket, path, /*version_id=*/ {}); |
There was a problem hiding this comment.
Does this make something faster?
getObjectInfo makes HeadObject request, getObjectIdentity makes GetObjectAttributes with fallback to getObjectInfo.
As I understand, GetObjectAttributes give profit if replaced several different requests, but in current case it replaces only one.
Fallback makes only worse for some old S3-compatible storages without GetObjectAttributes support.
There was a problem hiding this comment.
Does this make something faster?
No, it's not faster.
It was attempt to gather/cache real multiPart boundaries from S3 via GetObjectAttributes.
But, it need special conditions (multiPart upload with extra checksums) AND have access to GetObjectAttributes.
So, probably if it will be implemented using Head is more reliable.
| if (auto global_context = Context::getGlobalContextInstance()) | ||
| identity_cache = global_context->getObjectStorageIdentityCache(); | ||
|
|
||
| if (identity_cache) |
There was a problem hiding this comment.
As I understand this caches also metadata during direct reading with S3 table function. Cache is good for immutable objects, "owned" by ClickHouse, but not for all objects on S3.
There was a problem hiding this comment.
As I understand this caches also metadata during direct reading with S3 table function.
It might, and if so, need to be fixed, but doesn't affect bench for now.
Cache is good for immutable objects, "owned" by ClickHouse, but not for all objects on S3.
I think, we should do it for "any" immutable kind of object. (either ClickHouse owned or Iceberg catalog).
We potentially could do it for any object of any source, if we use conditional GET later (GET object/range if etag match X, where X is cached value, so we can get cache invalidation & data request in one go, but it's unnecessary for Iceberg)
| size_t num_columns = std::max(entry.columns_infos.size(), entry.value_bounds.size()); | ||
| if (num_columns > 0) | ||
| { | ||
| constexpr size_t rows_per_row_group_guess = 1'000'000; |
There was a problem hiding this comment.
Does this number have grounding? From my point of view, guessing "set initial footer size based on random constant number of rows per group" is not worse and not better than "set initial footer size as 64 kB"
There was a problem hiding this comment.
is not worse and not better than "set initial footer size as 64 kB"
Requesting larger footer, up to 2MB is basically free.
And if we can save 1 GET request almost for free...
┌────────┬─────┬────────────┬─────────────────┬──────────────┐
│ footer │ RTs │ oracle p50 │ spec_serial p50 │ spec_par p50 │
├────────┼─────┼────────────┼─────────────────┼──────────────┤
│ 256 K │ 1 │ 26.7 │ 33.6 │ 34.3 │
├────────┼─────┼────────────┼─────────────────┼──────────────┤
│ 1 MB │ 1 │ 30.1 │ 33.9 │ 33.1 │
├────────┼─────┼────────────┼─────────────────┼──────────────┤
│ 2 MB │ 1 │ 33.7 │ 33.0 │ 33.9 │
├────────┼─────┼────────────┼─────────────────┼──────────────┤
But, logic for footer estimation can be done better, i think.
We should have list of columns (with types?), file size, number of rows from iceberg metadata.
Spark for example split by default using 128MB of uncompressed data, we can probably estimate that from types x col_num from one side and using compressed parquet size X ("average/good" parquet compression) from another.
There was a problem hiding this comment.
So may be just increase default const footer size to 2Mb instead of complex logic?
There was a problem hiding this comment.
Yeah, it's one option i keep in mind.
But, for very small files, it probably nice to somewhat scale it down.
Or, for big files, for >2MB estimated footer, we can (would like to) fire 2 requests in parallel, which allow to compress latency as well.
So, i think in the end we can decide on:
- complex estimator, which overestimate with good enough margin footer size.
- very simple estimator based on file size (just to reduce size of estimation for small one)
- just plain request 1-2 MB last bytes
|
AI found a lot of issues for most features, but I think make sense split PR on pieces, one feature in one PR, and recheck these PRs. |
Yes, and it was intendent as such.
I think, it bit early to do that. First we need to find (from bench on real data) which one we really (if any) need. |
`insertRowToLogTable` took the row content as an eagerly-built `String`, so the per-manifest-entry call sites evaluated `getContent(row_index)` (serializing the whole manifest entry) for every data-file entry on every query — even with `iceberg_metadata_log_level = None` (the default), where the result is immediately discarded by the level check inside the function. On wide Iceberg tables with many manifest entries this dominates query planning: on a 17,492-file table the single-threaded manifest pruning spent ~25s (summed across reader threads) building strings that were thrown away. The eager evaluation was the regression that made `iceberg_metadata_processing_threads` look load-bearing — it was mostly parallelizing wasted work. Make the row lazy: `insertRowToLogTable` now takes `std::function<String()>` and invokes it only after the log-level check passes. All six call sites pass a closure instead of a pre-built string. Measured on a synthetic wide table (62x1 GiB files, ~17.5k manifest entries, q reads 8 of 437 columns), warm, single metadata thread: IcebergMetadataReadWaitTimeMicroseconds (summed): 24.8s -> 1.6s (~15x) query wall: 4.09s -> 1.54s (~2.7x) with identical pruning (17,492 files) and identical bytes read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
`estimateParquetFooterSize` used `per_column_chunk = 112` B, which overshot the real thrift-compact ColumnMetaData size. For wide / few-stats-column Iceberg tables the footer is dominated by the `num_columns * per_column_chunk * num_row_groups` term, so the whole prediction ran ~2.2x large: on a 437-column, 8-row-group, ~1 GiB file the predictor asked for 519 KiB when the footer is 240 KiB. That is safe (an overshoot only reads a slightly larger tail) but wasteful. Measured the actual per-chunk cost at ~51 B/chunk on that file (large offsets serialize to ~5 B varints); set `per_column_chunk = 64` (headroom for longer column names) and reduce the safety margin 1.33x -> 1.25x. Validated the predicted-vs-actual footer size across all 62 files of a synthetic wide table (437 cols, 8 row groups): prediction drops from 519 KiB (2.16x) to ~288 KiB (1.17x) with zero underestimates, so the footer is still captured in a single speculative tail read with much less wasted read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
The existing `input_format_parquet_hedged_read_threshold_ms` hedges a remote Parquet read once its *total* time exceeds a budget. On object storage that conflates two very different situations: a connection that has stalled (no bytes yet) and a large transfer that is streaming normally but simply takes a while. Budgeting on total time either hedges healthy large reads (wasteful duplicate GETs) or sets the budget so high it no longer cuts the stall tail. Add `input_format_parquet_hedged_read_ttfb_threshold_ms`: fire the hedge when the primary read has not received its *first byte* within the threshold. Time-to-first-byte is largely independent of read size, so this isolates a stalled/slow frontend from a slow-but-progressing transfer. When set (> 0) it takes precedence over the total-time threshold; both remain bounded by `input_format_parquet_hedged_read_max_bytes` and `input_format_parquet_hedged_read_max_inflight`. Mechanism: the primary read now passes a progress callback into `readBigAt` that notifies a new per-task `first_byte` latch on the first reported bytes; `hedgeReadSync` waits on `first_byte` (TTFB mode) instead of `completion`. `first_byte` is also notified unconditionally when the primary finishes, so a read that produced no progress callback (cached region, split path) never leaves a hedge waiting. Measured on a synthetic wide-Iceberg table (62x1 GiB files, q reads ~690 MiB across part-sized S3 GETs, v3 + parallel Iceberg metadata): ttfb_ms p50ms maxms readMiB hedges wins 0(off) 6558 7499 690 0 0 5 6149 6490 1075 158 158 20 5987 6179 1043 133 133 40 5341 5942 749 21 21 At 40 ms: p50 -19%, tail max -21%, +9% bytes (21 hedges, all winning) - the trigger fires only on genuinely stalled frontends. Result is unchanged with the hedge on (identical output hash across thresholds); the loser read is discarded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
This experiment branch (Parquet v3 constant column skip) is not for merge; it exists to get a green build for testing the parquet-v3 read path. The `04401_system_reset_ddl_worker_access` stateless test comes from the antalya-26.6 base backport of ClickHouse#108460 and fails there (the SYSTEM RESET DDL WORKER privilege isn't enforced - unprivileged access is not denied), unrelated to the parquet changes on this branch. Dropping it here unblocks the build. This is NOT a fix for the underlying access-control regression, which lives in the antalya-26.6 base (commit 03e147a) and should be fixed or reverted there; a future merge of that base will reintroduce this test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
…Attributes behind a read setting Reading object metadata on a lake scan (`StorageObjectStorageSource`) needs the object size and etag (the etag keys the page / filesystem / Parquet-metadata caches). The identity fast path fetched these via `GetObjectAttributes`, which also returns the multipart part layout used for part-aligned reads - but that request is heavier than a plain `HEAD` and is issued per file, so a cold, cache-empty scan paid one `GetObjectAttributes` per object (measured ~2x slower cold on a wide-Iceberg benchmark) for part offsets it usually does not use. Default `S3ObjectStorage::getObjectMetadata` to a plain `HEAD` (size + etag, no part offsets), and add a read setting `object_storage_identity_cache_fetch_part_offsets` (default off) that opts back into `GetObjectAttributes` when part-aligned reads want the layout. A new 3-argument `IObjectStorage::getObjectMetadata` overload carries the flag; its default implementation ignores it, so non-S3 storages and the many callers that do not need part offsets are unchanged. The etag is still returned on both paths, so the object-storage caches keep working. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Resolve conflict in RestCatalog::dropTable: keep the branch's namespace-allowed guard and adopt antalya-26.6's endpoint construction (config.prefix + encodeNamespaceForURI + NAMESPACES_ENDPOINT). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
The parallel manifest-entry pruning hands entries from the producer threads to the consumer through a `ConcurrentBoundedQueue`, one entry at a time. The per-entry work is a tiny min/max prune, so on a wide, many-file table (tens of thousands of manifest entries) the queue's mutex/condvar became the dominant cost: a cold scan spent ~850 ms with the metadata threads ~99% blocked in `__lll_lock_wait` / `__futex` under `IcebergIterator::next` -> `blocking_queue`, doing almost no actual work (the CPU profiler saw ~0 samples there). More threads only added contention, which is why cold was flat across thread counts and no faster than the single-threaded 26.3 path. Hand off entries in batches of `producer_batch_size` (256) instead of one at a time: each producer accumulates into a local vector and pushes a full batch (flushing the partial batch when its work ends), and the consumer serves entries from a locally-held batch under a dedicated `consumer_mutex`, touching the shared queue only once per batch to refill. This amortises the shared-queue lock ~256x, moving the per-entry path onto cheap local buffers. Queue bound is now counted in batches. No change to ordering guarantees (there were none) or results. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
…hand-off" This reverts commit eac9b83.
The parallel manifest-entry producers shared a single "current manifest file" guarded by `manifest_advance_mutex`, and the manifest fetch itself (`getManifestFile`, an S3 read) happened while holding that lock. So even with many producer threads, the manifest files were read from S3 one at a time - on a cold, cache-empty scan of a table with tens of manifest files this serialized into tens of back-to-back S3 round-trips and dominated planning time, with the producer threads blocked in `__lll_lock_wait` on the advance mutex and the consumer idle-waiting for the queue to fill (profiled ~850 ms, ~0 CPU). Make the manifest cursor a `std::atomic<size_t>` in `SingleThreadIcebergKeysIterator::nextManifestFile`, so each producer claims a distinct manifest index lock-free and performs the S3 read outside any shared lock. Each worker now pulls, reads and fully drains one manifest file on its own, then pulls the next - so up to `iceberg_metadata_processing_threads` manifest files are read from S3 concurrently, turning N serialized reads into ceil(N/threads) rounds. The `manifest_advance_mutex` / shared-current-manifest state is removed. Per-entry pruning work is unchanged; results are unchanged (there was no ordering guarantee). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Resolve conflicts: metadata-log lazy-logging now uses the base's named `dump_*` closures (same lazy behavior); keep both `S3GetObjectAttributes` and the base's `S3HeadObjectMicroseconds` events; keep the random-access `createReadBuffer` path while adopting the base's `getCompressionMethod()` accessor; take the base for the RestCatalog namespace-filter parameterization, comment/typo fixes and docs. The identity->HEAD read setting and parallel manifest-read changes are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Under `object_storage_cluster` (distributed object-storage reads), the WHERE predicate did not reach `ReadFromCluster`, so Iceberg min/max file pruning was silently skipped (`IcebergMinMaxIndexPrunedFiles=0`) and selective queries scanned the whole table. Measured on IcebergBench q16: 3.16B vs 78.7M rows, ~7.0s vs ~0.8s (~60x read amplification). The prune-only `ObjectFilterStep` that carries the predicate to the cluster task iterator (`getTaskIteratorExtension`) was gated on `use_hive_partitioning`, so a non-hive Iceberg cluster read got a null filter. Add `ObjectFilterStep` for any `ReadFromCluster` with a WHERE, not just hive-partitioned tables. `ObjectFilterStep::updatePipeline` is a no-op, so it never filters rows on the initiator -- required at `WithMergeableState`, where the filter columns may be absent from the blocks returned by cluster replicas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts in the Iceberg object-storage read path, combining this branch's Parquet-v3 / parallel-manifest work with antalya-26.6's new secondary-storages (resolved-storage) feature: - IcebergIterator.h: keep both the atomic parallel manifest cursor and the new secondary_storages member. - IcebergIterator.cpp: keep the nextManifestFile() refactor and thread *secondary_storages into its getManifestFile call. - IcebergDataObjectInfo.cpp: keep both the resolved_storage/resolved_key chassert and the Parquet footer-size-hint precomputation. - StorageObjectStorageSource.cpp: read through getResolvedStorageFromObjectInfo while keeping the random-access read_settings/format_is_random_access args. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ter to physical column names The prior fix delivered the WHERE predicate to ReadFromCluster's ObjectFilterStep, but its DAG inputs are analyzer column identifiers (e.g. `__table1.date`) while the Iceberg pruner resolves against physical schema names (`date`). So `tryGetColumnIDByName` returned nullopt for every input, no min/max key conditions were built, and `IcebergMinMaxIndexPrunedFiles` stayed 0 (full-table over-read on selective cluster queries). On the single-node (FetchColumns) path this works only because a "change names to identifiers" ExpressionStep sits below the WHERE FilterStep and optimizePrimaryKeyConditionAndLimit merges the filter through it (identifiers -> names). At WithMergeableState (the cluster path) that step is absent. Rewrite the ObjectFilterStep's DAG in addObjectFilterStep: build a rename DAG whose inputs are physical names aliased to the identifiers, then ActionsDAG::merge the filter through it, so the DAG that reaches getTaskIteratorExtension -> IcebergMetadata::iterate -> ManifestFileIterator carries physical column names and the min/max pruner resolves them. Mirrors the single-node rename mechanism. Analyzer path only (the non-analyzer InterpreterSelectQuery path is not supported). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1: scaffold) Un-gate the CLICKHOUSE_CLOUD distributed-read/serialization surface on ReadFromObjectStorageStep and provide OSS bodies so make_distributed_plan can distribute object-storage/iceberg reads (currently rejects them: Code 344 "not serializable for remote execution"). - serialize(): ship the source config as table-function args via createArgsWithAccessData()->formatWithSecretsOneLine() (same faithful config object_storage_cluster already sends to workers) + columns + flags + max_block_size/num_streams + distributed_read_bucket_count + filter DAG. - setDistributedRead / getShardsForDistributedRead: bucket-count plumbing. - register the step in QueryPlanStepRegistry. - deserialize(): stubbed (throws) — implemented in the next increment. Compiles clean (NINJA_RC=0). Not yet functional end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent 2) Reconstruct the StorageObjectStorage on the worker from the serialized table-function form (engine + createArgsWithAccessData args): parse the function AST, resolve it via TableFunctionFactory, execute to get the storage, then pull object_storage / configuration / snapshot and rebuild ReadFromFormatInfo via prepareReadingFromFormat. Construct the step with distributed_processing=true so the worker reads its assigned files from the coordinator's task iterator (getClusterFunctionReadTaskCallback), reusing antalya's object_storage_cluster distribution. Re-applies the deserialized filter DAG and bucket count. Compiles clean. Not yet wired into makeDistributed (next increment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rement 3) Un-gate the object-storage branch of tryMakeDistributedRead so make_distributed_plan replaces ReadFromObjectStorage with a distributed (GatherExchange + distributed read) subtree in the OSS/antalya build. Drops the CLICKHOUSE_CLOUD-only totalRows() small- table heuristic (not available in OSS) and always distributes object-storage reads for now (TODO: add an OSS row-count estimate). With increments 1-2 (serialize/deserialize/register), make_distributed_plan can now ship the optimized object-storage scan (filter attached) to workers, so Iceberg min/max pruning applies for aggregate + join queries by construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ectly (increment 4) Fixes discovered by validating `make_distributed_plan` + `distributed_plan_execute_locally` end-to-end on `s3()`: - `serialize` now writes the table-function name (e.g. `s3`, `icebergS3`) via `objectStorageEngineToTableFunction`, not the engine name (`S3`), so `deserialize` can look it up in `TableFunctionFactory`. - `deserialize` unwraps the `StorageTableFunctionProxy` returned by `ITableFunction::execute` via `getNested` before casting to `StorageObjectStorage` (the cast previously failed on the `Proxy`). - Reconstruct the worker step with `distributed_processing=false`: the distributed_plan model ships the self-listing scan to each worker, which is a different mechanism from the `object_storage_cluster` task callback (`distributed_processing=true` deadlocked waiting for tasks that this path never feeds). - Give the deserialized `SelectQueryInfo` a minimal `ASTSelectQuery` so worker-side prewhere optimization (`SelectQueryInfo::isFinal`) does not dereference a null query AST when a filter is present. Validated in clickhouse-local: `SELECT count() FROM s3(...)` returns the same result with and without distributed_plan (1000000), and a filtered `SELECT count(), sum(JavaEnable) ... WHERE JavaEnable = 1` matches the baseline (544651) - the filter DAG survives the round-trip and the worker prewhere pass runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`QueryPlanStepRegistry::registerPlanSteps` was only called from the server (`programs/server/Server.cpp`), so `make_distributed_plan` with `distributed_plan_execute_locally=1` failed in clickhouse-local with "Unknown query plan step: GatherReceive". Register the steps in `LocalServer` too, alongside the other `register*` calls, so the single-process distributed executor works under clickhouse-local. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (increment 5) With `distributed_plan_default_reader_bucket_count > 1`, the distributed executor runs several bucket tasks, each executing the shipped `ReadFromObjectStorage` step. Until now every task listed and read all files, so an N-bucket read over M files would scan the data N times and over-count. Mirror what `ReadFromMergeTree` does with mark ranges, but for whole files. `initializePipeline` now reads the per-task `bucket_id` / `total_buckets` parameters (`parameter_lookup`) and, when `total_buckets > 1`, wraps the file iterator in `BucketFilterObjectIterator`, which keeps only files whose path hashes to this bucket (`sipHash64(path) % total_buckets == bucket_id`). Path-hash bucketing is deterministic across workers regardless of listing order or thread scheduling: each file belongs to exactly one bucket, so the union over all workers is the full, non-overlapping file set. This keeps results correct while distributing the scan (and thus per-file Iceberg min/max pruning) across the cluster. Not exercised by `distributed_plan_execute_locally` (single worker, one bucket); validated for correctness on a multi-shard cluster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 1051d19 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 0dc3eef I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 34823b1 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 524989d I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: b1b5542 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: e89ce36 Signed-off-by: UnamedRus <dtitmoav@gmail.com>
I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 5859b70 I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: 453018c I, UnamedRus <dtitmoav@gmail.com>, hereby add my Signed-off-by to this commit: c236978 Signed-off-by: UnamedRus <dtitmoav@gmail.com>
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
...
Documentation entry for user-facing changes
...
CI/CD Options
Exclude tests:
Regression jobs to run: