Skip to content

[Bug] Sequence column: base/full compaction drops delete-sign rows, so a later write with a smaller sequence value brings a deleted row back #69409

Open

Description

@hanke580

Search before asking

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

Version

  • master 15eb3f4 (2026-10-18), built from source (build.sh --fe --be), 1 FE + 1 BE
  • 4.1.5 (official apache/doris:fe-4.1.5 / be-4.1.5 images, build 917b20b): same result

What's Wrong?

On a UNIQUE KEY table with function_column.sequence_col, a delete (a row with __DORIS_DELETE_SIGN__ = 1 and sequence 900) correctly rejects a later write of the same key with a smaller sequence value (500): the key stays deleted. If a base or full compaction runs between the delete and the late write, the same late write is stored and the deleted key comes back.

Base and full compaction remove rows with the delete sign (enable_prune_delete_sign_when_base_compaction, default true). For a table with a sequence column, the delete-sign row is the only record that the key was deleted at sequence 900. Once it is gone, nothing is left to compare the late write against. So the result of the same writes depends on whether a background compaction happened to run in between. Merge-on-write and merge-on-read tables both behave this way, and so does a plain DELETE FROM ... WHERE statement.

This breaks the out-of-order guarantee that the sequence column exists for (for example, CDC where a delete and an older update arrive out of order). #17739 shows the intended behaviour: after a delete, a re-insert with a smaller sequence value is ignored, and a maintainer confirmed that this is expected.

What You Expected?

Key 1 stays deleted whether or not a compaction ran, as it does without the compaction. Compaction should not change query results.

How to Reproduce?

1 FE + 1 BE, default configuration, any MySQL client:

CREATE DATABASE seqdel; USE seqdel;

-- run once with t_plain and once with t_compacted
CREATE TABLE t_compacted (k INT, v VARCHAR(10), s BIGINT)
UNIQUE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES ('replication_num' = '1',
            'enable_unique_key_merge_on_write' = 'true',   -- 'false' (merge-on-read) gives the same result
            'function_column.sequence_col' = 's',
            'disable_auto_compaction' = 'true');           -- only to make the timing deterministic

INSERT INTO t_compacted VALUES (1, 'a', 100), (2, 'b', 100);
INSERT INTO t_compacted (k, v, s, __DORIS_DELETE_SIGN__) VALUES (1, NULL, 900, 1);  -- delete k=1 at seq 900
SELECT k, v, s FROM t_compacted ORDER BY k;          -- (2,'b',100)

-- only for t_compacted: full compaction, then wait until the tablet has a single rowset
-- (SHOW TABLETS FROM t_compacted -> CompactionStatus URL, "last full success time" set)
ADMIN COMPACT TABLE t_compacted PARTITION t_compacted WHERE type = 'full';

INSERT INTO t_compacted VALUES (1, 'late', 500);     -- older than the delete, must be ignored
SELECT k, v, s FROM t_compacted ORDER BY k;

Observed on master:

                          after the delete    after the late INSERT (seq 500)
mow, no compaction        (2,'b',100)         (2,'b',100)                       <- correct
mow, full compaction      (2,'b',100)         (1,'late',500), (2,'b',100)       <- k=1 is back
mor, no compaction        (2,'b',100)         (2,'b',100)                       <- correct
mor, full compaction      (2,'b',100)         (1,'late',500), (2,'b',100)       <- k=1 is back

After the full compaction the tablet has one rowset [0-3] with 1 row: the delete-sign row for k=1 is gone. With skip_delete_sign, skip_delete_bitmap and skip_storage_engine_merge set to true, the physical rows are (1,'a',100,0), (1,NULL,900,1), (2,'b',100,0) before the compaction and only (2,'b',100,0) after it.

Base compaction gives the same result. Sequence: two loads and a full compaction (base rowset [0-3]), then the delete-sign row, 5 more loads, type = 'cumulative', then type = 'base' (new base rowset [0-8]). The late write is then stored.

A plain DELETE statement on a merge-on-write table gives the same result. Insert (27,'old',30), run DELETE FROM t WHERE k = 27, then INSERT (27,'new',7). Without a compaction in between, the INSERT reports VISIBLE but is ignored (sequence 7 < 30). After a full compaction, the row (27,'new',7) appears.

The same thing happens with stream load merge_type=DELETE and columns: k,s.

Anything Else?

Likely cause (master 15eb3f4):

  • be/src/storage/tablet/tablet_reader.cpp:457-461 (TabletReader::_init_delete_condition): _delete_sign_available is true for READER_BASE_COMPACTION / READER_FULL_COMPACTION when config::enable_prune_delete_sign_when_base_compaction (be/src/common/config.cpp:1987, default true). There is no check for a sequence column. The compaction readers then drop the delete-sign rows (be/src/storage/iterator/vertical_block_reader.cpp:530, block_reader.cpp:850).
  • Merge-on-write: the publish of the late write (BaseTablet::calc_segment_delete_bitmap -> lookup_row_key, be/src/storage/tablet/base_tablet.cpp:518, :648) no longer finds a row with a larger sequence value for the key, so the late row is not marked deleted. Merge-on-read: the read-time merge by sequence has no delete row to compare with.

