ClickHouse / ClickHouse/ClickHouse
vector_search_use_quantized_codes reads codes plus the full column for every row once the table has a patch part (more bytes than a plain scan)
- Dominant language
- C++
- Stars
- 49.9k
- Forks
- 9k
- Avg merge
- 21h 32m
- Merged PRs (30d)
- 515
Description
### Company or project name
_No response_
### Describe what's wrong
`vector_search_use_quantized_codes = 1` is implemented on top of lazy materialization: read `vec.quantized` for all rows, sort, then `LazilyReadFromMergeTree` fetches the full-precision `vec` only for the shortlist.
Since #102904 lazy materialization is disabled while a table has patch parts (see #116120). But the quantized rewrite still fires in that state. The plan keeps the `quantized shortlist` steps, loses the lazy step, and `ReadFromMergeTree` now reads **both** `vec.quantized` and the full `vec` for every row. So the query reads more bytes than the plain scan it was supposed to replace.
One lightweight `UPDATE` of one row in an unrelated column is enough to trigger it, and `OPTIMIZE TABLE ... FINAL` / `ALTER TABLE ... APPLY PATCHES` did not clear the patch part for me.
### Does it reproduce on the most recent release?
Yes: 26.8.3.105 (`clickhouse/clickhouse-server:latest`), also 26.8.1.2041.
### How to reproduce
```sql
SET enable_quantized_codec = 1;
SET allow_experimental_lightweight_update = 1;
CREATE TABLE repro
(
id UInt32,
status UInt8 DEFAULT 1,
vec Array(BFloat16) CODEC(Quantized('rabitq', 1024, 0))
)
ENGINE = MergeTree ORDER BY id
SETTINGS index_granularity = 256, enable_block_number_column = 1, enable_block_offset_column = 1;
INSERT INTO repro (id, vec)
SELECT number, arrayMap(i -> toBFloat16(randCanonical() - 0.5), range(1024)) FROM numbers(20000);
-- A: plain
SELECT id FROM repro ORDER BY cosineDistance(vec, (SELECT vec FROM repro WHERE id = 123)) LIMIT 5
SETTINGS log_comment = 'A' FORMAT Null;
-- B: codes, no patch parts yet
SELECT id FROM repro ORDER BY cosineDistance(vec, (SELECT vec FROM repro WHERE id = 123)) LIMIT 5
SETTINGS vector_search_use_quantized_codes = 1, vector_search_index_fetch_multiplier = 1, log_comment = 'B' FORMAT Null;
UPDATE repro SET status = 2 WHERE id = 7; -- one patch part
-- C: codes, with the patch part
SELECT id FROM repro ORDER BY cosineDistance(vec, (SELECT vec FROM repro WHERE id = 123)) LIMIT 5
SETTINGS vector_search_use_quantized_codes = 1, vector_search_index_fetch_multiplier = 1, log_comment = 'C' FORMAT Null;
-- D: same as C, patches ignored (control)
SELECT id FROM repro ORDER BY cosineDistance(vec, (SELECT vec FROM repro WHERE id = 123)) LIMIT 5
SETTINGS vector_search_use_quantized_codes = 1, vector_search_index_fetch_multiplier = 1, apply_patch_parts = 0, log_comment = 'D' FORMAT Null;
SYSTEM FLUSH LOGS;
SELECT log_comment, round(read_bytes / 1048576, 1) AS read_mib, read_rows
FROM system.query_log WHERE type = 'QueryFinish' AND log_comment IN ('A', 'B', 'C', 'D')
ORDER BY event_time_microseconds;
```
Result on 26.8.3.105:
| | read_mib | read_rows |
|---|---:|---:|
| A plain | 39.6 | 20512 |
| B codes, no patch part | 3.0 | 20512 |
| C codes, one patch part | **42.1** | 20256 |
| D codes, `apply_patch_parts = 0` | 3.0 | 20512 |
`EXPLAIN PLAN header = 1` for C: the `Limit (quantized shortlist limit)` / `Sorting (quantized shortlist sort)` steps are there, `JoinLazyColumnsStep` / `LazilyReadFromMergeTree` are gone, and the `ReadFromMergeTree` header contains both `vec.quantized FixedString(132)` and `vec Array(BFloat16)`. For B the read step header is `_part_offset, vec.quantized` only.
### Expected behavior
Either the rewrite should not fire when the lazy step cannot be inserted (fall back to the plain scan, 39.6 MiB), or the two-stage read should work without lazy materialization. Reading codes plus the full column for every row is the worst of both.
### Additional context
Same shape as #114762 (full-precision column read next to the quantized subcolumn), different trigger. Root limitation is #102904 / #116120; this report is only about the quantized-codes rewrite not accounting for it.
Contributor guide
Research direction
Start by reproducing the B, C, and D queries and comparing EXPLAIN PLAN header = 1 with system.query_log read_bytes. Trace the quantized-codes rewrite alongside the patch-part condition that removes JoinLazyColumnsStep and LazilyReadFromMergeTree. Done means the rewrite no longer reads both vec.quantized and full vec for every row when lazy materialization is unavailable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, sql
- Domain
- databases, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100