Search before asking
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?
Code of Conduct
Search before asking
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.
__DORIS_BINLOG_TSO__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__0ConstantColumnReaderbuilt inSegment::new_iterator/new_column_iterator,be/src/storage/segment/segment.cpp:422-437,:947-966__DORIS_VERSION_COL__0(defaultZERO_NUMBER)SegmentIterator::_replace_version_col_if_needed,be/src/storage/segment/segment_iterator.cpp:2449The
ColumnPredicatepaths already handle the two TSO columns.Segment::new_iteratorsynthesises{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 onlySegment::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 returnskNoMatchbefore looking at any bound:be/src/exprs/function/functions_comparison.h:337-338,:380-382, andbe/src/exprs/expr_zonemap_filter.cpp:498forIS 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, includingColumnPredicate, and it is reachable from plain SQL: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).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.computeOutputbuilds fromgetBaseSchema(true),fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java:826). Existing regression cases already name it directly.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 isREPLACEand stays above the merge._read_columns_by_indexthen_replace_version_col_if_neededthen_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>withn != 0returns 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:The two TSO columns need a pushed common expression on them rather than a
ColumnPredicate;IS NULL/IS NOT NULLqualify 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()(or0when it is-1) for the TSO columns,version.secondfor the version column, both always non-null. Note that the comment atbe/src/storage/segment/segment_iterator.cpp:2478claims 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 theColumnPredicatepath already uses atbe/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_binlogreads (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?
Code of Conduct