A possible fix: for tables with a sequence column, keep the newest delete-sign row of each key in base/full compaction, or keep the (key, sequence) of deleted keys. Pruning would still be safe for tables without a sequence column.

Related but different: #48241 added the config that controls the pruning of delete-sign rows in base compaction, and #51874 extended the pruning to full compaction. Neither mentions sequence columns. #59441 (closed, not merged) proposed a time condition for that pruning. #24011 and #26721 cover delete bitmaps for delete-sign rows without a sequence column. #32574 and #29468 cover delete sign plus sequence column in partial update and replica consistency. #17739 shows the expected behaviour without compaction.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Activity

  1. Doris-Breakwater commented on Oct 21, 2026

    @Doris-Breakwater
    Collaborator

    Breakwater-GitHub-Analysis-Slot: slot_d814bb63ac39

    The report warrants storage/compaction correctness triage: source inspection supports the proposed loss of the deleted key's sequence boundary. The important distinction is that compaction need not change the immediate SELECT result; it changes which subsequent writes are accepted for that key.

    Evidence and confidence

    • Reported runtime behavior: the supplied reproduction demonstrates resurrection after base/full compaction for MOW and MOR, on master 15eb3f4e432b1d4a39f89457956135da80b90940 and 4.1.5 build 917b20be8. SQL DELETE and stream-load DELETE are also reported affected. I have not independently executed these reproductions.
    • Verified in source: I inspected the exact master commit and 917b20be83878e98f94e3ebf7efccc80346e3e50, which the local 4.1.5-release tag resolves to. Both contain the relevant pruning and sequence-comparison paths described below. The issue currently has no labels; suggested classification is Bug / storage / compaction, with elevated priority because older data can become visible after a delete.

    Verified code mechanism

    1. At the reported master commit, TabletReader::_init_delete_condition enables delete-sign pruning for base and full compaction when enable_prune_delete_sign_when_base_compaction is true, without checking for a sequence column. VerticalBlockReader and BlockReader then filter the winning delete-sign row out of the compaction output.
    2. For MOW, Segment::lookup_row_key returns KEY_ALREADY_EXISTS when the incoming sequence is smaller than the stored sequence. BaseTablet::calc_segment_delete_bitmap marks that incoming row deleted; KEY_NOT_FOUND instead skips this conflict handling. Once compaction removes the last row for key 1, the lookup has no sequence-900 entry against which to reject sequence 500. This explains why a successful/visible load can be ignored before compaction and become query-visible afterward.
    3. For MOR, the merge comparator prioritizes the larger sequence for equal keys. Removing the winning tombstone also removes the comparison candidate needed to suppress a later, smaller-sequence row.
    4. The 4.1.5 build has the same base/full pruning condition. This is consistent with the cross-version report, rather than evidence of a master-only regression.

    The mechanism is therefore strongly supported by static inspection. Independent runtime confirmation, the complete scope across other compaction paths, and a validated fix remain outstanding. #17739 supports the expected suppression of older writes while the delete record exists; it does not establish a tombstone garbage-collection boundary. #51874 extended pruning to full compaction. #59441 is closed and unmerged, so it should not be treated as an available fix.

    Remaining verification and next steps

    1. Run the supplied script on fresh tables with pruning enabled versus enable_prune_delete_sign_when_base_compaction=false, set before the delete/compaction, for MOW/MOR and base/full compaction. Record the effective BE setting, actual successful compaction status and input/output version ranges, physical tombstone presence, and the final normal SELECT. The expected distinguishing result is that retaining the sequence-900 tombstone continues to suppress sequence 500. The report already provides enough version, DDL, and sequencing information to begin reproduction; logs around publish/compaction are useful if this control behaves differently.
    2. Treat that setting as a diagnostic control: the configuration source explicitly marks it “only for test”. Its production use needs maintainer validation and a tombstone storage-cost assessment. Changing it after tombstones have been pruned cannot recover their sequence values.
    3. Evaluate preserving the winning tombstone for sequence-enabled UNIQUE KEY compaction, or storing an equivalent persistent per-key sequence boundary. A time delay alone cannot guarantee rejection of arbitrarily late older writes without an explicit lateness bound. Keep query/checksum deletion semantics correct: _delete_sign_available also serves checksum and cold-data readers, so a blanket change to this shared flag needs separate review.
    4. Add regression coverage for MOW/MOR, base/full, both compaction reader implementations, repeated compactions, and an all-rows-deleted output. Cover explicit delete-sign writes, stream-load DELETE, and SQL DELETE on MOW. Verify that smaller sequences remain suppressed and a larger sequence can reinsert; test equal-sequence semantics separately. Preserve existing behavior for tables without sequence columns. The existing test_config_prune_delete_sign.groovy covers a MOW table without a sequence column and does not exercise the late-write boundary here.

    No code changes were made during this investigation.

  2. RoanHeNaN commented on Nov 4, 2026

    @RoanHeNaN
    Contributor

    I met a same problem.

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