8000 Add `object_storage_cluster_join_mode='distributed'` for whole-query JOIN dispatch by VighneshPath · Pull Request #2832 · Altinity/ClickHouse · GitHub
[go: up one dir, main page]

Skip to content

Add object_storage_cluster_join_mode='distributed' for whole-query JOIN dispatch - #2832

Open
VighneshPath wants to merge 6 commits into
Altinity:antalya-26.8from
VighneshPath:feature/antalya-26.8/object-storage-cluster-distributed-join
Open

VighneshPath wants to merge 6 commits into
Altinity:antalya-26.8from
VighneshPath:feature/antalya-26.8/object-storage-cluster-distributed-join

Conversation

@VighneshPath
Copy link
Copy Markdown

Port of #2383 to antalya-26.8, with that PR's review comments addressed.

With object_storage_cluster_join_mode = 'distributed', a JOIN query whose driving table is a DataLake catalog table with object_storage_cluster set is sent whole to that cluster. Each node runs the JOIN and any GROUP BY over its share of the driver's files, and the initiator only merges the partial aggregate states. Before this, the JOIN and GROUP BY always ran on the initiator.

  • findDistributedObjectStorageCandidate walks the left side of the query, through INNER ALL/LEFT joins and through subqueries that do not combine rows, to the driver. Every other table must be a catalog table with no row policy and with SELECT access. Otherwise the whole query is planned as before.
  • buildDistributedObjectStorageQueryPlan sends the query with every table replaced by the table function its own cluster read would send: the driver as its *Cluster function, which reads the files the initiator hands out, and every other table as its plain function, which each node reads in full.
  • ReadFromClusterQuery sends the query over the same transport as ReadFromCluster. It hands out the driver's files, pruned by the driver's own filter.

Commits:

  1. The feature, as squashed on antalya-26.6.
  2. Readability cleanup, as on antalya-26.6.
  3. Adapt to antalya-26.8:
    • Follow the new query-tree accessors.
    • Do not dispatch while building a plan serialized for a shard.
    • Strip initiator-only settings such as limit from the context sent to workers, as ReadFromCluster now does.
    • Under the reworked allow mode, a declined dispatch takes the allow path. 8000
  4. Review comments from Add object_storage_cluster_join_mode='distributed' for whole-query … #2383:
    • Reuse hasAggregateFunctionNodes/hasWindowFunctionNodes.
    • Rename ReadFromClusterQuery::updateSettings to createRemoteContext and build it with createCopy + setSetting.
    • Decline dispatch when a nested subquery or CTE has its own SETTINGS clause. A nested additional_table_filters or object_storage_cluster was invisible to the eligibility checks.
    • The IMPORTANT driver settings, the unusable flag and return left were already gone after the redesign.

The query condition cache fix from #2383 is not ported: antalya-26.8 already keys object storage entries by full path and ETag.

Related: #2383

Changelog category (leave one):

  • Experimental Feature

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

Add object_storage_cluster_join_mode = 'distributed', which sends a whole JOIN query over DataLake catalog tables to the cluster of its driving table, so that the JOIN and GROUP BY run on the cluster's nodes instead of on the initiator.

Documentation entry for user-facing changes

The distributed value is described in the documentation of the object_storage_cluster_join_mode setting.

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Unit tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • CAS (content-addressed storage; Antalya only)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

🤖 Generated with Claude Code

VighneshPath and others added 4 commits November 12, 2026 20:20
…JOIN dispatch

With the new mode, a `JOIN` query whose driving table is a `DataLake` catalog
table with a cluster is sent whole to that cluster. Each node runs the `JOIN`
and any `GROUP BY` over its share of the driver's files, and the initiator only
merges the partial aggregate states.

- findDistributedObjectStorageCandidate walks the left side of the query, through
  `INNER ALL`/`LEFT` joins and subqueries that do not combine rows, to the driver.
  Every other table must be a catalog table with no row policy and `SELECT`
  access. The decision is made once per query, next to parallel replicas, and
  kept in `GlobalPlannerContext`.
