Conversation
The expected result matches initiator-side join semantics; the test currently fails because shards emit extra unmatched right-side rows. Related: #1664 Co-authored-by: Cursor <cursoragent@cursor.com>
Broadcast the original left table and keep the preserved right side sharded, so unmatched right rows are not duplicated on every shard. Related: #1664 Co-authored-by: Cursor <cursoragent@cursor.com>
After ClickHouse#94748 the planner always wrapped the left cluster source, so object_storage_cluster_join_mode='global' never saw the JOIN and ran on the initiator. Keep wrapping for allow/local and for RIGHT/FULL to avoid duplicated unmatched rows. Co-authored-by: Cursor <cursoragent@cursor.com>
CI triage for #2315 (@ianton-ru)Verdict: 0 of the failures are caused by this PR. All red checks are pre-existing flakiness / infra / runner-environment issues in subsystems your change never touches. This PR only edits Head SHA: Per-failure breakdown1. Source upload — infra (not PR) 2. Integration tests (amd_asan_ubsan, db disk, old analyzer, 4/8) — pre-existing/infra (not PR) 3. Integration tests (amd_asan_ubsan, db disk, old analyzer, 7/8) — pre-existing/infra (not PR) 4. Stateless tests (amd_binary, cas s3 storage, parallel) — flaky storage layer (not PR) 5. Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2) — environment / flaky (not PR)
6. Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2) — server death, collateral (not PR) 7. Stress test (amd_asan_ubsan) — infra (not PR) Bottom lineNothing here needs a code change. The PR-relevant signal is all green: builds pass on every arch, (No fix pushed — there's nothing PR-caused to fix. Happy to re-check after a re-run if any of these persist.) — @blau-ai |
Port of the review fixes made for the `master` pull request ClickHouse#118611 `tryRewriteGlobalRightJoinAsLeftJoin` only recognised a narrow shape of the query it was meant to fix, so a `GLOBAL RIGHT JOIN` whose left table is a sharded `Distributed` table still emitted the rows preserved from the broadcast side once per shard in these cases: * `USING` and `NATURAL` joins were skipped, because only `ON` was accepted. A `USING` key stores its sides positionally, so the key pairs are now swapped together with the table expressions, and a key that does not hold a plain column per side leaves the query alone. * The rewrite required the right side to be a sharded `Distributed` table too. Only the left table fans the query out across shards, so the gate is now on the left table alone, and a local table, a one shard `Distributed` table, a subquery or a table function on the right is handled as well. `Any`, `Semi` and `Anti` join over to the swapped sides the same way `All` does and are accepted; `Asof` and `RightAny` do not mirror and stay out. * Only the topmost join was examined. Join trees are left deep, so the walk now descends to the join that reads the leftmost table. A `RIGHT` or `FULL` join that survives the rewrite, such as the one in `(A INNER C) RIGHT B`, cannot be swapped without moving a join into the right table expression. Those now read the leftmost table through a subquery, which keeps the join on the initiator, the only remaining way to emit the preserved rows once. `getDistributedStorageFromTableExpression` also missed `remote` and `clusterAllReplicas` on the left, because it only looked at `TableNode`. Co-authored-by: Cursor <cursoragent@cursor.com>
…istributed tables. An `Alias`, `Buffer`, `MaterializedView` or `StorageProxy` in front of a sharded `Distributed` table still fans the query out across shards, so unmatched right rows would otherwise be duplicated once per shard. Co-authored-by: Cursor <cursoragent@cursor.com>
…al-right-join-test
|
Did you consider another approach? Keep the RIGHT JOIN and still send the right table to shards, but add a row number to it. Each shard returns the set of row numbers that found a match, and the initiator merges these sets and adds the rows without a match itself. With this, big GLOBAL RIGHT JOIN small sends only the small table over the network, and FULL JOIN can also run on shards. I understand it is harder (the shard has to collect row numbers before WHERE/GROUP BY), so I wonder why you chose the current approach. |
|
Medium:
|
…al-right-join-test
After the join is rewritten as a LEFT JOIN, a `PREWHERE` bound to the original left table followed it into a `Memory` table and shards threw `ILLEGAL_PREWHERE`. Move the predicate into the broadcast subquery when its columns belong to that table, and skip the swap when `PREWHERE` has no column so the initiator wrapper keeps the filter. When wrapping a remote table that owns `PREWHERE`, move the full predicate onto the wrap and drop the initiator actions so nested `GLOBAL IN` sets are not materialized twice. Port of ClickHouse#119780 Co-authored-by: Cursor <cursoragent@cursor.com>
|
@Selfeer Yes, thanks. The same was found in PR for upstream,(ClickHouse#118611), fix (ClickHouse#119780) ported here. |
|
@k-morozov Good idea, looks too global, introduce additional information in responses from nodes. Need to think. |
| /// `Alias`, `MaterializedView`, `Buffer` and `StorageProxy` (for example `lazy_load_tables`) | ||
| /// forward `read` to a nested storage. If that nested storage is `Distributed`, the join still | ||
| /// fans out across shards, so look through the wrappers before deciding. | ||
| for (size_t i = 0; storage && i < 16; ++i) |
There was a problem hiding this comment.
Surprised that '16' passed through AI review.
Funny, but Ok.
| { | ||
| const auto distributed_product_mode = settings[Setting::distributed_product_mode]; | ||
| return join_node.getLocality() == JoinLocality::Global | ||
| || distributed_product_mode == DistributedProductMode::GLOBAL |
There was a problem hiding this comment.
Not sure that I get this.
getLocality returns JoinLocality::Local, but we believe that it is actually Global? Why?
There was a problem hiding this comment.
I don't catch.
getLocality is compared here with JoinLocality::Global, not with JoinLocality::Local.
There was a problem hiding this comment.
Even if getLocality is JoinLocality::Local, we can consider the join global based on other conditions. Why?
There was a problem hiding this comment.
Settings change behaviour. distributed_product_mode=global and prefer_global_in_and_join=true are exactly to use global variants, when from query semantic itself local is returned.
https://clickhouse.com/docs/reference/settings/session-settings/distributed#distributed_product_mode
https://clickhouse.com/docs/reference/settings/session-settings/prefer#prefer_global_in_and_join
Solved #1664
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Fix distributed global right join
Documentation entry for user-facing changes
Swap left and right tables for distributed right join.
Fix for
object_storage_cluster_join_mode='global'- it was broken after frontports and executed as local.CI/CD Options
Exclude tests:
Regression jobs to run: