Skip to content

LOGICAL_ERROR header mismatch after PREWHERE pushdown on ReplacingMergeTree FINAL queries #105521

@nickpresta

Description

@nickpresta

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.

Metadata

Metadata

Labels

bugConfirmed user-visible misbehaviour in official release

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions