This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new e0729979c71 [refactor](be) remove CHAR padding on read  (#63291)
e0729979c71 is described below

commit e0729979c710736f70c203e0724cb77e98667d81
Author: Chenyang Sun <[email protected]>
AuthorDate: Mon Jun 1 10:00:52 2026 +0800

    [refactor](be) remove CHAR padding on read  (#63291)
    
    - https://github.com/apache/doris-website/pull/3759/
    
    - Problem: The CHAR padding contract leaked from the storage layer into
    the
    compute / predicate layers — every scan stripped padding at the Block
    level,
    while predicates re-padded values to match the on-disk shape. Logic was
    spread
       out and wasted work on every read.
    - Fix: On-disk format unchanged. The convertor still pads CHAR to the
    schema
    length on write, but the strip is pushed down to the page pre-decoder —
    the
    page cache holds unpadded data. All shrink_* / pad_* code above the page
    cache
       (SegmentIterator, Block, RowCursor, predicates) is removed.
    - BloomFilter: BF probing is skipped (return true, fall back to scan)
    for CHAR
    predicates — the BF hashes padded bytes but predicate values are
    unpadded, so
    the probe would never match. Other indexes (ZoneMap / inverted / bitmap)
    are
      unaffected.
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/3759-->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 be/src/core/block/block.cpp                        |  15 --
 be/src/core/block/block.h                          |   3 -
 be/src/core/column/column.h                        |   4 -
 be/src/core/column/column_array.cpp                |   4 -
 be/src/core/column/column_array.h                  |   2 -
 be/src/core/column/column_dictionary.h             |  27 +--
 be/src/core/column/column_map.cpp                  |   5 -
 be/src/core/column/column_map.h                    |   1 -
 be/src/core/column/column_nullable.cpp             |   4 -
 be/src/core/column/column_nullable.h               |   2 -
 be/src/core/column/column_string.cpp               |  23 --
 be/src/core/column/column_string.h                 |   2 -
 be/src/core/column/column_struct.cpp               |   6 -
 be/src/core/column/column_struct.h                 |   2 -
 be/src/core/column/predicate_column.h              |   6 +-
 .../data_type_serde/data_type_string_serde.cpp     |  23 +-
 be/src/exec/rowid_fetcher.cpp                      |  46 ----
 be/src/exprs/bloom_filter_func_adaptor.h           |  14 +-
 be/src/exprs/bloom_filter_func_impl.h              |  15 +-
 be/src/service/point_query_executor.cpp            |  12 +-
 be/src/storage/delete/delete_handler.cpp           |  20 +-
 be/src/storage/predicate/comparison_predicate.h    |   7 +-
 be/src/storage/predicate/in_list_predicate.h       |  24 +-
 be/src/storage/predicate/like_column_predicate.h   |   2 +-
 .../predicate/predicate_creator_comparison.cpp     |  18 +-
 .../predicate/predicate_creator_in_list_in.cpp     |  35 ++-
 .../predicate/predicate_creator_in_list_not_in.cpp |  33 +--
 be/src/storage/row_cursor.cpp                      |  11 -
 be/src/storage/row_cursor.h                        |   5 -
 .../storage/segment/binary_dict_page_pre_decoder.h |  30 ++-
 .../binary_plain_page_char_strip_pre_decoder.h     |  98 ++++++++
 .../segment/binary_plain_page_v2_pre_decoder.h     | 248 ++++++++++++---------
 be/src/storage/segment/binary_prefix_page.h        |   2 +-
 be/src/storage/segment/column_reader.cpp           |   5 +-
 be/src/storage/segment/encoding_info.cpp           |  27 ++-
 be/src/storage/segment/page_io.cpp                 |  14 +-
 be/src/storage/segment/segment_iterator.cpp        |  48 +---
 be/src/storage/segment/segment_iterator.h          |   8 +-
 be/test/core/block/block_test.cpp                  |   2 -
 be/test/core/column/column_array_test.cpp          |   5 -
 be/test/core/column/column_string_test.cpp         |  22 --
 be/test/core/column/common_column_test.h           |  37 ---
 be/test/exprs/bloom_filter_func_test.cpp           |   6 +-
 be/test/storage/olap_type_test.cpp                 |  48 ++++
 be/test/storage/segment/binary_dict_page_test.cpp  |   6 +-
 .../storage/segment/binary_plain_page_v2_test.cpp  | 145 +++++++++++-
 be/test/storage/segment/encoding_info_test.cpp     |  43 +++-
 .../segment_writer_full_encode_keys_test.cpp       | 100 ---------
 be/test/storage/segment/zone_map_index_test.cpp    |  48 ++--
 49 files changed, 623 insertions(+), 690 deletions(-)

diff --git a/be/src/core/block/block.cpp b/be/src/core/block/block.cpp
index c556eb3242c..a0598dcc812 100644
--- a/be/src/core/block/block.cpp
+++ b/be/src/core/block/block.cpp
@@ -1258,21 +1258,6 @@ std::unique_ptr<Block> 
Block::create_same_struct_block(size_t size, bool is_rese
     return temp_block;
 }
 
-void Block::shrink_char_type_column_suffix_zero(const std::vector<size_t>& 
char_type_idx) {
-    for (auto idx : char_type_idx) {
-        if (idx < data.size()) {
-            auto& col_and_name = this->get_by_position(idx);
-            if (col_and_name.column->is_exclusive()) {
-                col_and_name.column->assert_mutable()->shrink_padding_chars();
-            } else {
-                auto mutable_col = std::move(*col_and_name.column).mutate();
-                mutable_col->shrink_padding_chars();
-                col_and_name.column = std::move(mutable_col);
-            }
-        }
-    }
-}
-
 size_t MutableBlock::allocated_bytes() const {
     size_t res = 0;
     for (const auto& col : _columns) {
diff --git a/be/src/core/block/block.h b/be/src/core/block/block.h
index 3b97cc0fcf8..6207aa8f49d 100644
--- a/be/src/core/block/block.h
+++ b/be/src/core/block/block.h
@@ -413,9 +413,6 @@ public:
         return res;
     }
 
-    // for String type or Array<String> type
-    void shrink_char_type_column_suffix_zero(const std::vector<size_t>& 
char_type_idx);
-
     void clear_column_mem_not_keep(const std::vector<bool>& column_keep_flags,
                                    bool need_keep_first);
 
diff --git a/be/src/core/column/column.h b/be/src/core/column/column.h
index 6a443b6cac6..06934c4c712 100644
--- a/be/src/core/column/column.h
+++ b/be/src/core/column/column.h
@@ -113,10 +113,6 @@ public:
         return nullptr;
     }
 
-    // shrink the end zeros for ColumnStr(also for who has it nested). so nest 
column will call it for all nested.
-    // for non-str col, will reach here(do nothing). only ColumnStr will 
really shrink itself.
-    virtual void shrink_padding_chars() {}
-
     // Only used in ColumnVariant to handle lifecycle of variant. Other 
columns would do nothing.
     virtual void finalize() {}
 
diff --git a/be/src/core/column/column_array.cpp 
b/be/src/core/column/column_array.cpp
index e1d3e42e545..70b219012b6 100644
--- a/be/src/core/column/column_array.cpp
+++ b/be/src/core/column/column_array.cpp
@@ -121,10 +121,6 @@ ColumnArray::ColumnArray(SharedTag, ColumnPtr 
nested_column, ColumnPtr offsets_c
                            *static_cast<const IColumn::Ptr&>(offsets));
 }
 
-void ColumnArray::shrink_padding_chars() {
-    data->shrink_padding_chars();
-}
-
 std::string ColumnArray::get_name() const {
     return "Array(" + get_data().get_name() + ")";
 }
diff --git a/be/src/core/column/column_array.h 
b/be/src/core/column/column_array.h
index 27dfbdc893f..24afb744d19 100644
--- a/be/src/core/column/column_array.h
+++ b/be/src/core/column/column_array.h
@@ -130,8 +130,6 @@ public:
         offsets->sanity_check();
     }
 
-    void shrink_padding_chars() override;
-
     /** On the index i there is an offset to the beginning of the i + 1 -th 
element. */
     using ColumnOffsets = ColumnOffset64;
 
diff --git a/be/src/core/column/column_dictionary.h 
b/be/src/core/column/column_dictionary.h
index a1835b817a7..7ac780e2bde 100644
--- a/be/src/core/column/column_dictionary.h
+++ b/be/src/core/column/column_dictionary.h
@@ -231,7 +231,7 @@ public:
         _dict.initialize_hash_values_for_runtime_filter();
     }
 
-    uint32_t get_hash_value(uint32_t idx) const { return 
_dict.get_hash_value(_codes[idx], _type); }
+    uint32_t get_hash_value(uint32_t idx) const { return 
_dict.get_hash_value(_codes[idx]); }
 
     template <typename HybridSetType>
     void find_codes(const HybridSetType* values, std::vector<UInt8>& selected) 
const {
@@ -278,14 +278,6 @@ public:
 
     inline const StringRef& get_value(value_type code) const { return 
_dict.get_value(code); }
 
-    inline StringRef get_shrink_value(value_type code) const {
-        StringRef result = _dict.get_value(code);
-        if (_type == FieldType::OLAP_FIELD_TYPE_CHAR) {
-            result.size = strnlen(result.data, result.size);
-        }
-        return result;
-    }
-
     size_t dict_size() const { return _dict.size(); }
 
     std::string dict_debug_string() const { return _dict.debug_string(); }
@@ -326,26 +318,13 @@ public:
             }
         }
 
