Skip to content

[Bug] Zone-map pruning trusts placeholder summaries for the hidden TSO and version columns, dropping matching rows #67995

Description

@LuciferYang

Search before asking

  • I had searched in the issues and found no similar issues.

Version

master, 846d9b2.

What's Wrong?

Three hidden columns store a placeholder on disk and get their real value substituted at read time, so their zone maps describe the placeholder rather than the values rows come back with. Zone-map pruning that trusts those summaries drops rows that match.

column on disk substituted at read time by
__DORIS_BINLOG_TSO__ all NULL (ColumnNullable::insert_many_defaults, be/src/storage/transform/row_binlog_derive.cpp:126-129) SegmentIterator::_update_tso_col_if_needed, be/src/storage/segment/segment_iterator.cpp:2476
__DORIS_COMMIT_TSO_COL__ 0 a ConstantColumnReader built in Segment::new_iterator / new_column_iterator, be/src/storage/segment/segment.cpp:422-437, :947-966
__DORIS_VERSION_COL__ 0 (default ZERO_NUMBER) SegmentIterator::_replace_version_col_if_needed, be/src/storage/segment/segment_iterator.cpp:2449

The ColumnPredicate paths already handle the two TSO columns. Segment::new_iterator synthesises {min = max = commit_tso, has_not_null = true} for the binlog TSO column and prunes against the real value (be/src/storage/segment/segment.cpp:449-465), the page-level loop skips it (be/src/storage/segment/segment_iterator.cpp:1153), and the commit TSO column is covered by the constant reader above.

The expression zone-map paths have neither guard. build_segment_zonemap_context (be/src/storage/segment/segment.cpp:94-145) and the page-level expression loop (be/src/storage/segment/segment_iterator.cpp:3406-3415) check only Segment::can_apply_predicate_safely, which is about variant target types and returns true here.

For the binlog TSO column the failure is stronger than a wrong bound. Its on-disk zone map has has_not_null = false, and every evaluator treats that as "the comparison is NULL on every row, so no row can be TRUE" and returns kNoMatch before looking at any bound: be/src/exprs/function/functions_comparison.h:337-338, :380-382, and be/src/exprs/expr_zonemap_filter.cpp:498 for IS NOT NULL. So one comparison on that column prunes the whole single-version binlog segment, for any operator and any other operand, even though after substitution the rows carry a non-null TSO.

__DORIS_VERSION_COL__ has no guard on any path, including ColumnPredicate, and it is reachable from plain SQL:

  • It exists on effectively every unique-key table: added when Config.enable_hidden_version_column_by_default && keysType == UNIQUE_KEYS && !hasSeqMapping (fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java:1070-1076), and that config defaults to true (fe/fe-common/src/main/java/org/apache/doris/common/Config.java:1500).
  • It is invisible, but invisibility only suppresses star expansion (fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java:436-441); binding an explicitly named slot has no visibility check, and it is in the scan output (LogicalOlapScan.computeOutput builds from getBaseSchema(true), fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java:826). Existing regression cases already name it directly.
  • On merge-on-write its aggregation is NONE (CreateTableInfo.java:1071), so a predicate on it lands in the storage-layer set rather than the post-merge one (TabletReader::_init_column_predicates, be/src/storage/tablet/tablet_reader.cpp:389-402) and reaches zone-map pruning. On merge-on-read it is REPLACE and stays above the merge.
  • The substitution happens before predicate evaluation, so the two disagree: _read_columns_by_index then _replace_version_col_if_needed then _evaluate_vectorization_predicate (be/src/storage/segment/segment_iterator.cpp:2959, :2960, :2978). Row evaluation sees the real version while the zone map says [0, 0].

So on a unique merge-on-write table, WHERE __DORIS_VERSION_COL__ = <n> with n != 0 returns nothing on single-version rowsets, while the rows it asks for exist and row-level evaluation would have matched them.

What You Expected?

A column whose stored value is a placeholder either does not drive zone-map pruning, or drives it through a summary of the value rows will actually have. Pruning and row-level evaluation should not disagree about the same predicate.

How to Reproduce?

The version column case is plain SQL on a unique merge-on-write table with n != 0, comparing results with pruning on and off:

CREATE TABLE t (k INT, v INT) UNIQUE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES ("replication_num" = "1", "enable_unique_key_merge_on_write" = "true");
INSERT INTO t VALUES (1, 1);
INSERT INTO t VALUES (2, 2);
SELECT k, __DORIS_VERSION_COL__ FROM t;              -- shows the real versions
SELECT count(*) FROM t WHERE __DORIS_VERSION_COL__ = <a version from above>;

The two TSO columns need a pushed common expression on them rather than a ColumnPredicate; IS NULL / IS NOT NULL qualify today (be/src/exprs/function/is_null.h:67, be/src/exprs/function/is_not_null.h:69), and column-vs-column comparison would once #67774 lands, which is what surfaced this.

I have not run the SQL above; it is derived from the code path, and the aggregation-type split means it should reproduce on merge-on-write and not on merge-on-read.

Anything Else?

The substituted values are known exactly where the builders run, so the fix can synthesise a real summary rather than only skip: commit_tso.end_tso() (or 0 when it is -1) for the TSO columns, version.second for the version column, both always non-null. Note that the comment at be/src/storage/segment/segment_iterator.cpp:2478 claims the physical-time part of the commit TSO is used; the code inserts the raw value (:2493, :2500-2503, :2511-2513, :2525), matching the formula the ColumnPredicate path already uses at be/src/storage/segment/segment.cpp:456-461. The comment is stale and misleading.

Routing the binlog TSO column through the constant-reader cache instead would be wrong: its substitution only happens for read_row_binlog reads (be/src/storage/segment/segment_iterator.cpp:2483-2485), and a cached constant reader would then be served to non-binlog reads of the same segment. That cache has a separate problem, filed as #67994.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions