ClickHouse / ClickHouse/ClickHouse

Direct typed gather for `hash`/`parallel_hash` emit, and remaining column types

Open
#116,833 13 comments 0 reactions 1 assignee Claimed by @harikrishnan94 View on GitHub
comp-joins performance
Dominant language
C++
Stars
49.9k
Forks
9k
Avg merge
21h 32m
Merged PRs (30d)
515

Description

### Company or project name

ClickHouse

### Use case

Join emit of right-side payload columns. On a wide build side the cost is not the lookup: it is expanding every match into a `(block, row)` pair and then walking block → column → value per output column, two dependent random loads and no overlap. The direct typed gather already in `PartitionedHashJoin` indexes a per-block pointer table from the encoded ref word and prefetches 32 rows ahead. Values are identical; the win scales with the number of right-side output columns. None of that needs partitioning, and `hash` / `parallel_hash` already resolve the emit table (`emit_direct_gather`) — they just never set `LazyOutput::use_direct_typed_gather`.

### Describe the solution you'd like

1. Turn the gather on for `hash` and `parallel_hash`. Set `use_direct_typed_gather` in `HashJoinMethodsImpl` with the same guards `PartitionedHashJoin` already uses (lazy emit, not ANY, not ASOF). Prefer deleting the PHJ-only comment and making the flag the default on that path rather than a second call-site special case. `joinGet` and ASOF stay on their existing paths; they never resolve the emit table.

2. Use the gather in both `LazyOutput::buildOutputFromBlocks` and `LazyOutput::buildOutputFromBlocksLimitAndOffset`. The limit/offset path still walks refs to apply `rows_offset`, `rows_limit` and `bytes_limit`; after that walk, gather from the selected ref words instead of building `ColumnsWithRowNumbers` and calling `fillFromBlocksAndRowNumbers`. `buildOutputFromRowRefLists` / `fillFromRowRefs` stay generic unless a follow-up rewires them.

3. Keep using `IColumn::insertDefaultInto` for a zero ref word wherever the type's default is not a zeroed value (`Enum*` is the existing example).

4. Extend `gatherColumnDirect` / `resolveEmitColumns` past the current numeric/`Date*`/`UUID`/`Decimal*` switch. Fall back to `fillFromBlocksAndRowNumbers` per column when a source cannot take the path (`direct_gather_ok` already does this).

Checklist (recursive: a nested type is admitted only when every nested column is):

- [ ] Remaining contiguous fixed-width: `FixedString`, `Time`, `Time64`, `Interval`, `IPv4`, `IPv6`
- [ ] `Nullable(T)` — gather the null map (`UInt8`) and `T`
- [ ] `String` — gather `offsets` + `chars` (append a slice, not `out[i] = src[row]`)
- [ ] `Variant` — gather local discriminators, offsets, and each admitted nested variant column
- [ ] `Tuple` — recurse per nested column with the same ref words
- [ ] `Array(T)` — same slice shape as `String`: source offsets → nested range → dest offsets
- [ ] `ColumnReplicated` — not a type; use `repl_by_block`, gather `indexes[row]` then nested at that index (index width is `ColumnIndex`'s `UInt8`/`UInt16`/`UInt32`/`UInt64`)

Out of this issue, because they are not a pointer-table gather:

- `JSON` / `ColumnObject` (and `Dynamic` when variant/path sets must merge). `ColumnObject::insertFrom` merges typed paths, dynamic paths, and shared data; those sets can differ per stored block. There is no `const void *` to index. A later emit for those types would group rows by block and `insertRangeFrom`, which is a different change.
- `Enum8` / `Enum16`, until the unmatched-row default is the zero value (`DataTypeEnum::insertDefaultInto` writes the first enum value).
- `LowCardinality`, `Map`, `QBit`.

### Additional context

The mechanism and the payload-width numbers are in https://github.com/ClickHouse/ClickHouse/pull/115254 (section "Wide build-side payloads: the emit was the cost, not the lookup"). At 96 threads a `UInt64` key with a 64-byte build payload was 3.2× vs 1.3× with a key-only build side.

Related: https://github.com/ClickHouse/ClickHouse/pull/115254

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.