-        inline uint32_t get_hash_value(Int32 code, FieldType type) const {
+        inline uint32_t get_hash_value(Int32 code) const {
             if (_compute_hash_value_flags[code]) {
                 return _hash_values[code];
             } else {
                 auto& sv = (*_dict_data)[code];
-                // The char data is stored in the disk with the schema length,
-                // and zeros are filled if the length is insufficient
-
-                // When reading data, use 
shrink_char_type_column_suffix_zero(_char_type_idx)
-                // Remove the suffix 0
-                // When writing data, use the CharField::consume function to 
fill in the trailing 0.
-
-                // For dictionary data of char type, sv.size is the schema 
length,
-                // so use strnlen to remove the 0 at the end to get the actual 
length.
-                size_t len = sv.size;
-                if (type == FieldType::OLAP_FIELD_TYPE_CHAR) {
-                    len = strnlen(sv.data, sv.size);
-                }
                 uint32_t hash_val =
-                        crc32c::Extend(0, (const uint8_t*)sv.data, 
static_cast<uint32_t>(len));
+                        crc32c::Extend(0, (const uint8_t*)sv.data, 
static_cast<uint32_t>(sv.size));
                 _hash_values[code] = hash_val;
                 _compute_hash_value_flags[code] = 1;
                 return _hash_values[code];
diff --git a/be/src/core/column/column_map.cpp 
b/be/src/core/column/column_map.cpp
index 399f558caf5..be984c15727 100644
--- a/be/src/core/column/column_map.cpp
+++ b/be/src/core/column/column_map.cpp
@@ -700,11 +700,6 @@ Status ColumnMap::deduplicate_keys(bool recursive) {
     return Status::OK();
 }
 
-void ColumnMap::shrink_padding_chars() {
-    keys_column->shrink_padding_chars();
-    values_column->shrink_padding_chars();
-}
-
 void ColumnMap::reserve(size_t n) {
     get_offsets().reserve(n);
     keys_column->reserve(n);
diff --git a/be/src/core/column/column_map.h b/be/src/core/column/column_map.h
index 3566ab8759d..1747d9ef17c 100644
--- a/be/src/core/column/column_map.h
+++ b/be/src/core/column/column_map.h
@@ -115,7 +115,6 @@ public:
     const char* deserialize_and_insert_from_arena(const char* pos) override;
 
     void update_hash_with_value(size_t n, SipHash& hash) const override;
-    void shrink_padding_chars() override;
     ColumnPtr filter(const Filter& filt, ssize_t result_size_hint) const 
override;
     size_t filter(const Filter& filter) override;
     MutableColumnPtr permute(const Permutation& perm, size_t limit) const 
override;
diff --git a/be/src/core/column/column_nullable.cpp 
b/be/src/core/column/column_nullable.cpp
index 119c165c136..d3ce21fc8e3 100644
--- a/be/src/core/column/column_nullable.cpp
+++ b/be/src/core/column/column_nullable.cpp
@@ -122,10 +122,6 @@ void ColumnNullable::replace_columns(ColumnPtr 
nested_column, ColumnPtr null_map
     check_const_only_in_top_level();
 }
 
-void ColumnNullable::shrink_padding_chars() {
-    get_nested_column_ptr()->shrink_padding_chars();
-}
-
 void ColumnNullable::update_xxHash_with_value(size_t start, size_t end, 
uint64_t& hash,
                                               const uint8_t* __restrict 
null_data) const {
     if (!has_null(start, end)) {
diff --git a/be/src/core/column/column_nullable.h 
b/be/src/core/column/column_nullable.h
index 371edf8fc63..73bab4ff156 100644
--- a/be/src/core/column/column_nullable.h
+++ b/be/src/core/column/column_nullable.h
@@ -93,8 +93,6 @@ public:
         _nested_column->sanity_check();
     }
 
-    void shrink_padding_chars() override;
-
     bool is_variable_length() const override { return 
_nested_column->is_variable_length(); }
 
     std::string get_name() const override { return "Nullable(" + 
_nested_column->get_name() + ")"; }
diff --git a/be/src/core/column/column_string.cpp 
b/be/src/core/column/column_string.cpp
index e21e2bb83cb..399c0952b89 100644
--- a/be/src/core/column/column_string.cpp
+++ b/be/src/core/column/column_string.cpp
@@ -85,29 +85,6 @@ MutableColumnPtr ColumnStr<T>::clone_resized(size_t to_size) 
const {
     return res;
 }
 
-template <typename T>
-void ColumnStr<T>::shrink_padding_chars() {
-    if (size() == 0) {
-        return;
-    }
-    char* data = reinterpret_cast<char*>(chars.data());
-    auto* offset = offsets.data();
-    size_t size = offsets.size();
-
-    // deal the 0-th element. no need to move.
-    auto next_start = offset[0];
-    offset[0] = static_cast<T>(strnlen(data, size_at(0)));
-    for (size_t i = 1; i < size; i++) {
-        // get the i-th length and whole move it to cover the last's trailing 
void
-        auto length = strnlen(data + next_start, offset[i] - next_start);
-        memmove(data + offset[i - 1], data + next_start, length);
-        // offset i will be changed. so save the old value for (i+1)-th to get 
its length.
-        next_start = offset[i];
-        offset[i] = offset[i - 1] + static_cast<T>(length);
-    }
-    chars.resize_fill(offsets.back()); // just call it to shrink memory here. 
no possible to expand.
-}
-
 // This method is only called by MutableBlock::merge_ignore_overflow
 // by hash join operator to collect build data to avoid
 // the total string length of a ColumnStr<uint32_t> column exceeds the 4G 
limit.
diff --git a/be/src/core/column/column_string.h 
b/be/src/core/column/column_string.h
index 5a1537ff2c0..7bae712f849 100644
--- a/be/src/core/column/column_string.h
+++ b/be/src/core/column/column_string.h
@@ -145,8 +145,6 @@ public:
 
     MutableColumnPtr clone_resized(size_t to_size) const override;
 
-    void shrink_padding_chars() override;
-
     Field operator[](size_t n) const override;
 
     void get(size_t n, Field& res) const override;
diff --git a/be/src/core/column/column_struct.cpp 
b/be/src/core/column/column_struct.cpp
index 5cf8f390634..57463f6c435 100644
--- a/be/src/core/column/column_struct.cpp
+++ b/be/src/core/column/column_struct.cpp
@@ -338,12 +338,6 @@ MutableColumnPtr ColumnStruct::permute(const Permutation& 
perm, size_t limit) co
     return ColumnStruct::create(new_columns);
 }
 
-void ColumnStruct::shrink_padding_chars() {
-    for (auto& column : columns) {
-        column->shrink_padding_chars();
-    }
-}
-
 void ColumnStruct::reserve(size_t n) {
     const size_t tuple_size = columns.size();
     for (size_t i = 0; i < tuple_size; ++i) {
diff --git a/be/src/core/column/column_struct.h 
b/be/src/core/column/column_struct.h
index bee55370b85..e1f81950ddc 100644
--- a/be/src/core/column/column_struct.h
+++ b/be/src/core/column/column_struct.h
@@ -150,8 +150,6 @@ public:
 
     int compare_at(size_t n, size_t m, const IColumn& rhs_, int 
nan_direction_hint) const override;
 
-    void shrink_padding_chars() override;
-
     void reserve(size_t n) override;
     void resize(size_t n) override;
     size_t byte_size() const override;
diff --git a/be/src/core/column/predicate_column.h 
b/be/src/core/column/predicate_column.h
index 2dbea5da684..0d7df961def 100644
--- a/be/src/core/column/predicate_column.h
+++ b/be/src/core/column/predicate_column.h
@@ -104,11 +104,7 @@ public:
 
     StringRef get_data_at(size_t n) const override {
         if constexpr (std::is_same_v<T, StringRef>) {
-            auto res = reinterpret_cast<const StringRef&>(data[n]);
-            if constexpr (Type == TYPE_CHAR) {
-                res.size = strnlen(res.data, res.size);
-            }
-            return res;
+            return reinterpret_cast<const StringRef&>(data[n]);
         } else {
             throw doris::Exception(
                     ErrorCode::INTERNAL_ERROR,
diff --git a/be/src/core/data_type_serde/data_type_string_serde.cpp 
b/be/src/core/data_type_serde/data_type_string_serde.cpp
index b7a59b3c07e..08d87cc38ca 100644
--- a/be/src/core/data_type_serde/data_type_string_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_string_serde.cpp
@@ -462,25 +462,16 @@ Status 
DataTypeStringSerDeBase<ColumnType>::from_string(StringRef& str, IColumn&
 
 // Deserializes a STRING/VARCHAR/CHAR value from its OLAP string representation
 // (e.g. from ZoneMap protobuf). This is the inverse of to_olap_string().
-//
-// For CHAR type: if the string is shorter than the declared column length 
(_len),
-// pads with '\0' bytes to reach _len. This preserves CHAR's fixed-length 
semantics.
-// For STRING/VARCHAR: stores the string as-is.
-//
-// Examples:
-//   CHAR(10), str="hello"  => field = "hello\0\0\0\0\0" (10 bytes)
-//   VARCHAR,  str="hello"  => field = "hello" (5 bytes)
 template <typename ColumnType>
 Status DataTypeStringSerDeBase<ColumnType>::from_olap_string(const 
std::string& str, Field& field,
                                                              const 
FormatOptions& options) const {
-    if (cast_set<int>(str.size()) < _len) {
-        DCHECK_EQ(_type, TYPE_CHAR);
-        std::string tmp(_len, '\0');
-        memcpy(tmp.data(), str.data(), str.size());
-        field = Field::create_field<TYPE_CHAR>(std::move(tmp));
-    } else {
-        field = Field::create_field<TYPE_STRING>(str);
-    }
+    // CHAR(N) writes through OlapColumnDataConvertorChar are zero-padded to
+    // the declared schema length, so the serialized OLAP string carries
+    // trailing '\0' bytes. strnlen() drops that padding to surface the
+    // logical character content in the Field. VARCHAR / STRING never write
+    // trailing '\0' through this path, so strnlen is a no-op for them.
+    size_t len = strnlen(str.data(), str.size());
+    field = Field::create_field<TYPE_STRING>(std::string(str.data(), len));
     return Status::OK();
 }
 
diff --git a/be/src/exec/rowid_fetcher.cpp b/be/src/exec/rowid_fetcher.cpp
index 66dbbfcaf39..a3790862cd2 100644
--- a/be/src/exec/rowid_fetcher.cpp
+++ b/be/src/exec/rowid_fetcher.cpp
@@ -208,30 +208,6 @@ Status RowIDFetcher::_merge_rpc_results(const 
PMultiGetRequest& request,
     return Status::OK();
 }
 
-bool _has_char_type(const DataTypePtr& type) {
-    switch (type->get_primitive_type()) {
-    case TYPE_CHAR: {
-        return true;
-    }
-    case TYPE_ARRAY: {
-        const auto* arr_type = assert_cast<const 
DataTypeArray*>(remove_nullable(type).get());
-        return _has_char_type(arr_type->get_nested_type());
-    }
-    case TYPE_MAP: {
-        const auto* map_type = assert_cast<const 
DataTypeMap*>(remove_nullable(type).get());
-        return _has_char_type(map_type->get_key_type()) ||
-               _has_char_type(map_type->get_value_type());
-    }
-    case TYPE_STRUCT: {
-        const auto* struct_type = assert_cast<const 
DataTypeStruct*>(remove_nullable(type).get());
-        return std::any_of(struct_type->get_elements().begin(), 
struct_type->get_elements().end(),
-                           [&](const DataTypePtr& dt) -> bool { return 
_has_char_type(dt); });
-    }
-    default:
-        return false;
-    }
-}
-
 Status RowIDFetcher::fetch(const ColumnPtr& column_row_ids, Block* res_block) {
     CHECK(!_stubs.empty());
     PMultiGetRequest mget_req = _init_fetch_request(
@@ -281,16 +257,6 @@ Status RowIDFetcher::fetch(const ColumnPtr& 
column_row_ids, Block* res_block) {
     }
     // Check row consistency
     RETURN_IF_CATCH_EXCEPTION(res_block->check_number_of_rows());
-    // shrink for char type
-    std::vector<size_t> char_type_idx;
-    for (size_t i = 0; i < _fetch_option.desc->slots().size(); i++) {
-        const auto& column_desc = _fetch_option.desc->slots()[i];
-        const auto type = column_desc->type();
-        if (_has_char_type(type)) {
-            char_type_idx.push_back(i);
-        }
-    }
-    res_block->shrink_char_type_column_suffix_zero(char_type_idx);
     VLOG_DEBUG << "dump block:" << res_block->dump_data(0, 10);
     return Status::OK();
 }
@@ -595,15 +561,6 @@ Status RowIdStorageReader::read_by_rowids(const 
PMultiGetRequestV2& request,
                 for (const auto& pslot : request_block_desc.slots()) {
                     slots.push_back(SlotDescriptor(pslot));
                 }
-                // prepare block char vector shrink for char type
-                std::vector<size_t> char_type_idx;
-                for (int j = 0; j < slots.size(); ++j) {
-                    auto slot = slots[j];
-                    if (_has_char_type(slot.type())) {
-                        char_type_idx.push_back(j);
-                    }
-                }
-
                 try {
                     if (first_file_mapping->type == FileMappingType::INTERNAL) 
{
                         RETURN_IF_ERROR(read_batch_doris_format_row(
@@ -621,9 +578,6 @@ Status RowIdStorageReader::read_by_rowids(const 
PMultiGetRequestV2& request,
                     return Status::Error<false>(e.code(), "Row id fetch failed 
because {}",
                                                 e.what());
                 }
-
-                // after read the block, shrink char type block
-                
result_blocks[i].shrink_char_type_column_suffix_zero(char_type_idx);
             }
 
             [[maybe_unused]] size_t compressed_size = 0;
diff --git a/be/src/exprs/bloom_filter_func_adaptor.h 
b/be/src/exprs/bloom_filter_func_adaptor.h
index d41a12ff648..5cc5d33215d 100644
--- a/be/src/exprs/bloom_filter_func_adaptor.h
+++ b/be/src/exprs/bloom_filter_func_adaptor.h
@@ -243,18 +243,6 @@ struct StringFindOp : 
CommonFindOp<fixed_len_to_uint32_method, StringRef> {
     }
 };
 
-// We do not need to judge whether data is empty, because null will not appear
-// when filer used by the storage engine
-template <typename fixed_len_to_uint32_method>
-struct FixedStringFindOp : public StringFindOp<fixed_len_to_uint32_method> {
-    static uint16_t find_batch_olap_engine(const BloomFilterAdaptor& 
bloom_filter, const char* data,
-                                           const uint8_t* nullmap, uint16_t* 
offsets, int number,
-                                           const bool is_parse_column) {
-        return find_batch_olap<fixed_len_to_uint32_method, StringRef, true>(
-                bloom_filter, data, nullmap, offsets, number, is_parse_column);
-    }
-};
-
 template <typename fixed_len_to_uint32_method, PrimitiveType type>
 struct BloomFilterTypeTraits {
     using T = typename PrimitiveTypeTraits<type>::CppType;
@@ -263,7 +251,7 @@ struct BloomFilterTypeTraits {
 
 template <typename fixed_len_to_uint32_method>
 struct BloomFilterTypeTraits<fixed_len_to_uint32_method, TYPE_CHAR> {
-    using FindOp = FixedStringFindOp<fixed_len_to_uint32_method>;
+    using FindOp = StringFindOp<fixed_len_to_uint32_method>;
 };
 
 template <typename fixed_len_to_uint32_method>
diff --git a/be/src/exprs/bloom_filter_func_impl.h 
b/be/src/exprs/bloom_filter_func_impl.h
index fe47ec18b3b..4ecb39cb267 100644
--- a/be/src/exprs/bloom_filter_func_impl.h
+++ b/be/src/exprs/bloom_filter_func_impl.h
@@ -54,23 +54,12 @@ struct fixed_len_to_uint32_v2 {
     }
 };
 
-template <typename fixed_len_to_uint32_method, typename T, bool need_trim = 
false>
+template <typename fixed_len_to_uint32_method, typename T>
 uint16_t find_batch_olap(const BloomFilterAdaptor& bloom_filter, const char* 
data,
                          const uint8_t* nullmap, uint16_t* offsets, int number,
                          const bool is_parse_column) {
     auto get_element = [](const char* input_data, int idx) {
-        if constexpr (std::is_same_v<T, StringRef> && need_trim) {
-            const auto value = ((const StringRef*)(input_data))[idx];
-            int64_t size = value.size;
-            const char* data = value.data;
-            // CHAR type may pad the tail with \0, need to trim
-            while (size > 0 && data[size - 1] == '\0') {
-                size--;
-            }
-            return StringRef(value.data, size);
-        } else {
-            return ((const T*)(input_data))[idx];
-        }
+        return ((const T*)(input_data))[idx];
     };
 
     uint16_t new_size = 0;
diff --git a/be/src/service/point_query_executor.cpp 
b/be/src/service/point_query_executor.cpp
index 433cabb777d..8e3e83cfd46 100644
--- a/be/src/service/point_query_executor.cpp
+++ b/be/src/service/point_query_executor.cpp
@@ -568,15 +568,9 @@ Status PointQueryExecutor::_lookup_row_data() {
                     StorageReadOptions storage_read_options;
                     storage_read_options.stats = &_read_stats;
                     storage_read_options.io_ctx.reader_type = 
ReaderType::READER_QUERY;
-                    auto st = 
segment->seek_and_read_by_rowid(*_tablet->tablet_schema(), slot,
-                                                              row_ids, column, 
storage_read_options,
-                                                              iter);
-                    if (st.ok() && _tablet->tablet_schema()
-                                           
->column_by_uid(slot->col_unique_id())
-                                           .has_char_type()) {
-                        column->shrink_padding_chars();
-                    }
-                    RETURN_IF_ERROR(st);
+                    
RETURN_IF_ERROR(segment->seek_and_read_by_rowid(*_tablet->tablet_schema(), slot,
+                                                                    row_ids, 
column,
+                                                                    
storage_read_options, iter));
                 }
             }
         }
diff --git a/be/src/storage/delete/delete_handler.cpp 
b/be/src/storage/delete/delete_handler.cpp
index 8aab3c42296..1781c7734de 100644
--- a/be/src/storage/delete/delete_handler.cpp
+++ b/be/src/storage/delete/delete_handler.cpp
@@ -112,9 +112,6 @@ Status convert(const DataTypePtr& data_type, const 
std::list<std::string>& str,
 // Parses a single condition value string into a Field and creates a 
comparison predicate.
 // Uses serde->from_fe_string to do the parsing, which handles all 
type-specific
 // conversions (including decimal scale, etc.).
-// For CHAR type, the value is padded with '\0' to the declared column length, 
consistent
-// with the IN list path in convert() above.
-// For VARCHAR/STRING, the Field is created directly from the raw string.
 Status parse_to_predicate(const uint32_t index, const std::string col_name, 
const DataTypePtr& type,
                           DeleteHandler::ConditionParseResult& res, Arena& 
arena,
                           std::shared_ptr<ColumnPredicate>& predicate) {
@@ -128,22 +125,7 @@ Status parse_to_predicate(const uint32_t index, const 
std::string col_name, cons
     }
 
     Field v;
-    if (type->get_primitive_type() == TYPE_CHAR) {
-        // CHAR type: create Field and pad with '\0' to the declared column 
length,
-        // consistent with IN list path (convert() above) and 
create_comparison_predicate.
-        const auto& str = res.value_str.front();
-        auto char_len = cast_set<size_t>(
-                assert_cast<const 
DataTypeString*>(remove_nullable(type).get())->len());
-        auto target = std::max(char_len, str.size());
-        if (target > str.size()) {
-            std::string padded(target, '\0');
-            memcpy(padded.data(), str.data(), str.size());
-            v = Field::create_field<TYPE_CHAR>(std::move(padded));
-        } else {
-            v = Field::create_field<TYPE_CHAR>(str);
-        }
-    } else if (is_string_type(type->get_primitive_type())) {
-        // VARCHAR/STRING: create Field directly from the raw string, no 
padding needed.
+    if (is_string_type(type->get_primitive_type())) {
         v = Field::create_field<TYPE_STRING>(res.value_str.front());
     } else {
         auto serde = type->get_serde();
diff --git a/be/src/storage/predicate/comparison_predicate.h 
b/be/src/storage/predicate/comparison_predicate.h
index e1ebae39f8f..192188cc7dc 100644
--- a/be/src/storage/predicate/comparison_predicate.h
+++ b/be/src/storage/predicate/comparison_predicate.h
@@ -280,7 +280,12 @@ public:
             if (bf->is_ngram_bf()) {
                 return true;
             }
-            if constexpr (is_string_type(Type)) {
+            if constexpr (Type == TYPE_CHAR) {
+                // CHAR BFs hash zero-padded bytes while the predicate value is
+                // unpadded, so probing the BF would always miss. Skip BF
+                // pruning for CHAR entirely and let the scan filter the rows.
+                return true;
+            } else if constexpr (is_string_type(Type)) {
                 return bf->test_bytes(_value.data(), _value.size());
             } else {
                 // DecimalV2 using decimal12_t in bloom filter, should convert 
value to decimal12_t
diff --git a/be/src/storage/predicate/in_list_predicate.h 
b/be/src/storage/predicate/in_list_predicate.h
index 6c92290e5be..468760ac7ab 100644
--- a/be/src/storage/predicate/in_list_predicate.h
+++ b/be/src/storage/predicate/in_list_predicate.h
@@ -68,18 +68,16 @@ public:
     ENABLE_FACTORY_CREATOR(InListPredicateBase);
     using T = typename PrimitiveTypeTraits<Type>::CppType;
     InListPredicateBase(uint32_t column_id, std::string col_name,
-                        const std::shared_ptr<HybridSetBase>& hybrid_set, bool 
is_opposite,
-                        size_t char_length = 0)
+                        const std::shared_ptr<HybridSetBase>& hybrid_set, bool 
is_opposite)
             : ColumnPredicate(column_id, col_name, Type, is_opposite),
               _min_value(type_limit<T>::max()),
               _max_value(type_limit<T>::min()) {
         CHECK(hybrid_set != nullptr);
 
         // String types need a copy because:
-        // 1. The caller's set is StringSet<DynamicContainer<std::string>>, 
but here we want
-        //    StringSet<FixedContainer<std::string, N>> for small-set 
optimization — different
-        //    C++ types, cannot share the pointer.
-        // 2. CHAR type additionally needs padding to char_length.
+        // The caller's set is StringSet<DynamicContainer<std::string>>, but 
here we want
+        // StringSet<FixedContainer<std::string, N>> for small-set 
optimization — different
+        // C++ types, cannot share the pointer.
         //
         // Date/DECIMALV2 types do NOT need a copy: their ElementType 
(CppType) is identical
         // between the caller's HybridSet and InListPredicateBase's, and 
InListPredicateBase
@@ -93,13 +91,7 @@ public:
             HybridSetBase::IteratorBase* iter = hybrid_set->begin();
             while (iter->has_next()) {
                 const auto* value = (const StringRef*)(iter->get_value());
-                if constexpr (Type == TYPE_CHAR) {
-                    std::string padded(std::max(char_length, value->size), 
'\0');
-                    memcpy(padded.data(), value->data, value->size);
-                    _values->insert((void*)padded.data(), padded.length());
-                } else {
-                    _values->insert((void*)value->data, value->size);
-                }
+                _values->insert((void*)value->data, value->size);
                 iter->next();
             }
         } else {
@@ -350,6 +342,12 @@ public:
             if (bf->is_ngram_bf()) {
                 return true;
             }
+            if constexpr (Type == TYPE_CHAR) {
+                // CHAR BFs hash zero-padded bytes while the predicate value is
+                // unpadded, so probing the BF would always miss. Skip BF
+                // pruning for CHAR entirely.
+                return true;
+            }
             HybridSetBase::IteratorBase* iter = _values->begin();
             while (iter->has_next()) {
                 if constexpr (is_string_type(Type)) {
diff --git a/be/src/storage/predicate/like_column_predicate.h 
b/be/src/storage/predicate/like_column_predicate.h
index 24f129b0fb7..15251845051 100644
--- a/be/src/storage/predicate/like_column_predicate.h
+++ b/be/src/storage/predicate/like_column_predicate.h
@@ -155,7 +155,7 @@ private:
 
         std::vector<bool> tmp_res(column.dict_size(), false);
         for (int i = 0; i < column.dict_size(); i++) {
-            StringRef cell_value = column.get_shrink_value(i);
+            StringRef cell_value = column.get_value(i);
             unsigned char flag = 0;
             THROW_IF_ERROR((_state->scalar_function)(
                     &_like_state, StringRef(cell_value.data, cell_value.size), 
pattern, &flag));
diff --git a/be/src/storage/predicate/predicate_creator_comparison.cpp 
b/be/src/storage/predicate/predicate_creator_comparison.cpp
index bfec1262cfc..b10a175b016 100644
--- a/be/src/storage/predicate/predicate_creator_comparison.cpp
+++ b/be/src/storage/predicate/predicate_creator_comparison.cpp
@@ -77,21 +77,9 @@ std::shared_ptr<ColumnPredicate> 
create_comparison_predicate(const uint32_t cid,
                                                                            
opposite);
     }
     case TYPE_CHAR: {
-        auto target = std::max(cast_set<size_t>(assert_cast<const 
DataTypeString*>(
-                                                        
remove_nullable(data_type).get())
-                                                        ->len()),
-                               value.template get<TYPE_CHAR>().size());
-        if (target > value.template get<TYPE_CHAR>().size()) {
-            std::string tmp(target, '\0');
-            memcpy(tmp.data(), value.template get<TYPE_CHAR>().data(),
-                   value.template get<TYPE_CHAR>().size());
-            return ComparisonPredicateBase<TYPE_CHAR, PT>::create_shared(
-                    cid, col_name, 
Field::create_field<TYPE_CHAR>(std::move(tmp)), opposite);
-        } else {
-            return ComparisonPredicateBase<TYPE_CHAR, PT>::create_shared(
-                    cid, col_name, 
Field::create_field<TYPE_CHAR>(value.template get<TYPE_CHAR>()),
-                    opposite);
-        }
+        return ComparisonPredicateBase<TYPE_CHAR, PT>::create_shared(
+                cid, col_name, Field::create_field<TYPE_CHAR>(value.template 
get<TYPE_CHAR>()),
+                opposite);
     }
     case TYPE_VARCHAR:
     case TYPE_STRING: {
diff --git a/be/src/storage/predicate/predicate_creator_in_list_in.cpp 
b/be/src/storage/predicate/predicate_creator_in_list_in.cpp
index e720da632a8..f1ad365b4f5 100644
--- a/be/src/storage/predicate/predicate_creator_in_list_in.cpp
+++ b/be/src/storage/predicate/predicate_creator_in_list_in.cpp
@@ -26,42 +26,34 @@ namespace doris {
 template <PrimitiveType TYPE, PredicateType PT>
 static std::shared_ptr<ColumnPredicate> create_in_list_predicate_impl(
         const uint32_t cid, const std::string col_name, const 
std::shared_ptr<HybridSetBase>& set,
-        bool is_opposite, size_t char_length = 0) {
+        bool is_opposite) {
     // Only string types construct their own HybridSetType in the constructor 
(to convert
     // from DynamicContainer to FixedContainer<std::string, N>), so N dispatch 
is only needed
     // for them. All other types directly share the caller's hybrid_set.
     if constexpr (!is_string_type(TYPE)) {
         return InListPredicateBase<TYPE, PT, FIXED_CONTAINER_MAX_SIZE + 
1>::create_shared(
-                cid, col_name, set, is_opposite, char_length);
+                cid, col_name, set, is_opposite);
     } else {
         auto set_size = set->size();
         if (set_size == 1) {
-            return InListPredicateBase<TYPE, PT, 1>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 1>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 2) {
-            return InListPredicateBase<TYPE, PT, 2>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 2>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 3) {
-            return InListPredicateBase<TYPE, PT, 3>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 3>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 4) {
-            return InListPredicateBase<TYPE, PT, 4>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 4>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 5) {
-            return InListPredicateBase<TYPE, PT, 5>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 5>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 6) {
-            return InListPredicateBase<TYPE, PT, 6>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 6>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 7) {
-            return InListPredicateBase<TYPE, PT, 7>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 7>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == FIXED_CONTAINER_MAX_SIZE) {
-            return InListPredicateBase<TYPE, PT, 8>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 8>::create_shared(cid, 
col_name, set, is_opposite);
         } else {
             return InListPredicateBase<TYPE, PT, FIXED_CONTAINER_MAX_SIZE + 
1>::create_shared(
-                    cid, col_name, set, is_opposite, char_length);
+                    cid, col_name, set, is_opposite);
         }
     }
 }
@@ -120,9 +112,8 @@ std::shared_ptr<ColumnPredicate> 
create_in_list_predicate<PredicateType::IN_LIST
                 cid, col_name, set, is_opposite);
     }
     case TYPE_CHAR: {
-        return create_in_list_predicate_impl<TYPE_CHAR, 
PredicateType::IN_LIST>(
-                cid, col_name, set, is_opposite,
-                assert_cast<const 
DataTypeString*>(remove_nullable(data_type).get())->len());
+        return create_in_list_predicate_impl<TYPE_CHAR, 
PredicateType::IN_LIST>(cid, col_name, set,
+                                                                               
 is_opposite);
     }
     case TYPE_VARCHAR: {
         return create_in_list_predicate_impl<TYPE_VARCHAR, 
PredicateType::IN_LIST>(
diff --git a/be/src/storage/predicate/predicate_creator_in_list_not_in.cpp 
b/be/src/storage/predicate/predicate_creator_in_list_not_in.cpp
index 63e8eb37b18..e4cb4731a57 100644
--- a/be/src/storage/predicate/predicate_creator_in_list_not_in.cpp
+++ b/be/src/storage/predicate/predicate_creator_in_list_not_in.cpp
@@ -26,42 +26,34 @@ namespace doris {
 template <PrimitiveType TYPE, PredicateType PT>
 static std::shared_ptr<ColumnPredicate> create_in_list_predicate_impl(
         const uint32_t cid, const std::string col_name, const 
std::shared_ptr<HybridSetBase>& set,
-        bool is_opposite, size_t char_length = 0) {
+        bool is_opposite) {
     // Only string types construct their own HybridSetType in the constructor 
(to convert
     // from DynamicContainer to FixedContainer<std::string, N>), so N dispatch 
is only needed
     // for them. All other types directly share the caller's hybrid_set.
     if constexpr (!is_string_type(TYPE)) {
         return InListPredicateBase<TYPE, PT, FIXED_CONTAINER_MAX_SIZE + 
1>::create_shared(
-                cid, col_name, set, is_opposite, char_length);
+                cid, col_name, set, is_opposite);
     } else {
         auto set_size = set->size();
         if (set_size == 1) {
-            return InListPredicateBase<TYPE, PT, 1>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 1>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 2) {
-            return InListPredicateBase<TYPE, PT, 2>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 2>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 3) {
-            return InListPredicateBase<TYPE, PT, 3>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 3>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 4) {
-            return InListPredicateBase<TYPE, PT, 4>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 4>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 5) {
-            return InListPredicateBase<TYPE, PT, 5>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 5>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 6) {
-            return InListPredicateBase<TYPE, PT, 6>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 6>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == 7) {
-            return InListPredicateBase<TYPE, PT, 7>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 7>::create_shared(cid, 
col_name, set, is_opposite);
         } else if (set_size == FIXED_CONTAINER_MAX_SIZE) {
-            return InListPredicateBase<TYPE, PT, 8>::create_shared(cid, 
col_name, set, is_opposite,
-                                                                   
char_length);
+            return InListPredicateBase<TYPE, PT, 8>::create_shared(cid, 
col_name, set, is_opposite);
         } else {
             return InListPredicateBase<TYPE, PT, FIXED_CONTAINER_MAX_SIZE + 
1>::create_shared(
-                    cid, col_name, set, is_opposite, char_length);
+                    cid, col_name, set, is_opposite);
         }
     }
 }
@@ -121,8 +113,7 @@ std::shared_ptr<ColumnPredicate> 
create_in_list_predicate<PredicateType::NOT_IN_
     }
     case TYPE_CHAR: {
         return create_in_list_predicate_impl<TYPE_CHAR, 
PredicateType::NOT_IN_LIST>(
-                cid, col_name, set, is_opposite,
-                assert_cast<const 
DataTypeString*>(remove_nullable(data_type).get())->len());
+                cid, col_name, set, is_opposite);
     }
     case TYPE_VARCHAR: {
         return create_in_list_predicate_impl<TYPE_VARCHAR, 
PredicateType::NOT_IN_LIST>(
diff --git a/be/src/storage/row_cursor.cpp b/be/src/storage/row_cursor.cpp
index e0fb9ade80f..9e09593991f 100644
--- a/be/src/storage/row_cursor.cpp
+++ b/be/src/storage/row_cursor.cpp
@@ -123,17 +123,6 @@ RowCursor RowCursor::clone() const {
     return result;
 }
 
-void RowCursor::pad_char_fields() {
-    for (size_t i = 0; i < _fields.size(); ++i) {
-        const TabletColumn* col = _schema->column(cast_set<uint32_t>(i));
-        if (col->type() == FieldType::OLAP_FIELD_TYPE_CHAR && 
!_fields[i].is_null()) {
-            String padded = _fields[i].get<TYPE_CHAR>();
-            padded.resize(col->length(), '\0');
-            _fields[i] = Field::create_field<TYPE_CHAR>(std::move(padded));
-        }
-    }
-}
-
 std::string RowCursor::to_string() const {
     std::string result;
     for (size_t i = 0; i < _fields.size(); ++i) {
diff --git a/be/src/storage/row_cursor.h b/be/src/storage/row_cursor.h
index bc40439b19a..19850f9604b 100644
--- a/be/src/storage/row_cursor.h
+++ b/be/src/storage/row_cursor.h
@@ -70,11 +70,6 @@ public:
     // Returns a deep copy of this RowCursor with the same schema and field 
values.
     RowCursor clone() const;
 
-    // Pad all CHAR-type fields in-place to their declared column length using 
'\0'.
-    // RowCursor holds CHAR values in compute format (unpadded). Call this 
before
-    // comparing against storage-format data (e.g. _seek_block) where CHAR is 
padded.
-    void pad_char_fields();
-
     // Output row cursor content in string format
     std::string to_string() const;
 
diff --git a/be/src/storage/segment/binary_dict_page_pre_decoder.h 
b/be/src/storage/segment/binary_dict_page_pre_decoder.h
index b488e83f402..c6f6721e70b 100644
--- a/be/src/storage/segment/binary_dict_page_pre_decoder.h
+++ b/be/src/storage/segment/binary_dict_page_pre_decoder.h
@@ -19,6 +19,7 @@
 
 #include "storage/cache/page_cache.h"
 #include "storage/segment/binary_dict_page.h"
+#include "storage/segment/binary_plain_page_char_strip_pre_decoder.h"
 #include "storage/segment/binary_plain_page_v2_pre_decoder.h"
 #include "storage/segment/bitshuffle_page_pre_decoder.h"
 #include "storage/segment/encoding_info.h"
@@ -33,12 +34,18 @@ namespace segment_v2 {
  * BinaryDictPage data pages can have different encoding types:
  * 1. DICT_ENCODING: header(4 bytes) + bitshuffle encoded codeword page
  * 2. PLAIN_ENCODING_V2: header(4 bytes) + BinaryPlainPageV2 encoded data
- * 3. PLAIN_ENCODING: header(4 bytes) + BinaryPlainPage encoded data (no 
pre-decode needed)
+ * 3. PLAIN_ENCODING: header(4 bytes) + BinaryPlainPage encoded data (no 
pre-decode needed
+ *    for non-CHAR; CHAR pages get their trailing '\0' padding stripped inline)
  *
  * This pre-decoder reads the encoding type from the first 4 bytes, strips the 
header,
  * dispatches to the appropriate pre-decoder (BitShufflePagePreDecoder or
- * BinaryPlainPageV2PreDecoder), and then restores the header.
+ * BinaryPlainPageV2PreDecoder<IS_CHAR>), and then restores the header.
+ *
+ * When IS_CHAR is true the inline-binary paths (PLAIN_ENCODING / 
PLAIN_ENCODING_V2)
+ * use the CHAR-strip variants so the dict-fallback data pages are also 
unpadded
+ * once at page load time.
  */
+template <bool IS_CHAR>
 struct BinaryDictPagePreDecoder : public DataPagePreDecoder {
     /**
      * @brief Decode BinaryDictPage data page
@@ -72,8 +79,9 @@ struct BinaryDictPagePreDecoder : public DataPagePreDecoder {
                     "PLAIN_ENCODING_V2, PLAIN_ENCODING>",
                     encoding_type, file_path);
         }
-        // For PLAIN_ENCODING, no pre-decoding needed
-        if (encoding_type == PLAIN_ENCODING) {
+        // For PLAIN_ENCODING, non-CHAR pages can be used as-is; CHAR pages
+        // are routed through the CHAR-strip pre-decoder below.
+        if (encoding_type == PLAIN_ENCODING && !IS_CHAR) {
             return Status::OK();
         }
 
@@ -102,12 +110,22 @@ struct BinaryDictPagePreDecoder : public 
DataPagePreDecoder {
             break;
         }
         case PLAIN_ENCODING_V2: {
-            // Use BinaryPlainPageV2PreDecoder with total_prefix to reserve 
space
-            BinaryPlainPageV2PreDecoder v2_decoder;
+            BinaryPlainPageV2PreDecoder<IS_CHAR> v2_decoder;
             status = v2_decoder.decode(&decoded_page, &data_without_header, 
size_of_tail,
                                        _use_cache, page_type, file_path, 
total_prefix);
             break;
         }
+        case PLAIN_ENCODING: {
+            // Non-CHAR is short-circuited above; CHECK that the invariant
+            // holds in case the short-circuit gets removed accidentally.
+            CHECK(IS_CHAR) << "BinaryDictPagePreDecoder<false> reached 
PLAIN_ENCODING "
+                              "dict-fallback path; expected to short-circuit 
above. file: "
+                           << file_path;
+            BinaryPlainPageCharStripPreDecoder v1_decoder;
+            status = v1_decoder.decode(&decoded_page, &data_without_header, 
size_of_tail,
+                                       _use_cache, page_type, file_path, 
total_prefix);
+            break;
+        }
         default:
             // Unknown encoding type, no pre-decoding needed
             return Status::OK();
diff --git a/be/src/storage/segment/binary_plain_page_char_strip_pre_decoder.h 
b/be/src/storage/segment/binary_plain_page_char_strip_pre_decoder.h
new file mode 100644
index 00000000000..11947c5abcb
--- /dev/null
+++ b/be/src/storage/segment/binary_plain_page_char_strip_pre_decoder.h
@@ -0,0 +1,98 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <cstring>
+#include <vector>
+
+#include "storage/cache/page_cache.h"
+#include "storage/segment/binary_plain_page_v2_pre_decoder.h" // 
BinaryPlainV1Entry, write_binary_plain_v1_output
+#include "storage/segment/encoding_info.h"
+#include "util/coding.h"
+
+namespace doris::segment_v2 {
+
+// Pre-decoder for BinaryPlainPage (V1) data pages of CHAR columns.
+//
+// Segments store CHAR(N) zero-padded to N bytes (the on-disk format). The
+// pre-decoder strnlens each slice once at page load time, then rewrites the
+// page as a tight V1 layout (no trailing '\0' bytes, adjusted offsets) before
+// it is placed in the page cache, so the compute layer reads unpadded CHAR.
+//
+// Both input and output layout are V1:
+//   Data:    |binary1|binary2|...|binaryN|
+//   Trailer: |offset1|offset2|...|offsetN| num_elems (32-bit)
+//
+// Reuses the same writer as the V2 pre-decoders (write_binary_plain_v1_output)
+// for steps 3-7; only the input scan differs (offsets array instead of varint
+// lengths).
+struct BinaryPlainPageCharStripPreDecoder : public DataPagePreDecoder {
+    Status decode(std::unique_ptr<DataPage>* page, Slice* page_slice, size_t 
size_of_tail,
+                  bool _use_cache, segment_v2::PageTypePB page_type, const 
std::string& file_path,
+                  size_t size_of_prefix = 0) override {
+        // Step 1: validate and locate the V1 trailer.
+        if (page_slice->size < size_of_tail + sizeof(uint32_t)) {
+            return Status::Corruption(
+                    "Invalid CHAR plain page size: {}, expected at least {} in 
file: {}",
+                    page_slice->size, size_of_tail + sizeof(uint32_t), 
file_path);
+        }
+        Slice data(page_slice->data, page_slice->size - size_of_tail);
+        uint32_t num_elems = decode_fixed32_le(
+                reinterpret_cast<const uint8_t*>(&data[data.size - 
sizeof(uint32_t)]));
+
+        // Always rewrite the page even for num_elems==0 — callers (e.g.
+        // BinaryDictPagePreDecoder) may pass a non-zero `size_of_prefix`
+        // expecting a fresh output buffer with that prefix area reserved.
+        size_t offsets_pos = data.size - (num_elems + 1) * sizeof(uint32_t);
+        if (num_elems > 0 && offsets_pos > data.size - sizeof(uint32_t)) {
+            return Status::Corruption(
+                    "CHAR plain page corruption: offsets pos beyond data, 
size={}, num_elems={}, "
+                    "offsets_pos={} in file: {}",
+                    data.size, num_elems, offsets_pos, file_path);
+        }
+        const auto* offsets_in = reinterpret_cast<const 
uint8_t*>(&data[offsets_pos]);
+
+        // Step 2: scan entries, strnlen-ing each to drop trailing '\0' 
padding.
+        std::vector<BinaryPlainV1Entry> entries;
+        entries.reserve(num_elems);
+        uint32_t total_out_len = 0;
+        for (uint32_t i = 0; i < num_elems; ++i) {
+            uint32_t start = decode_fixed32_le(offsets_in + i * 
sizeof(uint32_t));
+            uint32_t end = (i + 1 < num_elems)
+                                   ? decode_fixed32_le(offsets_in + (i + 1) * 
sizeof(uint32_t))
+                                   : static_cast<uint32_t>(offsets_pos);
+            if (end < start || end > offsets_pos) {
+                return Status::Corruption(
+                        "CHAR plain page corruption: bad offset at {}, 
start={}, end={}, "
+                        "offsets_pos={} in file: {}",
+                        i, start, end, offsets_pos, file_path);
+            }
+            uint32_t raw_size = end - start;
+            uint32_t out_len = static_cast<uint32_t>(strnlen(data.data + 
start, raw_size));
+            entries.push_back({reinterpret_cast<const uint8_t*>(data.data + 
start), out_len});
+            total_out_len += out_len;
+        }
+
+        // Steps 3-7 (shared with V2 pre-decoders).
+        return write_binary_plain_v1_output(entries, num_elems, total_out_len, 
*page_slice,
+                                            size_of_tail, size_of_prefix, 
_use_cache, page_type,
+                                            page, page_slice);
+    }
+};
+
+} // namespace doris::segment_v2
diff --git a/be/src/storage/segment/binary_plain_page_v2_pre_decoder.h 
b/be/src/storage/segment/binary_plain_page_v2_pre_decoder.h
index bea58094c8a..b400d583fc9 100644
--- a/be/src/storage/segment/binary_plain_page_v2_pre_decoder.h
+++ b/be/src/storage/segment/binary_plain_page_v2_pre_decoder.h
@@ -17,6 +17,9 @@
 
 #pragma once
 
+#include <cstring>
+#include <vector>
+
 #include "storage/cache/page_cache.h"
 #include "storage/segment/encoding_info.h"
 #include "util/coding.h"
@@ -24,6 +27,63 @@
 namespace doris {
 namespace segment_v2 {
 
+// One source entry feeding the V1 output writer. Variants differ only in how
+// `out_len` is derived from the raw input length (raw, strnlen'd, etc.).
+struct BinaryPlainV1Entry {
+    const uint8_t* start;
+    uint32_t out_len;
+};
+
+// Allocate a V1 BinaryPlainPage layout output buffer and write
+// binary -> offsets -> num_elems -> tail. Shared by V2 pre-decoders (after
+// they iterate varint lengths) and the V1 CHAR-strip pre-decoder (after it
+// iterates the V1 offsets array). `size_of_prefix` reserves room before the
+// V1 data for callers that wrap the page (e.g. BinaryDictPagePreDecoder
+// prepending the dict-page header).
+inline Status write_binary_plain_v1_output(const 
std::vector<BinaryPlainV1Entry>& entries,
+                                           uint32_t num_elems, uint32_t 
total_out_len,
+                                           const Slice& source_page_slice, 
size_t size_of_tail,
+                                           size_t size_of_prefix, bool 
use_cache,
+                                           segment_v2::PageTypePB page_type,
+                                           std::unique_ptr<DataPage>* out_page,
+                                           Slice* out_page_slice) {
+    size_t offsets_size = num_elems * sizeof(uint32_t);
+    size_t v1_data_size = total_out_len + offsets_size + sizeof(uint32_t);
+    size_t total_size = size_of_prefix + v1_data_size + size_of_tail;
+
+    std::unique_ptr<DataPage> decoded_page =
+            std::make_unique<DataPage>(total_size, use_cache, page_type);
+    Slice decoded_slice(decoded_page->data(), total_size);
+    char* output = decoded_slice.data + size_of_prefix;
+
+    // Binary payload.
+    for (const auto& e : entries) {
+        memcpy(output, e.start, e.out_len);
+        output += e.out_len;
+    }
+
+    // Offsets array (running cursor).
+    uint32_t running = 0;
+    for (const auto& e : entries) {
+        encode_fixed32_le(reinterpret_cast<uint8_t*>(output), running);
+        output += sizeof(uint32_t);
+        running += e.out_len;
+    }
+
+    // num_elems trailer.
+    encode_fixed32_le(reinterpret_cast<uint8_t*>(output), num_elems);
+    output += sizeof(uint32_t);
+
+    // Tail (footer + null map).
+    if (size_of_tail > 0) {
+        memcpy(output, source_page_slice.data + source_page_slice.size - 
size_of_tail,
+               size_of_tail);
+    }
+    *out_page_slice = decoded_slice;
+    *out_page = std::move(decoded_page);
+    return Status::OK();
+}
+
 /**
  * @brief Pre-decoder for BinaryPlainPageV2
  *
@@ -37,127 +97,101 @@ namespace segment_v2 {
  * V1 format (output):
  *   Data: |binary1|binary2|...
  *   Trailer: |offset1(32-bit)|offset2(32-bit)|...| num_elems (32-bit)
+ *
+ * The decode pipeline is 7 steps:
+ *   1. parse header (validate sizes + extract num_elems + iteration bounds)
+ *   2. scan entries: record (data_start, out_len) per entry, sum total out_len
+ *   3. allocate the V1 output page (size_of_prefix + binary + offsets + 
trailer + tail)
+ *   4. write binary payload
+ *   5. write offsets array (running cursor over out_len)
+ *   6. write num_elems trailer
+ *   7. copy tail (footer + null map) and publish output params
+ *
+ * IS_CHAR=true picks the strnlen transform in step 2, so CHAR pages emit
+ * unpadded slices to the cached page. IS_CHAR=false keeps raw V2 lengths.
+ * The branch is `if constexpr` — compile-time dispatched, no overhead.
  */
+template <bool IS_CHAR>
 struct BinaryPlainPageV2PreDecoder : public DataPagePreDecoder {
-    /**
-     * @brief Decode BinaryPlainPageV2 data to BinaryPlainPage format
-     *
-     * @param page unique_ptr to hold page data, will be replaced by decoded 
data
-     * @param page_slice data to decode, will be updated to point to decoded 
data
-     * @param size_of_tail including size of footer and null map
-     * @param _use_cache whether to use page cache
-     * @param page_type the type of page
-     * @param file_path file path for error reporting
-     * @param size_of_prefix size of prefix space to reserve (for dict page 
header)
-     * @return Status
-     */
     Status decode(std::unique_ptr<DataPage>* page, Slice* page_slice, size_t 
size_of_tail,
                   bool _use_cache, segment_v2::PageTypePB page_type, const 
std::string& file_path,
                   size_t size_of_prefix = 0) override {
-        // Validate input
-        if (page_slice->size < sizeof(uint32_t) + size_of_tail) {
-            return Status::Corruption("Invalid page size: {}, expected at 
least {} in file: {}",
-                                      page_slice->size, sizeof(uint32_t) + 
size_of_tail, file_path);
+        // Step 1.
+        Slice data;
+        uint32_t num_elems = 0;
+        const uint8_t* ptr = nullptr;
+        const uint8_t* limit = nullptr;
+        RETURN_IF_ERROR(parse_header(*page_slice, size_of_tail, file_path, 
&data, &num_elems, &ptr,
+                                     &limit));
+
+        // Step 2: out_len derived per IS_CHAR.
+        std::vector<BinaryPlainV1Entry> entries;
+        entries.reserve(num_elems);
+        uint32_t total_out_len = 0;
+        for (uint32_t i = 0; i < num_elems; ++i) {
+            uint32_t raw_len = 0;
+            const uint8_t* data_start = nullptr;
+            RETURN_IF_ERROR(decode_one(ptr, limit, file_path, i, &data_start, 
&raw_len));
+            uint32_t out_len;
+            if constexpr (IS_CHAR) {
+                out_len = static_cast<uint32_t>(
+                        strnlen(reinterpret_cast<const char*>(data_start), 
raw_len));
+            } else {
+                out_len = raw_len;
+            }
+            entries.push_back({data_start, out_len});
+            total_out_len += out_len;
+            ptr = data_start + raw_len;
         }
 
-        // Calculate data portion (excluding tail)
-        Slice data(page_slice->data, page_slice->size - size_of_tail);
+        // Steps 3-7.
+        return write_binary_plain_v1_output(entries, num_elems, total_out_len, 
*page_slice,
+                                            size_of_tail, size_of_prefix, 
_use_cache, page_type,
+                                            page, page_slice);
+    }
 
-        // Read num_elems from the last 4 bytes of data portion
-        if (data.size < sizeof(uint32_t)) {
-            return Status::Corruption("Data too small to contain num_elems in 
file: {}", file_path);
+private:
+    // Step 1: validate the V2 page and extract iteration bounds.
+    static inline Status parse_header(const Slice& page_slice, size_t 
size_of_tail,
+                                      const std::string& file_path, Slice* 
out_data,
+                                      uint32_t* out_num_elems, const uint8_t** 
out_ptr,
+                                      const uint8_t** out_limit) {
+        if (page_slice.size < sizeof(uint32_t) + size_of_tail) {
+            return Status::Corruption("Invalid page size: {}, expected at 
least {} in file: {}",
+                                      page_slice.size, sizeof(uint32_t) + 
size_of_tail, file_path);
         }
-
-        uint32_t num_elems = decode_fixed32_le(
-                reinterpret_cast<const uint8_t*>(&data[data.size - 
sizeof(uint32_t)]));
-
-        // Calculate required size for V1 format
-        // V1 format: binary_data + offsets_array + num_elems + tail
-        // We need to parse V2 to calculate the total binary data size
-        const auto* ptr = reinterpret_cast<const uint8_t*>(data.data);
-        const uint8_t* limit = ptr + data.size - sizeof(uint32_t);
-
-        std::vector<uint32_t> offsets;
-        offsets.reserve(num_elems);
-
-        uint32_t current_offset = 0;
-        for (uint32_t i = 0; i < num_elems; i++) {
-            if (ptr >= limit) {
-                return Status::Corruption(
-                        "Unexpected end of data while parsing element {} in 
file: {}", i,
-                        file_path);
-            }
-
-            // Decode varuint length
-            uint32_t length;
-            const uint8_t* data_start = decode_varint32_ptr(ptr, limit, 
&length);
-            if (data_start == nullptr) {
-                return Status::Corruption("Failed to decode varuint for 
element {} in file: {}", i,
-                                          file_path);
-            }
-
-            // Store offset for this element
-            offsets.push_back(current_offset);
-            current_offset += length;
-
-            // Move to next entry
-            ptr = data_start + length;
-
-            if (ptr > limit) {
-                return Status::Corruption("Data extends beyond page for 
element {} in file: {}", i,
-                                          file_path);
-            }
+        *out_data = Slice(page_slice.data, page_slice.size - size_of_tail);
+        if (out_data->size < sizeof(uint32_t)) {
+            return Status::Corruption("Data too small to contain num_elems in 
file: {}", file_path);
         }
+        *out_num_elems = decode_fixed32_le(
+                reinterpret_cast<const uint8_t*>(&(*out_data)[out_data->size - 
sizeof(uint32_t)]));
+        *out_ptr = reinterpret_cast<const uint8_t*>(out_data->data);
+        *out_limit = *out_ptr + out_data->size - sizeof(uint32_t);
+        return Status::OK();
+    }
 
-        // Calculate size for V1 format
-        size_t binary_data_size = current_offset;
-        size_t offsets_size = num_elems * sizeof(uint32_t);
-        size_t v1_data_size = binary_data_size + offsets_size + 
sizeof(uint32_t);
-        size_t total_size = size_of_prefix + v1_data_size + size_of_tail;
-
-        // Allocate new page
-        Slice decoded_slice;
-        decoded_slice.size = total_size;
-        std::unique_ptr<DataPage> decoded_page =
-                std::make_unique<DataPage>(decoded_slice.size, _use_cache, 
page_type);
-        decoded_slice.data = decoded_page->data();
-
-        // Write V1 format data after the prefix
-        char* output = decoded_slice.data + size_of_prefix;
-
-        // Step 1: Write binary data (without varint prefixes)
-        ptr = reinterpret_cast<const uint8_t*>(data.data);
-        for (uint32_t i = 0; i < num_elems; i++) {
-            uint32_t length;
-            const uint8_t* data_start = decode_varint32_ptr(ptr, limit, 
&length);
-
-            // Copy binary data
-            memcpy(output, data_start, length);
-            output += length;
-
-            // Move to next entry
-            ptr = data_start + length;
+    // Step 2 helper: decode one varint length and validate the entry bounds.
+    // The scan loop in decode() / overrides walks the input with this helper
+    // and decides what `out_len` to record (raw_len here, strnlen'd in the
+    // CHAR variant).
+    static inline Status decode_one(const uint8_t* ptr, const uint8_t* limit,
+                                    const std::string& file_path, uint32_t i,
+                                    const uint8_t** out_data_start, uint32_t* 
out_raw_len) {
+        if (ptr >= limit) {
+            return Status::Corruption("Unexpected end of data while parsing 
element {} in file: {}",
+                                      i, file_path);
         }
-
-        // Step 2: Write offsets array
-        for (uint32_t offset : offsets) {
-            encode_fixed32_le(reinterpret_cast<uint8_t*>(output), offset);
-            output += sizeof(uint32_t);
+        const uint8_t* data_start = decode_varint32_ptr(ptr, limit, 
out_raw_len);
+        if (data_start == nullptr) {
+            return Status::Corruption("Failed to decode varuint for element {} 
in file: {}", i,
+                                      file_path);
         }
-
-        // Step 3: Write num_elems
-        encode_fixed32_le(reinterpret_cast<uint8_t*>(output), num_elems);
-        output += sizeof(uint32_t);
-
-        // Step 4: Copy tail (footer and null map)
-        if (size_of_tail > 0) {
-            memcpy(output, page_slice->data + page_slice->size - size_of_tail, 
size_of_tail);
+        if (data_start + *out_raw_len > limit) {
+            return Status::Corruption("Data extends beyond page for element {} 
in file: {}", i,
+                                      file_path);
         }
-
-        // Update output parameters
-        *page_slice = decoded_slice;
-        *page = std::move(decoded_page);
-
+        *out_data_start = data_start;
         return Status::OK();
     }
 };
diff --git a/be/src/storage/segment/binary_prefix_page.h 
b/be/src/storage/segment/binary_prefix_page.h
index ce2b363e934..3e7da14b476 100644
--- a/be/src/storage/segment/binary_prefix_page.h
+++ b/be/src/storage/segment/binary_prefix_page.h
@@ -94,7 +94,7 @@ private:
 
 class BinaryPrefixPageDecoder : public PageDecoder {
 public:
-    BinaryPrefixPageDecoder(Slice data, const PageDecoderOptions& options)
+    BinaryPrefixPageDecoder(Slice data, const PageDecoderOptions& /*options*/)
             : _data(data), _parsed(false) {}
 
     Status init() override;
diff --git a/be/src/storage/segment/column_reader.cpp 
b/be/src/storage/segment/column_reader.cpp
index e1f9a8bdf7a..ea6fffe5bc6 100644
--- a/be/src/storage/segment/column_reader.cpp
+++ b/be/src/storage/segment/column_reader.cpp
@@ -2442,7 +2442,10 @@ Status FileColumnIterator::_read_dict_data() {
     RETURN_IF_ERROR(_reader->read_page(_opts, 
_reader->get_dict_page_pointer(), &_dict_page_handle,
                                        &dict_data, &dict_footer, 
_compress_codec));
     const EncodingInfo* encoding_info;
-    RETURN_IF_ERROR(EncodingInfo::get(FieldType::OLAP_FIELD_TYPE_VARCHAR,
+    // The dict pool stores strings of the outer column's type. Using the
+    // outer type (CHAR vs VARCHAR/STRING) lets the EncodingInfo pick a
+    // CHAR-strip pre-decoder so the cached dict page is already unpadded.
+    RETURN_IF_ERROR(EncodingInfo::get(_reader->get_meta_type(),
                                       
dict_footer.dict_page_footer().encoding(), &encoding_info));
     RETURN_IF_ERROR(encoding_info->create_page_decoder(dict_data, {}, 
_dict_decoder));
     RETURN_IF_ERROR(_dict_decoder->init());
diff --git a/be/src/storage/segment/encoding_info.cpp 
b/be/src/storage/segment/encoding_info.cpp
index 30700fe0f41..752627c2e28 100644
--- a/be/src/storage/segment/encoding_info.cpp
+++ b/be/src/storage/segment/encoding_info.cpp
@@ -33,6 +33,7 @@
 #include "storage/segment/binary_dict_page.h"
 #include "storage/segment/binary_dict_page_pre_decoder.h"
 #include "storage/segment/binary_plain_page.h"
+#include "storage/segment/binary_plain_page_char_strip_pre_decoder.h"
 #include "storage/segment/binary_plain_page_v2.h"
 #include "storage/segment/binary_plain_page_v2_pre_decoder.h"
 #include "storage/segment/binary_prefix_page.h"
@@ -433,15 +434,29 @@ EncodingInfo::EncodingInfo(TraitsClass traits)
     if (_encoding == BIT_SHUFFLE) {
         _data_page_pre_decoder = std::make_unique<BitShufflePagePreDecoder>();
     } else if (_encoding == DICT_ENCODING) {
-        _data_page_pre_decoder = std::make_unique<BinaryDictPagePreDecoder>();
+        if constexpr (TraitsClass::type == FieldType::OLAP_FIELD_TYPE_CHAR) {
+            _data_page_pre_decoder = 
std::make_unique<BinaryDictPagePreDecoder<true>>();
+        } else {
+            _data_page_pre_decoder = 
std::make_unique<BinaryDictPagePreDecoder<false>>();
+        }
+    } else if (_encoding == PLAIN_ENCODING) {
+        // CHAR plain pages may contain trailing '\0' padding written by older
+        // BEs; strip it once at page load so the cached page is unpadded.
+        if constexpr (TraitsClass::type == FieldType::OLAP_FIELD_TYPE_CHAR) {
+            _data_page_pre_decoder = 
std::make_unique<BinaryPlainPageCharStripPreDecoder>();
+        }
     } else if (_encoding == PLAIN_ENCODING_V2) {
         // Only binary types (Slice) need the predecoder for PLAIN_ENCODING_V2 
— it converts
         // varint-encoded lengths to an offset-array format that downstream 
Slice decoders expect.
-        // All current (type, PLAIN_ENCODING_V2) registrations are Slice 
(CHAR/VARCHAR/STRING/
-        // JSONB/VARIANT/HLL/BITMAP/QUANTILE_STATE/AGG_STATE per 
storage/types.h). The else throws
-        // at construction time to fail loudly if a future non-Slice 
registration is added.
-        if constexpr (std::is_same_v<typename TraitsClass::CppType, Slice>) {
-            _data_page_pre_decoder = 
std::make_unique<BinaryPlainPageV2PreDecoder>();
+        // CHAR pages additionally strip trailing '\0' padding written by the 
convertor; other
+        // Slice types use the non-CHAR specialization. All current (type, 
PLAIN_ENCODING_V2)
+        // registrations are Slice 
(CHAR/VARCHAR/STRING/JSONB/VARIANT/HLL/BITMAP/QUANTILE_STATE/
+        // AGG_STATE per storage/types.h). The else throws at construction 
time to fail loudly
+        // if a future non-Slice registration is added.
+        if constexpr (TraitsClass::type == FieldType::OLAP_FIELD_TYPE_CHAR) {
+            _data_page_pre_decoder = 
std::make_unique<BinaryPlainPageV2PreDecoder<true>>();
+        } else if constexpr (std::is_same_v<typename TraitsClass::CppType, 
Slice>) {
+            _data_page_pre_decoder = 
std::make_unique<BinaryPlainPageV2PreDecoder<false>>();
         } else {
             throw Exception(Status::FatalError(
                     "PLAIN_ENCODING_V2 is only supported for Slice (binary) 
types, but got "
diff --git a/be/src/storage/segment/page_io.cpp 
b/be/src/storage/segment/page_io.cpp
index cd415d93d5d..0bc86deaf24 100644
--- a/be/src/storage/segment/page_io.cpp
+++ b/be/src/storage/segment/page_io.cpp
@@ -232,11 +232,15 @@ Status PageIO::read_and_decompress_page_(const 
PageReadOptions& opts, PageHandle
     if (opts.pre_decode) {
         const auto* encoding_info = opts.encoding_info;
         if (footer->type() == DICTIONARY_PAGE) {
-            // dict page uses its own encoding from 
footer->dict_page_footer().encoding()
-            // to look up the pre_decoder
-            
RETURN_IF_ERROR(EncodingInfo::get(FieldType::OLAP_FIELD_TYPE_VARCHAR,
-                                              
footer->dict_page_footer().encoding(),
-                                              &encoding_info));
+            // Look up the dict page's encoding_info using the outer column's
+            // field type (not a hardcoded VARCHAR), so CHAR columns reach the
+            // CharStrip pre-decoder for the dict pool too. Falls back to
+            // VARCHAR only when the caller didn't set opts.encoding_info.
+            FieldType dict_field_type = opts.encoding_info != nullptr
+                                                ? opts.encoding_info->type()
+                                                : 
FieldType::OLAP_FIELD_TYPE_VARCHAR;
+            RETURN_IF_ERROR(EncodingInfo::get(
+                    dict_field_type, footer->dict_page_footer().encoding(), 
&encoding_info));
         }
         if (encoding_info) {
             auto* pre_decoder = encoding_info->get_data_page_pre_decoder();
diff --git a/be/src/storage/segment/segment_iterator.cpp 
b/be/src/storage/segment/segment_iterator.cpp
index ef2076713a3..7e9be2653a6 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -654,7 +654,7 @@ Status SegmentIterator::_lazy_init(Block* block) {
     }
     _current_return_columns.resize(_schema->columns().size());
 
-    _vec_init_char_column_id(block);
+    _vec_init_char_column_id();
     for (size_t i = 0; i < _schema->column_ids().size(); i++) {
         ColumnId cid = _schema->column_ids()[i];
         const auto* column_desc = _schema->column(cid);
@@ -1816,13 +1816,6 @@ Status 
SegmentIterator::_lookup_ordinal_from_sk_index(const RowCursor& key, bool
 
     const auto& key_col_ids = key.schema()->column_ids();
 
-    // Clone the key once and pad CHAR fields to storage format before the 
binary search.
-    // _seek_block holds storage-format data where CHAR is zero-padded to 
column length,
-    // while RowCursor holds CHAR in compute format (unpadded). Padding once 
here avoids
-    // repeated allocation inside the comparison loop.
-    RowCursor padded_key = key.clone();
-    padded_key.pad_char_fields();
-
     ssize_t start_block_id = 0;
     auto start_iter = sk_index_decoder->lower_bound(index_key);
     if (start_iter.valid()) {
@@ -1850,7 +1843,7 @@ Status 
SegmentIterator::_lookup_ordinal_from_sk_index(const RowCursor& key, bool
     while (start < end) {
         rowid_t mid = (start + end) / 2;
         RETURN_IF_ERROR(_seek_and_peek(mid));
-        int cmp = _compare_short_key_with_seek_block(padded_key, key_col_ids);
+        int cmp = _compare_short_key_with_seek_block(key, key_col_ids);
         if (cmp > 0) {
             start = mid + 1;
         } else if (cmp == 0) {
@@ -2206,29 +2199,8 @@ bool 
SegmentIterator::_can_evaluated_by_vectorized(std::shared_ptr<ColumnPredica
     }
 }
 
-bool SegmentIterator::_has_char_type(const TabletColumn& column_desc) {
-    switch (column_desc.type()) {
-    case FieldType::OLAP_FIELD_TYPE_CHAR:
-        return true;
-    case FieldType::OLAP_FIELD_TYPE_ARRAY:
-        return _has_char_type(column_desc.get_sub_column(0));
-    case FieldType::OLAP_FIELD_TYPE_MAP:
-        return _has_char_type(column_desc.get_sub_column(0)) ||
-               _has_char_type(column_desc.get_sub_column(1));
-    case FieldType::OLAP_FIELD_TYPE_STRUCT:
-        for (uint32_t idx = 0; idx < column_desc.get_subtype_count(); ++idx) {
-            if (_has_char_type(column_desc.get_sub_column(idx))) {
-                return true;
-            }
-        }
-        return false;
-    default:
-        return false;
-    }
-};
-
-void SegmentIterator::_vec_init_char_column_id(Block* block) {
-    if (!_char_type_idx.empty()) {
+void SegmentIterator::_vec_init_char_column_id() {
+    if (!_is_char_type.empty()) {
         return;
     }
     _is_char_type.resize(_schema->columns().size(), false);
@@ -2236,14 +2208,6 @@ void SegmentIterator::_vec_init_char_column_id(Block* 
block) {
         auto cid = _schema->column_id(i);
         const TabletColumn* column_desc = _schema->column(cid);
 
-        // The additional deleted filter condition will be in the materialized 
column at the end of the block.
-        // After _output_column_by_sel_idx, it will be erased, so we do not 
need to shrink it.
-        if (i < block->columns()) {
-            if (_has_char_type(*column_desc)) {
-                _char_type_idx.emplace_back(i);
-            }
-        }
-
         if (column_desc->type() == FieldType::OLAP_FIELD_TYPE_CHAR) {
             _is_char_type[cid] = true;
         }
@@ -3125,8 +3089,6 @@ Status SegmentIterator::_next_batch_internal(Block* 
block) {
         _output_index_result_column(vir_ctxs, sel_rowid_idx, _selected_size, 
block);
     }
     RETURN_IF_ERROR(_materialization_of_virtual_column(block));
-    // shrink char_type suffix zero data
-    block->shrink_char_type_column_suffix_zero(_char_type_idx);
     if (_opts.read_limit > 0) {
         _rows_returned += block->rows();
     }
@@ -3236,7 +3198,6 @@ Status SegmentIterator::_process_common_expr(uint16_t* 
sel_rowid_idx, uint16_t&
         common_ctxs.push_back(ctx.get());
     }
     _output_index_result_column(common_ctxs, _sel_rowid_idx.data(), 
_selected_size, block);
-    block->shrink_char_type_column_suffix_zero(_char_type_idx);
     RETURN_IF_ERROR(_execute_common_expr(_sel_rowid_idx.data(), 
_selected_size, block));
 
     if (need_mock_col) {
@@ -3639,7 +3600,6 @@ Status 
SegmentIterator::_materialization_of_virtual_column(Block* block) {
                     idx_in_block, block->columns(), 
_vir_cid_to_idx_in_block.size(),
                     column_expr->root()->debug_string());
         }
-        block->shrink_char_type_column_suffix_zero(_char_type_idx);
         if (check_and_get_column<const ColumnNothing>(
                     block->get_by_position(idx_in_block).column.get())) {
             VLOG_DEBUG << fmt::format("Virtual column is doing 
materialization, cid {}, col idx {}",
diff --git a/be/src/storage/segment/segment_iterator.h 
b/be/src/storage/segment/segment_iterator.h
index a54aae7db89..c7faf3fdb51 100644
--- a/be/src/storage/segment/segment_iterator.h
+++ b/be/src/storage/segment/segment_iterator.h
@@ -204,11 +204,7 @@ private:
     bool _is_literal_node(const TExprNodeType::type& node_type);
 
     Status _vec_init_lazy_materialization();
-    // TODO: Fix Me
-    // CHAR type in storage layer padding the 0 in length. But query engine 
need ignore the padding 0.
-    // so segment iterator need to shrink char column before output it. only 
use in vec query engine.
-    void _vec_init_char_column_id(Block* block);
-    bool _has_char_type(const TabletColumn& column_desc);
+    void _vec_init_char_column_id();
 
     uint32_t segment_id() const { return _segment->id(); }
     uint32_t num_rows() const { return _segment->num_rows(); }
@@ -431,8 +427,6 @@ private:
 
     io::FileReaderSPtr _file_reader;
 
-    // char_type or array<char> type columns cid
-    std::vector<size_t> _char_type_idx;
     std::vector<bool> _is_char_type;
 
     // used for compaction, record selectd rowids of current batch
diff --git a/be/test/core/block/block_test.cpp 
b/be/test/core/block/block_test.cpp
index bb60f2fabf7..3b29f819e09 100644
--- a/be/test/core/block/block_test.cpp
+++ b/be/test/core/block/block_test.cpp
@@ -1337,8 +1337,6 @@ TEST(BlockTest, others) {
     auto block = ColumnHelper::create_block<DataTypeInt32>({1, 2, 3});
     block.insert(ColumnHelper::create_column_with_name<DataTypeString>({"abc", 
"efg", "hij"}));
 
-    block.shrink_char_type_column_suffix_zero({1, 2});
-
     SipHash hash;
     block.update_hash(hash);
 
diff --git a/be/test/core/column/column_array_test.cpp 
b/be/test/core/column/column_array_test.cpp
index 163fdfe87be..3af184caccf 100644
--- a/be/test/core/column/column_array_test.cpp
+++ b/be/test/core/column/column_array_test.cpp
@@ -605,11 +605,6 @@ TEST_F(ColumnArrayTest, ColumnStringFuncsTest) {
     assert_column_string_funcs(array_columns);
 }
 
-// test shrink_padding_chars_callback
-TEST_F(ColumnArrayTest, ShrinkPaddingCharsTest) {
-    shrink_padding_chars_callback(array_columns, serdes);
-}
-
 //////////////////////// special function from column_array.h 
////////////////////////
 TEST_F(ColumnArrayTest, SharedCreateValidatesOffsetsAndDataSize) {
     auto data_mut = ColumnInt32::create();
diff --git a/be/test/core/column/column_string_test.cpp 
b/be/test/core/column/column_string_test.cpp
index e57c02c3155..6d29b555367 100644
--- a/be/test/core/column/column_string_test.cpp
+++ b/be/test/core/column/column_string_test.cpp
@@ -1326,28 +1326,6 @@ TEST_F(ColumnStringTest, TestStringInsert) {
         }
     }
 }
-TEST_F(ColumnStringTest, shrink_padding_chars) {
-    ColumnString::MutablePtr col = ColumnString::create();
-    col->shrink_padding_chars();
-
-    col->insert_data("123\0   ", 7);
-    col->insert_data("456\0xx", 6);
-    col->insert_data("78", 2);
-    col->shrink_padding_chars();
-
-    EXPECT_EQ(col->size(), 3);
-    EXPECT_EQ(col->get_data_at(0), StringRef("123"));
-    EXPECT_EQ(col->get_data_at(0).size, 3);
-    EXPECT_EQ(col->get_data_at(1), StringRef("456"));
-    EXPECT_EQ(col->get_data_at(1).size, 3);
-    EXPECT_EQ(col->get_data_at(2), StringRef("78"));
-    EXPECT_EQ(col->get_data_at(2).size, 2);
-
-    col->insert_data("xyz", 2); // only xy
-
-    EXPECT_EQ(col->size(), 4);
-    EXPECT_EQ(col->get_data_at(3), StringRef("xy"));
-}
 TEST_F(ColumnStringTest, sort_column) {
     column_string_common_test(assert_sort_column_callback, false);
 }
diff --git a/be/test/core/column/common_column_test.h 
b/be/test/core/column/common_column_test.h
index 4a283670daf..38aae5461b2 100644
--- a/be/test/core/column/common_column_test.h
+++ b/be/test/core/column/common_column_test.h
@@ -2370,43 +2370,6 @@ public:
         }
     }
 
-    // get_shrinked_column should only happened in char-type column or nested 
char-type column,
-    // other column just return the origin column without any data changed, so 
check file content should be the same as the origin column
-    //  just shrink the end zeros for char-type column which happened in 
segmentIterator
-    //    eg. column_desc: char(6), insert into char(3), the char(3) will 
padding the 3 zeros at the end for writing to disk.
-    //       but we select should just print the char(3) without the padding 
zeros
-    //  limit and topN operation will trigger this function call
-    void shrink_padding_chars_callback(MutableColumns& load_cols, 
DataTypeSerDeSPtrs serders) {
-        auto option = DataTypeSerDe::FormatOptions();
-        std::vector<std::vector<std::string>> res;
-        for (size_t i = 0; i < load_cols.size(); i++) {
-            auto& source_column = load_cols[i];
-            LOG(INFO) << "now we are in shrink_padding_chars column : " << 
load_cols[i]->get_name()
-                      << " for column size : " << source_column->size();
-            source_column->shrink_padding_chars();
-            // check after get_shrinked_column: 1 in selector present the load 
cols data is selected and data should be default value
-            auto ser_col = ColumnString::create();
-            ser_col->reserve(source_column->size());
-            VectorBufferWriter buffer_writer(*ser_col.get());
-            std::vector<std::string> data;
-            data.push_back("column: " + source_column->get_name() +
-                           " with shrinked column size: " + 
std::to_string(source_column->size()));
-            for (size_t j = 0; j < source_column->size(); ++j) {
-                if (auto st = 
serders[i]->serialize_one_cell_to_json(*source_column, j,
-                                                                     
buffer_writer, option);
-                    !st) {
-                    LOG(ERROR) << "Failed to serialize column " << i << " at 
row " << j;
-                    break;
-                }
-                buffer_writer.commit();
-                std::string actual_str_value = 
ser_col->get_data_at(j).to_string();
-                data.push_back(actual_str_value);
-            }
-            res.push_back(data);
-        }
-        check_res_file("shrink_padding_chars", res);
-    }
-
     void assert_size_eq(MutableColumnPtr col, size_t expect_size) {
         EXPECT_EQ(col->size(), expect_size);
     }
diff --git a/be/test/exprs/bloom_filter_func_test.cpp 
b/be/test/exprs/bloom_filter_func_test.cpp
index 15406cbff7f..f06283ddf23 100644
--- a/be/test/exprs/bloom_filter_func_test.cpp
+++ b/be/test/exprs/bloom_filter_func_test.cpp
@@ -570,8 +570,10 @@ TEST_F(BloomFilterFuncTest, FindFixedLenOlapEngine) {
 
     bloom_filter_func2.insert_fixed_len(string_column->clone(), 0);
 
-    StringRef strings[] = {StringRef("aa"), StringRef("bb"), StringRef("cc"),
-                           StringRef("dd\0\0", 4), StringRef("ef\0\0", 4)};
+    // CHAR padding is stripped at the page decoder now, so the runtime BF
+    // probe sees natural-length StringRefs; no trailing '\0' bytes here.
+    StringRef strings[] = {StringRef("aa"), StringRef("bb"), StringRef("cc"), 
StringRef("dd"),
+                           StringRef("ef")};
 
     PODArray<uint16_t> offsets2(5);
     std::iota(offsets2.begin(), offsets2.end(), 0);
diff --git a/be/test/storage/olap_type_test.cpp 
b/be/test/storage/olap_type_test.cpp
index 05775b693e4..741f75bf47b 100644
--- a/be/test/storage/olap_type_test.cpp
+++ b/be/test/storage/olap_type_test.cpp
@@ -2083,4 +2083,52 @@ TEST_F(OlapTypeTest, timestamptz_type) {
                 << "serde mismatch for TIMESTAMPTZ expected=" << tc.expected;
     }
 }
+
+// from_olap_string for string types (CHAR / VARCHAR / STRING) strnlens the
+// input so any trailing '\0' bytes that came from a fixed-width CHAR write
+// are dropped before the value lands in the Field. VARCHAR / STRING ZoneMap
+// values do not normally carry trailing '\0' (the writers store the natural
+// byte length), so strnlen is a no-op for them.
+TEST_F(OlapTypeTest, from_olap_string_strings) {
+    struct Case {
+        PrimitiveType type;
+        std::string input;
+        std::string expected;
+    };
+    std::vector<Case> cases = {
+            // CHAR(N) ZoneMap min/max from the convertor is padded with '\0'
+            // — strnlen recovers the logical content.
+            {TYPE_CHAR, std::string("abc", 3) + std::string(7, '\0'), "abc"},
+            {TYPE_CHAR, std::string(10, '\0'), ""},
+            {TYPE_CHAR, "alpha", "alpha"},
+            // VARCHAR / STRING never carry trailing '\0' in their ZoneMap
+            // representation, so the helper is a transparent pass-through
+            // for the typical case.
+            {TYPE_VARCHAR, "hello", "hello"},
+            {TYPE_STRING, "world\nline2", "world\nline2"},
+            {TYPE_STRING, "", ""},
+    };
+
+    for (const auto& tc : cases) {
+        auto data_type = DataTypeFactory::instance().create_data_type(
+                tc.type, /*is_nullable=*/false, /*precision=*/0, /*scale=*/0,
+                /*length=*/static_cast<int>(tc.input.size()));
+        expect_from_storage_string_paths(data_type, tc.input, [&](const Field& 
field) {
+            EXPECT_EQ(field.get<TYPE_STRING>(), tc.expected)
+                    << "type=" << static_cast<int>(tc.type) << " input.size=" 
<< tc.input.size();
+        });
+    }
+}
+
+// VARCHAR / STRING values containing an embedded '\0' are truncated at the
+// first '\0' — the same strnlen behaviour applies to all string types. This
+// is acceptable in practice because Doris string columns do not store
+// embedded NULs in their ZoneMap representation; the test pins the contract.
+TEST_F(OlapTypeTest, from_olap_string_strings_embedded_null_truncates) {
+    auto data_type = DataTypeFactory::instance().create_data_type(
+            TYPE_VARCHAR, /*is_nullable=*/false, 0, 0, /*length=*/32);
+    expect_from_storage_string_paths(data_type, std::string("ab\0cd", 5), 
[](const Field& field) {
+        EXPECT_EQ(field.get<TYPE_STRING>(), "ab");
+    });
+}
 } // namespace doris
diff --git a/be/test/storage/segment/binary_dict_page_test.cpp 
b/be/test/storage/segment/binary_dict_page_test.cpp
index 36d455c72a5..79c76ba52d6 100644
--- a/be/test/storage/segment/binary_dict_page_test.cpp
+++ b/be/test/storage/segment/binary_dict_page_test.cpp
@@ -74,7 +74,7 @@ public:
                                                           
std::unique_ptr<DataPage>& decoded_page) {
         // Apply pre-decode for BinaryPlainPageV2
         if (encoding_type == PLAIN_ENCODING_V2) {
-            BinaryPlainPageV2PreDecoder pre_decoder;
+            BinaryPlainPageV2PreDecoder<false> pre_decoder;
             Status status = pre_decoder.decode(&decoded_page, &dict_slice, 0, 
false,
                                                PageTypePB::DATA_PAGE, "");
             if (!status.ok()) {
@@ -107,7 +107,7 @@ public:
     // Apply pre-decode for BinaryDictPage data pages
     // This method handles all encoding types (bitshuffle, plain V1, plain V2)
     Status apply_pre_decode(Slice& page_slice, std::unique_ptr<DataPage>& 
decoded_page) {
-        BinaryDictPagePreDecoder pre_decoder;
+        BinaryDictPagePreDecoder</*IS_CHAR=*/false> pre_decoder;
         return pre_decoder.decode(&decoded_page, &page_slice, 0, false, 
PageTypePB::DATA_PAGE, "");
     }
 
@@ -674,7 +674,7 @@ TEST_F(BinaryDictPageTest, 
TestConfigAffectsDictionaryPageEncoding) {
         // First apply pre-decode for BinaryPlainPageV2
         Slice dict_page_slice = dict_slice.slice();
         std::unique_ptr<DataPage> decoded_page;
-        BinaryPlainPageV2PreDecoder pre_decoder;
+        BinaryPlainPageV2PreDecoder<false> pre_decoder;
         status = pre_decoder.decode(&decoded_page, &dict_page_slice, 0, false,
                                     PageTypePB::DATA_PAGE, "");
         EXPECT_TRUE(status.ok());
diff --git a/be/test/storage/segment/binary_plain_page_v2_test.cpp 
b/be/test/storage/segment/binary_plain_page_v2_test.cpp
index 29b74961427..cc27f1ce95e 100644
--- a/be/test/storage/segment/binary_plain_page_v2_test.cpp
+++ b/be/test/storage/segment/binary_plain_page_v2_test.cpp
@@ -27,6 +27,8 @@
 #include "core/column/column_string.h"
 #include "storage/cache/page_cache.h"
 #include "storage/olap_common.h"
+#include "storage/segment/binary_plain_page.h"
+#include "storage/segment/binary_plain_page_char_strip_pre_decoder.h"
 #include "storage/segment/binary_plain_page_v2_pre_decoder.h"
 #include "storage/segment/page_builder.h"
 #include "storage/segment/page_decoder.h"
@@ -43,7 +45,7 @@ public:
     // Helper method to apply pre-decode step for BinaryPlainPageV2
     // Similar to decode_bitshuffle_page in BinaryDictPageTest
     Status apply_pre_decode(Slice& page_slice, std::unique_ptr<DataPage>& 
decoded_page) {
-        BinaryPlainPageV2PreDecoder pre_decoder;
+        BinaryPlainPageV2PreDecoder<false> pre_decoder;
         return pre_decoder.decode(&decoded_page, &page_slice, 0, false, 
PageTypePB::DATA_PAGE, "");
     }
 
@@ -553,5 +555,146 @@ TEST_F(BinaryPlainPageV2Test, TestSeekAndRead) {
     EXPECT_EQ("e", string_column->get_data_at(2).to_string());
 }
 
+// CHAR-specific roundtrip: write padded slices (the on-disk format produced
+// by OlapColumnDataConvertorChar) through both CHAR-strip pre-decoders and
+// confirm the post-decode column surfaces the unpadded logical content.
+
+namespace {
+
+// Build padded slices of fixed width `pad_len`: each input `s` is copied into
+// a buffer of size pad_len with trailing '\0' fill.
+// Build a vector of zero-padded buffers of fixed width `pad_len`. The caller
+// builds Slices into the returned buffers AFTER the vector is settled — keep
+// Slice creation outside this helper so the buffers' addresses don't move
+// underneath the Slices.
+std::vector<std::string> make_padded_buffers(const std::vector<std::string>& 
logical,
+                                             size_t pad_len) {
+    std::vector<std::string> buffers;
+    buffers.reserve(logical.size());
+    for (const auto& s : logical) {
+        EXPECT_LE(s.size(), pad_len);
+        std::string buf = s;
+        buf.resize(pad_len, '\0');
+        buffers.push_back(std::move(buf));
+    }
+    return buffers;
+}
+
+std::vector<Slice> slices_from(const std::vector<std::string>& buffers, size_t 
pad_len) {
+    std::vector<Slice> slices;
+    slices.reserve(buffers.size());
+    for (const auto& b : buffers) {
+        slices.emplace_back(b.data(), pad_len);
+    }
+    return slices;
+}
+
+void verify_unpadded_column(MutableColumnPtr& column, const 
std::vector<std::string>& expected) {
+    auto* str_col = assert_cast<ColumnString*>(column.get());
+    ASSERT_EQ(expected.size(), str_col->size());
+    for (size_t i = 0; i < expected.size(); ++i) {
+        auto got = str_col->get_data_at(i).to_string();
+        EXPECT_EQ(expected[i], got) << "row " << i;
+    }
+}
+
+} // namespace
+
+// V2 plain page with padded CHAR slices → BinaryPlainPageV2PreDecoder<true>
+// → BinaryPlainPageDecoder → strings come out unpadded.
+TEST_F(BinaryPlainPageV2Test, CharStripPreDecoder_V2_RoundtripPaddedSlices) {
+    constexpr size_t pad_len = 8;
+    std::vector<std::string> logical = {"a", "bc", "", "alpha", "alpaca12"};
+    auto buffers = make_padded_buffers(logical, pad_len);
+    auto slices = slices_from(buffers, pad_len);
+
+    PageBuilderOptions builder_options;
+    builder_options.data_page_size = 256 * 1024;
+    PageBuilder* builder_ptr = nullptr;
+    
ASSERT_TRUE(BinaryPlainPageV2Builder<FieldType::OLAP_FIELD_TYPE_CHAR>::create(&builder_ptr,
+                                                                               
   builder_options)
+                        .ok());
+    std::unique_ptr<PageBuilder> wrapper(builder_ptr);
+    auto* page_builder =
+            
static_cast<BinaryPlainPageV2Builder<FieldType::OLAP_FIELD_TYPE_CHAR>*>(builder_ptr);
+    size_t count = slices.size();
+    ASSERT_TRUE(page_builder->add(reinterpret_cast<const 
uint8_t*>(slices.data()), &count).ok());
+    ASSERT_EQ(slices.size(), count);
+
+    OwnedSlice owned_slice;
+    ASSERT_TRUE(page_builder->finish(&owned_slice).ok());
+
+    // Run the CHAR-strip pre-decoder (the one EncodingInfo would pick for a
+    // CHAR PLAIN_ENCODING_V2 page).
+    Slice page_slice = owned_slice.slice();
+    std::unique_ptr<DataPage> decoded_page;
+    BinaryPlainPageV2PreDecoder<true> pre_decoder;
+    ASSERT_TRUE(pre_decoder
+                        .decode(&decoded_page, &page_slice, /*size_of_tail=*/0,
+                                /*use_cache=*/false, PageTypePB::DATA_PAGE, "")
+                        .ok());
+
+    // Decode the resulting V1 layout and verify rows are unpadded.
+    PageDecoderOptions decoder_options;
+    BinaryPlainPageDecoder<FieldType::OLAP_FIELD_TYPE_CHAR> 
page_decoder(page_slice,
+                                                                         
decoder_options);
+    ASSERT_TRUE(page_decoder.init().ok());
+    ASSERT_EQ(logical.size(), page_decoder.count());
+
+    MutableColumnPtr column = ColumnString::create();
+    size_t num_to_read = logical.size();
+    ASSERT_TRUE(page_decoder.next_batch(&num_to_read, column).ok());
+    ASSERT_EQ(logical.size(), num_to_read);
+    verify_unpadded_column(column, logical);
+}
+
+// V1 plain page with padded CHAR slices → BinaryPlainPageCharStripPreDecoder
+// → BinaryPlainPageDecoder → strings come out unpadded. This mirrors the
+// V2 case above for the PLAIN_ENCODING path.
+TEST_F(BinaryPlainPageV2Test, CharStripPreDecoder_V1_RoundtripPaddedSlices) {
+    constexpr size_t pad_len = 6;
+    std::vector<std::string> logical = {"x", "abcd", "", "zzzzzz", "qw"};
+    auto buffers = make_padded_buffers(logical, pad_len);
+    auto slices = slices_from(buffers, pad_len);
+
+    // Build a V1 plain page (no varint length prefixes — data + offsets +
+    // num_elems trailer).
+    PageBuilderOptions builder_options;
+    builder_options.data_page_size = 256 * 1024;
+    PageBuilder* builder_ptr = nullptr;
+    
ASSERT_TRUE(BinaryPlainPageBuilder<FieldType::OLAP_FIELD_TYPE_CHAR>::create(&builder_ptr,
+                                                                               
 builder_options)
+                        .ok());
+    std::unique_ptr<PageBuilder> wrapper(builder_ptr);
+    auto* page_builder =
+            
static_cast<BinaryPlainPageBuilder<FieldType::OLAP_FIELD_TYPE_CHAR>*>(builder_ptr);
+    size_t count = slices.size();
+    ASSERT_TRUE(page_builder->add(reinterpret_cast<const 
uint8_t*>(slices.data()), &count).ok());
+    ASSERT_EQ(slices.size(), count);
+
+    OwnedSlice owned_slice;
+    ASSERT_TRUE(page_builder->finish(&owned_slice).ok());
+
+    Slice page_slice = owned_slice.slice();
+    std::unique_ptr<DataPage> decoded_page;
+    BinaryPlainPageCharStripPreDecoder pre_decoder;
+    ASSERT_TRUE(pre_decoder
+                        .decode(&decoded_page, &page_slice, /*size_of_tail=*/0,
+                                /*use_cache=*/false, PageTypePB::DATA_PAGE, "")
+                        .ok());
+
+    PageDecoderOptions decoder_options;
+    BinaryPlainPageDecoder<FieldType::OLAP_FIELD_TYPE_CHAR> 
page_decoder(page_slice,
+                                                                         
decoder_options);
+    ASSERT_TRUE(page_decoder.init().ok());
+    ASSERT_EQ(logical.size(), page_decoder.count());
+
+    MutableColumnPtr column = ColumnString::create();
+    size_t num_to_read = logical.size();
+    ASSERT_TRUE(page_decoder.next_batch(&num_to_read, column).ok());
+    ASSERT_EQ(logical.size(), num_to_read);
+    verify_unpadded_column(column, logical);
+}
+
 } // namespace segment_v2
 } // namespace doris
diff --git a/be/test/storage/segment/encoding_info_test.cpp 
b/be/test/storage/segment/encoding_info_test.cpp
index 641e84e0759..0a60c914e86 100644
--- a/be/test/storage/segment/encoding_info_test.cpp
+++ b/be/test/storage/segment/encoding_info_test.cpp
@@ -28,6 +28,7 @@
 #include "runtime/exec_env.h"
 #include "storage/olap_common.h"
 #include "storage/segment/binary_dict_page_pre_decoder.h"
+#include "storage/segment/binary_plain_page_char_strip_pre_decoder.h"
 #include "storage/segment/binary_plain_page_v2_pre_decoder.h"
 #include "storage/segment/bitshuffle_page_pre_decoder.h"
 #include "storage/types.h"
@@ -149,10 +150,15 @@ TEST_F(EncodingInfoTest, test_all_pre_decoders) {
         auto* pre_decoder = encoding_info->get_data_page_pre_decoder();
         ASSERT_NE(nullptr, pre_decoder) << "Type " << static_cast<int>(type)
                                         << " with DICT_ENCODING should have 
pre_decoder";
-        auto* dict_decoder = 
dynamic_cast<BinaryDictPagePreDecoder*>(pre_decoder);
-        EXPECT_NE(nullptr, dict_decoder)
-                << "Type " << static_cast<int>(type)
-                << " with DICT_ENCODING should have BinaryDictPagePreDecoder";
+        // CHAR uses the IS_CHAR=true specialization so it strips trailing '\0'
+        // padding from inline-binary dict fallbacks; other string types use
+        // the regular non-CHAR specialization.
+        bool is_dict_decoder =
+                (type == FieldType::OLAP_FIELD_TYPE_CHAR)
+                        ? 
dynamic_cast<BinaryDictPagePreDecoder<true>*>(pre_decoder) != nullptr
+                        : 
dynamic_cast<BinaryDictPagePreDecoder<false>*>(pre_decoder) != nullptr;
+        EXPECT_TRUE(is_dict_decoder) << "Type " << static_cast<int>(type)
+                                     << " with DICT_ENCODING should have 
BinaryDictPagePreDecoder";
     }
 
     // Test PLAIN_ENCODING_V2 with Slice types - should have 
BinaryPlainPageV2PreDecoder
@@ -173,10 +179,16 @@ TEST_F(EncodingInfoTest, test_all_pre_decoders) {
         auto* pre_decoder = encoding_info->get_data_page_pre_decoder();
         ASSERT_NE(nullptr, pre_decoder) << "Type " << static_cast<int>(type)
                                         << " with PLAIN_ENCODING_V2 should 
have pre_decoder";
-        auto* v2_decoder = 
dynamic_cast<BinaryPlainPageV2PreDecoder*>(pre_decoder);
-        EXPECT_NE(nullptr, v2_decoder) << "Type " << static_cast<int>(type)
-                                       << " with PLAIN_ENCODING_V2 should have 
"
-                                          "BinaryPlainPageV2PreDecoder";
+        // CHAR PLAIN_ENCODING_V2 is wired to BinaryPlainPageV2PreDecoder<true>
+        // so the trailing '\0' padding written by OlapColumnDataConvertorChar
+        // is stripped at page load time; other binary types use the regular
+        // <false> instantiation.
+        bool ok =
+                (type == FieldType::OLAP_FIELD_TYPE_CHAR)
+                        ? 
dynamic_cast<BinaryPlainPageV2PreDecoder<true>*>(pre_decoder) != nullptr
+                        : 
dynamic_cast<BinaryPlainPageV2PreDecoder<false>*>(pre_decoder) != nullptr;
+        EXPECT_TRUE(ok) << "Type " << static_cast<int>(type)
+                        << " with PLAIN_ENCODING_V2 should have V2 
pre-decoder";
     }
 
     // Test PLAIN_ENCODING - should NOT have pre_decoder
@@ -216,8 +228,19 @@ TEST_F(EncodingInfoTest, test_all_pre_decoders) {
         auto status = EncodingInfo::get(type, PLAIN_ENCODING, &encoding_info);
         if (status.ok() && encoding_info != nullptr) {
             auto* pre_decoder = encoding_info->get_data_page_pre_decoder();
-            EXPECT_EQ(nullptr, pre_decoder) << "Type " << 
static_cast<int>(type)
-                                            << " with PLAIN_ENCODING should 
NOT have pre_decoder";
+            if (type == FieldType::OLAP_FIELD_TYPE_CHAR) {
+                // CHAR PLAIN_ENCODING has a CHAR-strip pre-decoder that strips
+                // the trailing '\0' padding written by the convertor.
+                EXPECT_NE(nullptr, pre_decoder)
+                        << "CHAR with PLAIN_ENCODING should have a 
pre_decoder";
+                EXPECT_NE(nullptr, 
dynamic_cast<BinaryPlainPageCharStripPreDecoder*>(pre_decoder))
+                        << "CHAR PLAIN_ENCODING pre-decoder should be "
+                           "BinaryPlainPageCharStripPreDecoder";
+            } else {
+                EXPECT_EQ(nullptr, pre_decoder)
+                        << "Type " << static_cast<int>(type)
+                        << " with PLAIN_ENCODING should NOT have pre_decoder";
+            }
         }
     }
 
diff --git a/be/test/storage/segment/segment_writer_full_encode_keys_test.cpp 
b/be/test/storage/segment/segment_writer_full_encode_keys_test.cpp
deleted file mode 100644
index c2748a43fb9..00000000000
--- a/be/test/storage/segment/segment_writer_full_encode_keys_test.cpp
+++ /dev/null
@@ -1,100 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-#include <gtest/gtest.h>
-
-#include <string>
-#include <vector>
-
-#include "core/string_view.h" // hex_dump
-#include "storage/iterator/olap_data_convertor.h"
-#include "storage/key_coder.h"
-#include "storage/olap_common.h"
-#include "storage/segment/segment_writer.h"
-#include "util/slice.h"
-
-namespace doris {
-namespace segment_v2 {
-using namespace doris;
-
-auto create_string_accessor(const std::vector<std::string>& str) {
-    ColumnString::MutablePtr column = ColumnString::create();
-    // ASSERT_TRUE(!str.empty());
-    for (auto& s : str) column->insert_value(s);
-    DataTypePtr data_type =
-            
DataTypeFactory::instance().create_data_type(FieldType::OLAP_FIELD_TYPE_VARCHAR,
 0, 0);
-    ColumnWithTypeAndName typed_column(column->get_ptr(), data_type, 
"test_string_column");
-
-    // Create a VARCHAR convertor, a convertor is an accessor
-    auto convertor =
-            
std::make_shared<OlapBlockDataConvertor::OlapColumnDataConvertorVarChar>(false);
-    convertor->set_source_column(typed_column, 0, str.size()); // row_pos=0, 
num_rows=str.size()
-
-    // Convert to OLAP format
-    auto status = convertor->convert_to_olap();
-    EXPECT_TRUE(status.ok());
-    if (status.ok()) {
-        // Get the converted data
-        const void* data = convertor->get_data_at(0);
-        // const UInt8* nullmap = convertor->get_nullmap();
-        std::cout << ((StringRef*)data)->to_string() << std::endl;
-        std::cout << column->get_data_at(0) << std::endl;
-        // Use the converted data as needed
-    }
-    return convertor;
-}
-
-auto create_int_accessor(const 
std::vector<PrimitiveTypeTraits<TYPE_BIGINT>::CppType>& values) {
-    // ASSERT_TRUE(!values.empty());
-    auto column = ColumnInt64::create();
-    for (auto value : values) column->insert_value(value);
-    DataTypePtr data_type = 
DataTypeFactory::instance().create_data_type(TYPE_INT, 0, 0);
-    ColumnWithTypeAndName typed_column(column->get_ptr(), data_type, 
"test_int_column");
-    auto convertor =
-            
std::make_shared<OlapBlockDataConvertor::OlapColumnDataConvertorSimple<TYPE_BIGINT>>();
-    convertor->set_source_column(typed_column, 0,
-                                 values.size()); // row_pos=0, 
num_rows=values.size()
-    auto status = convertor->convert_to_olap();
-    EXPECT_TRUE(status.ok());
-    return convertor;
-}
-
-TEST(SegmentWriterFullEncodeKeysTest, TestSegmentWriterKeyEncoding) {
-    // 2 rows of key columns(int,string,string), expect encode bytes of row1 < 
row2
-    //               0x05050505, a,     bb
-    //               0x05050505, a\x01, cc
-    // however the ending byte of 2nd row is \x01 (smaller than 
KEY_NORMAL_MARKER)
-    // will be in reversed order after encoding
-    auto int_accessor = create_int_accessor({0x05050505, 0x05050505});
-    auto str_accessor0 = create_string_accessor({"a", "a\x01"});
-    auto str_accessor1 = create_string_accessor({"bb", "cc"});
-    std::vector<IOlapColumnDataAccessor*> key_columns = {int_accessor.get(), 
str_accessor0.get(),
-                                                         str_accessor1.get()};
-    auto int_coder = get_key_coder(FieldType::OLAP_FIELD_TYPE_INT);
-    auto str_coder = get_key_coder(FieldType::OLAP_FIELD_TYPE_VARCHAR);
-    std::vector<const KeyCoder*> key_coders = {int_coder, str_coder, 
str_coder};
-    
////////////////////////////////////////////////////////////////////////////
-    std::string encoded0 = SegmentWriter::_full_encode_keys(key_coders, 
key_columns, 0);
-    std::string encoded1 = SegmentWriter::_full_encode_keys(key_coders, 
key_columns, 1);
-    
////////////////////////////////////////////////////////////////////////////
-    std::cout << StringView(encoded0).dump_hex() << std::endl; // 
X'02850505050261026262'
-    std::cout << StringView(encoded1).dump_hex() << std::endl; // 
X'0285050505026101026363'
-    // EXPECT_LT(encoded0, encoded1); // BANG! not satisfied
-}
-
-} // namespace segment_v2
-} // namespace doris
diff --git a/be/test/storage/segment/zone_map_index_test.cpp 
b/be/test/storage/segment/zone_map_index_test.cpp
index 4ec894feac6..51f8c068947 100644
--- a/be/test/storage/segment/zone_map_index_test.cpp
+++ b/be/test/storage/segment/zone_map_index_test.cpp
@@ -249,16 +249,16 @@ public:
                 DataTypeFactory::instance().create_data_type(TYPE_CHAR, true, 
0, 0, length);
         auto tab_col = create_char_key(0, true, length);
         const TabletColumn* field = tab_col.get();
-        std::string s_less_than_schema_length1(length - 1, 'a');
-        std::string s_less_than_schema_length1_expect(length, 'a');
-        s_less_than_schema_length1_expect[length - 1] = '\0';
-        std::string s_less_than_schema_length2(length - 2, 'b');
-        std::string s_less_than_schema_length2_expect(length, 'b');
-        s_less_than_schema_length2_expect[length - 1] = '\0';
-        s_less_than_schema_length2_expect[length - 2] = '\0';
+        // ZoneMap writer stores whatever slice bytes it receives. In 
production
+        // OlapColumnDataConvertorChar pads CHAR slices to the declared length
+        // before they reach the writer; from_olap_string strnlens at read time
+        // so the materialized Field is always unpadded. This test passes raw
+        // shorter slices directly to the writer to exercise the strnlen path.
+        std::string s_less_than_char_len1(length - 1, 'a');
+        std::string s_less_than_char_len2(length - 2, 'b');
         std::unique_ptr<ZoneMapIndexWriter> writer;
         ASSERT_TRUE(ZoneMapIndexWriter::create(data_type, field, writer).ok());
-        Slice slices[] = {Slice(s_less_than_schema_length1), 
Slice(s_less_than_schema_length2)};
+        Slice slices[] = {Slice(s_less_than_char_len1), 
Slice(s_less_than_char_len2)};
         writer->add_values(&slices, 2);
         if (pass_all) {
             writer->invalid_page_zone_map();
@@ -274,13 +274,13 @@ public:
         ASSERT_TRUE(file_writer->close().ok());
 
         const auto& seg_zm = index_meta.zone_map_index().segment_zone_map();
-        // Min/Max should be truncated to MAX_ZONE_MAP_INDEX_SIZE and last 
byte of Max is incremented
-        EXPECT_EQ(seg_zm.min().size(), s_less_than_schema_length1.size());
-        EXPECT_EQ(seg_zm.min(), s_less_than_schema_length1);
-        EXPECT_EQ(seg_zm.max().size(), s_less_than_schema_length2.size());
-        EXPECT_EQ(seg_zm.max(), s_less_than_schema_length2);
+        // On-disk min/max preserve the raw (unpadded) bytes.
+        EXPECT_EQ(seg_zm.min().size(), s_less_than_char_len1.size());
+        EXPECT_EQ(seg_zm.min(), s_less_than_char_len1);
+        EXPECT_EQ(seg_zm.max().size(), s_less_than_char_len2.size());
+        EXPECT_EQ(seg_zm.max(), s_less_than_char_len2);
 
-        // Verify ZoneMap::from_proto can correctly parse the truncated zone 
map
+        // Verify ZoneMap::from_proto materializes the unpadded Field.
         ZoneMap seg_zone_map;
         ASSERT_TRUE(ZoneMap::from_proto(seg_zm, data_type, seg_zone_map).ok());
         EXPECT_EQ(seg_zone_map.has_null, false);
@@ -289,10 +289,10 @@ public:
         EXPECT_EQ(seg_zone_map.has_positive_inf, false);
         EXPECT_EQ(seg_zone_map.has_negative_inf, false);
         EXPECT_EQ(seg_zone_map.has_nan, false);
-        EXPECT_EQ(seg_zone_map.min_value.get<TYPE_CHAR>().size(), length);
-        EXPECT_EQ(seg_zone_map.min_value.get<TYPE_CHAR>(), 
s_less_than_schema_length1_expect);
-        EXPECT_EQ(seg_zone_map.max_value.get<TYPE_CHAR>().size(), length);
-        EXPECT_EQ(seg_zone_map.max_value.get<TYPE_CHAR>(), 
s_less_than_schema_length2_expect);
+        EXPECT_EQ(seg_zone_map.min_value.get<TYPE_CHAR>().size(), 
s_less_than_char_len1.size());
+        EXPECT_EQ(seg_zone_map.min_value.get<TYPE_CHAR>(), 
s_less_than_char_len1);
+        EXPECT_EQ(seg_zone_map.max_value.get<TYPE_CHAR>().size(), 
s_less_than_char_len2.size());
+        EXPECT_EQ(seg_zone_map.max_value.get<TYPE_CHAR>(), 
s_less_than_char_len2);
 
         io::FileReaderSPtr file_reader;
         EXPECT_TRUE(_fs->open_file(file_path, &file_reader).ok());
@@ -315,12 +315,12 @@ public:
             EXPECT_EQ(page_zone_map.has_negative_inf, false);
             EXPECT_EQ(page_zone_map.has_nan, false);
             if (!pass_all) {
-                EXPECT_EQ(page_zone_map.min_value.get<TYPE_CHAR>().size(), 
length);
-                EXPECT_EQ(page_zone_map.min_value.get<TYPE_CHAR>(),
-                          s_less_than_schema_length1_expect);
-                EXPECT_EQ(page_zone_map.max_value.get<TYPE_CHAR>().size(), 
length);
-                EXPECT_EQ(page_zone_map.max_value.get<TYPE_CHAR>(),
-                          s_less_than_schema_length2_expect);
+                EXPECT_EQ(page_zone_map.min_value.get<TYPE_CHAR>().size(),
+                          s_less_than_char_len1.size());
+                EXPECT_EQ(page_zone_map.min_value.get<TYPE_CHAR>(), 
s_less_than_char_len1);
+                EXPECT_EQ(page_zone_map.max_value.get<TYPE_CHAR>().size(),
+                          s_less_than_char_len2.size());
+                EXPECT_EQ(page_zone_map.max_value.get<TYPE_CHAR>(), 
s_less_than_char_len2);
             }
         }
     }


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

Reply via email to