Iceberg `OPTIMIZE` sums all live files into `added-records`: `count()` inflated
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 55/100
Research direction
The summation bug is at src/Storages/ObjectStorage/DataLakes/Iceberg/Compaction.cpp:1117-1126, passed to MetadataGenerator.cpp:91. Read compactIcebergTable and setSnapshotTotals to see how snapshot_id_to_data_files accumulates. Change added_records to a delta of new files vs parent and widen to 64-bit. Done when the reproducer's trivial count() returns 2 after OPTIMIZE.
Written by the indexing model from the issue text.
Description
Describe what's wrong
- After
OPTIMIZEon an Iceberg table, trivialcount()returns more rows than a full scan. - Trigger: two consecutive appends plus a position delete, e.g. INSERT 2 rows, INSERT 1 row, DELETE 1 row, then
OPTIMIZE. - Trivial
count()should return 2 but returns 3, while the full scan correctly returns 2. - The wrong
total-recordsis persisted in metadata, sosystem.tables.total_rowsand every later trivial count stay wrong until the next rewrite.
Root cause: In Compaction.cpp:1117-1126 writeMetadataFiles sums new_records_count over every live file of the snapshot (snapshot_id_to_data_files) and passes it as added_records. setSnapshotTotals (MetadataGenerator.cpp:91) then adds it to the rewritten parent's total-records, so totals accumulate (1 + 2 = 3).
Analysis details (evidence, affected locations, impact)
Why we believe this is a bug: IcebergMetadata::optimize (IcebergMetadata.cpp:622) -> compactIcebergTable -> writeMetadataFiles (Compaction.cpp:1117-1126) -> MetadataGenerator::generateNextMetadata -> setSnapshotTotals (MetadataGenerator.cpp:91,100). The trivial count reads total-records back in IcebergMetadata::totalRows (IcebergMetadata.cpp:1751).
Affected locations:
src/Storages/ObjectStorage/DataLakes/Iceberg/Compaction.cpp:1117—total_records_count= rows of every live file of the snapshot, not the rows it addedsrc/Storages/ObjectStorage/DataLakes/Iceberg/Compaction.cpp:1126— passed asadded_recordstogenerateNextMetadatasrc/Storages/ObjectStorage/DataLakes/Iceberg/Compaction.cpp:297—snapshot_id_to_data_filescollects every file listed by the snapshot's manifestssrc/Storages/ObjectStorage/DataLakes/Iceberg/MetadataGenerator.cpp:91—total-records= parent total +added_records
Impact: Silent wrong count() on an Iceberg table after OPTIMIZE (allow_experimental_iceberg_compaction = 1, open-source builds). Any history with two consecutive append snapshots and at least one position delete hits it: with N appends the reported total is the sum of all prefix totals. added-records/total-records in the snapshot summaries are wrong for every reader of the metadata.
Does it reproduce on most recent release?
Confirmed on master (dcccc2b5920c); not checked on a release.
How to reproduce
Requires allow_insert_into_iceberg = 1, mutations_sync = 2 and allow_experimental_iceberg_compaction = 1; if the parent snapshot is delete-only the bug does not show.
# - no-fasttest: requires `IcebergLocal` (USE_AVRO build option)
# Trivial count() must match a full scan after OPTIMIZE of a table with two consecutive appends and a DELETE.
TABLE="t_default_${RANDOM}"
TABLE_PATH="/var/lib/clickhouse/user_files/${TABLE}/"
trap 'rm -rf "${TABLE_PATH}" 2>/dev/null' EXIT
clickhouse-client --query "CREATE TABLE ${TABLE} (x Int64) ENGINE = IcebergLocal('${TABLE_PATH}', 'Parquet')"
clickhouse-client --allow_insert_into_iceberg=1 --query "INSERT INTO ${TABLE} VALUES (1), (2)"
clickhouse-client --allow_insert_into_iceberg=1 --query "INSERT INTO ${TABLE} VALUES (3)"
clickhouse-client --allow_insert_into_iceberg=1 --mutations_sync=2 --query "ALTER TABLE ${TABLE} DELETE WHERE x = 1"
clickhouse-client --allow_experimental_iceberg_compaction=1 --query "OPTIMIZE TABLE ${TABLE}"
clickhouse-client --query "SELECT count() FROM ${TABLE} SETTINGS optimize_trivial_count_query = 1"
clickhouse-client --query "SELECT count() FROM ${TABLE} SETTINGS optimize_trivial_count_query = 0"
clickhouse-client --query "DROP TABLE ${TABLE} SYNC"
Verified: this block reproduces on current master (dcccc2b5920c) when pasted as is.
Expected behavior
Trivial count() after OPTIMIZE returns 2, matching the full scan.
Expected output of the reproducer above:
2
2
Error message and/or stacktrace
Trivial count() returns 3 because the snapshot's total-records counts the parent's rows twice.
Actual output of the reproducer above on master (dcccc2b5920c):
3
2
Suggested fix
Pass a real delta: the rows of the files the snapshot added (files not present in its parent), or total(snapshot) - total(rewritten parent). Make the accumulator 64-bit while there: Int32 total_records_count truncates once a snapshot holds more than 2^31 rows.
Additional context
Open risks:
Int32 total_records_count(Compaction.cpp:1117) also truncates the per-snapshot sum ofUInt64row counts past 2^31 rows, same lines.- Not introduced by #116941: the same summation and parent accumulation are in v26.3.9.8-lts and v26.8.9.10-lts. #116941 only lets
OPTIMIZEthrough on more tables; its own new mixed-format test has the append, append, delete shape and checks rows with a full scan, so it does not see the wrong count. - A test merged into the suite needs the
IS_CLOUDhandling used by04513/04815: on the Cloud buildOPTIMIZEdoes not run this code and reports an exception instead.
Found during automated review of PR #116941; the bug predates that PR (it reproduces on the master build just before it merged), so the introducing change is not identified yet. Severity P1 · Finding h_pr116941_001
- Dominant language
- C++
- Stars
- 50.6k
- Forks
- 9.2k
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 524
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ClickHouse/ClickHouse
-
ai_p3 comp-promql
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
ClickHouse/ClickHouse#128448 ·
Maintainers usually reply within 1 day
-
ai_p1 comp-view external potential bug
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
ClickHouse/ClickHouse#128410 ·
Maintainers usually reply within 1 day
-
ai_p3 comp-sql-syntax
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
ClickHouse/ClickHouse#128399 · 2 comments ·
Maintainers usually reply within 1 day
-
comp-documentation external
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
ClickHouse/ClickHouse#128350 ·
Maintainers usually reply within 1 day
-
Paimon `DATE` is read as `Date` instead of `Date32`: dates before 1970 or after 2149 fail with VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPEPossibly taken @groeneai claimed this 2 days ago. Openai_p2 bug comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
ClickHouse/ClickHouse#128306 · 3 comments ·
Maintainers usually reply within 1 day
All issues in ClickHouse/ClickHouse
Similar issues
-
Issue-Bug Needs-Triage
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
microsoft/winget-cli#6645 ·
Maintainers usually reply within 2 days
-
cleanup help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
ESPresense/ESPresense#2602 ·
Maintainers usually reply within 1 day
-
devex good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
artifact:simulation-scenario domain:physics good first issue help wanted role:validation testing
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 3 days