LuciferYang opened a new issue, #67995:
URL: https://github.com/apache/doris/issues/67995

   ### Search before asking
   
   - [X] I had searched in the issues and found no similar issues.
   
   ### Version
   
   master, 846d9b2ebfc.
   
   ### 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:
   
   ```sql
   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?
   
   - [X] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [X] I agree to follow this project's [Code of 
Conduct](https://www.apache.org/foundation/policies/conduct)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to