Company or project name
Shopify
Describe what's wrong
A ReplacingMergeTree(version_column) query using FINAL can throw LOGICAL_ERROR when optimize_move_to_prewhere_if_final = 1 and/or unused-column removal is enabled.
Fiddle: https://fiddle.clickhouse.com/70439e18-db46-4dbb-8b68-db522ab6cc32
The issue appears when PREWHERE pushdown runs on a FINAL read and the read step keeps an extra version/sort-key column required for FINAL merging, while the parent step has already removed that column. The adjacent plan headers then differ and ClickHouse throws:
Input-output header mismatch after removing unused columns after pushing down filters to prewhere
Does it reproduce on the most recent release?
Yes
How to reproduce
This happens on LTS 26.3, version 26.4.3.37, and HEAD (26.5.1)
Non-default settings: final = 1, optimize_move_to_prewhere_if_final = 1
DROP TABLE IF EXISTS t SYNC;
CREATE TABLE t (
event_timestamp DateTime64(3),
some_bool Bool
)
ENGINE = ReplacingMergeTree(event_timestamp)
ORDER BY (event_timestamp);
INSERT INTO t SELECT
toDateTime64('2026-01-01 00:00:00', 3) + INTERVAL number MINUTE,
number % 2 = 0
FROM numbers(5000);
WITH t AS (
SELECT *, toTimeZone(event_timestamp, 'UTC') AS event_timestamp
FROM t
WHERE event_timestamp BETWEEN TIMESTAMP '2026-01-01T00:00:00'
AND TIMESTAMP '2026-12-31T23:59:59'
)
SELECT
some_bool,
count()
FROM t
GROUP BY some_bool
SETTINGS final = 1, optimize_move_to_prewhere_if_final = 1;
Expected behavior
The query should complete successfully and return two grouped rows, each with count 2500.
Error message and/or stacktrace
Code: 49. DB::Exception. (LOGICAL_ERROR), Stack trace (when copying this message, always include the lines below):
0. DB::Exception::Exception(DB::Exception::MessageMasked&&, int, bool) @ 0x00000000164a732a
1. DB::Exception::Exception(String const&, int, String, bool) @ 0x00000000151d684e
2. DB::readException(DB::ReadBuffer&, String const&, bool) @ 0x0000000016624d53
3. DB::Connection::receiveException() const @ 0x000000001c7c469f
4. DB::Connection::receivePacket() @ 0x000000001c7d102d
5. DB::ClientBase::receiveAndProcessPacket(boost::intrusive_ptr<DB::IAST>, bool) @ 0x000000001c782ebd
6. DB::ClientBase::receiveResult(boost::intrusive_ptr<DB::IAST>, int, bool) @ 0x000000001c7822e7
7. DB::ClientBase::processParsedSingleQuery(std::basic_string_view<char, std::char_traits<char>>, boost::intrusive_ptr<DB::IAST>, bool&, unsigned long) @ 0x000000001c77e5b7
8. DB::ClientBase::executeMultiQuery(String const&) @ 0x000000001c78bbe1
9. DB::ClientBase::processQueryText(String const&) @ 0x000000001c78e01c
10. DB::ClientBase::runNonInteractive() @ 0x000000001c799bab
11. DB::Client::main(std::vector<String, std::allocator<String>> const&) @ 0x00000000167b1154
12. Poco::Util::Application::run() @ 0x00000000221d8bb1
13. mainEntryClickHouseClient(int, char**) @ 0x00000000167c30fc
14. main @ 0x000000000f80bc20
15. __pow_finite @ 0x0000000000029d90
16. __libc_start_main @ 0x0000000000029e40
17. _start @ 0x000000000892a4ae
Received exception from server (version 26.4.3):
Code: 49. DB::Exception: Received from localhost:9000. DB::Exception: Input-output header mismatch after removing unused columns after pushing down filters to prewhere. Input header: some_bool Bool UInt8(size = 0), output header: event_timestamp DateTime64(3) DateTime64(size = 0), some_bool Bool UInt8(size = 0). (LOGICAL_ERROR)
(query: WITH t AS (
SELECT *, toTimeZone(event_timestamp, 'UTC') AS event_timestamp
FROM t
WHERE event_timestamp BETWEEN TIMESTAMP '2026-01-01T00:00:00'
AND TIMESTAMP '2026-12-31T23:59:59'
)
SELECT
some_bool,
count()
FROM t
GROUP BY some_bool
SETTINGS final = 1, optimize_move_to_prewhere_if_final = 1;)
Additional context
This looks related to the optimizer path introduced in #89982 and to the in-flight position-based unused-column removal work in #100586.
/cc @antaljanosbenjamin as someone with more context on this work
Workarounds include:
SETTINGS optimize_move_to_prewhere_if_final = 0
and/or
SETTINGS query_plan_remove_unused_columns = 0
The second workaround turns off the new feature, but the first is narrower for this specific FINAL + PREWHERE path. It isn't clear (to me) which is preferred in which scenarios.
Company or project name
Shopify
Describe what's wrong
A
ReplacingMergeTree(version_column)query usingFINALcan throwLOGICAL_ERRORwhenoptimize_move_to_prewhere_if_final = 1and/or unused-column removal is enabled.Fiddle: https://fiddle.clickhouse.com/70439e18-db46-4dbb-8b68-db522ab6cc32
The issue appears when
PREWHEREpushdown runs on aFINALread and the read step keeps an extra version/sort-key column required forFINALmerging, while the parent step has already removed that column. The adjacent plan headers then differ and ClickHouse throws:Input-output header mismatch after removing unused columns after pushing down filters to prewhereDoes it reproduce on the most recent release?
Yes
How to reproduce
This happens on LTS 26.3, version
26.4.3.37, andHEAD(26.5.1)Non-default settings:
final = 1, optimize_move_to_prewhere_if_final = 1Expected behavior
The query should complete successfully and return two grouped rows, each with count 2500.
Error message and/or stacktrace
Additional context
This looks related to the optimizer path introduced in #89982 and to the in-flight position-based unused-column removal work in #100586.
/cc @antaljanosbenjamin as someone with more context on this work
Workarounds include:
and/or
The second workaround turns off the new feature, but the first is narrower for this specific FINAL + PREWHERE path. It isn't clear (to me) which is preferred in which scenarios.