Skip to content

[Bug] [High] MIN_DELTA stream applies value predicates before UPDATE_BEFORE reconstruction #65909

Closed

Description

@yujun777

Summary

MIN_DELTA row-binlog streams return incorrect results when a predicate references a non-key value column that changes in an update. The issue is independent of IVM and can be reproduced by querying a stream directly.

For an update from region = 'east' to region = 'north':

  • an unfiltered MIN_DELTA stream returns both UPDATE_BEFORE(east) and UPDATE_AFTER(north);
  • WHERE region = 'east' returns no rows, although it should return the before row;
  • WHERE region = 'north' returns only the after row, as expected after filtering the reconstructed change rows.

Environment

  • Local non-cloud deployment: 1 FE + 1 BE, replication factor 1.
  • FE: doris-0.0.0-cbe6751fc3f; BE: doris-0.0.0-b9b4f89b7c8.
  • Source checkout used for investigation: a4dae0b64e9e7c6db5983b566656442f1ae2bc5d.

Minimal reproduction

DROP DATABASE IF EXISTS stream_predicate_pushdown_probe;
CREATE DATABASE stream_predicate_pushdown_probe;
USE stream_predicate_pushdown_probe;

CREATE TABLE events (
    id BIGINT NOT NULL,
    region VARCHAR(16) NOT NULL,
    amount BIGINT NOT NULL
)
UNIQUE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
    'replication_num' = '1',
    'enable_unique_key_merge_on_write' = 'true',
    'binlog.enable' = 'true',
    'binlog.format' = 'ROW',
    'binlog.need_historical_value' = 'true'
);

INSERT INTO events VALUES (1, 'east', 10);

-- Use independent streams so each query has the same initial offset.
CREATE STREAM s_all ON TABLE events
PROPERTIES ('type' = 'min_delta', 'show_initial_rows' = 'false');
CREATE STREAM s_east ON TABLE events
PROPERTIES ('type' = 'min_delta', 'show_initial_rows' = 'false');
CREATE STREAM s_north ON TABLE events
PROPERTIES ('type' = 'min_delta', 'show_initial_rows' = 'false');

UPDATE events SET region = 'north', amount = 20 WHERE id = 1;

SELECT id, region, amount, __DORIS_STREAM_CHANGE_TYPE_COL__ AS change_type
FROM s_all
ORDER BY change_type;

SELECT id, region, amount, __DORIS_STREAM_CHANGE_TYPE_COL__ AS change_type
FROM s_east
WHERE region = 'east'
ORDER BY change_type;

SELECT id, region, amount, __DORIS_STREAM_CHANGE_TYPE_COL__ AS change_type
FROM s_north
WHERE region = 'north'
ORDER BY change_type;

SELECT stream_name, consumption_status, lag, last_consumption_time
FROM information_schema.table_stream_consumption
WHERE db_name = 'stream_predicate_pushdown_probe'
ORDER BY stream_name;

Observed result

The unfiltered stream correctly returns:

1 | north | 20 | UPDATE_AFTER
1 | east  | 10 | UPDATE_BEFORE

s_east WHERE region = 'east' returns zero rows. It should return:

1 | east | 10 | UPDATE_BEFORE

s_north WHERE region = 'north' returns only:

1 | north | 20 | UPDATE_AFTER

The three streams retain their initial consumption offsets and have positive lag after the SELECT queries, so the behavior is not caused by stream consumption.

Expected result

The predicate must be evaluated against each reconstructed change row. UPDATE_BEFORE should use its historical values and UPDATE_AFTER should use its new values. Therefore WHERE region = 'east' must return the before row.

Investigation

The row-binlog record stores the new value in the normal region column and the historical value in __BEFORE__region__. Storage predicates are currently pushed down before BlockReader::_min_delta_next_block() reconstructs UPDATE_BEFORE rows from the __BEFORE__* mirrors. As a result, region = 'east' filters the raw after-image (north) before the before-image can be produced.