- buildDistributedObjectStorageQueryPlan serializes the query and replaces every
  table in it with the table function its own cluster read sends: the driver's
  `*Cluster` function reads the files the initiator hands out, every other table's
  plain function reads all of them. A worker resolves no catalog table and runs
  the query as it would any cluster table function; an Iceberg table's function
  names the metadata file resolved on the initiator. A driver referenced twice
  is not dispatched.
- `ReadFromClusterQuery` sends the query over the same transport as
  `ReadFromCluster`, whose pipe building is now shared, and feeds it the driver's
  files pruned by the driver's own filter.
- An ordinary cluster read reached under the new mode behaves exactly like
  `allow`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Shorten the comments to the reason each step exists.
- `DistributedObjectStorageCandidate` keeps only the dispatch boundary and the
  driver's `TableNode`; its storage and filter key are derived from it.
- `findDriverOnLeftSpine` returns `std::optional` instead of an `unusable` flag.
- Rename `isSafeDataLakeLeaf` to `isSafeCatalogTable`.
- Move the driver filter extraction into `buildDriverFilter`.
- Replace the `prepareForWholeQueryDispatch` hook with lazy initialization in
  `StorageObjectStorageCluster::getTaskIteratorExtension`, limited to data lake
  configurations, and drop the test of the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `QueryNode::getJoinTree` and `JoinNode::getLeftTableExpression` now return a
  reference that asserts the node exists; use `getJoinTreeNode` and
  `getLeftTableExpressionNode`, which can still be null.
- Do not dispatch when `build_logical_plan` is set: that plan is serialized for
  a shard, and `ReadFromClusterQuery` has no serialized form. Plan-based
  parallel replicas step aside for the same reason.
- Strip initiator-only settings such as `limit` from the context sent to
  workers, as `ReadFromCluster::updateSettings` now does. A worker applying
  them would cut its partial result before the initiator merges it.
- The analyzer now checks table access while resolving identifiers, so the
  access tests analyze as a user with grants and check the candidate as one
  without.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Decline dispatch when a nested subquery or CTE has its own `SETTINGS`
  clause. The eligibility checks read the outer query's settings, so a nested
  `additional_table_filters` was not rejected, and a nested
  `object_storage_cluster` was ignored when choosing the driver's cluster.
- Reuse `hasAggregateFunctionNodes` and `hasWindowFunctionNodes` instead of a
  local copy of the same traversal.
- Rename `ReadFromClusterQuery::updateSettings` to `createRemoteContext`: it
  creates a new context rather than updating one. Build it with
  `Context::createCopy` and `setSetting`.

Related: Altinity#2383

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ianton-ru
Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector
chatgpt-codex-connector Bot commented Nov 12, 2026 •
Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T20:19:22.843905Z 46dd813 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ianton-ru
Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46dd813705

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


/// Initiator-only settings such as `limit` would cut each worker's partial result, as in `ReadFromCluster`.
Settings settings = remote_context->getSettingsCopy();
ClusterProxy::stripInitiatorOnlySettings(settings);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Downgrade the join mode before dispatching to workers

When this mode is enabled during a rolling upgrade, settings still marks object_storage_cluster_join_mode=distributed as changed after this strip, so RemoteQueryExecutor sends that value in the settings packet. A worker from before this commit already knows the setting name but cannot parse the new enum value, causing every otherwise eligible dispatched JOIN to fail before execution; mirror ReadFromCluster::updateSettings and change it to allow in this remote context.

Useful? React with 👍 / 👎.

return false;

const auto & storage = static_cast<const IStorageCluster &>(*table_node.getStorage());
return !storage.getClusterName(context).empty();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor local fallback before selecting the driver

When object_storage_cluster_fallback_to_local_if_empty=1 and the configured object-storage cluster is missing or has no nodes, getClusterName still returns its nonempty configured name, so this accepts a whole-query candidate. The custom path subsequently uses the throwing getClusterImpl in readPreparedClusterQuery instead of resolveClusterRead, causing a qualifying JOIN to fail although the setting promises an ordinary local read; resolve the cluster or decline dispatch before returning the candidate.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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

0