Relevant paths:

  • be/src/exec/scan/olap_scanner.cpp: _init_tablet_reader_params() copies slot predicates into storage predicates.
  • be/src/storage/iterator/block_reader.cpp: _min_delta_next_block() later reconstructs before/after rows.

Suggested direction: for row-binlog MIN_DELTA and DETAIL scans, retain TSO-range filtering but do not push down user value-column predicates, function filters, or common expression filters before change-row reconstruction. They should be evaluated by the upper scan/filter layer after reconstruction. A later optimization could safely push a predicate only when it is evaluated against both before and after images.

Scope

This is a direct stream/binlog reproduction and does not require IVM, complete refresh, or MV refresh. It causes IVM filtered incremental refreshes to retain rows when a source row stops matching a filter.

Related issues: #65265, #65418.

Activity

  1. changed the title [-][Bug] MIN_DELTA stream applies value predicates before UPDATE_BEFORE reconstruction[/-] [+][Bug] [High] MIN_DELTA stream applies value predicates before UPDATE_BEFORE reconstruction[/+] on Jul 22, 2026
  2. zclllyybb commented on Jul 22, 2026

    @zclllyybb
    Contributor

    Breakwater-GitHub-Analysis-Slot: slot_047e3257d370
    This content is generated by AI for reference only.

    I checked the report against the issue commit a4dae0b64e9e7c6db5983b566656442f1ae2bc5d and refreshed upstream/master (ab043a433cef0246bd81fbe76011a83b9a13d17b). The reported diagnosis looks valid.

    The current BE scan path can evaluate the region = 'east' predicate too early:

    • OlapScanner::_init_tablet_reader_params() copies slot predicates into _tablet_reader_params.predicates, copies function filters, and keeps common-expression pushdown before the row-binlog change rows are materialized.
    • TabletReader::_init_conditions_param() classifies a non-key column predicate such as region = 'east' as a value predicate.
    • For a UNIQUE KEY merge-on-write table, BetaRowsetReader::_should_push_down_value_predicates() can return true through enable_unique_key_merge_on_write, so BetaRowsetReader::get_segment_iterators() installs that value predicate into StorageReadOptions.column_predicates.
    • SegmentIterator applies those storage predicates to the raw row-binlog row. At that point the normal region column is the after image (north), while the historical value is still in __BEFORE__region__.
    • BlockReader::_min_delta_next_block() reconstructs UPDATE_BEFORE only later by resolving regular columns to their __BEFORE__* mirrors when emitting the before half of an update.

    So WHERE region = 'east' is evaluated against the raw after-image row and can discard the row before UPDATE_BEFORE(east) is constructed. This is a row-binlog scan correctness bug, not an IVM refresh-only issue and not a stream-consumption-offset issue.

    Recommended fix direction:

    • For row-binlog scans that materialize change rows (MIN_DELTA, and the same class of risk applies to DETAIL), do not push mutable user value-column predicates, function filters, or common-expression filters into the storage/segment layer before reconstruction.
    • Keep only predicates that are proven safe before reconstruction, such as the TSO range filter and any other before/after-invariant predicate after explicit validation.
    • Ensure the fenced predicates are still evaluated after _min_delta_next_block() / _detail_change_next_block() has produced the final change rows. If any planner/scan path currently treats a pushed predicate as storage-only, it should be kept or reintroduced as an upper filter for table streams.
    • Add a regression test using the direct stream reproduction from this issue, preferably covering both MIN_DELTA and DETAIL: update a non-key value column from east to north, then verify WHERE region = 'east' returns the reconstructed UPDATE_BEFORE row and WHERE region = 'north' returns the UPDATE_AFTER row.

    Missing information is small for root-cause triage because the issue already includes a minimal SQL reproduction and observed output. For release/backport decisions, maintainers still need to identify the first affected commit or affected release branches. If the regression test behaves differently in CI, the useful follow-up artifacts would be EXPLAIN VERBOSE for the filtered stream query and BE scan logs showing which predicates were pushed into the row-binlog tablet reader.

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