You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by el...@apache.org on 2025/11/26 15:31:26 UTC

(doris) 04/04: [feature][segment] Support external meta V3 with common format

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

eldenmoon pushed a commit to branch cs_opt_version-3.1
in repository https://gitbox.apache.org/repos/asf/doris.git

commit f6b14986719362c5534e31ae81ecbbe09b289234
Author: eldenmoon <li...@selectdb.com>
AuthorDate: Wed Oct 29 18:02:19 2025 +0800

    [feature][segment] Support external meta V3 with common format
---
 be/src/cloud/pb_convert.cpp                        |   4 +
 .../olap/rowset/segment_v2/binary_prefix_page.cpp  |   3 +-
 .../rowset/segment_v2/column_meta_accessor.cpp     | 253 +++++++++
 .../olap/rowset/segment_v2/column_meta_accessor.h  |  93 ++++
 be/src/olap/rowset/segment_v2/column_reader.cpp    |  26 +-
 be/src/olap/rowset/segment_v2/column_reader.h      |  18 +-
 .../olap/rowset/segment_v2/column_reader_cache.cpp | 180 ++++---
 .../olap/rowset/segment_v2/column_reader_cache.h   |  37 +-
 .../rowset/segment_v2/external_col_meta_util.cpp   | 199 +++++++
 .../rowset/segment_v2/external_col_meta_util.h     |  91 ++++
 be/src/olap/rowset/segment_v2/segment.cpp          | 120 ++---
 be/src/olap/rowset/segment_v2/segment.h            |  11 +-
 be/src/olap/rowset/segment_v2/segment_iterator.cpp |  16 +-
 be/src/olap/rowset/segment_v2/segment_writer.cpp   |  12 +-
 be/src/olap/rowset/segment_v2/stream_reader.h      |   3 +-
 .../segment_v2/variant/variant_column_reader.cpp   | 405 +++++++++++---
 .../segment_v2/variant/variant_column_reader.h     |  62 ++-
 .../variant/variant_column_writer_impl.h           | 169 ++++++
 .../segment_v2/variant/variant_ext_meta_writer.cpp | 199 +++++++
 .../segment_v2/variant/variant_ext_meta_writer.h   |  99 ++++
 .../variant/variant_external_meta_reader.cpp       | 246 +++++++++
 .../variant/variant_external_meta_reader.h         |  95 ++++
 .../segment_v2/variant_column_writer_impl.cpp      |   8 +-
 .../rowset/segment_v2/vertical_segment_writer.cpp  |  13 +
 be/src/olap/tablet_manager.cpp                     |   3 +-
 be/src/olap/tablet_meta.cpp                        |  30 +-
 be/src/olap/tablet_meta.h                          |   7 +-
 be/src/olap/tablet_schema.cpp                      |   8 +
 be/src/olap/tablet_schema.h                        |  12 +
 be/src/service/internal_service.cpp                |   4 +
 be/src/vec/common/schema_util.cpp                  |  28 +-
 be/src/vec/data_types/data_type_factory.cpp        |  12 +
 be/test/olap/date_bloom_filter_test.cpp            |  19 +-
 be/test/olap/delete_bitmap_calculator_test.cpp     |  13 +
 .../segment_v2/column_meta_accessor_test.cpp       | 593 +++++++++++++++++++++
 .../rowset/segment_v2/column_reader_cache_test.cpp |  40 +-
 .../segment_v2/external_col_meta_util_test.cpp     | 517 ++++++++++++++++++
 be/test/olap/rowset/segment_v2/mock/mock_segment.h |   6 -
 .../variant_column_writer_reader_test.cpp          | 129 +++--
 be/test/vec/common/schema_util_rowset_test.cpp     | 126 ++++-
 .../main/java/org/apache/doris/common/Config.java  | 241 +++++----
 .../src/main/java/org/apache/doris/DorisFE.java    |   2 +-
 .../apache/doris/alter/SchemaChangeHandler.java    |  14 +-
 .../apache/doris/common/util/PropertyAnalyzer.java |   7 +-
 .../trees/plans/commands/info/CreateTableInfo.java |  19 +-
 .../java/org/apache/doris/qe/SessionVariable.java  |  10 +
 .../org/apache/doris/task/CreateReplicaTask.java   |   1 -
 gensrc/proto/olap_file.proto                       |   6 +
 gensrc/proto/segment_v2.proto                      |  18 +
 gensrc/thrift/AgentService.thrift                  |   8 +-
 .../ext_meta/test_storage_format_v2_1.out          |  21 +
 .../test_variant_external_meta_concurrent.out      |  29 +
 .../test_variant_external_meta_edge_cases.out      | 174 ++++++
 .../test_variant_external_meta_integration.out     | 120 +++++
 .../test_variant_external_meta_with_sparse.out     | 197 +++++++
 .../data/variant_p0/query_subcolumns.out           | 211 ++++++++
 .../load.groovy                                    |  14 +
 regression-test/suites/variant_p0/agg.groovy       |  33 +-
 .../ext_meta/test_storage_format_v2_1.groovy       |  98 ++++
 .../test_variant_external_meta_concurrent.groovy   | 307 +++++++++++
 .../test_variant_external_meta_edge_cases.groovy   | 296 ++++++++++
 .../test_variant_external_meta_integration.groovy  | 355 ++++++++++++
 .../test_variant_external_meta_with_sparse.groovy  | 251 +++++++++
 ...est_variant_compaction_with_sparse_limit.groovy |   6 +-
 .../suites/variant_p0/query_subcolumns.groovy      |  94 ++++
 65 files changed, 5900 insertions(+), 541 deletions(-)

diff --git a/be/src/cloud/pb_convert.cpp b/be/src/cloud/pb_convert.cpp
index 352b9ae9356..7c9ac5f35c9 100644
--- a/be/src/cloud/pb_convert.cpp
+++ b/be/src/cloud/pb_convert.cpp
@@ -342,6 +342,7 @@ void doris_tablet_schema_to_cloud(TabletSchemaCloudPB* out, const TabletSchemaPB
     out->set_skip_write_index_on_load(in.skip_write_index_on_load());
     out->mutable_cluster_key_idxes()->CopyFrom(in.cluster_key_idxes());
     out->set_is_dynamic_schema(in.is_dynamic_schema());
+    out->set_is_external_segment_meta_used_default(in.is_external_segment_meta_used_default());
     out->mutable_row_store_column_unique_ids()->CopyFrom(in.row_store_column_unique_ids());
     out->set_inverted_index_storage_format(in.inverted_index_storage_format());
     out->set_enable_variant_flatten_nested(in.variant_enable_flatten_nested());
@@ -372,6 +373,7 @@ void doris_tablet_schema_to_cloud(TabletSchemaCloudPB* out, TabletSchemaPB&& in)
     out->set_skip_write_index_on_load(in.skip_write_index_on_load());
     out->mutable_cluster_key_idxes()->Swap(in.mutable_cluster_key_idxes());
     out->set_is_dynamic_schema(in.is_dynamic_schema());
+    out->set_is_external_segment_meta_used_default(in.is_external_segment_meta_used_default());
     out->mutable_row_store_column_unique_ids()->Swap(in.mutable_row_store_column_unique_ids());
     out->set_inverted_index_storage_format(in.inverted_index_storage_format());
     out->set_enable_variant_flatten_nested(in.variant_enable_flatten_nested());
@@ -415,6 +417,7 @@ void cloud_tablet_schema_to_doris(TabletSchemaPB* out, const TabletSchemaCloudPB
     out->set_skip_write_index_on_load(in.skip_write_index_on_load());
     out->mutable_cluster_key_idxes()->CopyFrom(in.cluster_key_idxes());
     out->set_is_dynamic_schema(in.is_dynamic_schema());
+    out->set_is_external_segment_meta_used_default(in.is_external_segment_meta_used_default());
     out->mutable_row_store_column_unique_ids()->CopyFrom(in.row_store_column_unique_ids());
     out->set_inverted_index_storage_format(in.inverted_index_storage_format());
     out->set_variant_enable_flatten_nested(in.enable_variant_flatten_nested());
@@ -446,6 +449,7 @@ void cloud_tablet_schema_to_doris(TabletSchemaPB* out, TabletSchemaCloudPB&& in)
     out->set_skip_write_index_on_load(in.skip_write_index_on_load());
     out->mutable_cluster_key_idxes()->Swap(in.mutable_cluster_key_idxes());
     out->set_is_dynamic_schema(in.is_dynamic_schema());
+    out->set_is_external_segment_meta_used_default(in.is_external_segment_meta_used_default());
     out->mutable_row_store_column_unique_ids()->Swap(in.mutable_row_store_column_unique_ids());
     out->set_inverted_index_storage_format(in.inverted_index_storage_format());
     out->set_variant_enable_flatten_nested(in.enable_variant_flatten_nested());
diff --git a/be/src/olap/rowset/segment_v2/binary_prefix_page.cpp b/be/src/olap/rowset/segment_v2/binary_prefix_page.cpp
index 34eb14951ae..0e5c1fdba45 100644
--- a/be/src/olap/rowset/segment_v2/binary_prefix_page.cpp
+++ b/be/src/olap/rowset/segment_v2/binary_prefix_page.cpp
@@ -210,7 +210,8 @@ Status BinaryPrefixPageDecoder::seek_at_or_after_value(const void* value, bool*
         _cur_pos++;
         auto st = _read_next_value();
         if (st.is<ErrorCode::END_OF_FILE>()) {
-            return Status::Error<ErrorCode::ENTRY_NOT_FOUND>("all value small than the value");
+            return Status::Error<ErrorCode::ENTRY_NOT_FOUND, false>(
+                    "all value small than the value");
         }
         if (!st.ok()) {
             return st;
diff --git a/be/src/olap/rowset/segment_v2/column_meta_accessor.cpp b/be/src/olap/rowset/segment_v2/column_meta_accessor.cpp
new file mode 100644
index 00000000000..3009633b9b6
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/column_meta_accessor.cpp
@@ -0,0 +1,253 @@
+// 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 "olap/rowset/segment_v2/column_meta_accessor.h"
+
+#include <cstdint>
+#include <string>
+
+#include "io/io_common.h"
+#include "olap/rowset/segment_v2/external_col_meta_util.h"
+#include "olap/rowset/segment_v2/segment.h" // kSegmentFooterVersionV3_ExtColMeta
+
+namespace doris::segment_v2 {
+
+// Abstract base implementation shared by both V2 and V3 layouts.
+class ColumnMetaAccessor::Impl {
+public:
+    virtual ~Impl() = default;
+    // Build uid -> col_id mapping based on the provided footer.
+    virtual Status build_uid_to_colid_map(
+            const SegmentFooterPB& footer,
+            std::unordered_map<int32_t, size_t>* uid_to_colid) const = 0;
+    // Lookup ColumnMetaPB by logical ordinal id using the provided footer.
+    virtual Status get_column_meta_by_column_ordinal_id(const SegmentFooterPB& footer,
+                                                        uint32_t column_ordinal_id,
+                                                        ColumnMetaPB* out) const = 0;
+    // Traverse all ColumnMetaPBs using the provided footer.
+    virtual Status traverse_metas(
+            const SegmentFooterPB& footer,
+            const std::function<void(const ColumnMetaPB&)>& visitor) const = 0;
+};
+
+// V2: inline layout using only footer.columns().
+class ColumnMetaAccessorV2 : public ColumnMetaAccessor::Impl {
+public:
+    explicit ColumnMetaAccessorV2(io::FileReaderSPtr file_reader)
+            : _file_reader(std::move(file_reader)) {}
+
+    Status build_uid_to_colid_map(
+            const SegmentFooterPB& footer,
+            std::unordered_map<int32_t, size_t>* uid_to_colid) const override {
+        uid_to_colid->clear();
+        uint32_t ordinal = 0;
+        for (const auto& column_meta : footer.columns()) {
+            // 跳过 variant 子列(unique_id == -1)
+            if (column_meta.unique_id() == -1) {
+                ordinal++;
+                continue;
+            }
+            uid_to_colid->try_emplace(column_meta.unique_id(), ordinal++);
+        }
+        return Status::OK();
+    }
+
+    Status get_column_meta_by_column_ordinal_id(const SegmentFooterPB& footer,
+                                                uint32_t column_ordinal_id,
+                                                ColumnMetaPB* out) const override {
+        if (footer.columns_size() == 0 ||
+            column_ordinal_id >= static_cast<uint32_t>(footer.columns_size())) {
+            return Status::Corruption(
+                    "no inline column meta available for column_id={} (columns_size={})",
+                    column_ordinal_id, footer.columns_size());
+        }
+        *out = footer.columns(static_cast<int>(column_ordinal_id));
+        return Status::OK();
+    }
+
+    Status traverse_metas(const SegmentFooterPB& footer,
+                          const std::function<void(const ColumnMetaPB&)>& visitor) const override {
+        if (footer.columns_size() == 0) {
+            return Status::Corruption("no column meta found in footer (inline V2)");
+        }
+        for (const auto& column : footer.columns()) {
+            visitor(column);
+        }
+        return Status::OK();
+    }
+
+private:
+    io::FileReaderSPtr _file_reader;
+};
+
+// V3: use external Column Meta Region + column_meta_entries layout.
+class ColumnMetaAccessorV3 : public ColumnMetaAccessor::Impl {
+public:
+    ColumnMetaAccessorV3(io::FileReaderSPtr file_reader,
+                         const ExternalColMetaUtil::ExternalMetaPointers& ptrs)
+            : _file_reader(std::move(file_reader)), _ptrs(ptrs) {}
+
+    Status build_uid_to_colid_map(
+            const SegmentFooterPB& footer,
+            std::unordered_map<int32_t, size_t>* uid_to_colid) const override {
+        uid_to_colid->clear();
+
+        // Prefer external uid->col_id map when available.
+        Status st = ExternalColMetaUtil::parse_uid_to_colid_map(footer, _ptrs, uid_to_colid);
+
+        // Compatibility: if external map is broken, fall back to inline columns.
+        if (!st.ok() && footer.columns_size() > 0) {
+            uint32_t ordinal = 0;
+            for (const auto& column_meta : footer.columns()) {
+                if (column_meta.unique_id() == -1) {
+                    ordinal++;
+                    continue;
+                }
+                uid_to_colid->try_emplace(column_meta.unique_id(), ordinal++);
+            }
+            return Status::OK();
+        }
+
+        if (!st.ok()) {
+            return Status::Corruption(
+                    "segment external meta mapping missing or corrupted and no inline columns; "
+                    "uid->col_id cannot be resolved. path={}, error={}",
+                    _file_reader ? _file_reader->path().native() : std::string("<unknown>"),
+                    st.to_string());
+        }
+
+        return Status::OK();
+    }
+
+    Status get_column_meta_by_column_ordinal_id(const SegmentFooterPB& footer,
+                                                uint32_t column_ordinal_id,
+                                                ColumnMetaPB* out) const override {
+        // Prefer external Column Meta Region first.
+        if (column_ordinal_id < _ptrs.num_columns) {
+            return ExternalColMetaUtil::read_col_meta(_file_reader, footer, _ptrs,
+                                                      column_ordinal_id, out);
+        }
+
+        // Fallback: inline footer.columns().
+        if (footer.columns_size() > 0 &&
+            column_ordinal_id < static_cast<uint32_t>(footer.columns_size())) {
+            *out = footer.columns(static_cast<int>(column_ordinal_id));
+            return Status::OK();
+        }
+
+        return Status::Corruption(
+                "no column meta available for column_id={} (inline/external missing)",
+                column_ordinal_id);
+    }
+
+    Status traverse_metas(const SegmentFooterPB& footer,
+                          const std::function<void(const ColumnMetaPB&)>& visitor) const override {
+        const uint64_t region_size = (_ptrs.region_end > _ptrs.region_start)
+                                             ? (_ptrs.region_end - _ptrs.region_start)
+                                             : 0;
+        if (region_size == 0) {
+            return Status::Corruption("invalid external meta region size");
+        }
+
+        // Read entire meta region once to reduce random I/O.
+        std::string region_buf;
+        region_buf.resize(static_cast<size_t>(region_size));
+        size_t br = 0;
+        io::IOContext io_ctx {.is_index_data = true};
+        RETURN_IF_ERROR(_file_reader->read_at(_ptrs.region_start, Slice(region_buf), &br, &io_ctx));
+        if (br != region_size) {
+            return Status::Corruption("short read on meta region");
+        }
+
+        if (footer.column_meta_entries_size() != _ptrs.num_columns) {
+            return Status::Corruption("column_meta_entries size mismatch");
+        }
+
+        uint64_t offset = _ptrs.region_start;
+        for (uint32_t i = 0; i < _ptrs.num_columns; ++i) {
+            const auto& entry = footer.column_meta_entries(static_cast<int>(i));
+            const auto sz = static_cast<uint64_t>(entry.length());
+            const uint64_t pos = offset;
+            if (!ExternalColMetaUtil::is_valid_meta_slice(pos, sz, _ptrs)) {
+                return Status::Corruption("external meta entry out of region bounds");
+            }
+            const uint64_t rel = pos - _ptrs.region_start;
+            ColumnMetaPB meta;
+            if (!meta.ParseFromArray(region_buf.data() + rel, static_cast<int>(sz))) {
+                return Status::Corruption("failed parse ColumnMetaPB from region");
+            }
+            visitor(meta);
+            offset += sz;
+        }
+        return Status::OK();
+    }
+
+private:
+    io::FileReaderSPtr _file_reader;
+    ExternalColMetaUtil::ExternalMetaPointers _ptrs;
+};
+
+ColumnMetaAccessor::ColumnMetaAccessor() = default;
+ColumnMetaAccessor::~ColumnMetaAccessor() = default;
+ColumnMetaAccessor::ColumnMetaAccessor(ColumnMetaAccessor&&) noexcept = default;
+ColumnMetaAccessor& ColumnMetaAccessor::operator=(ColumnMetaAccessor&&) noexcept = default;
+
+Status ColumnMetaAccessor::init(const SegmentFooterPB& footer, io::FileReaderSPtr file_reader) {
+    // First check footer version to see if external Column Meta Region might exist,
+    // then try to parse external layout; if parsing fails, fall back to V2.
+    if (footer.version() >= kSegmentFooterVersionV3_ExtColMeta) {
+        ExternalColMetaUtil::ExternalMetaPointers ptrs;
+        RETURN_IF_ERROR(ExternalColMetaUtil::parse_external_meta_pointers(footer, &ptrs));
+        _impl = std::make_unique<ColumnMetaAccessorV3>(std::move(file_reader), ptrs);
+    } else {
+        _impl = std::make_unique<ColumnMetaAccessorV2>(std::move(file_reader));
+    }
+    // Build inner uid -> column ordinal id mapping once during init.
+    _column_uid_to_column_ordinal.clear();
+    RETURN_IF_ERROR(_impl->build_uid_to_colid_map(footer, &_column_uid_to_column_ordinal));
+    return Status::OK();
+}
+
+Status ColumnMetaAccessor::get_column_meta_by_column_ordinal_id(const SegmentFooterPB& footer,
+                                                                uint32_t column_ordinal_id,
+                                                                ColumnMetaPB* out) const {
+    return _impl->get_column_meta_by_column_ordinal_id(footer, column_ordinal_id, out);
+}
+
+Status ColumnMetaAccessor::get_column_meta_by_uid(const SegmentFooterPB& footer, int32_t column_uid,
+                                                  ColumnMetaPB* out) const {
+    auto it = _column_uid_to_column_ordinal.find(column_uid);
+    if (it == _column_uid_to_column_ordinal.end()) {
+        *out = ColumnMetaPB();
+        return Status::Error<ErrorCode::NOT_FOUND, false>(
+                "column not found in segment meta, col_uid={}", column_uid);
+    }
+    return _impl->get_column_meta_by_column_ordinal_id(footer, static_cast<uint32_t>(it->second),
+                                                       out);
+}
+
+bool ColumnMetaAccessor::has_column_uid(int32_t column_uid) const {
+    return _column_uid_to_column_ordinal.contains(column_uid);
+}
+
+Status ColumnMetaAccessor::traverse_metas(
+        const SegmentFooterPB& footer,
+        const std::function<void(const ColumnMetaPB&)>& visitor) const {
+    return _impl->traverse_metas(footer, visitor);
+}
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/column_meta_accessor.h b/be/src/olap/rowset/segment_v2/column_meta_accessor.h
new file mode 100644
index 00000000000..a99f8f314dc
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/column_meta_accessor.h
@@ -0,0 +1,93 @@
+// 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 <functional>
+#include <memory>
+#include <unordered_map>
+
+#include "common/status.h"
+#include "gen_cpp/segment_v2.pb.h"
+#include "io/fs/file_reader.h"
+
+namespace doris::segment_v2 {
+
+// Centralized accessor for column metadata layout of a Segment.
+// It selects V2 (inline footer.columns) or V3 (external Column Meta Region)
+// implementation internally and exposes a unified API to callers, hiding
+// the physical layout (inline vs external CMO).
+class ColumnMetaAccessor {
+public:
+    class Impl;
+    ColumnMetaAccessor();
+    ~ColumnMetaAccessor();
+    ColumnMetaAccessor(ColumnMetaAccessor&&) noexcept;
+    ColumnMetaAccessor& operator=(ColumnMetaAccessor&&) noexcept;
+    ColumnMetaAccessor(const ColumnMetaAccessor&) = delete;
+    ColumnMetaAccessor& operator=(const ColumnMetaAccessor&) = delete;
+    // Initialize accessor and choose V2 / V3 implementation based on footer.
+    //
+    // The caller is responsible for ensuring that `footer` outlives this accessor.
+    Status init(const SegmentFooterPB& footer, io::FileReaderSPtr file_reader);
+
+    // Get ColumnMetaPB for the given column ordinal id using the provided footer.
+    //
+    // Semantics:
+    // - In legacy V2 segments, column_ordinal_id is the index into footer.columns().
+    // - In V3 segments with external CMO, column_ordinal_id indexes the external
+    //   Column Meta Region in column order; when external meta is missing or
+    //   we fall back to inline columns, the same logical ordinal is reused.
+    //
+    // Error convention:
+    // - If the ordinal is out of range or the corresponding meta is missing/corrupted,
+    //   returns ErrorCode::CORRUPTION.
+    Status get_column_meta_by_column_ordinal_id(const SegmentFooterPB& footer,
+                                                uint32_t column_ordinal_id,
+                                                ColumnMetaPB* out) const;
+
+    // Get ColumnMetaPB for the given column unique id using the provided footer.
+    // column_uid is TabletColumn.unique_id / ColumnMetaPB.unique_id.
+    //
+    // Mapping scope:
+    // - Only top-level physical columns (including Variant root columns) are indexed.
+    // - Variant extracted subcolumns (unique_id == -1) and path subcolumns are excluded.
+    //
+    // Error convention:
+    // - If the uid does not exist in this segment (no physical column for this uid),
+    //   returns ErrorCode::NOT_FOUND.
+    Status get_column_meta_by_uid(const SegmentFooterPB& footer, int32_t column_uid,
+                                  ColumnMetaPB* out) const;
+
+    // Fast existence check for a given column unique id.
+    //
+    // Only checks the in-memory uid -> ordinal map; does not trigger any I/O and
+    // does not parse ColumnMetaPB. Semantics are consistent with
+    // get_column_meta_by_uid(...) in terms of uid presence (NOT_FOUND vs OK).
+    bool has_column_uid(int32_t column_uid) const;
+
+    // Traverse all ColumnMetaPBs using the provided footer.
+    Status traverse_metas(const SegmentFooterPB& footer,
+                          const std::function<void(const ColumnMetaPB&)>& visitor) const;
+
+private:
+    std::unique_ptr<Impl> _impl;
+    // map column unique id ---> column ordinal id (footer ordinal in legacy mode, or external CMO column id)
+    std::unordered_map<int32_t, size_t> _column_uid_to_column_ordinal;
+};
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/column_reader.cpp b/be/src/olap/rowset/segment_v2/column_reader.cpp
index 4fe1a0cae57..698b6730d98 100644
--- a/be/src/olap/rowset/segment_v2/column_reader.cpp
+++ b/be/src/olap/rowset/segment_v2/column_reader.cpp
@@ -35,9 +35,7 @@
 #include "io/io_common.h"
 #include "olap/block_column_predicate.h"
 #include "olap/column_predicate.h"
-#include "olap/comparison_predicate.h"
 #include "olap/decimal12.h"
-#include "olap/inverted_index_parser.h"
 #include "olap/iterators.h"
 #include "olap/olap_common.h"
 #include "olap/rowset/segment_v2/binary_dict_page.h" // for BinaryDictPageDecoder
@@ -45,6 +43,7 @@
 #include "olap/rowset/segment_v2/bitmap_index_reader.h"
 #include "olap/rowset/segment_v2/bloom_filter.h"
 #include "olap/rowset/segment_v2/bloom_filter_index_reader.h"
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/encoding_info.h" // for EncodingInfo
 #include "olap/rowset/segment_v2/inverted_index/analyzer/analyzer.h"
 #include "olap/rowset/segment_v2/inverted_index_file_reader.h"
@@ -224,28 +223,6 @@ bool ColumnReader::is_compaction_reader_type(ReaderType type) {
            type == ReaderType::READER_FULL_COMPACTION;
 }
 
-Status ColumnReader::create_variant(const ColumnReaderOptions& opts, const SegmentFooterPB& footer,
-                                    uint32_t column_id, uint64_t num_rows,
-                                    const io::FileReaderSPtr& file_reader,
-                                    std::shared_ptr<ColumnReader>* reader) {
-    std::unique_ptr<VariantColumnReader> reader_local(new VariantColumnReader());
-    RETURN_IF_ERROR(reader_local->init(opts, footer, column_id, num_rows, file_reader));
-    *reader = std::move(reader_local);
-    return Status::OK();
-}
-
-Status ColumnReader::create(const ColumnReaderOptions& opts, const SegmentFooterPB& footer,
-                            uint32_t column_id, uint64_t num_rows,
-                            const io::FileReaderSPtr& file_reader,
-                            std::shared_ptr<ColumnReader>* reader) {
-    // create normal column reader or variant subcolumn reader with extracted columns info in footer
-    if ((FieldType)footer.columns(column_id).type() != FieldType::OLAP_FIELD_TYPE_VARIANT) {
-        return ColumnReader::create(opts, footer.columns(column_id), num_rows, file_reader, reader);
-    }
-    // create variant column reader with extracted columns info in footer with hierarchical data info
-    return create_variant(opts, footer, column_id, num_rows, file_reader, reader);
-}
-
 Status ColumnReader::create(const ColumnReaderOptions& opts, const ColumnMetaPB& meta,
                             uint64_t num_rows, const io::FileReaderSPtr& file_reader,
                             std::shared_ptr<ColumnReader>* reader) {
@@ -299,6 +276,7 @@ ColumnReader::ColumnReader(const ColumnReaderOptions& opts, const ColumnMetaPB&
     if (_meta_type == FieldType::OLAP_FIELD_TYPE_ARRAY) {
         _meta_children_column_type = (FieldType)meta.children_columns(0).type();
     }
+    _data_type = vectorized::DataTypeFactory::instance().create_data_type(meta);
     _meta_is_nullable = meta.is_nullable();
     _meta_dict_page = meta.dict_page();
     _meta_compression = meta.compression();
diff --git a/be/src/olap/rowset/segment_v2/column_reader.h b/be/src/olap/rowset/segment_v2/column_reader.h
index 76dd06ccbbc..45a85c6210f 100644
--- a/be/src/olap/rowset/segment_v2/column_reader.h
+++ b/be/src/olap/rowset/segment_v2/column_reader.h
@@ -69,7 +69,6 @@ struct Slice;
 struct StringRef;
 
 namespace segment_v2 {
-
 class EncodingInfo;
 class ColumnIterator;
 class BloomFilterIndexReader;
@@ -81,7 +80,6 @@ class InvertedIndexFileReader;
 class PageDecoder;
 class RowRanges;
 class ZoneMapIndexReader;
-struct VariantStatistics;
 
 struct ColumnReaderOptions {
     // whether verify checksum when read page
@@ -133,10 +131,7 @@ public:
     static Status create(const ColumnReaderOptions& opts, const ColumnMetaPB& meta,
                          uint64_t num_rows, const io::FileReaderSPtr& file_reader,
                          std::shared_ptr<ColumnReader>* reader);
-    static Status create(const ColumnReaderOptions& opts, const SegmentFooterPB& footer,
-                         uint32_t column_id, uint64_t num_rows,
-                         const io::FileReaderSPtr& file_reader,
-                         std::shared_ptr<ColumnReader>* reader);
+
     static Status create_array(const ColumnReaderOptions& opts, const ColumnMetaPB& meta,
                                const io::FileReaderSPtr& file_reader,
                                std::shared_ptr<ColumnReader>* reader);
@@ -149,10 +144,7 @@ public:
     static Status create_agg_state(const ColumnReaderOptions& opts, const ColumnMetaPB& meta,
                                    uint64_t num_rows, const io::FileReaderSPtr& file_reader,
                                    std::shared_ptr<ColumnReader>* reader);
-    static Status create_variant(const ColumnReaderOptions& opts, const SegmentFooterPB& footer,
-                                 uint32_t column_id, uint64_t num_rows,
-                                 const io::FileReaderSPtr& file_reader,
-                                 std::shared_ptr<ColumnReader>* reader);
+
     enum DictEncodingType { UNKNOWN_DICT_ENCODING, PARTIAL_DICT_ENCODING, ALL_DICT_ENCODING };
 
     static bool is_compaction_reader_type(ReaderType type);
@@ -234,11 +226,15 @@ public:
 
     void disable_index_meta_cache() { _use_index_page_cache = false; }
 
+    vectorized::DataTypePtr get_vec_data_type() { return _data_type; }
+
     virtual FieldType get_meta_type() { return _meta_type; }
 
     int64_t get_metadata_size() const override;
 
 private:
+    friend class VariantColumnReader;
+
     ColumnReader(const ColumnReaderOptions& opts, const ColumnMetaPB& meta, uint64_t num_rows,
                  io::FileReaderSPtr file_reader);
     Status init(const ColumnMetaPB* meta);
@@ -299,6 +295,8 @@ private:
 
     DictEncodingType _dict_encoding_type;
 
+    vectorized::DataTypePtr _data_type;
+
     TypeInfoPtr _type_info =
             TypeInfoPtr(nullptr, nullptr); // initialized in init(), may changed by subclasses.
     const EncodingInfo* _encoding_info =
diff --git a/be/src/olap/rowset/segment_v2/column_reader_cache.cpp b/be/src/olap/rowset/segment_v2/column_reader_cache.cpp
index 30b6e9528f5..366683aed8b 100644
--- a/be/src/olap/rowset/segment_v2/column_reader_cache.cpp
+++ b/be/src/olap/rowset/segment_v2/column_reader_cache.cpp
@@ -17,8 +17,10 @@
 
 #include "olap/rowset/segment_v2/column_reader_cache.h"
 
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/segment.h"
 #include "olap/rowset/segment_v2/variant/variant_column_reader.h"
+#
 
 bvar::Adder<int64_t> g_segment_column_cache_count("segment_column_cache_count");
 bvar::Adder<int64_t> g_segment_column_cache_hit_count("segment_column_cache_hit_count");
@@ -27,7 +29,18 @@ bvar::Adder<int64_t> g_segment_column_cache_evict_count("segment_column_cache_ev
 
 namespace doris::segment_v2 {
 
-ColumnReaderCache::ColumnReaderCache(Segment* segment) : _segment(segment) {}
+#include "common/compile_check_begin.h"
+
+ColumnReaderCache::ColumnReaderCache(
+        ColumnMetaAccessor* accessor, TabletSchemaSPtr tablet_schema,
+        io::FileReaderSPtr file_reader, uint64_t num_rows,
+        std::function<Status(std::shared_ptr<SegmentFooterPB>&, OlapReaderStatistics*)>
+                get_footer_cb)
+        : _accessor(accessor),
+          _tablet_schema(std::move(tablet_schema)),
+          _file_reader(std::move(file_reader)),
+          _num_rows(num_rows),
+          _get_footer_cb(std::move(get_footer_cb)) {}
 
 ColumnReaderCache::~ColumnReaderCache() {
     g_segment_column_cache_count << -_cache_map.size();
@@ -47,20 +60,8 @@ std::shared_ptr<ColumnReader> ColumnReaderCache::_lookup(const ColumnReaderCache
     return it->second->reader;
 }
 
-Status ColumnReaderCache::_insert(const ColumnReaderCacheKey& key, const ColumnReaderOptions& opts,
-                                  const SegmentFooterPB& footer, int32_t column_id,
-                                  const io::FileReaderSPtr& file_reader, size_t num_rows,
-                                  std::shared_ptr<ColumnReader>* column_reader) {
-    std::lock_guard<std::mutex> lock(_cache_mutex);
-    // If already exists, and move it to the front
-    if (_cache_map.find(key) != _cache_map.end()) {
-        g_segment_column_cache_hit_count << 1;
-        auto it = _cache_map[key];
-        DCHECK_EQ(it->key.first, key.first);
-        _lru_list.splice(_lru_list.begin(), _lru_list, it);
-        *column_reader = it->reader;
-        return Status::OK();
-    }
+void ColumnReaderCache::_insert_locked_nocheck(const ColumnReaderCacheKey& key,
+                                               const std::shared_ptr<ColumnReader>& reader) {
     // If capacity exceeded, remove least recently used (tail)
     if (_cache_map.size() >= config::max_segment_partial_column_cache_size) {
         g_segment_column_cache_count << -1;
@@ -70,21 +71,15 @@ Status ColumnReaderCache::_insert(const ColumnReaderCacheKey& key, const ColumnR
         _cache_map.erase(last_it->key);
         _lru_list.pop_back();
     }
-    std::shared_ptr<ColumnReader> reader;
-    RETURN_IF_ERROR(ColumnReader::create(opts, footer, column_id, num_rows, file_reader, &reader));
-    // Insert new node at the front
-    std::shared_ptr<ColumnReader> reader_ptr = std::move(reader);
-    g_segment_column_cache_count << 1;
-    DCHECK(key.first >= 0) << " col_uid: " << key.first
-                           << " relative_path: " << key.second.get_path();
-    _lru_list.push_front(CacheNode {key, reader_ptr, std::chrono::steady_clock::now()});
+    _lru_list.push_front(CacheNode {
+            .key = key, .reader = reader, .last_access = std::chrono::steady_clock::now()});
     _cache_map[key] = _lru_list.begin();
-    *column_reader = reader_ptr;
-    VLOG_DEBUG << "insert cache: " << key.first << " " << key.second.get_path()
-               << ", type: " << (int)reader_ptr->get_meta_type()
-               << ", cache_size: " << _cache_map.size() << ", list_size: " << _lru_list.size()
-               << ", cache_map: " << _cache_map.size() << ", lru_list: " << _lru_list.size();
-    return Status::OK();
+}
+
+void ColumnReaderCache::_insert_direct(const ColumnReaderCacheKey& key,
+                                       const std::shared_ptr<ColumnReader>& column_reader) {
+    std::lock_guard<std::mutex> lock(_cache_mutex);
+    _insert_locked_nocheck(key, column_reader);
 }
 
 std::map<int32_t, std::shared_ptr<ColumnReader>> ColumnReaderCache::get_available_readers(
@@ -107,78 +102,97 @@ Status ColumnReaderCache::get_column_reader(int32_t col_uid,
         *column_reader = cached;
         return Status::OK();
     }
-    auto it = _segment->_column_uid_to_footer_ordinal.find(col_uid);
-    if (it == _segment->_column_uid_to_footer_ordinal.end()) {
-        // no such column in this segment, return nullptr
-        *column_reader = nullptr;
-        return Status::OK();
-    }
+    // Load footer once under cache mutex (not thread-safe otherwise)
     std::shared_ptr<SegmentFooterPB> footer_pb_shared;
     {
         std::lock_guard<std::mutex> lock(_cache_mutex);
-        // keep the lock until the footer is loaded, since _get_segment_footer is not thread safe
-        RETURN_IF_ERROR(_segment->_get_segment_footer(footer_pb_shared, stats));
+        RETURN_IF_ERROR(_get_footer_cb(footer_pb_shared, stats));
+    }
+
+    // Lookup column meta by uid via ColumnMetaAccessor. If not initialized or not found, return NOT_FOUND.
+    ColumnMetaPB meta;
+    Status st_meta = _accessor->get_column_meta_by_uid(*footer_pb_shared, col_uid, &meta);
+    if (st_meta.is<ErrorCode::NOT_FOUND>()) {
+        *column_reader = nullptr;
+        return st_meta;
+    }
+    RETURN_IF_ERROR(st_meta);
+
+    ColumnReaderOptions opts {.kept_in_memory = _tablet_schema->is_in_memory(),
+                              .be_exec_version = _be_exec_version,
+                              .tablet_schema = _tablet_schema};
+
+    std::shared_ptr<ColumnReader> reader;
+    if ((FieldType)meta.type() == FieldType::OLAP_FIELD_TYPE_VARIANT) {
+        // Variant root columns require VariantColumnReader, which encapsulates
+        // subcolumn layout, sparse columns and external meta.
+        std::unique_ptr<VariantColumnReader> variant_reader(new VariantColumnReader());
+        RETURN_IF_ERROR(variant_reader->init(opts, _accessor, footer_pb_shared, col_uid, _num_rows,
+                                             _file_reader));
+        reader.reset(variant_reader.release());
+        VLOG_DEBUG << "insert cache (variant): uid=" << col_uid << " col_id=" << meta.column_id();
+    } else {
+        // For non-variant columns, we can create reader directly from ColumnMetaPB.
+        VLOG_DEBUG << "insert cache: uid=" << col_uid << " col_id=" << meta.column_id();
+        RETURN_IF_ERROR(ColumnReader::create(opts, meta, _num_rows, _file_reader, &reader));
     }
-    // lazy create column reader from footer
-    const auto& col_footer_pb = footer_pb_shared->columns(it->second);
-    ColumnReaderOptions opts {
-            .kept_in_memory = _segment->tablet_schema()->is_in_memory(),
-            .be_exec_version = _be_exec_version,
-            .tablet_schema = _segment->tablet_schema(),
-    };
-    VLOG_DEBUG << "insert cache: " << col_uid << " "
-               << ""
-               << ", type: " << (int)col_footer_pb.type() << ", footer_ordinal: " << it->second;
-    RETURN_IF_ERROR(_insert({col_uid, {}}, opts, *footer_pb_shared, it->second,
-                            _segment->_file_reader, _segment->num_rows(), column_reader));
+
+    _insert_direct({col_uid, {}}, reader);
+    *column_reader = std::move(reader);
     return Status::OK();
 }
 
-Status ColumnReaderCache::get_path_column_reader(uint32_t col_uid,
+Status ColumnReaderCache::get_path_column_reader(int32_t col_uid,
                                                  vectorized::PathInData relative_path,
                                                  std::shared_ptr<ColumnReader>* column_reader,
                                                  OlapReaderStatistics* stats,
                                                  const SubcolumnColumnMetaInfo::Node* node_hint) {
-    // Attempt to find in cache
+    // Attempt to find in cache first
     if (auto cached = _lookup({col_uid, relative_path})) {
         *column_reader = cached;
         return Status::OK();
     }
-    const SubcolumnColumnMetaInfo::Node* node = node_hint;
-    std::shared_ptr<ColumnReader> variant_column_reader;
-    if (node == nullptr) {
-        RETURN_IF_ERROR(get_column_reader(col_uid, &variant_column_reader, stats));
-        node = variant_column_reader
-                       ? static_cast<VariantColumnReader*>(variant_column_reader.get())
-                                 ->get_subcolumn_meta_by_path(relative_path)
-                       : nullptr;
+
+    if (!_accessor->has_column_uid(col_uid)) {
+        *column_reader = nullptr;
+        return Status::Error<ErrorCode::NOT_FOUND, false>("column not found in segment, col_uid={}",
+                                                          col_uid);
     }
-    if (node != nullptr) {
-        // lazy create column reader from footer
-        DCHECK_GE(node->data.footer_ordinal, 0);
-        std::shared_ptr<SegmentFooterPB> footer_pb_shared;
-        {
-            std::lock_guard<std::mutex> lock(_cache_mutex);
-            // keep the lock until the footer is loaded, since _get_segment_footer is not thread safe
-            RETURN_IF_ERROR(_segment->_get_segment_footer(footer_pb_shared, stats));
-        }
-        const auto& col_footer_pb = footer_pb_shared->columns(node->data.footer_ordinal);
-        ColumnReaderOptions opts {
-                .kept_in_memory = _segment->tablet_schema()->is_in_memory(),
-                .be_exec_version = _be_exec_version,
-                .tablet_schema = _segment->tablet_schema(),
-        };
-        VLOG_DEBUG << "insert cache: " << col_uid << " "
-                   << ""
-                   << ", type: " << (int)col_footer_pb.type()
-                   << ", footer_ordinal: " << node->data.footer_ordinal;
-        RETURN_IF_ERROR(_insert({col_uid, node->path}, opts, *footer_pb_shared,
-                                node->data.footer_ordinal, _segment->_file_reader,
-                                _segment->num_rows(), column_reader));
+
+    // Load footer once under cache mutex (not thread-safe otherwise)
+    std::shared_ptr<SegmentFooterPB> footer_pb_shared;
+    {
+        std::lock_guard<std::mutex> lock(_cache_mutex);
+        RETURN_IF_ERROR(_get_footer_cb(footer_pb_shared, stats));
+    }
+
+    // Ensure variant root reader is available in cache.
+    ColumnReaderOptions opts {.kept_in_memory = _tablet_schema->is_in_memory(),
+                              .be_exec_version = _be_exec_version,
+                              .tablet_schema = _tablet_schema};
+    std::shared_ptr<ColumnReader> variant_column_reader;
+    RETURN_IF_ERROR(get_column_reader(col_uid, &variant_column_reader, stats));
+
+    if (relative_path.empty()) {
+        *column_reader = std::move(variant_column_reader);
         return Status::OK();
     }
-    // no such column in this segment, return nullptr
-    *column_reader = nullptr;
+
+    // Delegate path-level reader creation to VariantColumnReader, which hides
+    // inline vs external meta details.
+    std::shared_ptr<ColumnReader> path_reader;
+    auto* vreader = static_cast<VariantColumnReader*>(variant_column_reader.get());
+    Status st = vreader->create_path_reader(relative_path, opts, _accessor, *footer_pb_shared,
+                                            _file_reader, _num_rows, &path_reader);
+    if (st.is<ErrorCode::NOT_FOUND>()) {
+        *column_reader = nullptr;
+        return st;
+    }
+    RETURN_IF_ERROR(st);
+
+    // Cache and return
+    _insert_direct({col_uid, relative_path}, path_reader);
+    *column_reader = std::move(path_reader);
     return Status::OK();
 }
 
diff --git a/be/src/olap/rowset/segment_v2/column_reader_cache.h b/be/src/olap/rowset/segment_v2/column_reader_cache.h
index ce4060a5f26..8efd829c3c1 100644
--- a/be/src/olap/rowset/segment_v2/column_reader_cache.h
+++ b/be/src/olap/rowset/segment_v2/column_reader_cache.h
@@ -23,8 +23,7 @@
 namespace doris::segment_v2 {
 
 class ColumnReaderCache;
-class Segment;
-using SegmentSPtr = std::shared_ptr<Segment>;
+class ColumnMetaAccessor;
 
 // Key: pair of column uid and variant path (empty for normal column)
 using ColumnReaderCacheKey = std::pair<int32_t, vectorized::PathInData>;
@@ -40,7 +39,14 @@ struct CacheNode {
 // Limit the column reader count to config::max_segment_partial_column_cache_size
 class ColumnReaderCache {
 public:
-    explicit ColumnReaderCache(Segment* segment);
+    // Main constructor used in production: cache is bound to a specific segment's
+    // ColumnMetaAccessor, TabletSchema, file reader and row count, plus a footer
+    // getter callback (Segment::_get_segment_footer).
+    ColumnReaderCache(
+            ColumnMetaAccessor* accessor, TabletSchemaSPtr tablet_schema,
+            io::FileReaderSPtr file_reader, uint64_t num_rows,
+            std::function<Status(std::shared_ptr<SegmentFooterPB>&, OlapReaderStatistics*)>
+                    get_footer_cb);
     virtual ~ColumnReaderCache();
     // Get all available readers
     // if include_subcolumns is true, return all available readers, including subcolumn readers
@@ -52,7 +58,7 @@ public:
                              OlapReaderStatistics* stats);
 
     // Get column reader by column unique id and path(leaf node of variant's subcolumn)
-    virtual Status get_path_column_reader(uint32_t col_uid, vectorized::PathInData relative_path,
+    virtual Status get_path_column_reader(int32_t col_uid, vectorized::PathInData relative_path,
                                           std::shared_ptr<ColumnReader>* column_reader,
                                           OlapReaderStatistics* stats,
                                           const SubcolumnColumnMetaInfo::Node* node_hint = nullptr);
@@ -60,11 +66,13 @@ public:
 private:
     // Lookup function remains similar
     std::shared_ptr<ColumnReader> _lookup(const ColumnReaderCacheKey& key);
-    // Insert and create column reader if not exists
-    Status _insert(const ColumnReaderCacheKey& key, const ColumnReaderOptions& opts,
-                   const SegmentFooterPB& footer, int32_t column_id,
-                   const io::FileReaderSPtr& file_reader, size_t num_rows,
-                   std::shared_ptr<ColumnReader>* column_reader);
+    // Insert helper: assumes _cache_mutex already locked, will evict if needed
+    void _insert_locked_nocheck(const ColumnReaderCacheKey& key,
+                                const std::shared_ptr<ColumnReader>& reader);
+
+    // Insert an already-created reader directly into cache
+    void _insert_direct(const ColumnReaderCacheKey& key,
+                        const std::shared_ptr<ColumnReader>& column_reader);
     // keep _lru_list and _cache_map thread safe
     std::mutex _cache_mutex;
     // Doubly-linked list to maintain LRU order
@@ -72,8 +80,15 @@ private:
     // Map from key to list iterator for O(1) access
     std::unordered_map<ColumnReaderCacheKey, std::list<CacheNode>::iterator> _cache_map;
     int _be_exec_version = BeExecVersionManager::get_newest_version();
-    // ATTN: Do not use share_ptr, reference will be recycled
-    Segment* _segment;
+
+    // Non-owning pointer to the segment's ColumnMetaAccessor.
+    ColumnMetaAccessor* _accessor = nullptr;
+    // Segment-level context needed to construct ColumnReader.
+    TabletSchemaSPtr _tablet_schema;
+    io::FileReaderSPtr _file_reader;
+    uint64_t _num_rows = 0;
+    // Callback to get footer, usually bound to Segment::_get_segment_footer.
+    std::function<Status(std::shared_ptr<SegmentFooterPB>&, OlapReaderStatistics*)> _get_footer_cb;
 };
 
 } // namespace doris::segment_v2
\ No newline at end of file
diff --git a/be/src/olap/rowset/segment_v2/external_col_meta_util.cpp b/be/src/olap/rowset/segment_v2/external_col_meta_util.cpp
new file mode 100644
index 00000000000..315713a44b9
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/external_col_meta_util.cpp
@@ -0,0 +1,199 @@
+// 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 "olap/rowset/segment_v2/external_col_meta_util.h"
+
+#include <limits>
+#include <memory>
+#include <vector>
+
+#include "io/io_common.h"
+#include "olap/rowset/segment_v2/variant/variant_ext_meta_writer.h"
+
+namespace doris::segment_v2 {
+
+Status ExternalColMetaUtil::parse_external_meta_pointers(
+        const SegmentFooterPB& footer, ExternalColMetaUtil::ExternalMetaPointers* out) {
+    out->region_start = footer.col_meta_region_start();
+    out->num_columns = static_cast<uint32_t>(footer.column_meta_entries_size());
+
+    // Validate pointers and basic consistency to ensure external meta is actually present.
+    if (out->region_start == 0) {
+        return Status::Corruption("external meta region_start is 0");
+    }
+    if (out->num_columns == 0) {
+        return Status::Corruption("external meta num_columns is 0");
+    }
+
+    // Compute region_end = region_start + sum(length_i) with overflow guards.
+    uint64_t region_size = 0;
+    for (int i = 0; i < footer.column_meta_entries_size(); ++i) {
+        const auto& entry = footer.column_meta_entries(i);
+        const auto len = static_cast<uint64_t>(entry.length());
+        if (len == 0) {
+            return Status::Corruption("column_meta_entries[{}] has zero length", i);
+        }
+        if (region_size + len < region_size) {
+            return Status::Corruption("region_size overflow at column_meta_entries[{}]", i);
+        }
+        region_size += len;
+    }
+
+    out->region_end = out->region_start + region_size;
+    if (out->region_end <= out->region_start) {
+        return Status::Corruption("invalid region_end: start={}, end={}", out->region_start,
+                                  out->region_end);
+    }
+    return Status::OK();
+}
+
+Status ExternalColMetaUtil::parse_uid_to_colid_map(
+        const SegmentFooterPB& footer, const ExternalColMetaUtil::ExternalMetaPointers& ptrs,
+        std::unordered_map<int32_t, size_t>* out) {
+    out->clear();
+    out->reserve(ptrs.num_columns);
+
+    for (int i = 0; i < footer.column_meta_entries_size(); ++i) {
+        const auto& entry = footer.column_meta_entries(i);
+        // Skip subcolumns (unique_id == -1) for top-level map
+        if (entry.unique_id() != -1) {
+            (*out)[entry.unique_id()] = static_cast<size_t>(i);
+        }
+    }
+    return Status::OK();
+}
+
+bool ExternalColMetaUtil::is_valid_meta_slice(uint64_t pos, uint64_t size,
+                                              const ExternalColMetaUtil::ExternalMetaPointers& p) {
+    if (size == 0) {
+        return false;
+    }
+    if (pos < p.region_start) {
+        return false;
+    }
+    if (pos + size < pos) {
+        return false;
+    } // overflow guard
+    if (pos + size > p.region_end) {
+        return false;
+    }
+    return true;
+}
+
+Status ExternalColMetaUtil::read_col_meta(const io::FileReaderSPtr& file_reader,
+                                          const SegmentFooterPB& footer,
+                                          const ExternalColMetaUtil::ExternalMetaPointers& p,
+                                          uint32_t col_id, ColumnMetaPB* out_meta) {
+    if (col_id >= p.num_columns) {
+        return Status::Corruption("col_id {} out of range {}", col_id, p.num_columns);
+    }
+    if (footer.column_meta_entries_size() != p.num_columns) {
+        return Status::Corruption("column_meta_entries size mismatch: entries={}, num_columns={}",
+                                  footer.column_meta_entries_size(), p.num_columns);
+    }
+    // compute meta offset via prefix-sum of entry lengths.
+    // TODO: prepare position and size in advance, and then read the meta.
+    uint64_t pos = p.region_start;
+    for (uint32_t i = 0; i < col_id; ++i) {
+        pos += static_cast<uint64_t>(footer.column_meta_entries(i).length());
+    }
+    const auto size = static_cast<uint64_t>(footer.column_meta_entries(col_id).length());
+    if (!ExternalColMetaUtil::is_valid_meta_slice(pos, size, p)) {
+        return Status::Corruption(
+                "invalid ColumnMetaPB bounds: col_id={}, pos={}, size={}, region_start={}, "
+                "region_end={}",
+                col_id, pos, size, p.region_start, p.region_end);
+    }
+    if (size > static_cast<uint64_t>(std::numeric_limits<size_t>::max())) {
+        return Status::Corruption("ColumnMetaPB size too large to allocate: {}", size);
+    }
+    std::string buf;
+    buf.resize(static_cast<size_t>(size));
+    size_t meta_read = 0;
+    io::IOContext io_ctx {.is_index_data = true};
+    RETURN_IF_ERROR(file_reader->read_at(pos, Slice(buf.data(), buf.size()), &meta_read, &io_ctx));
+    if (meta_read != size) {
+        return Status::Corruption("short read ColumnMetaPB: expect={}, actual={}", size, meta_read);
+    }
+    if (!out_meta->ParseFromArray(buf.data(), static_cast<int>(buf.size()))) {
+        return Status::Corruption("failed to parse ColumnMetaPB, col_id={}", col_id);
+    }
+    return Status::OK();
+}
+
+Status ExternalColMetaUtil::write_external_column_meta(
+        io::FileWriter* file_writer, SegmentFooterPB* footer, CompressionTypePB compression_type,
+        const std::function<Status(const std::vector<Slice>&)>& write_cb) {
+    if (footer->columns_size() == 0) {
+        return Status::OK();
+    }
+
+    std::vector<ColumnMetaPB> all_metas;
+    // 0) Externalize variant subcolumns first, then proceed with per-column meta region.
+    {
+        auto variant_ext_meta_agg =
+                std::make_unique<VariantExtMetaWriter>(file_writer, compression_type);
+        RETURN_IF_ERROR(variant_ext_meta_agg->externalize_from_footer(footer, &all_metas));
+    }
+
+    // Use the reorganized metas (including subcolumns)
+    const uint64_t meta_region_start = file_writer->bytes_appended();
+
+    footer->clear_column_meta_entries();
+
+    // 1) write contiguous meta blobs in col_id order and record lengths in footer.
+    for (const auto& meta : all_metas) {
+        std::string meta_bytes;
+        if (!meta.SerializeToString(&meta_bytes)) {
+            return Status::InternalError("failed to serialize column meta, unique_id={}",
+                                         meta.unique_id());
+        }
+        Slice s(meta_bytes);
+        RETURN_IF_ERROR(write_cb({s}));
+        auto* entry = footer->add_column_meta_entries();
+        entry->set_unique_id(static_cast<int32_t>(meta.unique_id()));
+        entry->set_length(static_cast<uint32_t>(meta_bytes.size()));
+    }
+
+    // 2) write pointers via proto fields
+    footer->set_col_meta_region_start(meta_region_start);
+
+    // 3) clear inline columns to enable true on-demand meta loading
+    // Note: footer->columns has already been pruned to only Top Level Columns in externalize_from_footer
+    // But for full externalization, we might want to clear them all or keep only necessary info?
+    // The original logic was footer->clear_columns().
+    // If we clear columns, the Reader needs to know how to reconstruct the schema.
+    // Currently, SegmentFooterPB.columns is used as the schema source if present.
+    // If we clear it, Reader must rely on External Meta.
+    // However, the Reader typically reads footer first. If columns is empty, it assumes V3 and reads external.
+    // So yes, we should clear it.
+    // But wait, in externalize_from_footer we carefully put Top Level columns back into footer->columns.
+    // Why? Because in previous logic, we might want to keep roots in footer?
+    // The previous logic: "replace Footer.columns with only the kept top-level columns".
+    // BUT then `write_external_column_meta` calls `footer->clear_columns()` at the end!
+    // So `footer->columns` will be empty anyway.
+    // The only reason to reconstruct `footer->columns` in `externalize_from_footer` is if `write_external_column_meta` logic depended on it.
+    // In my updated `write_external_column_meta`, I iterate over `all_metas` which is returned by `externalize_from_footer`.
+    // So I don't strictly need `footer->columns` to be correct in between.
+    // However, strictly following protocol: `externalize_from_footer` modifies footer to reflect "logical" columns (Top Level).
+    // And then `write_external_column_meta` finalizes it by clearing them and setting pointers.
+
+    footer->clear_columns();
+    return Status::OK();
+}
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/external_col_meta_util.h b/be/src/olap/rowset/segment_v2/external_col_meta_util.h
new file mode 100644
index 00000000000..6a0e245c2f9
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/external_col_meta_util.h
@@ -0,0 +1,91 @@
+// 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 <cstdint>
+#include <functional>
+#include <unordered_map>
+#include <vector>
+
+#include "common/status.h"
+#include "gen_cpp/segment_v2.pb.h"
+#include "io/fs/file_reader.h"
+#include "io/fs/file_writer.h"
+#include "util/slice.h"
+
+namespace doris::segment_v2 {
+// -----------------------------------------------------------------------------
+// External Column Meta layout and footer pointers
+// -----------------------------------------------------------------------------
+//
+// Segment tail:
+//
+//   ┌──────────────────────────────────────────────────────────────────────┐
+//   │ ... data pages ...                                                   │
+//   │                                                                      │
+//   │ Column Meta Region (variable-size, per top-level column, col_id asc) │
+//   │   [Meta[col0]] [Meta[col1]] ... [Meta[colN-1]]                       │
+//   │                                                                      │
+//   │ Footer (SegmentFooterPB)                                             │
+//   │   fields:                                                            │
+//   │     - col_meta_region_start      : uint64                            │
+//   │     - column_meta_entries        : repeated ColumnMetaEntryPB        │
+//   │           - unique_id            : int32                             │
+//   │           - length               : uint32                            │
+//   │     - num_columns                : uint32                            │
+//   └──────────────────────────────────────────────────────────────────────┘
+//
+
+// Wrapper class to expose static methods for external column meta utilities.
+class ExternalColMetaUtil {
+public:
+    struct ExternalMetaPointers {
+        uint64_t region_start = 0;
+        // region_end is one-past-the-end file offset of the Column Meta Region.
+        uint64_t region_end = 0;
+        uint32_t num_columns = 0;
+    };
+
+    // Parse three external meta pointers from Footer.
+    // Returns Status::OK() on success, or Status::Corruption() with detailed error message.
+    static Status parse_external_meta_pointers(const SegmentFooterPB& footer,
+                                               ExternalMetaPointers* out);
+
+    // Parse uid->col_id mapping from footer.column_meta_entries.
+    // Returns Status::OK() on success, or Status::Corruption() with detailed error message.
+    static Status parse_uid_to_colid_map(const SegmentFooterPB& footer,
+                                         const ExternalMetaPointers& ptrs,
+                                         std::unordered_map<int32_t, size_t>* out);
+
+    static bool is_valid_meta_slice(uint64_t pos, uint64_t size, const ExternalMetaPointers& p);
+
+    // Read one ColumnMetaPB from external Column Meta Region; use INDEX cache lane because
+    // it's small metadata. Column offsets are derived from footer.column_meta_lengths via
+    // prefix sums.
+    static Status read_col_meta(const io::FileReaderSPtr& file_reader,
+                                const SegmentFooterPB& footer, const ExternalMetaPointers& p,
+                                uint32_t col_id, ColumnMetaPB* out_meta);
+
+    // Write external ColumnMetaPB contiguous region + footer pointers + column_meta_entries.
+    // write_cb: a callback that appends slices to the underlying file (e.g., SegmentWriter::_write_raw_data).
+    static Status write_external_column_meta(
+            io::FileWriter* file_writer, SegmentFooterPB* footer,
+            CompressionTypePB compression_type,
+            const std::function<Status(const std::vector<Slice>&)>& write_cb);
+};
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/segment.cpp b/be/src/olap/rowset/segment_v2/segment.cpp
index d3d368b8fa4..d13aa16cb34 100644
--- a/be/src/olap/rowset/segment_v2/segment.cpp
+++ b/be/src/olap/rowset/segment_v2/segment.cpp
@@ -42,6 +42,7 @@
 #include "olap/olap_common.h"
 #include "olap/primary_key_index.h"
 #include "olap/rowset/rowset_reader_context.h"
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/column_reader.h"
 #include "olap/rowset/segment_v2/column_reader_cache.h"
 #include "olap/rowset/segment_v2/empty_segment_iterator.h"
@@ -240,7 +241,11 @@ Status Segment::new_iterator(SchemaSPtr schema, const StorageReadOptions& read_o
         }
         const TabletColumn& col = read_options.tablet_schema->column(column_id);
         std::shared_ptr<ColumnReader> reader;
-        RETURN_IF_ERROR(get_column_reader(col, &reader, read_options.stats));
+        auto st = get_column_reader(col, &reader, read_options.stats);
+        if (st.is<ErrorCode::NOT_FOUND>()) {
+            continue;
+        }
+        RETURN_IF_ERROR(st);
         if (!reader || !reader->has_zone_map()) {
             continue;
         }
@@ -266,9 +271,13 @@ Status Segment::new_iterator(SchemaSPtr schema, const StorageReadOptions& read_o
             and_predicate.add_column_predicate(
                     SingleColumnBlockPredicate::create_unique(runtime_predicate.get()));
             std::shared_ptr<ColumnReader> reader;
-            RETURN_IF_ERROR(get_column_reader(
+            auto st = get_column_reader(
                     read_options.tablet_schema->column(runtime_predicate->column_id()), &reader,
-                    read_options.stats));
+                    read_options.stats);
+            if (st.is<ErrorCode::NOT_FOUND>()) {
+                continue;
+            }
+            RETURN_IF_ERROR(st);
             if (reader &&
                 can_apply_predicate_safely(runtime_predicate->column_id(), runtime_predicate.get(),
                                            *schema, read_options.io_ctx.reader_type) &&
@@ -580,68 +589,24 @@ vectorized::DataTypePtr Segment::get_data_type_of(const TabletColumn& column,
 
     // Path exists, proceed with variant logic.
     vectorized::PathInData relative_path = path->copy_pop_front();
-    int32_t unique_id = column.unique_id() > 0 ? column.unique_id() : column.parent_unique_id();
+    int32_t unique_id = column.unique_id() >= 0 ? column.unique_id() : column.parent_unique_id();
 
-    // Find the reader for the base variant column.
-    if (!_column_uid_to_footer_ordinal.contains(unique_id)) {
+    // If this uid does not exist in segment meta, fallback to schema type.
+    if (!_column_meta_accessor->has_column_uid(unique_id)) {
         return vectorized::DataTypeFactory::instance().create_data_type(column);
     }
 
-    std::shared_ptr<ColumnReader> reader;
-    // get the parent variant column reader
+    std::shared_ptr<ColumnReader> v_reader;
+
+    // Get the parent variant column reader
     OlapReaderStatistics stats;
     // If status is not ok, it will throw exception(data corruption)
-    THROW_IF_ERROR(get_column_reader(unique_id, &reader, &stats));
-    const auto* variant_reader = static_cast<const VariantColumnReader*>(reader.get());
-
-    // Find the specific node within the variant structure using the relative path.
-    const auto* node = variant_reader->get_subcolumn_meta_by_path(relative_path);
-
-    if (relative_path.get_path() == SPARSE_COLUMN_PATH) {
-        return vectorized::DataTypeFactory::instance().create_data_type(column);
-    }
-
-    // Case 1: Node not found for the given path within the variant reader.
-    // If relative_path is empty, it means the original path pointed to the root
-    // of the variant column itself. We should return the Variant type.
-    // If node is nullptr, it means the path is not exist in the variant sub columns.
-    if (node == nullptr || relative_path.empty()) {
-        // when the path is in the sparse column or exceeded the limit, return the variant type.
-        if (variant_reader->exist_in_sparse_column(relative_path) ||
-            variant_reader->is_exceeded_sparse_column_limit()) {
-            if (column.is_nested_subcolumn()) {
-                return vectorized::DataTypeFactory::instance().create_data_type(column);
-            }
-            return column.is_nullable()
-                           ? vectorized::make_nullable(std::make_shared<vectorized::DataTypeObject>(
-                                     column.variant_max_subcolumns_count()))
-                           : std::make_shared<vectorized::DataTypeObject>(
-                                     column.variant_max_subcolumns_count());
-        }
-        // now, path is not in this segment, return the default type from column.
-        else {
-            return vectorized::DataTypeFactory::instance().create_data_type(column);
-        }
-    }
-
-    bool exist_in_sparse = variant_reader->exist_in_sparse_column(relative_path);
-    bool is_physical_leaf = node->children.empty();
-
-    if (is_physical_leaf && column.is_nested_subcolumn()) {
-        return node->data.file_column_type;
-    }
-
-    // Condition to return the specific underlying type of the node:
-    // 1. We are reading flat leaves (ignoring hierarchy).
-    // 2. OR It's a leaf in the physical column structure AND it doesn't *also* exist
-    //    in the sparse column (meaning it's purely a materialized leaf).
-    if (read_flat_leaves || (is_physical_leaf && !exist_in_sparse &&
-                             !variant_reader->is_exceeded_sparse_column_limit())) {
-        return node->data.file_column_type;
-    }
-
-    // not the compaction read, return the default type from column.
-    return vectorized::DataTypeFactory::instance().create_data_type(column);
+    THROW_IF_ERROR(get_column_reader(unique_id, &v_reader, &stats));
+    DCHECK(v_reader != nullptr);
+    const auto* variant_reader = static_cast<const VariantColumnReader*>(v_reader.get());
+    // Delegate type inference for variant paths to VariantColumnReader.
+    return variant_reader->infer_data_type_for_path(column, relative_path, read_flat_leaves,
+                                                    _column_reader_cache.get(), unique_id);
 }
 
 Status Segment::_create_column_meta_once(OlapReaderStatistics* stats) {
@@ -654,17 +619,14 @@ Status Segment::_create_column_meta_once(OlapReaderStatistics* stats) {
 }
 
 Status Segment::_create_column_meta(const SegmentFooterPB& footer) {
-    // unique_id -> idx in footer.columns()
-    uint32_t ordinal = 0;
-    for (const auto& column_meta : footer.columns()) {
-        // no need to create column reader for variant's subcolumn
-        if (column_meta.unique_id() == -1) {
-            ordinal++;
-            continue;
-        }
-        _column_uid_to_footer_ordinal.try_emplace(column_meta.unique_id(), ordinal++);
-    }
-    _column_reader_cache = std::make_unique<ColumnReaderCache>(this);
+    // Initialize column meta accessor which internally maintains uid -> column_ordinal mapping.
+    _column_meta_accessor = std::make_unique<ColumnMetaAccessor>();
+    RETURN_IF_ERROR(_column_meta_accessor->init(footer, _file_reader));
+    _column_reader_cache = std::make_unique<ColumnReaderCache>(
+            _column_meta_accessor.get(), _tablet_schema, _file_reader, _num_rows,
+            [this](std::shared_ptr<SegmentFooterPB>& footer_pb, OlapReaderStatistics* stats) {
+                return _get_segment_footer(footer_pb, stats);
+            });
     return Status::OK();
 }
 
@@ -706,11 +668,13 @@ Status Segment::new_column_iterator(const TabletColumn& tablet_column,
     // For compability reason unique_id may less than 0 for variant extracted column
     int32_t unique_id = tablet_column.unique_id() >= 0 ? tablet_column.unique_id()
                                                        : tablet_column.parent_unique_id();
-    // init default iterator
-    if (!_column_uid_to_footer_ordinal.contains(unique_id)) {
+
+    // If column meta for this uid is not found in this segment, use default iterator.
+    if (!_column_meta_accessor->has_column_uid(unique_id)) {
         RETURN_IF_ERROR(new_default_iterator(tablet_column, iter));
         return Status::OK();
     }
+
     // init iterator by unique id
     std::shared_ptr<ColumnReader> reader;
     RETURN_IF_ERROR(get_column_reader(unique_id, &reader, opt->stats));
@@ -773,7 +737,11 @@ Status Segment::new_bitmap_index_iterator(const TabletColumn& tablet_column,
                                           std::unique_ptr<BitmapIndexIterator>* iter) {
     RETURN_IF_ERROR(_create_column_meta_once(read_options.stats));
     std::shared_ptr<ColumnReader> reader;
-    RETURN_IF_ERROR(get_column_reader(tablet_column, &reader, read_options.stats));
+    auto st = get_column_reader(tablet_column, &reader, read_options.stats);
+    if (st.is<ErrorCode::NOT_FOUND>()) {
+        return Status::OK();
+    }
+    RETURN_IF_ERROR(st);
     if (reader != nullptr && reader->has_bitmap_index()) {
         BitmapIndexIterator* it;
         RETURN_IF_ERROR(reader->new_bitmap_index_iterator(&it));
@@ -792,7 +760,11 @@ Status Segment::new_inverted_index_iterator(const TabletColumn& tablet_column,
     }
     RETURN_IF_ERROR(_create_column_meta_once(read_options.stats));
     std::shared_ptr<ColumnReader> reader;
-    RETURN_IF_ERROR(get_column_reader(tablet_column, &reader, read_options.stats));
+    auto st = get_column_reader(tablet_column, &reader, read_options.stats);
+    if (st.is<ErrorCode::NOT_FOUND>()) {
+        return Status::OK();
+    }
+    RETURN_IF_ERROR(st);
     if (reader != nullptr && index_meta) {
         if (_inverted_index_file_reader == nullptr) {
             RETURN_IF_ERROR(
diff --git a/be/src/olap/rowset/segment_v2/segment.h b/be/src/olap/rowset/segment_v2/segment.h
index be74c28f68b..c2616dcd809 100644
--- a/be/src/olap/rowset/segment_v2/segment.h
+++ b/be/src/olap/rowset/segment_v2/segment.h
@@ -62,12 +62,19 @@ struct RowLocation;
 
 namespace segment_v2 {
 
+// SegmentFooterPB versioning:
+// 1 = V2 baseline
+// 2 = V3 (externalized ColumnMetaPB region + CMO present)
+static constexpr uint32_t kSegmentFooterVersionV2 = 1;
+static constexpr uint32_t kSegmentFooterVersionV3_ExtColMeta = 2;
+
 class BitmapIndexIterator;
 class Segment;
 class InvertedIndexIterator;
 class InvertedIndexFileReader;
 struct VariantStatistics;
 class ColumnReaderCache;
+class ColumnMetaAccessor;
 
 using SegmentSharedPtr = std::shared_ptr<Segment>;
 // A Segment is used to represent a segment in memory format. When segment is
@@ -265,8 +272,8 @@ private:
     // Limited cache for column readers
     std::unique_ptr<ColumnReaderCache> _column_reader_cache;
 
-    // map column unique id ---> it's footer ordinal
-    std::unordered_map<int32_t, size_t> _column_uid_to_footer_ordinal;
+    // Centralized accessor for column metadata layout and uid->column_ordinal mapping.
+    std::unique_ptr<ColumnMetaAccessor> _column_meta_accessor;
 
     // Init from ColumnMetaPB in SegmentFooterPB
     // map column unique id ---> it's inner data type
diff --git a/be/src/olap/rowset/segment_v2/segment_iterator.cpp b/be/src/olap/rowset/segment_v2/segment_iterator.cpp
index de9a6ba35a3..1c720b5948d 100644
--- a/be/src/olap/rowset/segment_v2/segment_iterator.cpp
+++ b/be/src/olap/rowset/segment_v2/segment_iterator.cpp
@@ -306,8 +306,6 @@ Status SegmentIterator::_init_impl(const StorageReadOptions& opts) {
         _record_rowids = true;
     }
 
-    RETURN_IF_ERROR(init_iterators());
-
     if (opts.output_columns != nullptr) {
         _output_columns = *(opts.output_columns);
     }
@@ -345,6 +343,8 @@ Status SegmentIterator::_init_impl(const StorageReadOptions& opts) {
         }
     }
 
+    RETURN_IF_ERROR(init_iterators());
+
     RETURN_IF_ERROR(_construct_compound_expr_context());
     _enable_common_expr_pushdown = !_common_expr_ctxs_push_down.empty();
     _initialize_predicate_results();
@@ -1089,6 +1089,8 @@ Status SegmentIterator::_init_inverted_index_iterators() {
             // We use this column to locate the metadata for the inverted index, which requires a unique_id and path.
             const auto& column = _opts.tablet_schema->column(cid);
             std::vector<const TabletIndex*> inverted_indexs;
+            // Keep shared_ptr alive to prevent use-after-free when accessing raw pointers
+            TabletIndexes inverted_indexs_holder;
             // If the column is an extracted column, we need to find the sub-column in the parent column reader.
             std::shared_ptr<ColumnReader> column_reader;
             if (column.is_extracted_column()) {
@@ -1097,8 +1099,14 @@ Status SegmentIterator::_init_inverted_index_iterators() {
                     column_reader == nullptr) {
                     continue;
                 }
-                inverted_indexs = assert_cast<VariantColumnReader*>(column_reader.get())
-                                          ->find_subcolumn_tablet_indexes(column.suffix_path());
+                inverted_indexs_holder =
+                        assert_cast<VariantColumnReader*>(column_reader.get())
+                                ->find_subcolumn_tablet_indexes(column,
+                                                                _storage_name_and_type[cid].second);
+                // Extract raw pointers from shared_ptr for iteration
+                for (const auto& index_ptr : inverted_indexs_holder) {
+                    inverted_indexs.push_back(index_ptr.get());
+                }
             }
             // If the column is not an extracted column, we can directly get the inverted index metadata from the tablet schema.
             else {
diff --git a/be/src/olap/rowset/segment_v2/segment_writer.cpp b/be/src/olap/rowset/segment_v2/segment_writer.cpp
index 3046871864a..b3c0c987207 100644
--- a/be/src/olap/rowset/segment_v2/segment_writer.cpp
+++ b/be/src/olap/rowset/segment_v2/segment_writer.cpp
@@ -46,10 +46,12 @@
 #include "olap/rowset/rowset_writer_context.h" // RowsetWriterContext
 #include "olap/rowset/segment_creator.h"
 #include "olap/rowset/segment_v2/column_writer.h" // ColumnWriter
+#include "olap/rowset/segment_v2/external_col_meta_util.h"
 #include "olap/rowset/segment_v2/inverted_index_file_writer.h"
 #include "olap/rowset/segment_v2/inverted_index_writer.h"
 #include "olap/rowset/segment_v2/page_io.h"
 #include "olap/rowset/segment_v2/page_pointer.h"
+#include "olap/rowset/segment_v2/variant/variant_ext_meta_writer.h"
 #include "olap/rowset/segment_v2/variant_stats_calculator.h"
 #include "olap/segment_loader.h"
 #include "olap/short_key_index.h"
@@ -73,7 +75,6 @@
 #include "vec/jsonb/serialize.h"
 #include "vec/olap/olap_data_convertor.h"
 #include "vec/runtime/vdatetime_value.h"
-
 namespace doris {
 namespace segment_v2 {
 
@@ -1207,6 +1208,15 @@ Status SegmentWriter::_write_primary_key_index() {
 
 Status SegmentWriter::_write_footer() {
     _footer.set_num_rows(_row_count);
+    // Decide whether to externalize ColumnMetaPB by tablet default, and stamp footer version
+    if (_tablet_schema->is_external_segment_meta_used_default()) {
+        _footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+        VLOG_DEBUG << "use external column meta";
+        // External ColumnMetaPB writing (optional)
+        RETURN_IF_ERROR(ExternalColMetaUtil::write_external_column_meta(
+                _file_writer, &_footer, _opts.compression_type,
+                [this](const std::vector<Slice>& slices) { return _write_raw_data(slices); }));
+    }
 
     // Footer := SegmentFooterPB, FooterPBSize(4), FooterPBChecksum(4), MagicNumber(4)
     std::string footer_buf;
diff --git a/be/src/olap/rowset/segment_v2/stream_reader.h b/be/src/olap/rowset/segment_v2/stream_reader.h
index 134792b1ffd..b009df3b20e 100644
--- a/be/src/olap/rowset/segment_v2/stream_reader.h
+++ b/be/src/olap/rowset/segment_v2/stream_reader.h
@@ -18,8 +18,7 @@
 #pragma once
 
 #include <memory>
-
-// #include "olap/rowset/segment_v2/column_reader.h"
+#include "olap/rowset/segment_v2/column_reader.h"
 #include "vec/columns/column.h"
 #include "vec/columns/subcolumn_tree.h"
 #include "vec/data_types/data_type.h"
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
index f4a1431bcfe..d38533fe0ed 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.cpp
@@ -26,14 +26,17 @@
 #include "common/config.h"
 #include "common/status.h"
 #include "io/fs/file_reader.h"
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/column_reader.h"
 #include "olap/rowset/segment_v2/column_reader_cache.h"
+#include "olap/rowset/segment_v2/indexed_column_reader.h"
 #include "olap/rowset/segment_v2/page_handle.h"
 #include "olap/rowset/segment_v2/segment.h"
 #include "olap/rowset/segment_v2/variant/hierarchical_data_iterator.h"
 #include "olap/rowset/segment_v2/variant/sparse_column_extract_iterator.h"
 #include "olap/rowset/segment_v2/variant/sparse_column_merge_iterator.h"
 #include "olap/tablet_schema.h"
+#include "util/slice.h"
 #include "vec/columns/column_array.h"
 #include "vec/columns/column_nullable.h"
 #include "vec/columns/column_object.h"
@@ -114,6 +117,10 @@ Status VariantColumnReader::_create_hierarchical_reader(ColumnIteratorUPtr* read
                                                         const SubcolumnColumnMetaInfo::Node* root,
                                                         ColumnReaderCache* column_reader_cache,
                                                         OlapReaderStatistics* stats) {
+    // make sure external meta is loaded otherwise can't find any meta data for extracted columns
+    // TODO(lhy): this will load all external meta if not loaded, and memory will be consumed.
+    RETURN_IF_ERROR(load_external_meta_once());
+
     // Node contains column with children columns or has correspoding sparse columns
     // Create reader with hirachical data.
     std::unique_ptr<SubstreamIterator> sparse_iter;
@@ -124,6 +131,9 @@ Status VariantColumnReader::_create_hierarchical_reader(ColumnIteratorUPtr* read
         sparse_iter = std::make_unique<SubstreamIterator>(
                 vectorized::ColumnObject::create_sparse_column_fn(), std::move(iter), nullptr);
     }
+    if (node == nullptr) {
+        node = _subcolumns_meta_info->find_exact(path);
+    }
     // If read the full path of variant read in MERGE_ROOT, otherwise READ_DIRECT
     HierarchicalDataIterator::ReadType read_type =
             (path == root->path) ? HierarchicalDataIterator::ReadType::MERGE_ROOT
@@ -229,6 +239,9 @@ Status VariantColumnReader::_new_iterator_with_flat_leaves(ColumnIteratorUPtr* i
                                                            bool exceeded_sparse_column_limit,
                                                            bool existed_in_sparse_column,
                                                            ColumnReaderCache* column_reader_cache) {
+    // make sure external meta is loaded otherwise can't find any meta data for extracted columns
+    RETURN_IF_ERROR(load_external_meta_once());
+
     DCHECK(opts != nullptr);
     auto relative_path = target_col.path_info_ptr()->copy_pop_front();
     // compaction need to read flat leaves nodes data to prevent from amplification
@@ -289,6 +302,43 @@ Status VariantColumnReader::_new_iterator_with_flat_leaves(ColumnIteratorUPtr* i
     return Status::OK();
 }
 
+bool VariantColumnReader::has_prefix_path(const vectorized::PathInData& relative_path) const {
+    if (relative_path.empty()) {
+        return true;
+    }
+    const std::string path = relative_path.get_path();
+    const std::string dot_prefix = relative_path.get_path() + ".";
+
+    // 1) exact node exists and has children.
+    if (const auto* node = _subcolumns_meta_info->find_exact(relative_path)) {
+        if (!node->children.empty()) {
+            return true;
+        }
+    }
+
+    // 2) Check sparse column stats: use lower_bound to test the `p.` prefix range
+    // example sparse columns path: a.b.c, a.b.e, access prefix: a.b.
+    // then we must read the sparse columns
+    if (_statistics && !_statistics->sparse_column_non_null_size.empty()) {
+        auto it = _statistics->sparse_column_non_null_size.lower_bound(dot_prefix);
+        if (it != _statistics->sparse_column_non_null_size.end() &&
+            it->first.starts_with(dot_prefix)) {
+            return true;
+        }
+    }
+
+    // 3) Check external meta store (if available).
+    if (_ext_meta_reader && _ext_meta_reader->available()) {
+        bool has = false;
+        // Pass strict prefix `p.` to avoid false positives like `a.b` matching `a.bc`.
+        if (_ext_meta_reader->has_prefix(dot_prefix, &has).ok() && has) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 Status VariantColumnReader::new_iterator(ColumnIteratorUPtr* iterator,
                                          const TabletColumn* target_col,
                                          const StorageReadOptions* opt) {
@@ -319,8 +369,7 @@ Status VariantColumnReader::new_iterator(ColumnIteratorUPtr* iterator,
     // Check if path exist in sparse column
     bool existed_in_sparse_column =
             !_statistics->sparse_column_non_null_size.empty() &&
-            _statistics->sparse_column_non_null_size.find(relative_path.get_path()) !=
-                    _statistics->sparse_column_non_null_size.end();
+            _statistics->sparse_column_non_null_size.contains(relative_path.get_path());
 
     // Otherwise the prefix is not exist and the sparse column size is reached limit
     // which means the path maybe exist in sparse_column
@@ -346,15 +395,8 @@ Status VariantColumnReader::new_iterator(ColumnIteratorUPtr* iterator,
     }
 
     // Check if path is prefix, example sparse columns path: a.b.c, a.b.e, access prefix: a.b.
-    // then we must read the sparse columns
-    const std::string& prefix = relative_path.get_path() + ".";
-    bool prefix_existed_in_sparse_column =
-            !_statistics->sparse_column_non_null_size.empty() &&
-            (_statistics->sparse_column_non_null_size.lower_bound(prefix) !=
-             _statistics->sparse_column_non_null_size.end()) &&
-            _statistics->sparse_column_non_null_size.lower_bound(prefix)->first.starts_with(prefix);
-    // if prefix exists in sparse column, read sparse column with hierarchical reader
-    if (prefix_existed_in_sparse_column || exceeded_sparse_column_limit) {
+    // Or access root path
+    if (has_prefix_path(relative_path)) {
         // Example {"b" : {"c":456,"e":7.111}}
         // b.c is sparse column, b.e is subcolumn, so b is both the prefix of sparse column and subcolumn
         return _create_hierarchical_reader(iterator, col_uid, relative_path, node, root,
@@ -376,42 +418,125 @@ Status VariantColumnReader::new_iterator(ColumnIteratorUPtr* iterator,
         return Status::OK();
     }
 
+    // read leaf node
     if (node != nullptr) {
-        // relative_path means the root node, should always use HierarchicalDataIterator
-        if (node->is_leaf_node() && !relative_path.empty()) {
-            // Node contains column without any child sub columns and no corresponding sparse columns
-            // Direct read extracted columns
-            const auto* node = _subcolumns_meta_info->find_leaf(relative_path);
-            std::shared_ptr<ColumnReader> column_reader;
-            RETURN_IF_ERROR(column_reader_cache->get_path_column_reader(
-                    col_uid, node->path, &column_reader, opt->stats, node));
-            RETURN_IF_ERROR(column_reader->new_iterator(iterator, nullptr));
-        } else {
-            RETURN_IF_ERROR(_create_hierarchical_reader(iterator, col_uid, relative_path, node,
-                                                        root, column_reader_cache, opt->stats));
-        }
+        DCHECK(node->is_leaf_node());
+        // Node contains column without any child sub columns and no corresponding sparse columns
+        // Direct read extracted columns
+        const auto* leaf_node = _subcolumns_meta_info->find_leaf(relative_path);
+        std::shared_ptr<ColumnReader> leaf_column_reader;
+        RETURN_IF_ERROR(column_reader_cache->get_path_column_reader(
+                col_uid, leaf_node->path, &leaf_column_reader, opt->stats, leaf_node));
+        RETURN_IF_ERROR(leaf_column_reader->new_iterator(iterator, nullptr));
     } else {
+        if (_ext_meta_reader && _ext_meta_reader->available()) {
+            // Get path reader from external meta
+            std::shared_ptr<ColumnReader> leaf_column_reader;
+            Status st = column_reader_cache->get_path_column_reader(
+                    col_uid, relative_path, &leaf_column_reader, opt->stats, nullptr);
+            DCHECK(!has_prefix_path(relative_path));
+            if (st.ok()) {
+                // Try external meta fallback: build a leaf reader on demand from externalized meta
+                RETURN_IF_ERROR(leaf_column_reader->new_iterator(iterator, nullptr));
+                return Status::OK();
+            }
+            if (!st.is<ErrorCode::NOT_FOUND>()) {
+                return st;
+            }
+            // not found, need continue
+        }
+        if (exceeded_sparse_column_limit) {
+            // maybe exist prefix path in sparse column
+            return _create_hierarchical_reader(iterator, col_uid, relative_path, node, root,
+                                               column_reader_cache, opt->stats);
+        }
         // Sparse column not exists and not reached stats limit, then the target path is not exist, get a default iterator
         RETURN_IF_ERROR(Segment::new_default_iterator(*target_col, iterator));
     }
     return Status::OK();
 }
 
-Status VariantColumnReader::init(const ColumnReaderOptions& opts, const SegmentFooterPB& footer,
-                                 uint32_t column_id, uint64_t num_rows,
-                                 io::FileReaderSPtr file_reader) {
+Status VariantColumnReader::init(const ColumnReaderOptions& opts, ColumnMetaAccessor* accessor,
+                                 const std::shared_ptr<SegmentFooterPB>& footer, int32_t column_uid,
+                                 uint64_t num_rows, io::FileReaderSPtr file_reader) {
     // init sub columns
     _subcolumns_meta_info = std::make_unique<SubcolumnColumnMetaInfo>();
     _statistics = std::make_unique<VariantStatistics>();
-    const ColumnMetaPB& self_column_pb = footer.columns(column_id);
+
+    // Prefer external root ColumnMetaPB via ColumnMetaAccessor when available.
+    ColumnMetaPB self_column_pb;
+    {
+        // Locate root column meta by unique id; this hides inline vs external CMO layout.
+        RETURN_IF_ERROR(accessor->get_column_meta_by_uid(*footer, column_uid, &self_column_pb));
+        // root column
+        // root subcolumn is ColumnVariant::MostCommonType which is jsonb
+        vectorized::DataTypePtr root_type =
+                self_column_pb.is_nullable()
+                        ? make_nullable(
+                                  std::make_unique<vectorized::ColumnObject::MostCommonType>())
+                        : std::make_unique<vectorized::ColumnObject::MostCommonType>();
+        int32_t root_footer_ordinal = -1;
+        if (self_column_pb.has_column_id()) {
+            // Narrow explicitly to avoid implicit narrowing warnings.
+            root_footer_ordinal = static_cast<int32_t>(self_column_pb.column_id());
+        }
+        _subcolumns_meta_info->create_root(SubcolumnMeta {
+                .file_column_type = root_type,
+                // Use column_id from meta as footer ordinal when inline footer is available.
+                .footer_ordinal = root_footer_ordinal});
+        RETURN_IF_ERROR(ColumnReader::create(opts, self_column_pb, num_rows, file_reader,
+                                             &_root_column_reader));
+    }
+
+    _data_type = vectorized::DataTypeFactory::instance().create_data_type(self_column_pb);
+    _root_unique_id = self_column_pb.unique_id();
     const auto& parent_index = opts.tablet_schema->inverted_indexs(self_column_pb.unique_id());
     // record variant_sparse_column_statistics_size from parent column
     _variant_sparse_column_statistics_size =
             opts.tablet_schema->column_by_uid(self_column_pb.unique_id())
                     .variant_max_sparse_column_statistics_size();
+    _tablet_schema = opts.tablet_schema;
+    // helper to handle sparse meta (single or bucket) from a ColumnMetaPB
+    auto handle_sparse_meta = [&](const ColumnMetaPB& col, bool* handled) -> Status {
+        *handled = false;
+        if (!col.has_column_path_info()) {
+            return Status::OK();
+        }
+        vectorized::PathInData path;
+        path.from_protobuf(col.column_path_info());
+        auto relative = path.copy_pop_front();
+        if (relative.empty()) {
+            return Status::OK();
+        }
+        std::string rel_str = relative.get_path();
+        if (rel_str == SPARSE_COLUMN_PATH) {
+            DCHECK(col.has_variant_statistics()) << col.DebugString();
+            const auto& variant_stats = col.variant_statistics();
+            for (const auto& [subpath, size] : variant_stats.sparse_column_non_null_size()) {
+                _statistics->sparse_column_non_null_size.emplace(subpath, size);
+            }
+            std::shared_ptr<ColumnReader> single_reader;
+            RETURN_IF_ERROR(ColumnReader::create(opts, col, footer->num_rows(), file_reader,
+                                                 &single_reader));
+            *handled = true;
+            _sparse_column_reader = single_reader;
+            return Status::OK();
+        }
+        return Status::OK();
+    };
+
+    // First try initialize sparse from root's embedded children_columns (new segments)
+    if (self_column_pb.children_columns_size() > 0) {
+        for (int i = 0; i < self_column_pb.children_columns_size(); ++i) {
+            const ColumnMetaPB& child_pb = self_column_pb.children_columns(i);
+            bool handled = false;
+            RETURN_IF_ERROR(handle_sparse_meta(child_pb, &handled));
+        }
+    }
 
-    for (int32_t ordinal = 0; ordinal < footer.columns_size(); ++ordinal) {
-        const ColumnMetaPB& column_pb = footer.columns(ordinal);
+    // init from inline columns meta
+    for (int32_t ordinal = 0; ordinal < footer->columns_size(); ++ordinal) {
+        const ColumnMetaPB& column_pb = footer->columns(ordinal);
         // Find all columns belonging to the current variant column
         // 1. not the variant column
         if (!column_pb.has_column_path_info()) {
@@ -433,84 +558,124 @@ Status VariantColumnReader::init(const ColumnReaderOptions& opts, const SegmentF
         vectorized::PathInData path;
         path.from_protobuf(column_pb.column_path_info());
 
-        // init sparse column
-        if (path.copy_pop_front().get_path() == SPARSE_COLUMN_PATH) {
-            DCHECK(column_pb.has_variant_statistics()) << column_pb.DebugString();
-            const auto& variant_stats = column_pb.variant_statistics();
-            for (const auto& [path, size] : variant_stats.sparse_column_non_null_size()) {
-                _statistics->sparse_column_non_null_size.emplace(path, size);
+        // init sparse column readers
+        auto relative_sparse = path.copy_pop_front();
+        auto rel_str = relative_sparse.get_path();
+        {
+            bool handled = false;
+            RETURN_IF_ERROR(handle_sparse_meta(column_pb, &handled));
+            if (handled) {
+                continue;
             }
-            RETURN_IF_ERROR(ColumnReader::create(opts, column_pb, footer.num_rows(), file_reader,
-                                                 &_sparse_column_reader));
-            continue;
         }
 
         // init subcolumns
         auto relative_path = path.copy_pop_front();
-        auto get_data_type_fn = [&]() {
-            // root subcolumn is ColumnObject::MostCommonType which is jsonb
-            if (relative_path.empty()) {
-                return self_column_pb.is_nullable()
-                               ? make_nullable(std::make_unique<
-                                               vectorized::ColumnObject::MostCommonType>())
-                               : std::make_unique<vectorized::ColumnObject::MostCommonType>();
-            }
-            return vectorized::DataTypeFactory::instance().create_data_type(column_pb);
-        };
-        // init subcolumns
-        if (_subcolumns_meta_info->get_root() == nullptr) {
-            _subcolumns_meta_info->create_root(SubcolumnMeta {});
-        }
         if (relative_path.empty()) {
-            // root column
-            _subcolumns_meta_info->get_mutable_root()->modify_to_scalar(
-                    SubcolumnMeta {get_data_type_fn(), ordinal});
-            RETURN_IF_ERROR(ColumnReader::create(opts, column_pb, num_rows, file_reader,
-                                                 &_root_column_reader));
-        } else {
-            // check the root is already a leaf node
-            if (column_pb.has_none_null_size()) {
-                _statistics->subcolumns_non_null_size.emplace(relative_path.get_path(),
-                                                              column_pb.none_null_size());
-            }
-            _subcolumns_meta_info->add(relative_path, SubcolumnMeta {get_data_type_fn(), ordinal});
-            TabletSchema::SubColumnInfo sub_column_info;
-            // if subcolumn has index, add index to _variant_subcolumns_indexes
-            if (vectorized::schema_util::generate_sub_column_info(
-                        *opts.tablet_schema, self_column_pb.unique_id(), relative_path.get_path(),
-                        &sub_column_info) &&
-                !sub_column_info.indexes.empty()) {
-                _variant_subcolumns_indexes[path.get_path()] = std::move(sub_column_info.indexes);
-            }
-            // if parent column has index, add index to _variant_subcolumns_indexes
-            else if (!parent_index.empty()) {
-                vectorized::schema_util::inherit_index(
-                        parent_index, _variant_subcolumns_indexes[path.get_path()], column_pb);
-            }
+            continue;
+        }
+        // check the root is already a leaf node
+        if (column_pb.has_none_null_size()) {
+            _statistics->subcolumns_non_null_size.emplace(relative_path.get_path(),
+                                                          column_pb.none_null_size());
         }
+        _subcolumns_meta_info->add(
+                relative_path,
+                SubcolumnMeta {
+                        .file_column_type =
+                                vectorized::DataTypeFactory::instance().create_data_type(column_pb),
+                        .footer_ordinal = ordinal});
     }
 
     // init sparse column set in stats
     if (self_column_pb.has_variant_statistics()) {
-        _statistics = std::make_unique<VariantStatistics>();
         const auto& variant_stats = self_column_pb.variant_statistics();
         for (const auto& [path, size] : variant_stats.sparse_column_non_null_size()) {
             _statistics->sparse_column_non_null_size.emplace(path, size);
         }
     }
+    _segment_file_reader = file_reader;
+    _num_rows = num_rows;
+    // try build external meta readers (optional)
+    _ext_meta_reader = std::make_unique<VariantExternalMetaReader>();
+    RETURN_IF_ERROR(_ext_meta_reader->init_from_footer(footer, file_reader, _root_unique_id));
     return Status::OK();
 }
+    
+Status VariantColumnReader::create_reader_from_external_meta(const std::string& path,
+                                                             const ColumnReaderOptions& opts,
+                                                             const io::FileReaderSPtr& file_reader,
+                                                             uint64_t num_rows,
+                                                             std::shared_ptr<ColumnReader>* out) {
+    if (!_ext_meta_reader || !_ext_meta_reader->available()) {
+        return Status::Error<ErrorCode::NOT_FOUND, false>("no external variant meta");
+    }
+    ColumnMetaPB meta;
+    RETURN_IF_ERROR(_ext_meta_reader->lookup_meta_by_path(path, &meta));
+    return ColumnReader::create(opts, meta, num_rows, file_reader, out);
+}
 
-std::vector<const TabletIndex*> VariantColumnReader::find_subcolumn_tablet_indexes(
-        const std::string& path) {
-    auto it = _variant_subcolumns_indexes.find(path);
-    std::vector<const TabletIndex*> indexes;
-    if (it != _variant_subcolumns_indexes.end()) {
-        for (const auto& index : it->second) {
-            indexes.push_back(index.get());
-        }
+Status VariantColumnReader::create_path_reader(const vectorized::PathInData& relative_path,
+                                               const ColumnReaderOptions& opts,
+                                               ColumnMetaAccessor* accessor,
+                                               const SegmentFooterPB& footer,
+                                               const io::FileReaderSPtr& file_reader,
+                                               uint64_t num_rows,
+                                               std::shared_ptr<ColumnReader>* out) {
+    // 1) Try inline subcolumn meta if available (footer_ordinal >= 0)
+    const auto* node = get_subcolumn_meta_by_path(relative_path);
+    if (node != nullptr && node->data.footer_ordinal >= 0) {
+        // leaf node, get the column meta by footer ordinal
+        const int32_t column_ordinal = node->data.footer_ordinal;
+        ColumnMetaPB meta;
+        RETURN_IF_ERROR(
+                accessor->get_column_meta_by_column_ordinal_id(footer, column_ordinal, &meta));
+        return ColumnReader::create(opts, meta, num_rows, file_reader, out);
+    }
+
+    // 2) Try external meta layout (if available)
+    Status st = create_reader_from_external_meta(relative_path.get_path(), opts, file_reader,
+                                                 num_rows, out);
+    if (st.is<ErrorCode::NOT_FOUND>()) {
+        *out = nullptr;
+        return st;
+    }
+    return st;
+}
+
+Status VariantColumnReader::load_external_meta_once() {
+    if (!_ext_meta_reader || !_ext_meta_reader->available()) {
+        return Status::OK();
+    }
+    return _ext_meta_reader->load_all_once(_subcolumns_meta_info.get(), _statistics.get());
+}
+
+TabletIndexes VariantColumnReader::find_subcolumn_tablet_indexes(
+        const TabletColumn& column, const vectorized::DataTypePtr& data_type) {
+    TabletSchema::SubColumnInfo sub_column_info;
+    const auto& parent_index = _tablet_schema->inverted_indexs(column.parent_unique_id());
+    auto relative_path = column.path_info_ptr()->copy_pop_front();
+    // if subcolumn has index, add index to _variant_subcolumns_indexes
+    if (vectorized::schema_util::generate_sub_column_info(
+                *_tablet_schema, column.parent_unique_id(), relative_path.get_path(),
+                &sub_column_info) &&
+        !sub_column_info.indexes.empty()) {
+    }
+    // if parent column has index, add index to _variant_subcolumns_indexes
+    else if (!parent_index.empty() &&
+             data_type->get_storage_field_type() != FieldType::OLAP_FIELD_TYPE_VARIANT &&
+             data_type->get_storage_field_type() != FieldType::OLAP_FIELD_TYPE_MAP /*SPARSE COLUMN*/) {
+        // type in column maynot be real type, so use data_type to get the real type
+        TabletColumn target_column = vectorized::schema_util::get_column_by_type(
+                data_type, column.name(),
+                {.unique_id = -1,
+                 .parent_unique_id = column.parent_unique_id(),
+                 .path_info = *column.path_info_ptr()});
+        vectorized::schema_util::inherit_index(parent_index, sub_column_info.indexes,
+                                               target_column);
     }
-    return indexes;
+    // Return shared_ptr directly to maintain object lifetime
+    return sub_column_info.indexes;
 }
 
 void VariantColumnReader::get_subcolumns_types(
@@ -540,6 +705,70 @@ void VariantColumnReader::get_nested_paths(
     }
 }
 
+vectorized::DataTypePtr VariantColumnReader::infer_data_type_for_path(
+        const TabletColumn& column, const vectorized::PathInData& relative_path,
+        bool read_flat_leaves, ColumnReaderCache* cache, int32_t col_uid) const {
+    // english only in comments
+    // Locate the subcolumn node by path.
+    const auto* node = get_subcolumn_meta_by_path(relative_path);
+
+    // If path explicitly refers to sparse column internal path, fall back to schema type.
+    if (relative_path.get_path().find(SPARSE_COLUMN_PATH) != std::string::npos) {
+        return vectorized::DataTypeFactory::instance().create_data_type(column);
+    }
+
+    // Use variant type when the path is a prefix of any existing subcolumn path.
+    if (has_prefix_path(relative_path)) {
+        return vectorized::DataTypeFactory::instance().create_data_type(column);
+    }
+
+    // Try to reuse an existing leaf ColumnReader to infer its vectorized data type.
+    if (cache != nullptr) {
+        std::shared_ptr<ColumnReader> leaf_reader;
+        if (cache->get_path_column_reader(col_uid, relative_path, &leaf_reader, nullptr).ok() &&
+            leaf_reader != nullptr) {
+            return leaf_reader->get_vec_data_type();
+        }
+    }
+
+    // Node not found for the given path within the variant subcolumns.
+    if (node == nullptr) {
+        // Nested subcolumn is not present in sparse column: keep schema type.
+        if (column.is_nested_subcolumn()) {
+            return vectorized::DataTypeFactory::instance().create_data_type(column);
+        }
+
+        // When the path is in the sparse column or exceeded the limit, return variant type.
+        if (exist_in_sparse_column(relative_path) || is_exceeded_sparse_column_limit()) {
+            return column.is_nullable() ? vectorized::make_nullable(
+                                                  std::make_shared<vectorized::DataTypeObject>(
+                                                          column.variant_max_subcolumns_count()))
+                                        : std::make_shared<vectorized::DataTypeObject>(
+                                                  column.variant_max_subcolumns_count());
+        }
+        // Path is not present in this segment: use default schema type.
+        return vectorized::DataTypeFactory::instance().create_data_type(column);
+    }
+
+    // For nested subcolumns, always use the physical file_column_type recorded in meta.
+    if (column.is_nested_subcolumn()) {
+        return node->data.file_column_type;
+    }
+
+    const bool exist_in_sparse = exist_in_sparse_column(relative_path);
+
+    // Condition to return the specific underlying type of the node:
+    // 1. We are reading flat leaves (ignoring hierarchy).
+    // 2. OR the path does not exist in sparse column and sparse column limit is not exceeded
+    //    (meaning it is a pure materialized leaf).
+    if (read_flat_leaves || (!exist_in_sparse && !is_exceeded_sparse_column_limit())) {
+        return node->data.file_column_type;
+    }
+
+    // For non-compaction reads where we still treat this as logical VARIANT.
+    return vectorized::DataTypeFactory::instance().create_data_type(column);
+}
+
 Status VariantRootColumnIterator::_process_root_column(
         vectorized::MutableColumnPtr& dst, vectorized::MutableColumnPtr& root_column,
         const vectorized::DataTypePtr& most_common_type) {
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
index f22809eed52..a217a7eee48 100644
--- a/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_reader.h
@@ -27,8 +27,10 @@
 #include <vector>
 
 #include "olap/rowset/segment_v2/column_reader.h"
+#include "olap/rowset/segment_v2/indexed_column_reader.h"
 #include "olap/rowset/segment_v2/page_handle.h"
 #include "olap/rowset/segment_v2/variant_statistics.h"
+#include "olap/rowset/segment_v2/variant/variant_external_meta_reader.h"
 #include "olap/tablet_schema.h"
 #include "vec/columns/column_object.h"
 #include "vec/columns/subcolumn_tree.h"
@@ -38,6 +40,7 @@ namespace doris {
 
 class TabletIndex;
 class StorageReadOptions;
+class TabletSchema;
 
 namespace segment_v2 {
 
@@ -45,12 +48,14 @@ class ColumnIterator;
 class InvertedIndexIterator;
 class InvertedIndexFileReader;
 class ColumnReaderCache;
+class ColumnMetaAccessor;
 
 class VariantColumnReader : public ColumnReader {
 public:
     VariantColumnReader() = default;
 
-    Status init(const ColumnReaderOptions& opts, const SegmentFooterPB& footer, uint32_t column_id,
+    Status init(const ColumnReaderOptions& opts, ColumnMetaAccessor* accessor,
+                const std::shared_ptr<SegmentFooterPB>& footer, int32_t column_uid,
                 uint64_t num_rows, io::FileReaderSPtr file_reader);
 
     Status new_iterator(ColumnIteratorUPtr* iterator, const TabletColumn* col,
@@ -70,7 +75,9 @@ public:
 
     int64_t get_metadata_size() const override;
 
-    std::vector<const TabletIndex*> find_subcolumn_tablet_indexes(const std::string&);
+    // Return shared_ptr to ensure the lifetime of TabletIndex objects
+    TabletIndexes find_subcolumn_tablet_indexes(const TabletColumn& target_column,
+                                                const vectorized::DataTypePtr& data_type);
 
     bool exist_in_sparse_column(const vectorized::PathInData& path) const;
 
@@ -80,15 +87,54 @@ public:
         return _subcolumns_meta_info.get();
     }
 
+    // Get the types of all subcolumns in the variant column.
     void get_subcolumns_types(
             std::unordered_map<vectorized::PathInData, vectorized::DataTypes,
                                vectorized::PathInData::Hash>* subcolumns_types) const;
 
+    // Get the typed paths in the variant column.
     void get_typed_paths(std::unordered_set<std::string>* typed_paths) const;
 
+    // Get the nested paths in the variant column.
     void get_nested_paths(std::unordered_set<vectorized::PathInData, vectorized::PathInData::Hash>*
                                   nested_paths) const;
 
+    // Infer the storage data type for a variant subcolumn identified by `relative_path`.
+    // This encapsulates the same decision logic as Segment::get_data_type_of for variant paths,
+    // including handling of sparse columns, prefix paths, and flat-leaf compaction mode.
+    vectorized::DataTypePtr infer_data_type_for_path(const TabletColumn& column,
+                                                     const vectorized::PathInData& relative_path,
+                                                     bool read_flat_leaves,
+                                                     ColumnReaderCache* cache,
+                                                     int32_t col_uid) const;
+
+    // Create a ColumnReader for a sub-column identified by `relative_path`.
+    // This method will first try inline footer.columns via footer_ordinal and then
+    // fall back to external meta if available. Callers do not need to care about
+    // the underlying layout (inline vs external).
+    Status create_path_reader(const vectorized::PathInData& relative_path,
+                              const ColumnReaderOptions& opts, ColumnMetaAccessor* accessor,
+                              const SegmentFooterPB& footer, const io::FileReaderSPtr& file_reader,
+                              uint64_t num_rows, std::shared_ptr<ColumnReader>* out);
+
+    // Try create a ColumnReader from externalized meta (path -> ColumnMetaPB bytes) if present.
+    // Only used internally by create_path_reader. External callers should not rely
+    // on external meta details directly.
+    Status create_reader_from_external_meta(const std::string& path,
+                                            const ColumnReaderOptions& opts,
+                                            const io::FileReaderSPtr& file_reader,
+                                            uint64_t num_rows, std::shared_ptr<ColumnReader>* out);
+
+    // Ensure external meta is loaded only once across concurrent callers.
+    Status load_external_meta_once();
+
+    // Determine whether `path` is a strict prefix of any existing subcolumn path.
+    // Consider three sources:
+    // 1) Extracted subcolumns in `_subcolumns_meta_info`
+    // 2) Sparse column statistics in `_statistics->sparse_column_non_null_size`
+    // 3) Externalized metas via `_ext_meta_reader`
+    bool has_prefix_path(const vectorized::PathInData& relative_path) const;
+
 private:
     // init for compaction read
     Status _new_default_iter_with_same_nested(ColumnIteratorUPtr* iterator, const TabletColumn& col,
@@ -114,11 +160,19 @@ private:
     std::shared_ptr<ColumnReader> _sparse_column_reader;
     std::shared_ptr<ColumnReader> _root_column_reader;
     std::unique_ptr<VariantStatistics> _statistics;
-    // key: subcolumn path, value: subcolumn indexes
-    std::unordered_map<std::string, TabletIndexes> _variant_subcolumns_indexes;
+    std::shared_ptr<TabletSchema> _tablet_schema;
     // variant_sparse_column_statistics_size
     size_t _variant_sparse_column_statistics_size =
             BeConsts::DEFAULT_VARIANT_MAX_SPARSE_COLUMN_STATS_SIZE;
+
+    // Externalized meta reader (optional)
+    std::unique_ptr<VariantExternalMetaReader> _ext_meta_reader;
+
+    io::FileReaderSPtr _segment_file_reader;
+    uint64_t _num_rows {0};
+    uint32_t _root_unique_id {0};
+
+    // call-once guard moved into VariantExternalMetaReader
 };
 
 class VariantRootColumnIterator : public ColumnIterator {
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
new file mode 100644
index 00000000000..d04c6be1d2c
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/variant_column_writer_impl.h
@@ -0,0 +1,169 @@
+// 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 <gen_cpp/segment_v2.pb.h>
+
+#include "common/status.h"
+#include "olap/rowset/segment_v2/column_writer.h"
+#include "olap/rowset/segment_v2/indexed_column_writer.h"
+#include "olap/rowset/segment_v2/variant_statistics.h"
+#include "olap/tablet_schema.h"
+#include "vec/columns/column.h"
+#include "vec/common/schema_util.h"
+
+namespace doris {
+
+namespace vectorized {
+class ColumnVariant;
+class OlapBlockDataConvertor;
+} // namespace vectorized
+namespace segment_v2 {
+
+#include "common/compile_check_begin.h"
+
+class ColumnWriter;
+class ScalarColumnWriter;
+
+// Unifies writing of Variant sparse data in two modes:
+// 1) Single sparse column: one Map(String,String) column `__DORIS_VARIANT_SPARSE__`
+// 2) Bucketized sparse columns: N Map columns `__DORIS_VARIANT_SPARSE__.b{i}`
+//
+// Responsibilities:
+// - Initialize column writers and metas (consuming column_id identically to previous logic)
+// - Convert and append Variant's serialized sparse data to storage writers
+// - Emit per-column (or per-bucket) sparse path statistics and set meta num_rows
+//
+// Invariants:
+// - `_first_column_id` stores the column_id assigned to the first sparse meta created by init*
+//   and is used to bind converter column ids deterministically.
+class UnifiedSparseColumnWriter {
+public:
+    // Initialize single sparse column writer and consume one column_id.
+    Status init_single(const TabletColumn& sparse_column, int& column_id,
+                       const ColumnWriterOptions& base_opts, SegmentFooterPB* footer);
+
+    // Initialize N bucket writers and consume N column_ids.
+    Status init_buckets(int bucket_num, const TabletColumn& parent_column, int& column_id,
+                        const ColumnWriterOptions& base_opts, SegmentFooterPB* footer);
+
+    bool has_single() const { return static_cast<bool>(_single_writer); }
+    bool has_buckets() const { return !_bucket_writers.empty(); }
+    bool is_bucket_mode() const { return has_buckets(); }
+    int bucket_num() const { return static_cast<int>(_bucket_writers.size()); }
+
+    ColumnWriter* single_writer() const { return _single_writer.get(); }
+    ColumnWriterOptions* single_opts() { return &_single_opts; }
+    const ColumnWriterOptions* single_opts() const { return &_single_opts; }
+
+    std::vector<std::unique_ptr<ColumnWriter>>& bucket_writers() { return _bucket_writers; }
+    const std::vector<std::unique_ptr<ColumnWriter>>& bucket_writers() const {
+        return _bucket_writers;
+    }
+    std::vector<ColumnWriterOptions>& bucket_opts() { return _bucket_opts; }
+    const std::vector<ColumnWriterOptions>& bucket_opts() const { return _bucket_opts; }
+
+    // Delegate lifecycle operations to the underlying writer(s).
+    uint64_t estimate_buffer_size() const;
+    Status finish();
+    Status write_data();
+    Status write_ordinal_index();
+
+    // Append data from ColumnVariant into single/bucket writers and fill statistics/meta.
+    // - For single mode: updates out_statistics and writes stats to single meta, sets num_rows
+    // - For bucket mode: writes per-bucket stats to each bucket meta, sets num_rows
+    // Convert and append Variant's sparse data:
+    // - Single mode: convert src.get_sparse_column() and append to the single writer, populate
+    //   out_stats (merged) and write stats to meta
+    // - Bucket mode: materialize N ColumnMap temporaries, distribute entries by
+    //   schema_util::variant_sparse_shard_of(path), convert and append per bucket, and write
+    //   per-bucket stats to metas
+    Status append_from_variant(const vectorized::ColumnVariant& src, size_t num_rows,
+                               vectorized::OlapBlockDataConvertor* converter,
+                               const TabletColumn& tablet_column, VariantStatistics* out_stats);
+
+private:
+    // Single sparse writer and its options/meta
+    std::unique_ptr<ColumnWriter> _single_writer;
+    ColumnWriterOptions _single_opts;
+    // Bucketized sparse writers and their options/metas (size == bucket_num)
+    std::vector<std::unique_ptr<ColumnWriter>> _bucket_writers;
+    std::vector<ColumnWriterOptions> _bucket_opts;
+    // remember assigned column ids for conversion
+    int _first_column_id = -1;
+};
+
+class VariantColumnWriterImpl {
+public:
+    VariantColumnWriterImpl(const ColumnWriterOptions& opts, const TabletColumn* column);
+    Status finalize();
+    Status init();
+    bool is_finalized() const;
+
+    Status append_data(const uint8_t** ptr, size_t num_rows);
+
+    Status finish();
+    Status write_data();
+    Status write_ordinal_index();
+    Status write_zone_map();
+    Status write_bitmap_index();
+    Status write_inverted_index();
+    Status write_bloom_filter_index();
+    uint64_t estimate_buffer_size();
+    Status append_nullable(const uint8_t* null_map, const uint8_t** ptr, size_t num_rows);
+
+private:
+    Status _process_root_column(vectorized::ColumnVariant* ptr,
+                                vectorized::OlapBlockDataConvertor* converter, size_t num_rows,
+                                int& column_id);
+    Status _process_sparse_column(vectorized::ColumnVariant* ptr,
+                                  vectorized::OlapBlockDataConvertor* converter, size_t num_rows,
+                                  int& column_id);
+    Status _process_subcolumns(vectorized::ColumnVariant* ptr,
+                               vectorized::OlapBlockDataConvertor* converter, size_t num_rows,
+                               int& column_id);
+    // prepare a column for finalize
+    doris::vectorized::MutableColumnPtr _column;
+    doris::vectorized::ColumnUInt8 _null_column;
+    ColumnWriterOptions _opts;
+    const TabletColumn* _tablet_column = nullptr;
+    bool _is_finalized = false;
+    // for root column
+    std::unique_ptr<ColumnWriter> _root_writer;
+    // unified sparse writers (single or bucket mode)
+    UnifiedSparseColumnWriter _sparse_writer;
+    std::vector<std::unique_ptr<ColumnWriter>> _subcolumn_writers;
+    std::vector<ColumnWriterOptions> _subcolumn_opts;
+
+    // staticstics which will be persisted in the footer
+    VariantStatistics _statistics;
+
+    // hold the references of subcolumns indexes
+    std::vector<TabletIndexes> _subcolumns_indexes;
+
+    // hold the references of subcolumns info
+    std::unordered_map<std::string, TabletSchema::SubColumnInfo> _subcolumns_info;
+};
+
+void _init_column_meta(ColumnMetaPB* meta, uint32_t column_id, const TabletColumn& column,
+                       CompressionTypePB compression_type);
+
+#include "common/compile_check_end.h"
+
+} // namespace segment_v2
+} // namespace doris
\ No newline at end of file
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.cpp b/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.cpp
new file mode 100644
index 00000000000..b31a9ceff20
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.cpp
@@ -0,0 +1,199 @@
+// 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 "olap/rowset/segment_v2/variant/variant_ext_meta_writer.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "olap/rowset/segment_v2/indexed_column_writer.h"
+#include "olap/types.h"
+#include "vec/common/schema_util.h"
+#include "vec/json/path_in_data.h"
+
+namespace doris::segment_v2 {
+
+Status VariantExtMetaWriter::_ensure_inited(Writers* w) {
+    if (w->inited) {
+        return Status::OK();
+    }
+
+    // key writer: VARCHAR, value index ON, ordinal index ON
+    IndexedColumnWriterOptions dict_opts;
+    dict_opts.write_value_index = true;
+    dict_opts.write_ordinal_index = true;
+    dict_opts.encoding = PREFIX_ENCODING;
+    dict_opts.compression = _comp;
+    const TypeInfo* dict_type = get_scalar_type_info<FieldType::OLAP_FIELD_TYPE_VARCHAR>();
+    w->key_writer = std::make_unique<IndexedColumnWriter>(dict_opts, dict_type, _fw);
+    RETURN_IF_ERROR(w->key_writer->init());
+
+    w->inited = true;
+    return Status::OK();
+}
+
+Status VariantExtMetaWriter::add(int32_t root_uid, const Slice& key) {
+    auto& w = _writers_by_uid[root_uid];
+    RETURN_IF_ERROR(_ensure_inited(&w));
+
+    // Only store the relative path string as key. The concrete column_id in
+    // Column Meta Region will be derived on the reader side using:
+    //   col_id = root_column_id + 1 + key_ordinal
+    // based on the agreed layout of ColumnMetaEntryPB in SegmentFooterPB.
+    RETURN_IF_ERROR(w.key_writer->add(&key));
+    ++w.count;
+    return Status::OK();
+}
+
+Status VariantExtMetaWriter::flush_to_footer(SegmentFooterPB* footer) {
+    for (auto& [uid, w] : _writers_by_uid) {
+        if (!w.inited || w.count == 0) {
+            continue;
+        }
+        doris::segment_v2::IndexedColumnMetaPB key_meta;
+        RETURN_IF_ERROR(w.key_writer->finish(&key_meta));
+
+        // keys
+        std::string k = std::string("variant_meta_keys.") + std::to_string(uid);
+        std::string v;
+        key_meta.AppendToString(&v);
+        auto* p1 = footer->add_file_meta_datas();
+        p1->set_key(k);
+        p1->set_value(v);
+    }
+    _writers_by_uid.clear();
+    return Status::OK();
+}
+
+// Helper struct to manage column classification
+struct ColumnClassification {
+    std::vector<ColumnMetaPB> top_level_columns;
+    std::unordered_map<int32_t, std::vector<std::pair<std::string, ColumnMetaPB>>> pending_subcols;
+    std::unordered_map<int32_t, std::vector<ColumnMetaPB>> pending_sparse;
+    size_t externalized_count = 0;
+};
+
+// Separate function to classify columns
+static void classify_columns(const SegmentFooterPB& footer, ColumnClassification& classification) {
+    classification.top_level_columns.reserve(footer.columns_size());
+
+    for (int i = 0; i < footer.columns_size(); ++i) {
+        const ColumnMetaPB& col = footer.columns(i);
+        bool is_subcol = false;
+        if (col.has_column_path_info()) {
+            vectorized::PathInData full_path;
+            full_path.from_protobuf(col.column_path_info());
+            vectorized::PathInData rel = full_path.copy_pop_front();
+            if (!rel.empty()) {
+                is_subcol = true;
+                std::string rel_path = rel.get_path();
+                int32_t root_uid = col.column_path_info().parrent_column_unique_id();
+
+                if (rel_path.find("__DORIS_VARIANT_SPARSE__") != std::string::npos) {
+                    classification.pending_sparse[root_uid].emplace_back(col);
+                } else {
+                    classification.pending_subcols[root_uid].emplace_back(rel_path, col);
+                    classification.externalized_count++;
+                }
+            }
+        }
+
+        if (!is_subcol) {
+            classification.top_level_columns.emplace_back(col);
+        }
+    }
+}
+
+Status VariantExtMetaWriter::externalize_from_footer(SegmentFooterPB* footer,
+                                                     std::vector<ColumnMetaPB>* out_metas) {
+    // Variant meta pre-processing and reorganization:
+    // 1. Identify Variant Root columns and their subcolumns.
+    // 2. Collect sparse subcolumns to embed in Root.
+    // 3. Collect non-sparse subcolumns to externalize to Meta Region.
+    // 4. Rebuild the column list (out_metas) ensuring subcolumns follow their Root.
+    // 5. Record mapping (Path -> Index in out_metas) for externalized subcolumns.
+
+    ColumnClassification classification;
+    classify_columns(*footer, classification);
+
+    // Rebuild out_metas
+    out_metas->clear();
+    out_metas->reserve(footer->columns_size()); // Rough estimate
+
+    std::vector<ColumnMetaPB> kept_footer_columns;
+    kept_footer_columns.reserve(footer->columns_size());
+
+    for (auto& col : classification.top_level_columns) {
+        // Add Top Level Column
+        out_metas->emplace_back(col);
+        // Keep it in footer columns as well
+        kept_footer_columns.emplace_back(col);
+        ColumnMetaPB* current_root_meta = &out_metas->back();
+        ColumnMetaPB* footer_root_meta = &kept_footer_columns.back();
+
+        // Check if it is a Variant Root
+        if (col.type() == int(FieldType::OLAP_FIELD_TYPE_VARIANT)) {
+            int32_t root_uid = col.unique_id();
+
+            // 1. Embed sparse subcolumns
+            if (auto it = classification.pending_sparse.find(root_uid);
+                it != classification.pending_sparse.end()) {
+                for (const auto& sparse_meta : it->second) {
+                    current_root_meta->add_children_columns()->CopyFrom(sparse_meta);
+                    footer_root_meta->add_children_columns()->CopyFrom(sparse_meta);
+                }
+            }
+
+            // 2. Append non-sparse subcolumns to out_metas and record path-only keys
+            if (auto it = classification.pending_subcols.find(root_uid);
+                it != classification.pending_subcols.end()) {
+                // Sort by path for consistent index order
+                std::sort(it->second.begin(), it->second.end(),
+                          [](const auto& a, const auto& b) { return a.first < b.first; });
+
+                for (auto& [path, meta] : it->second) {
+                    // Append to out_metas
+                    out_metas->emplace_back(meta);
+                    // Mark as subcolumn placeholder for footer logic (unique_id = -1)
+                    out_metas->back().set_unique_id(-1);
+
+                    // Record path key for this subcolumn. Column id will be derived
+                    // by reader as (root_col_id + 1 + ordinal).
+                    RETURN_IF_ERROR(add(root_uid, Slice(path)));
+                }
+            }
+        }
+    }
+
+    // Write the index to footer
+    RETURN_IF_ERROR(flush_to_footer(footer));
+
+    // Update footer.columns to only contain Top Level Columns (with embedded sparse cols)
+    footer->clear_columns();
+    for (const auto& c : kept_footer_columns) {
+        footer->add_columns()->CopyFrom(c);
+    }
+
+    VLOG_DEBUG << "VariantExtMetaWriter::externalize_from_footer, externalized subcolumns: "
+               << classification.externalized_count
+               << ", kept columns: " << kept_footer_columns.size()
+               << ", total meta entries: " << out_metas->size();
+    return Status::OK();
+}
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.h b/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.h
new file mode 100644
index 00000000000..e70429983dd
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/variant_ext_meta_writer.h
@@ -0,0 +1,99 @@
+// 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 <memory>
+#include <unordered_map>
+#include <vector>
+
+#include "common/status.h"
+#include "gen_cpp/segment_v2.pb.h"
+#include "io/fs/file_reader_writer_fwd.h"
+#include "olap/rowset/segment_v2/indexed_column_writer.h"
+#include "olap/types.h"
+#include "util/slice.h"
+
+namespace doris::segment_v2 {
+
+// -----------------------------------------------------------------------------
+// Variant External Meta (path -> ColumnMetaPB bytes) storage format
+// -----------------------------------------------------------------------------
+// Purpose: externalize ColumnMetaPB of variant subcolumns as a mapping from
+// relative path to column meta entries in the external Column Meta Region,
+// instead of packing all into Footer.columns. This enables on-demand subcolumn
+// loading and fast path lookup.
+//
+// Physical layout (per variant root uid): one IndexedColumn of keys is written
+//   - variant_meta_keys.<root_uid>    : IndexedColumn(VARCHAR)
+//       • value index: ON   (fast existence/lookup by path)
+//       • ordinal index: ON (enable full scan / load_all)
+//       • encoding: PREFIX_ENCODING
+//       • content: Key = Path (relative path string only)
+//
+// Footer carrier:
+//   footer.file_meta_datas contains one entry per root uid:
+//     - key = "variant_meta_keys.<root_uid>"    , value = IndexedColumnMetaPB bytes
+//
+// Interop with external column meta (column_meta_entries):
+//   - Variant root ColumnMetaPB is externalized via column_meta_entries (addressed by col_id)
+//   - Variant subcolumn ColumnMetaPB:
+//       • non-sparse: stored in Column Meta Region, addressed by a unique col_id (in column_meta_entries).
+//         The mapping from Path -> col_id is stored in this class (VariantExtMetaWriter).
+//       • sparse (including buckets): embedded into root's ColumnMetaPB.children_columns and
+//         loaded together with root meta via column_meta_entries
+//
+class VariantExtMetaWriter {
+public:
+    VariantExtMetaWriter(io::FileWriter* fw, CompressionTypePB comp) : _fw(fw), _comp(comp) {}
+
+    // Add one path entry for the specified variant root uid.
+    // key: subcolumn path (VARCHAR)
+    // The concrete ColumnMetaPB location (column_id) is derived on read side
+    // via (root_column_id + 1 + ordinal) based on the agreed Column Meta Region
+    // layout.
+    Status add(int32_t root_uid, const Slice& key);
+
+    // Finish writers and append their metas into footer.file_meta_datas.
+    Status flush_to_footer(SegmentFooterPB* footer);
+
+    // Scan footer.columns, find variant extracted subcolumns, externalize them into
+    // ext meta index and remove them from footer.columns.
+    // This method reorganizes the columns:
+    // 1. Separates Top Level Columns (kept in footer) and Variant Subcolumns.
+    // 2. Records Subcolumns' indices (relative to the combined list) into the Variant Index.
+    // 3. Populates out_metas with the combined list of all columns (Top Level + Subcolumns),
+    //    ordered such that Subcolumns immediately follow their Root Variant.
+    Status externalize_from_footer(SegmentFooterPB* footer, std::vector<ColumnMetaPB>* out_metas);
+
+    bool empty() const { return _writers_by_uid.empty(); }
+
+private:
+    struct Writers {
+        std::unique_ptr<IndexedColumnWriter> key_writer; // value index
+        size_t count = 0;
+        bool inited = false;
+    };
+
+    Status _ensure_inited(Writers* w);
+
+    io::FileWriter* _fw;
+    CompressionTypePB _comp;
+    std::unordered_map<int32_t, Writers> _writers_by_uid;
+};
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.cpp b/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.cpp
new file mode 100644
index 00000000000..9673f0093d3
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.cpp
@@ -0,0 +1,246 @@
+// 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 "olap/rowset/segment_v2/variant/variant_external_meta_reader.h"
+
+#include <gen_cpp/segment_v2.pb.h>
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+#include "common/status.h"
+#include "olap/rowset/segment_v2/indexed_column_reader.h"
+#include "vec/columns/column_string.h"
+#include "vec/data_types/data_type_factory.hpp"
+#include "vec/json/path_in_data.h"
+
+namespace doris::segment_v2 {
+
+#include "common/compile_check_begin.h"
+
+Status VariantExternalMetaReader::_find_key_meta(const SegmentFooterPB& footer, int32_t root_uid,
+                                                 const MetadataPairPB** keys_meta_pair) const {
+    *keys_meta_pair = nullptr;
+    // prefer suffixed pairs
+    std::string suffix = "." + std::to_string(root_uid);
+    for (const auto& m : footer.file_meta_datas()) {
+        if (m.key() == std::string("variant_meta_keys") + suffix) {
+            *keys_meta_pair = &m;
+            return Status::OK();
+        }
+    }
+    // fallback: legacy single-variant footer
+    if (!*keys_meta_pair) {
+        for (const auto& m : footer.file_meta_datas()) {
+            if (m.key() == "variant_meta_keys") {
+                *keys_meta_pair = &m;
+                return Status::OK();
+            }
+        }
+    }
+    return Status::OK();
+}
+
+Status VariantExternalMetaReader::init_from_footer(std::shared_ptr<const SegmentFooterPB> footer,
+                                                   const io::FileReaderSPtr& file_reader,
+                                                   int32_t root_uid) {
+    _footer = std::move(footer);
+    _file_reader = file_reader;
+
+    const MetadataPairPB* keys_meta_pair = nullptr;
+    RETURN_IF_ERROR(_find_key_meta(*_footer, root_uid, &keys_meta_pair));
+    // If keys meta is absent, external meta is considered unavailable.
+    if (!keys_meta_pair) {
+        return Status::OK();
+    }
+
+    // Parse external Column Meta Region pointers and uid->col_id map.
+    RETURN_IF_ERROR(ExternalColMetaUtil::parse_external_meta_pointers(*_footer, &_meta_ptrs));
+    std::unordered_map<int32_t, size_t> uid2colid;
+    RETURN_IF_ERROR(ExternalColMetaUtil::parse_uid_to_colid_map(*_footer, _meta_ptrs, &uid2colid));
+    auto it = uid2colid.find(root_uid);
+    if (it == uid2colid.end()) {
+        return Status::Corruption("root uid {} not found in external uid->col_id map", root_uid);
+    }
+    _root_col_id = static_cast<uint32_t>(it->second);
+
+    // Open the path-key indexed column.
+    doris::segment_v2::IndexedColumnMetaPB key_meta;
+    if (!key_meta.ParseFromArray(keys_meta_pair->value().data(),
+                                 static_cast<int>(keys_meta_pair->value().size()))) {
+        return Status::Corruption("bad variant_meta_keys meta");
+    }
+
+    _key_reader = std::make_unique<segment_v2::IndexedColumnReader>(file_reader, key_meta);
+    RETURN_IF_ERROR(_key_reader->load(true, false));
+    return Status::OK();
+}
+
+Status VariantExternalMetaReader::lookup_meta_by_path(const std::string& rel_path,
+                                                      ColumnMetaPB* out_meta) const {
+    if (!available()) {
+        return Status::Error<ErrorCode::NOT_FOUND, false>("no external variant meta");
+    }
+    // 1. Seek for Path
+    auto key_it = segment_v2::IndexedColumnIterator(_key_reader.get());
+    bool exact = false;
+    Status st = key_it.seek_at_or_after(&rel_path, &exact);
+    if (st.is<ErrorCode::ENTRY_NOT_FOUND>()) {
+        return Status::Error<ErrorCode::NOT_FOUND, false>("variant meta key not found");
+    }
+    if (!st.ok()) {
+        return st;
+    }
+
+    // Record ordinal immediately after seek; get_current_ordinal() is only valid
+    // while iterator is in "seeked" state (before calling next_batch()).
+    auto ordinal = static_cast<uint32_t>(key_it.get_current_ordinal());
+
+    // 2. Read key and verify exact match (keys store pure relative path).
+    size_t n = 1;
+    auto col = vectorized::ColumnString::create();
+    vectorized::MutableColumnPtr dst = std::move(col);
+    RETURN_IF_ERROR(key_it.next_batch(&n, dst));
+    if (n != 1) {
+        return Status::Corruption("variant meta key read failed");
+    }
+    auto* s = assert_cast<vectorized::ColumnString*>(dst.get());
+    auto ref = s->get_data_at(0);
+
+    if (ref.size != rel_path.size() || memcmp(ref.data, rel_path.data(), ref.size) != 0) {
+        // seek_at_or_after found something greater, but path doesn't match
+        return Status::Error<ErrorCode::NOT_FOUND, false>("variant meta key not found");
+    }
+
+    // 3. Derive column id from root_col_id and key ordinal, then read ColumnMetaPB.
+    auto col_id = _root_col_id + 1 + ordinal;
+    return ExternalColMetaUtil::read_col_meta(_file_reader, *_footer, _meta_ptrs, col_id, out_meta);
+}
+
+Status VariantExternalMetaReader::load_all(SubcolumnColumnMetaInfo* out_meta_tree,
+                                           VariantStatistics* out_stats) {
+    if (!available()) {
+        return Status::OK();
+    }
+
+    auto key_it = segment_v2::IndexedColumnIterator(_key_reader.get());
+    RETURN_IF_ERROR(key_it.seek_to_ordinal(0));
+    auto total = static_cast<size_t>(_key_reader->num_values());
+    size_t built = 0;
+
+    while (built < total) {
+        size_t n = total - built;
+        auto col = vectorized::ColumnString::create();
+        vectorized::MutableColumnPtr dst = std::move(col);
+        RETURN_IF_ERROR(key_it.next_batch(&n, dst));
+        if (n == 0) {
+            break;
+        }
+        for (size_t i = 0; i < n; ++i) {
+            // Derive column id for this key by its global ordinal.
+            auto ordinal = static_cast<uint32_t>(built + i);
+            auto col_id = _root_col_id + 1 + ordinal;
+
+            ColumnMetaPB meta;
+            RETURN_IF_ERROR(ExternalColMetaUtil::read_col_meta(_file_reader, *_footer, _meta_ptrs,
+                                                               col_id, &meta));
+
+            if (!meta.has_column_path_info()) {
+                continue;
+            }
+            vectorized::PathInData full_path;
+            full_path.from_protobuf(meta.column_path_info());
+            auto relative_path = full_path.copy_pop_front();
+            if (relative_path.empty()) {
+                continue; // skip root
+            }
+            if (out_meta_tree->find_leaf(relative_path)) {
+                continue; // already exists
+            }
+            if (meta.has_none_null_size() && out_stats != nullptr) {
+                out_stats->subcolumns_non_null_size.emplace(relative_path.get_path(),
+                                                            meta.none_null_size());
+            }
+            auto file_type = vectorized::DataTypeFactory::instance().create_data_type(meta);
+            out_meta_tree->add(relative_path,
+                               SubcolumnMeta {.file_column_type = file_type, .footer_ordinal = -1});
+        }
+        built += n;
+    }
+    return Status::OK();
+}
+
+Status VariantExternalMetaReader::has_prefix(const std::string& prefix, bool* out) const {
+    // english only in comments
+    DCHECK(out != nullptr);
+    DCHECK(available());
+    *out = false;
+    // Empty prefix means everything matches; guard for safety
+    if (prefix.empty()) {
+        *out = true;
+        return Status::OK();
+    }
+
+    segment_v2::IndexedColumnIterator key_it(_key_reader.get());
+    bool exact = false;
+    Status st = key_it.seek_at_or_after(&prefix, &exact);
+    if (st.is<ErrorCode::ENTRY_NOT_FOUND>()) {
+        *out = false;
+        return Status::OK();
+    }
+    if (!st.ok()) {
+        return st;
+    }
+
+    size_t n = 1;
+    auto col = vectorized::ColumnString::create();
+    vectorized::MutableColumnPtr dst = std::move(col);
+    RETURN_IF_ERROR(key_it.next_batch(&n, dst));
+    if (n == 0) {
+        *out = false;
+        return Status::OK();
+    }
+    auto* s = assert_cast<vectorized::ColumnString*>(dst.get());
+    auto ref = s->get_data_at(0);
+    std::string_view key_sv(ref.data, ref.size);
+
+    // starts_with check. Key is Path + ID.
+    // If prefix is just path part, it matches.
+    // If prefix contains ID part (unlikely use case), it also matches.
+    *out = key_sv.size() >= prefix.size() && key_sv.starts_with(prefix);
+    return Status::OK();
+}
+
+Status VariantExternalMetaReader::load_all_once(SubcolumnColumnMetaInfo* out_meta_tree,
+                                                VariantStatistics* out_stats) {
+    if (!available()) {
+        return Status::OK();
+    }
+    return _load_once_call.call([&]() -> Status {
+        if (_loaded) {
+            return Status::OK();
+        }
+        RETURN_IF_ERROR(load_all(out_meta_tree, out_stats));
+        _loaded = true;
+        return Status::OK();
+    });
+}
+
+#include "common/compile_check_end.h"
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.h b/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.h
new file mode 100644
index 00000000000..e07ff105e24
--- /dev/null
+++ b/be/src/olap/rowset/segment_v2/variant/variant_external_meta_reader.h
@@ -0,0 +1,95 @@
+// 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 <gen_cpp/segment_v2.pb.h>
+
+#include <memory>
+#include <string>
+
+#include "common/status.h"
+#include "olap/rowset/segment_v2/external_col_meta_util.h"
+#include "olap/rowset/segment_v2/indexed_column_reader.h"
+#include "olap/rowset/segment_v2/stream_reader.h"
+#include "olap/rowset/segment_v2/variant_statistics.h"
+#include "util/once.h"
+
+namespace doris::segment_v2 {
+
+#include "common/compile_check_begin.h"
+
+// Encapsulates reading of externalized Variant subcolumn metas.
+// It discovers key/value indexed-columns from SegmentFooterPB, supports:
+// - availability check
+// - lookup ColumnMetaPB by relative path
+// - one-time bulk load of all metas into SubcolumnColumnMetaInfo and VariantStatistics
+class VariantExternalMetaReader {
+public:
+    VariantExternalMetaReader() = default;
+
+    // Initialize by locating and opening ext meta key/value indexed columns.
+    // root_uid is used to find suffixed keys like: variant_meta_keys.<root_uid>
+    //
+    // NOTE: footer is kept as shared_ptr to ensure its lifetime is at least as
+    // long as this reader; callers should pass in the same footer shared_ptr
+    // they own (e.g. from Segment::_get_segment_footer).
+    Status init_from_footer(std::shared_ptr<const SegmentFooterPB> footer,
+                            const io::FileReaderSPtr& file_reader, int32_t root_uid);
+
+    bool available() const { return _key_reader != nullptr; }
+
+    // Lookup a single ColumnMetaPB by relative path. Returns NOT_FOUND if missing/unavailable.
+    Status lookup_meta_by_path(const std::string& rel_path, ColumnMetaPB* out_meta) const;
+
+    // Check whether there exists any key in external meta that starts with `prefix`.
+    // This performs a lower_bound (seek_at_or_after) on the sorted key column
+    // and verifies the first key is prefixed by `prefix`.
+    Status has_prefix(const std::string& prefix, bool* out) const;
+
+    // Ensure external metas are loaded exactly once and merged into provided structures.
+    Status load_all_once(SubcolumnColumnMetaInfo* out_meta_tree, VariantStatistics* out_stats);
+
+    // Load and merge all external metas without call-once guard.
+    Status load_all(SubcolumnColumnMetaInfo* out_meta_tree, VariantStatistics* out_stats);
+
+private:
+    // helpers
+    Status _find_key_meta(const SegmentFooterPB& footer, int32_t root_uid,
+                          const MetadataPairPB** keys_meta_pair) const;
+
+    std::unique_ptr<IndexedColumnReader> _key_reader;
+
+    // Shared footer to access Column Meta Region info (column_meta_entries).
+    // Using shared_ptr prevents dangling pointer when the original owner releases it.
+    std::shared_ptr<const SegmentFooterPB> _footer;
+    ExternalColMetaUtil::ExternalMetaPointers _meta_ptrs;
+    io::FileReaderSPtr _file_reader;
+
+    // Global column_id of the variant root in external Column Meta Region.
+    // For its non-sparse subcolumns, their column_id is derived as:
+    //   root_col_id + 1 + key_ordinal
+    uint32_t _root_col_id = 0;
+
+    // call-once guard for bulk loading
+    DorisCallOnce<Status> _load_once_call;
+    bool _loaded = false;
+};
+
+#include "common/compile_check_end.h"
+
+} // namespace doris::segment_v2
diff --git a/be/src/olap/rowset/segment_v2/variant_column_writer_impl.cpp b/be/src/olap/rowset/segment_v2/variant_column_writer_impl.cpp
index 6cb59d186da..c5fcefa0f5b 100644
--- a/be/src/olap/rowset/segment_v2/variant_column_writer_impl.cpp
+++ b/be/src/olap/rowset/segment_v2/variant_column_writer_impl.cpp
@@ -31,9 +31,12 @@
 #include "olap/rowset/rowset_fwd.h"
 #include "olap/rowset/rowset_writer_context.h"
 #include "olap/rowset/segment_v2/column_writer.h"
+#include "olap/rowset/segment_v2/indexed_column_writer.h"
 #include "olap/segment_loader.h"
 #include "olap/tablet_schema.h"
+#include "olap/types.h"
 #include "util/simd/bits.h"
+#include "util/slice.h"
 #include "vec/columns/column.h"
 #include "vec/columns/column_nullable.h"
 #include "vec/columns/column_object.h"
@@ -250,6 +253,9 @@ Status VariantColumnWriterImpl::_process_subcolumns(vectorized::ColumnObject* pt
     };
     _subcolumns_indexes.resize(ptr->get_subcolumns().size());
     // convert sub column data from engine format to storage layer format
+    // NOTE: We only keep up to variant_max_subcolumns_count as extracted columns; others are externalized.
+    // uint32_t extracted = 0;
+    // uint32_t extract_limit = _tablet_column->variant_max_subcolumns_count();
     for (const auto& entry :
          vectorized::schema_util::get_sorted_subcolumns(ptr->get_subcolumns())) {
         const auto& least_common_type = entry->data.get_least_common_type();
@@ -263,7 +269,7 @@ Status VariantColumnWriterImpl::_process_subcolumns(vectorized::ColumnObject* pt
         }
         CHECK(entry->data.is_finalized());
 
-        // create subcolumn writer
+        // create subcolumn writer if under limit; otherwise externalize ColumnMetaPB via IndexedColumn
         int current_column_id = column_id++;
         TabletColumn tablet_column;
         int64_t none_null_value_size = entry->data.get_non_null_value_size();
diff --git a/be/src/olap/rowset/segment_v2/vertical_segment_writer.cpp b/be/src/olap/rowset/segment_v2/vertical_segment_writer.cpp
index adb4986c2be..72a43873855 100644
--- a/be/src/olap/rowset/segment_v2/vertical_segment_writer.cpp
+++ b/be/src/olap/rowset/segment_v2/vertical_segment_writer.cpp
@@ -49,10 +49,12 @@
 #include "olap/rowset/rowset_writer_context.h" // RowsetWriterContext
 #include "olap/rowset/segment_creator.h"
 #include "olap/rowset/segment_v2/column_writer.h" // ColumnWriter
+#include "olap/rowset/segment_v2/external_col_meta_util.h"
 #include "olap/rowset/segment_v2/inverted_index_desc.h"
 #include "olap/rowset/segment_v2/inverted_index_file_writer.h"
 #include "olap/rowset/segment_v2/page_io.h"
 #include "olap/rowset/segment_v2/page_pointer.h"
+#include "olap/rowset/segment_v2/variant/variant_ext_meta_writer.h"
 #include "olap/segment_loader.h"
 #include "olap/short_key_index.h"
 #include "olap/tablet_schema.h"
@@ -1447,6 +1449,17 @@ Status VerticalSegmentWriter::_write_primary_key_index() {
 Status VerticalSegmentWriter::_write_footer() {
     _footer.set_num_rows(_row_count);
 
+    // Decide whether to externalize ColumnMetaPB by tablet default, and stamp footer version
+
+    if (_tablet_schema->is_external_segment_meta_used_default()) {
+        _footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+        VLOG_DEBUG << "use external column meta";
+        // External ColumnMetaPB writing (optional)
+        RETURN_IF_ERROR(ExternalColMetaUtil::write_external_column_meta(
+                _file_writer, &_footer, _opts.compression_type,
+                [this](const std::vector<Slice>& slices) { return _write_raw_data(slices); }));
+    }
+
     // Footer := SegmentFooterPB, FooterPBSize(4), FooterPBChecksum(4), MagicNumber(4)
     VLOG_DEBUG << "footer " << _footer.DebugString();
     std::string footer_buf;
diff --git a/be/src/olap/tablet_manager.cpp b/be/src/olap/tablet_manager.cpp
index 727b259a13b..f2672da5f6e 100644
--- a/be/src/olap/tablet_manager.cpp
+++ b/be/src/olap/tablet_manager.cpp
@@ -1556,7 +1556,8 @@ Status TabletManager::_create_tablet_meta_unlocked(const TCreateTabletReq& reque
             (*tablet_meta)->set_preferred_rowset_type(_engine.default_rowset_type());
         } else if (request.storage_format == TStorageFormat::V1) {
             (*tablet_meta)->set_preferred_rowset_type(ALPHA_ROWSET);
-        } else if (request.storage_format == TStorageFormat::V2) {
+        } else if (request.storage_format == TStorageFormat::V2 ||
+                   request.storage_format == TStorageFormat::V3) {
             (*tablet_meta)->set_preferred_rowset_type(BETA_ROWSET);
         } else {
             return Status::Error<CE_CMD_PARAMS_ERROR>("invalid TStorageFormat: {}",
diff --git a/be/src/olap/tablet_meta.cpp b/be/src/olap/tablet_meta.cpp
index da1aae33780..424ffa6bc51 100644
--- a/be/src/olap/tablet_meta.cpp
+++ b/be/src/olap/tablet_meta.cpp
@@ -108,12 +108,16 @@ TabletMetaSharedPtr TabletMeta::create(
             break;
         }
     }
+    // Decide storage format for this tablet. DEFAULT / not-set fall back to V2 on BE side.
+    TStorageFormat::type storage_format =
+            request.__isset.storage_format ? request.storage_format : TStorageFormat::V2;
     return std::make_shared<TabletMeta>(
             request.table_id, request.partition_id, request.tablet_id, request.replica_id,
             request.tablet_schema.schema_hash, shard_id, request.tablet_schema, next_unique_id,
             col_ordinal_to_unique_id, tablet_uid,
             request.__isset.tablet_type ? request.tablet_type : TTabletType::TABLET_TYPE_DISK,
-            request.compression_type, request.storage_policy_id,
+            request.__isset.compression_type ? request.compression_type : TCompressionType::LZ4F,
+            request.__isset.storage_policy_id ? request.storage_policy_id : -1,
             request.__isset.enable_unique_key_merge_on_write
                     ? request.enable_unique_key_merge_on_write
                     : false,
@@ -123,7 +127,7 @@ TabletMetaSharedPtr TabletMeta::create(
             request.time_series_compaction_time_threshold_seconds,
             request.time_series_compaction_empty_rowsets_threshold,
             request.time_series_compaction_level_threshold, inverted_index_file_storage_format,
-            request.tde_algorithm);
+            request.tde_algorithm, storage_format);
 }
 
 TabletMeta::~TabletMeta() {
@@ -151,10 +155,12 @@ TabletMeta::TabletMeta(int64_t table_id, int64_t partition_id, int64_t tablet_id
                        int64_t time_series_compaction_empty_rowsets_threshold,
                        int64_t time_series_compaction_level_threshold,
                        TInvertedIndexFileStorageFormat::type inverted_index_file_storage_format,
-                       TEncryptionAlgorithm::type tde_algorithm)
+                       TEncryptionAlgorithm::type tde_algorithm,
+                       TStorageFormat::type storage_format)
         : _tablet_uid(0, 0),
           _schema(new TabletSchema),
-          _delete_bitmap(new DeleteBitmap(tablet_id)) {
+          _delete_bitmap(new DeleteBitmap(tablet_id)),
+          _storage_format(storage_format) {
     TabletMetaPB tablet_meta_pb;
     tablet_meta_pb.set_table_id(table_id);
     tablet_meta_pb.set_partition_id(partition_id);
@@ -395,6 +401,22 @@ TabletMeta::TabletMeta(int64_t table_id, int64_t partition_id, int64_t tablet_id
         tablet_meta_pb.set_encryption_algorithm(EncryptionAlgorithmPB::PLAINTEXT);
     }
 
+    // Initialize default external ColumnMeta usage according to storage format.
+    // V2: legacy behavior, inline ColumnMetaPB only.
+    // V3: V2 + external ColumnMetaPB (CMO) enabled by default.
+    switch (_storage_format) {
+    case TStorageFormat::V2:
+    case TStorageFormat::DEFAULT:
+    case TStorageFormat::V1:
+        break;
+    case TStorageFormat::V3:
+        schema->set_is_external_segment_meta_used_default(true);
+        _schema->set_external_segment_meta_used_default(true);
+        break;
+    default:
+        break;
+    }
+
     init_from_pb(tablet_meta_pb);
 }
 
diff --git a/be/src/olap/tablet_meta.h b/be/src/olap/tablet_meta.h
index 68f6d323bbc..4846d046f0e 100644
--- a/be/src/olap/tablet_meta.h
+++ b/be/src/olap/tablet_meta.h
@@ -118,7 +118,8 @@ public:
                int64_t time_series_compaction_level_threshold = 1,
                TInvertedIndexFileStorageFormat::type inverted_index_file_storage_format =
                        TInvertedIndexFileStorageFormat::V2,
-               TEncryptionAlgorithm::type tde_algorithm = TEncryptionAlgorithm::PLAINTEXT);
+               TEncryptionAlgorithm::type tde_algorithm = TEncryptionAlgorithm::PLAINTEXT,
+               TStorageFormat::type storage_format = TStorageFormat::V2);
     // If need add a filed in TableMeta, filed init copy in copy construct function
     TabletMeta(const TabletMeta& tablet_meta);
     TabletMeta(TabletMeta&& tablet_meta) = delete;
@@ -369,6 +370,10 @@ private:
 
     EncryptionAlgorithmPB _encryption_algorithm = PLAINTEXT;
 
+    // Persisted storage format for this tablet (e.g. V2, V3). Used to derive
+    // schema-level defaults such as external ColumnMeta usage.
+    TStorageFormat::type _storage_format = TStorageFormat::V2;
+
     mutable std::shared_mutex _meta_lock;
 };
 
diff --git a/be/src/olap/tablet_schema.cpp b/be/src/olap/tablet_schema.cpp
index 255e15546af..561a7a6ef57 100644
--- a/be/src/olap/tablet_schema.cpp
+++ b/be/src/olap/tablet_schema.cpp
@@ -1129,6 +1129,11 @@ void TabletSchema::init_from_pb(const TabletSchemaPB& schema, bool ignore_extrac
                                         schema.row_store_column_unique_ids().end());
     _variant_enable_flatten_nested = schema.variant_enable_flatten_nested();
     _vl_field_mem_size += _row_store_column_unique_ids.capacity() * sizeof(int32_t);
+    if (schema.has_is_external_segment_meta_used_default()) {
+        _is_external_segment_meta_used_default = schema.is_external_segment_meta_used_default();
+    } else {
+        _is_external_segment_meta_used_default = false;
+    }
     update_metadata_size();
 }
 
@@ -1384,6 +1389,8 @@ void TabletSchema::to_schema_pb(TabletSchemaPB* tablet_schema_pb) const {
     tablet_schema_pb->mutable_row_store_column_unique_ids()->Assign(
             _row_store_column_unique_ids.begin(), _row_store_column_unique_ids.end());
     tablet_schema_pb->set_variant_enable_flatten_nested(_variant_enable_flatten_nested);
+    tablet_schema_pb->set_is_external_segment_meta_used_default(
+        _is_external_segment_meta_used_default);
 }
 
 size_t TabletSchema::row_size() const {
@@ -1713,6 +1720,7 @@ bool operator==(const TabletSchema& a, const TabletSchema& b) {
     if (a._storage_dict_page_size != b._storage_dict_page_size) return false;
     if (a._skip_write_index_on_load != b._skip_write_index_on_load) return false;
     if (a._variant_enable_flatten_nested != b._variant_enable_flatten_nested) return false;
+    if (a._is_external_segment_meta_used_default != b._is_external_segment_meta_used_default) return false;
     return true;
 }
 
diff --git a/be/src/olap/tablet_schema.h b/be/src/olap/tablet_schema.h
index ad4897adc9b..3bae9ed4f21 100644
--- a/be/src/olap/tablet_schema.h
+++ b/be/src/olap/tablet_schema.h
@@ -655,6 +655,15 @@ public:
 
     void clear_path_set_info() { _path_set_info_map.clear(); }
 
+    // Whether new segments use externalized ColumnMetaPB layout (CMO) by default
+    bool is_external_segment_meta_used_default() const {
+        return _is_external_segment_meta_used_default;
+    }
+
+    void set_external_segment_meta_used_default(bool v) {
+        _is_external_segment_meta_used_default = v;
+    }
+
 private:
     friend bool operator==(const TabletSchema& a, const TabletSchema& b);
     friend bool operator!=(const TabletSchema& a, const TabletSchema& b);
@@ -731,6 +740,9 @@ private:
     // value: index
     using PatternToIndex = std::unordered_map<std::string, std::vector<TabletIndexPtr>>;
     std::unordered_map<int32_t, PatternToIndex> _index_by_unique_id_with_pattern;
+
+    // Default behavior for new segments: use external ColumnMeta region + CMO table if true
+    bool _is_external_segment_meta_used_default = false;
 };
 
 bool operator==(const TabletSchema& a, const TabletSchema& b);
diff --git a/be/src/service/internal_service.cpp b/be/src/service/internal_service.cpp
index a1c5c98ce45..15ddf30ce53 100644
--- a/be/src/service/internal_service.cpp
+++ b/be/src/service/internal_service.cpp
@@ -1132,6 +1132,10 @@ void PInternalService::fetch_remote_tablet_schema(google::protobuf::RpcControlle
     bool ret = _heavy_work_pool.try_offer([request, response, done]() {
         brpc::ClosureGuard closure_guard(done);
         Status st = Status::OK();
+        std::shared_ptr<MemTrackerLimiter> mem_tracker = MemTrackerLimiter::create_shared(
+            MemTrackerLimiter::Type::OTHER,
+            fmt::format("InternalService::fetch_remote_tablet_schema"));
+        SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(mem_tracker);
         if (request->is_coordinator()) {
             // Spawn rpc request to none coordinator nodes, and finally merge them all
             PFetchRemoteSchemaRequest remote_request(*request);
diff --git a/be/src/vec/common/schema_util.cpp b/be/src/vec/common/schema_util.cpp
index aa21db80d29..49a94630a37 100644
--- a/be/src/vec/common/schema_util.cpp
+++ b/be/src/vec/common/schema_util.cpp
@@ -759,8 +759,10 @@ Status aggregate_path_to_stats(
             }
 
             CHECK(column_reader->get_meta_type() == FieldType::OLAP_FIELD_TYPE_VARIANT);
-            const auto* variant_column_reader =
-                    assert_cast<const segment_v2::VariantColumnReader*>(column_reader.get());
+            auto* variant_column_reader =
+                    assert_cast<segment_v2::VariantColumnReader*>(column_reader.get());
+            // load external meta before getting stats
+            RETURN_IF_ERROR(variant_column_reader->load_external_meta_once());
             const auto* source_stats = variant_column_reader->get_stats();
             CHECK(source_stats);
 
@@ -798,8 +800,10 @@ Status aggregate_variant_extended_info(
             }
 
             CHECK(column_reader->get_meta_type() == FieldType::OLAP_FIELD_TYPE_VARIANT);
-            const auto* variant_column_reader =
-                    assert_cast<const segment_v2::VariantColumnReader*>(column_reader.get());
+            auto* variant_column_reader =
+                    assert_cast<segment_v2::VariantColumnReader*>(column_reader.get());
+            // load external meta before getting stats
+            RETURN_IF_ERROR(variant_column_reader->load_external_meta_once());
             const auto* source_stats = variant_column_reader->get_stats();
             CHECK(source_stats);
 
@@ -819,14 +823,17 @@ Status aggregate_variant_extended_info(
             auto& paths_types =
                     (*uid_to_variant_extended_info)[column->unique_id()].path_to_data_types;
             variant_column_reader->get_subcolumns_types(&paths_types);
+            VLOG_DEBUG << "path_to_data_types size: " << paths_types.size();
 
             // 3. extract typed paths
             auto& typed_paths = (*uid_to_variant_extended_info)[column->unique_id()].typed_paths;
             variant_column_reader->get_typed_paths(&typed_paths);
+            VLOG_DEBUG << "typed_paths size: " << typed_paths.size();
 
             // 4. extract nested paths
             auto& nested_paths = (*uid_to_variant_extended_info)[column->unique_id()].nested_paths;
             variant_column_reader->get_nested_paths(&nested_paths);
+            VLOG_DEBUG << "nested_paths size: " << nested_paths.size();
         }
     }
     return Status::OK();
@@ -1490,9 +1497,16 @@ TabletSchemaSPtr calculate_variant_extended_schema(const std::vector<RowsetShare
                 }
 
                 CHECK(column_reader->get_meta_type() == FieldType::OLAP_FIELD_TYPE_VARIANT);
-                const auto* subcolumn_meta_info =
-                        assert_cast<VariantColumnReader*>(column_reader.get())
-                                ->get_subcolumns_meta_info();
+                auto* variant_column_reader =
+                        assert_cast<segment_v2::VariantColumnReader*>(column_reader.get());
+                // load external meta before getting subcolumn meta info
+                st = variant_column_reader->load_external_meta_once();
+                if (!st.ok()) {
+                    LOG(WARNING) << "Failed to load external meta for column: " << column->name()
+                                 << " error: " << st.to_string();
+                    continue;
+                }
+                const auto* subcolumn_meta_info = variant_column_reader->get_subcolumns_meta_info();
                 for (const auto& entry : *subcolumn_meta_info) {
                     if (entry->path.empty()) {
                         continue;
diff --git a/be/src/vec/data_types/data_type_factory.cpp b/be/src/vec/data_types/data_type_factory.cpp
index bfcda534ecc..5129376de19 100644
--- a/be/src/vec/data_types/data_type_factory.cpp
+++ b/be/src/vec/data_types/data_type_factory.cpp
@@ -384,6 +384,18 @@ DataTypePtr DataTypeFactory::_create_primitive_data_type(const FieldType& type,
     case FieldType::OLAP_FIELD_TYPE_INT:
         result = std::make_shared<vectorized::DataTypeInt32>();
         break;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_TINYINT:
+        result = std::make_shared<vectorized::DataTypeUInt8>();
+        break;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_SMALLINT:
+        result = std::make_shared<vectorized::DataTypeUInt16>();
+        break;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_INT:
+        result = std::make_shared<vectorized::DataTypeUInt32>();
+        break;
+    case FieldType::OLAP_FIELD_TYPE_UNSIGNED_BIGINT:
+        result = std::make_shared<vectorized::DataTypeUInt64>();
+        break;
     case FieldType::OLAP_FIELD_TYPE_FLOAT:
         result = std::make_shared<vectorized::DataTypeFloat32>();
         break;
diff --git a/be/test/olap/date_bloom_filter_test.cpp b/be/test/olap/date_bloom_filter_test.cpp
index ddb413bc2fd..ccd51d0bfd0 100644
--- a/be/test/olap/date_bloom_filter_test.cpp
+++ b/be/test/olap/date_bloom_filter_test.cpp
@@ -18,6 +18,7 @@
 #include <gen_cpp/segment_v2.pb.h>
 #include <gtest/gtest.h>
 
+#include "common/status.h"
 #include "olap/comparison_predicate.h"
 #include "olap/in_list_predicate.h"
 #include "olap/rowset/beta_rowset.h"
@@ -144,15 +145,21 @@ TEST_F(DateBloomFilterTest, query_index_test) {
     columns[0]->insert_many_fix_len_data(reinterpret_cast<const char*>(&olap_date_value), 1);
     columns[1]->insert_many_fix_len_data(reinterpret_cast<const char*>(&olap_datetime_value), 1);
 
-    EXPECT_TRUE(rowset_writer->add_block(&block).ok());
-    EXPECT_TRUE(rowset_writer->flush().ok());
-    EXPECT_TRUE(rowset_writer->build(rowset).ok());
-    EXPECT_TRUE(_tablet->add_rowset(rowset).ok());
+    Status st;
+    st = rowset_writer->add_block(&block);
+    ASSERT_TRUE(st.ok()) << st; // add_block failed, no valid segment will be written
+    st = rowset_writer->flush();
+    ASSERT_TRUE(st.ok()) << st; // flush failed
+    st = rowset_writer->build(rowset);
+    ASSERT_TRUE(st.ok()) << st; // build rowset failed
+    st = _tablet->add_rowset(rowset);
+    ASSERT_TRUE(st.ok()) << st; // add_rowset failed
 
     segment_v2::SegmentSharedPtr segment;
-    EXPECT_TRUE(((BetaRowset*)rowset.get())->load_segment(0, &segment).ok());
+    st = ((BetaRowset*)rowset.get())->load_segment(0, nullptr, &segment);
+    ASSERT_TRUE(st.ok()) << st; // load_segment failed, segment may be null
     std::shared_ptr<SegmentFooterPB> footer_pb_shared;
-    auto st = segment->_get_segment_footer(footer_pb_shared, nullptr);
+    st = segment->_get_segment_footer(footer_pb_shared, nullptr);
     EXPECT_TRUE(st.ok());
     st = segment->_create_column_meta(*footer_pb_shared);
     EXPECT_TRUE(st.ok());
diff --git a/be/test/olap/delete_bitmap_calculator_test.cpp b/be/test/olap/delete_bitmap_calculator_test.cpp
index 42e1d279f0f..efd0906915e 100644
--- a/be/test/olap/delete_bitmap_calculator_test.cpp
+++ b/be/test/olap/delete_bitmap_calculator_test.cpp
@@ -109,6 +109,19 @@ void build_segment(SegmentWriterOptions opts, TabletSchemaSPtr build_schema, siz
     EXPECT_EQ(nrows, (*res)->num_rows());
 }
 
+// Convenience overload that also returns the on-disk path of the built segment.
+void build_segment(SegmentWriterOptions opts, TabletSchemaSPtr build_schema, size_t segment_id,
+                   TabletSchemaSPtr query_schema, size_t nrows, Generator generator,
+                   std::shared_ptr<Segment>* res, std::string segment_dir, std::string* out_path) {
+    std::string filename = fmt::format("{}_{}.dat", rowset_id.to_string(), segment_id);
+    std::string path = fmt::format("{}/{}", segment_dir, filename);
+    if (out_path != nullptr) {
+        *out_path = path;
+    }
+    build_segment(std::move(opts), std::move(build_schema), segment_id, std::move(query_schema),
+                  nrows, std::move(generator), res, std::move(segment_dir));
+}
+
 TabletSchemaSPtr create_schema(const std::vector<TabletColumnPtr>& columns,
                                KeysType keys_type = UNIQUE_KEYS) {
     TabletSchemaSPtr res = std::make_shared<TabletSchema>();
diff --git a/be/test/olap/rowset/segment_v2/column_meta_accessor_test.cpp b/be/test/olap/rowset/segment_v2/column_meta_accessor_test.cpp
new file mode 100644
index 00000000000..b8a8db1a2ff
--- /dev/null
+++ b/be/test/olap/rowset/segment_v2/column_meta_accessor_test.cpp
@@ -0,0 +1,593 @@
+// 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 "olap/rowset/segment_v2/column_meta_accessor.h"
+
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "io/fs/local_file_system.h"
+#include "olap/rowset/segment_v2/segment.h"
+#include "util/coding.h"
+#include "util/crc32c.h"
+
+using namespace doris;
+using namespace doris::segment_v2;
+
+namespace {
+
+constexpr std::string_view kTestDir = "./ut_dir/column_meta_accessor_test";
+
+// Helper to create a clean test directory
+std::string make_test_file_path(const std::string& file_name) {
+    auto fs = io::global_local_filesystem();
+    static_cast<void>(fs->delete_directory(kTestDir));
+    CHECK(fs->create_directory(kTestDir).ok());
+    return std::string(kTestDir) + "/" + file_name;
+}
+
+// Helper to write segment footer trailer (footer + metadata)
+Status append_footer_trailer(io::FileWriter* fw, SegmentFooterPB* footer) {
+    std::string footer_buf;
+    if (!footer->SerializeToString(&footer_buf)) {
+        return Status::InternalError("failed to serialize SegmentFooterPB");
+    }
+    faststring fixed_buf;
+    put_fixed32_le(&fixed_buf, static_cast<uint32_t>(footer_buf.size()));
+    uint32_t checksum = crc32c::Value(footer_buf.data(), footer_buf.size());
+    put_fixed32_le(&fixed_buf, checksum);
+    fixed_buf.append("D0R1", 4);
+    std::vector<Slice> slices {Slice(footer_buf), Slice(fixed_buf)};
+    RETURN_IF_ERROR(fw->appendv(slices.data(), slices.size()));
+    return Status::OK();
+}
+
+// Helper to read footer from file
+Status read_footer_from_file(const io::FileReaderSPtr& fr, SegmentFooterPB* footer) {
+    const auto file_size = fr->size();
+    if (file_size < 12) {
+        return Status::Corruption("file too small");
+    }
+    uint8_t fixed_buf[12];
+    size_t bytes_read = 0;
+    RETURN_IF_ERROR(fr->read_at(file_size - 12, Slice(fixed_buf, sizeof(fixed_buf)), &bytes_read));
+    if (bytes_read != sizeof(fixed_buf)) {
+        return Status::Corruption("short read footer trailer");
+    }
+    const uint32_t footer_length = decode_fixed32_le(fixed_buf);
+    if (file_size < 12 + footer_length) {
+        return Status::Corruption("bad footer length");
+    }
+    std::string footer_buf;
+    footer_buf.resize(footer_length);
+    bytes_read = 0;
+    RETURN_IF_ERROR(fr->read_at(file_size - 12 - footer_length,
+                                Slice(footer_buf.data(), footer_buf.size()), &bytes_read));
+    if (bytes_read != footer_length) {
+        return Status::Corruption("short read footer");
+    }
+    if (!footer->ParseFromArray(footer_buf.data(), static_cast<int>(footer_buf.size()))) {
+        return Status::Corruption("failed to parse footer");
+    }
+    return Status::OK();
+}
+
+} // namespace
+
+// Test V2 (inline) mode with valid column metadata
+TEST(ColumnMetaAccessorTest, V2_BasicInlineColumns) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV2);
+
+    // Add 3 columns with different types
+    auto* col0 = footer.add_columns();
+    col0->set_unique_id(10);
+    col0->set_column_id(0);
+    col0->set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+
+    auto* col1 = footer.add_columns();
+    col1->set_unique_id(20);
+    col1->set_column_id(1);
+    col1->set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+
+    auto* col2 = footer.add_columns();
+    col2->set_unique_id(30);
+    col2->set_column_id(2);
+    col2->set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_BIGINT));
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+    // Test get_column_meta_by_column_ordinal_id
+    ColumnMetaPB meta;
+    ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, 0, &meta).ok());
+    EXPECT_EQ(meta.unique_id(), 10);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+
+    ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, 1, &meta).ok());
+    EXPECT_EQ(meta.unique_id(), 20);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+
+    ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, 2, &meta).ok());
+    EXPECT_EQ(meta.unique_id(), 30);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_BIGINT));
+
+    // Test has_column_uid for existing and non-existing uids
+    EXPECT_TRUE(accessor.has_column_uid(10));
+    EXPECT_TRUE(accessor.has_column_uid(20));
+    EXPECT_TRUE(accessor.has_column_uid(30));
+    EXPECT_FALSE(accessor.has_column_uid(999));
+
+    // Test get_column_meta_by_uid
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 10, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 0);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 20, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 1);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 30, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 2);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_BIGINT));
+
+    // Test traverse_metas
+    std::vector<int32_t> traversed_uids;
+    ASSERT_TRUE(accessor.traverse_metas(footer,
+                                        [&](const ColumnMetaPB& m) {
+                                            traversed_uids.push_back(m.unique_id());
+                                        })
+                        .ok());
+    EXPECT_EQ(traversed_uids.size(), 3);
+    EXPECT_EQ(traversed_uids[0], 10);
+    EXPECT_EQ(traversed_uids[1], 20);
+    EXPECT_EQ(traversed_uids[2], 30);
+}
+
+// Test V2 mode with variant subcolumns (unique_id == -1 should be skipped)
+TEST(ColumnMetaAccessorTest, V2_SkipVariantSubcolumns) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV2);
+
+    // Regular column
+    auto* col0 = footer.add_columns();
+    col0->set_unique_id(10);
+    col0->set_column_id(0);
+
+    // Variant subcolumn (should be skipped in uid->colid map)
+    auto* col1 = footer.add_columns();
+    col1->set_unique_id(-1);
+    col1->set_column_id(1);
+
+    // Regular column
+    auto* col2 = footer.add_columns();
+    col2->set_unique_id(30);
+    col2->set_column_id(2);
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+    // Only regular columns should be accessible by uid; variant subcolumns (uid == -1) are skipped.
+    ColumnMetaPB meta;
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 10, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 0);
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 30, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 2);
+    Status st = accessor.get_column_meta_by_uid(footer, -1, &meta);
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::NOT_FOUND>());
+
+    // has_column_uid should also reflect the same semantics
+    EXPECT_TRUE(accessor.has_column_uid(10));
+    EXPECT_TRUE(accessor.has_column_uid(30));
+    EXPECT_FALSE(accessor.has_column_uid(-1));
+}
+
+// Test V2 mode error cases
+TEST(ColumnMetaAccessorTest, V2_ErrorCases) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV2);
+
+    // Add one column
+    auto* col0 = footer.add_columns();
+    col0->set_unique_id(10);
+    col0->set_column_id(0);
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+    // Test get_column_meta_by_column_ordinal_id with invalid column id
+    ColumnMetaPB meta;
+    Status st = accessor.get_column_meta_by_column_ordinal_id(footer, 999, &meta);
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+
+    // Test empty footer
+    SegmentFooterPB empty_footer;
+    empty_footer.set_version(kSegmentFooterVersionV2);
+    ColumnMetaAccessor empty_accessor;
+    ASSERT_TRUE(empty_accessor.init(empty_footer, nullptr).ok());
+
+    st = empty_accessor.get_column_meta_by_column_ordinal_id(empty_footer, 0, &meta);
+    EXPECT_FALSE(st.ok());
+
+    st = empty_accessor.traverse_metas(empty_footer, [](const ColumnMetaPB&) {});
+    EXPECT_FALSE(st.ok());
+}
+
+// Test V3 (external) mode with valid external meta region
+TEST(ColumnMetaAccessorTest, V3_ExternalMetaRegion) {
+    // 1. Create a V3 footer with external meta layout
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+
+    // Prepare 2 columns
+    std::vector<ColumnMetaPB> columns;
+    ColumnMetaPB col0;
+    col0.set_unique_id(100);
+    col0.set_column_id(0);
+    col0.set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+    col0.set_length(4);
+    columns.push_back(col0);
+
+    ColumnMetaPB col1;
+    col1.set_unique_id(200);
+    col1.set_column_id(1);
+    col1.set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+    col1.set_length(16);
+    columns.push_back(col1);
+
+    // 2. Write to file with external meta region
+    std::string file_path = make_test_file_path("v3_external_meta.bin");
+    auto fs = io::global_local_filesystem();
+    io::FileWriterPtr fw;
+    ASSERT_TRUE(fs->create_file(file_path, &fw).ok());
+
+    // Write some dummy data before meta region
+    std::string dummy_data(128, 'X');
+    ASSERT_TRUE(fw->append(Slice(dummy_data)).ok());
+
+    uint64_t meta_region_start = fw->bytes_appended();
+    footer.set_col_meta_region_start(meta_region_start);
+
+    // Write each column meta and record entry
+    for (const auto& col : columns) {
+        std::string meta_bytes;
+        ASSERT_TRUE(col.SerializeToString(&meta_bytes));
+        ASSERT_TRUE(fw->append(Slice(meta_bytes)).ok());
+
+        auto* entry = footer.add_column_meta_entries();
+        entry->set_unique_id(col.unique_id());
+        entry->set_length(static_cast<uint32_t>(meta_bytes.size()));
+    }
+
+    // Write footer trailer
+    ASSERT_TRUE(append_footer_trailer(fw.get(), &footer).ok());
+    ASSERT_TRUE(fw->close().ok());
+
+    // 3. Read back and test accessor
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    ASSERT_TRUE(fs->open_file(file_path, &fr, &opts).ok());
+
+    SegmentFooterPB loaded_footer;
+    ASSERT_TRUE(read_footer_from_file(fr, &loaded_footer).ok());
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(loaded_footer, fr).ok());
+
+    // Test get_column_meta_by_column_ordinal_id
+    ColumnMetaPB meta;
+    ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(loaded_footer, 0, &meta).ok());
+    EXPECT_EQ(meta.unique_id(), 100);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+
+    ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(loaded_footer, 1, &meta).ok());
+    EXPECT_EQ(meta.unique_id(), 200);
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+
+    // Test get_column_meta_by_uid
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(loaded_footer, 100, &meta).ok());
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(loaded_footer, 200, &meta).ok());
+    EXPECT_EQ(meta.type(), static_cast<int>(FieldType::OLAP_FIELD_TYPE_STRING));
+
+    // Test traverse_metas
+    std::vector<int32_t> traversed_uids;
+    ASSERT_TRUE(accessor.traverse_metas(loaded_footer,
+                                        [&](const ColumnMetaPB& m) {
+                                            traversed_uids.push_back(m.unique_id());
+                                        })
+                        .ok());
+    EXPECT_EQ(traversed_uids.size(), 2);
+    EXPECT_EQ(traversed_uids[0], 100);
+    EXPECT_EQ(traversed_uids[1], 200);
+}
+
+// Test V3 mode error cases
+TEST(ColumnMetaAccessorTest, V3_ErrorCases_InvalidColumnId) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+
+    // Create minimal valid external meta
+    std::string file_path = make_test_file_path("v3_error_test.bin");
+    auto fs = io::global_local_filesystem();
+    io::FileWriterPtr fw;
+    ASSERT_TRUE(fs->create_file(file_path, &fw).ok());
+
+    // Write some padding before meta region so that region_start is non-zero.
+    // ExternalColMetaUtil::parse_external_meta_pointers() treats region_start == 0
+    // as invalid layout.
+    std::string dummy_data(16, 'X');
+    ASSERT_TRUE(fw->append(Slice(dummy_data)).ok());
+    uint64_t meta_region_start = fw->bytes_appended();
+    footer.set_col_meta_region_start(meta_region_start);
+
+    ColumnMetaPB col0;
+    col0.set_unique_id(10);
+    col0.set_column_id(0);
+    std::string meta_bytes;
+    ASSERT_TRUE(col0.SerializeToString(&meta_bytes));
+    ASSERT_TRUE(fw->append(Slice(meta_bytes)).ok());
+
+    auto* entry = footer.add_column_meta_entries();
+    entry->set_unique_id(10);
+    entry->set_length(static_cast<uint32_t>(meta_bytes.size()));
+
+    ASSERT_TRUE(append_footer_trailer(fw.get(), &footer).ok());
+    ASSERT_TRUE(fw->close().ok());
+
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    ASSERT_TRUE(fs->open_file(file_path, &fr, &opts).ok());
+
+    SegmentFooterPB loaded_footer;
+    ASSERT_TRUE(read_footer_from_file(fr, &loaded_footer).ok());
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(loaded_footer, fr).ok());
+
+    // Test invalid column_id
+    ColumnMetaPB meta;
+    Status st = accessor.get_column_meta_by_column_ordinal_id(loaded_footer, 999, &meta);
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+}
+
+// Test V3 mode with empty external meta region
+TEST(ColumnMetaAccessorTest, V3_EmptyExternalMetaRegion) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+    footer.set_col_meta_region_start(100);
+    // No column_meta_entries added (empty)
+
+    ColumnMetaAccessor accessor;
+    // For an empty external meta region (num_columns == 0), the current
+    // implementation treats this as a corrupted layout and init() fails.
+    Status st = accessor.init(footer, nullptr);
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+}
+
+// Test V3 mode with mismatched entries count
+TEST(ColumnMetaAccessorTest, V3_MismatchedEntriesCount) {
+    std::string file_path = make_test_file_path("v3_mismatch.bin");
+    auto fs = io::global_local_filesystem();
+    io::FileWriterPtr fw;
+    ASSERT_TRUE(fs->create_file(file_path, &fw).ok());
+
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+
+    // Write some padding before meta region so that region_start is non-zero.
+    std::string dummy_data(16, 'X');
+    ASSERT_TRUE(fw->append(Slice(dummy_data)).ok());
+    uint64_t meta_region_start = fw->bytes_appended();
+    footer.set_col_meta_region_start(meta_region_start);
+
+    // Write 2 column metas
+    for (int i = 0; i < 2; ++i) {
+        ColumnMetaPB col;
+        col.set_unique_id(10 + i);
+        col.set_column_id(i);
+        std::string meta_bytes;
+        ASSERT_TRUE(col.SerializeToString(&meta_bytes));
+        ASSERT_TRUE(fw->append(Slice(meta_bytes)).ok());
+
+        auto* entry = footer.add_column_meta_entries();
+        entry->set_unique_id(10 + i);
+        entry->set_length(static_cast<uint32_t>(meta_bytes.size()));
+    }
+
+    ASSERT_TRUE(append_footer_trailer(fw.get(), &footer).ok());
+    ASSERT_TRUE(fw->close().ok());
+
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    ASSERT_TRUE(fs->open_file(file_path, &fr, &opts).ok());
+
+    SegmentFooterPB loaded_footer;
+    ASSERT_TRUE(read_footer_from_file(fr, &loaded_footer).ok());
+
+    ColumnMetaAccessor accessor;
+    // Initialize accessor with the original footer (entries count matches num_columns).
+    ASSERT_TRUE(accessor.init(loaded_footer, fr).ok());
+
+    // Manually corrupt the footer by adding extra entry after init so that
+    // footer.column_meta_entries_size() != ptrs.num_columns, which should be
+    // detected by traverse_metas.
+    loaded_footer.add_column_meta_entries()->set_unique_id(999);
+
+    // traverse_metas should detect mismatch
+    Status st = accessor.traverse_metas(loaded_footer, [](const ColumnMetaPB&) {});
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+}
+
+// Test V3 mode with corrupted protobuf data
+TEST(ColumnMetaAccessorTest, V3_CorruptedProtobufData) {
+    std::string file_path = make_test_file_path("v3_corrupted.bin");
+    auto fs = io::global_local_filesystem();
+    io::FileWriterPtr fw;
+    ASSERT_TRUE(fs->create_file(file_path, &fw).ok());
+
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+
+    // Write some padding before meta region so that region_start is non-zero.
+    std::string dummy_data(16, 'X');
+    ASSERT_TRUE(fw->append(Slice(dummy_data)).ok());
+    uint64_t meta_region_start = fw->bytes_appended();
+    footer.set_col_meta_region_start(meta_region_start);
+
+    // Write garbage data instead of valid protobuf
+    std::string garbage(100, 'Z');
+    ASSERT_TRUE(fw->append(Slice(garbage)).ok());
+
+    auto* entry = footer.add_column_meta_entries();
+    entry->set_unique_id(10);
+    entry->set_length(100);
+
+    ASSERT_TRUE(append_footer_trailer(fw.get(), &footer).ok());
+    ASSERT_TRUE(fw->close().ok());
+
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    ASSERT_TRUE(fs->open_file(file_path, &fr, &opts).ok());
+
+    SegmentFooterPB loaded_footer;
+    ASSERT_TRUE(read_footer_from_file(fr, &loaded_footer).ok());
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(loaded_footer, fr).ok());
+
+    // traverse_metas should fail to parse corrupted data
+    Status st = accessor.traverse_metas(loaded_footer, [](const ColumnMetaPB&) {});
+    EXPECT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+}
+
+// Test version detection and automatic V2/V3 selection
+TEST(ColumnMetaAccessorTest, VersionDetection) {
+    // V1 footer should use V2 accessor (inline mode)
+    {
+        SegmentFooterPB footer;
+        footer.set_version(1);
+        auto* col = footer.add_columns();
+        col->set_unique_id(10);
+        col->set_column_id(0);
+
+        ColumnMetaAccessor accessor;
+        ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+        ColumnMetaPB meta;
+        ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, 0, &meta).ok());
+        EXPECT_EQ(meta.unique_id(), 10);
+    }
+
+    // V2 footer should use V2 accessor
+    {
+        SegmentFooterPB footer;
+        footer.set_version(kSegmentFooterVersionV2);
+        auto* col = footer.add_columns();
+        col->set_unique_id(20);
+        col->set_column_id(0);
+
+        ColumnMetaAccessor accessor;
+        ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+        ColumnMetaPB meta;
+        ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, 0, &meta).ok());
+        EXPECT_EQ(meta.unique_id(), 20);
+    }
+
+    // V3 footer without valid external meta is treated as corrupted layout and
+    // init() should fail (no automatic fallback to V2 inline mode).
+    {
+        SegmentFooterPB footer;
+        footer.set_version(kSegmentFooterVersionV3_ExtColMeta);
+        auto* col = footer.add_columns();
+        col->set_unique_id(30);
+        col->set_column_id(0);
+
+        ColumnMetaAccessor accessor;
+        Status st = accessor.init(footer, nullptr);
+        EXPECT_FALSE(st.ok());
+        EXPECT_TRUE(st.is<ErrorCode::CORRUPTION>());
+    }
+}
+
+// Test large number of columns
+TEST(ColumnMetaAccessorTest, LargeNumberOfColumns) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV2);
+
+    const int num_columns = 1000;
+    for (int i = 0; i < num_columns; ++i) {
+        auto* col = footer.add_columns();
+        col->set_unique_id(i);
+        col->set_column_id(i);
+        col->set_type(static_cast<int>(FieldType::OLAP_FIELD_TYPE_INT));
+    }
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+    // Spot check mapping via get_column_meta_by_uid
+    ColumnMetaPB meta;
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 0, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 0);
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 500, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 500);
+    ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, 999, &meta).ok());
+    EXPECT_EQ(meta.column_id(), 999);
+
+    // Test traverse_metas counts correctly
+    int count = 0;
+    ASSERT_TRUE(accessor.traverse_metas(footer, [&](const ColumnMetaPB&) { count++; }).ok());
+    EXPECT_EQ(count, num_columns);
+}
+
+// Test concurrent access (thread safety not guaranteed by ColumnMetaAccessor itself,
+// but test that multiple sequential calls work correctly)
+TEST(ColumnMetaAccessorTest, MultipleSequentialAccesses) {
+    SegmentFooterPB footer;
+    footer.set_version(kSegmentFooterVersionV2);
+
+    for (int i = 0; i < 10; ++i) {
+        auto* col = footer.add_columns();
+        col->set_unique_id(i);
+        col->set_column_id(i);
+    }
+
+    ColumnMetaAccessor accessor;
+    ASSERT_TRUE(accessor.init(footer, nullptr).ok());
+
+    // Multiple calls should give consistent results
+    for (int round = 0; round < 100; ++round) {
+        for (int i = 0; i < 10; ++i) {
+            ColumnMetaPB meta;
+            ASSERT_TRUE(accessor.get_column_meta_by_column_ordinal_id(footer, i, &meta).ok());
+            EXPECT_EQ(meta.unique_id(), i);
+            // ensure uid lookup also works repeatedly
+            ASSERT_TRUE(accessor.get_column_meta_by_uid(footer, i, &meta).ok());
+        }
+    }
+}
diff --git a/be/test/olap/rowset/segment_v2/column_reader_cache_test.cpp b/be/test/olap/rowset/segment_v2/column_reader_cache_test.cpp
index 814877ad9b1..6a2448d0468 100644
--- a/be/test/olap/rowset/segment_v2/column_reader_cache_test.cpp
+++ b/be/test/olap/rowset/segment_v2/column_reader_cache_test.cpp
@@ -24,12 +24,11 @@
 #include <thread>
 #include <vector>
 
-#include "agent/be_exec_version_manager.h"
 #include "common/config.h"
-#include "gen_cpp/olap_file.pb.h"
 #include "gen_cpp/segment_v2.pb.h"
 #include "io/fs/file_reader.h"
 #include "mock/mock_segment.h"
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/column_reader.h"
 #include "olap/rowset/segment_v2/segment.h"
 #include "olap/rowset/segment_v2/variant/variant_column_reader.h"
@@ -65,11 +64,22 @@ protected:
         // Create mock segment
         _mock_segment = std::make_unique<StrictMock<MockSegment>>();
 
-        // Create cache
-        _cache = std::make_unique<ColumnReaderCache>(_mock_segment.get());
-
-        // Set up basic mock expectations
+        // Set up basic mock expectations before constructing the cache, because
+        // ColumnReaderCache's ctor reads _mock_segment->num_rows().
         setup_basic_mocks();
+
+        // Create cache bound to mock segment context.
+        // ColumnMetaAccessor will be initialized later in setup_segment_footer()
+        // once the test-specific footer has been constructed.
+        io::FileReaderSPtr file_reader; // nullptr is fine for inline-only tests
+        auto footer_cb = [this](std::shared_ptr<SegmentFooterPB>& footer_pb_shared,
+                                OlapReaderStatistics* stats) -> Status {
+            // Delegate to MockSegment::_get_segment_footer
+            return _mock_segment->_get_segment_footer(footer_pb_shared, stats);
+        };
+        _cache = std::make_unique<ColumnReaderCache>(&_accessor, _mock_segment->tablet_schema(),
+                                                     file_reader, _mock_segment->num_rows(),
+                                                     footer_cb);
     }
 
     void TearDown() override {
@@ -108,9 +118,13 @@ protected:
             *footer->add_columns() = col;
         }
         _mock_segment->set_footer(footer);
+        // Initialize ColumnMetaAccessor with the current footer; external meta is not
+        // used in these tests so we can pass a null file reader.
+        CHECK(_accessor.init(*footer, nullptr).ok());
     }
 
     std::unique_ptr<StrictMock<MockSegment>> _mock_segment;
+    ColumnMetaAccessor _accessor;
     std::unique_ptr<ColumnReaderCache> _cache;
     OlapReaderStatistics _stats;
 };
@@ -354,17 +368,17 @@ TEST_F(ColumnReaderCacheTest, DifferentColumnTypes) {
             FieldType::OLAP_FIELD_TYPE_DOUBLE, FieldType::OLAP_FIELD_TYPE_BOOL};
 
     for (size_t i = 0; i < types.size(); ++i) {
-        setup_column_uid_mapping(i + 1, 0);
+        setup_column_uid_mapping(static_cast<int32_t>(i + 1), 0);
 
         ColumnMetaPB col_meta;
         col_meta.set_type(static_cast<int32_t>(types[i]));
-        col_meta.set_unique_id(i + 1);
+        col_meta.set_unique_id(static_cast<int32_t>(i + 1));
         col_meta.set_encoding(EncodingTypePB::DEFAULT_ENCODING);
         col_meta.mutable_indexes()->Add()->set_type(ORDINAL_INDEX);
         setup_segment_footer({col_meta});
 
         std::shared_ptr<ColumnReader> reader;
-        Status status = _cache->get_column_reader(i + 1, &reader, &_stats);
+        Status status = _cache->get_column_reader(static_cast<int32_t>(i + 1), &reader, &_stats);
         EXPECT_TRUE(status.ok());
         EXPECT_NE(reader, nullptr);
     }
@@ -396,13 +410,13 @@ TEST_F(ColumnReaderCacheTest, NodeHintParameter) {
     vectorized::PathInData path("hint_field");
     std::shared_ptr<ColumnReader> reader;
     Status status = _cache->get_path_column_reader(1, path, &reader, &_stats, &node_hint);
-    EXPECT_TRUE(status.ok());
-    EXPECT_NE(reader, nullptr);
+    EXPECT_TRUE(status.is<ErrorCode::NOT_FOUND>());
+    EXPECT_EQ(reader, nullptr);
 
     // Test cache hit
     status = _cache->get_path_column_reader(1, path, &reader, &_stats, &node_hint);
-    EXPECT_TRUE(status.ok());
-    EXPECT_NE(reader, nullptr);
+    EXPECT_TRUE(status.is<ErrorCode::NOT_FOUND>());
+    EXPECT_EQ(reader, nullptr);
 }
 
 // Test cache performance under load
diff --git a/be/test/olap/rowset/segment_v2/external_col_meta_util_test.cpp b/be/test/olap/rowset/segment_v2/external_col_meta_util_test.cpp
new file mode 100644
index 00000000000..1453a18f0f7
--- /dev/null
+++ b/be/test/olap/rowset/segment_v2/external_col_meta_util_test.cpp
@@ -0,0 +1,517 @@
+// 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 "olap/rowset/segment_v2/external_col_meta_util.h"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "io/fs/local_file_system.h"
+#include "olap/rowset/segment_v2/segment.h"
+#include "olap/rowset/segment_v2/segment_writer.h"
+#include "olap/rowset/segment_v2/variant/variant_ext_meta_writer.h"
+#include "olap/rowset/segment_v2/variant/variant_external_meta_reader.h"
+#include "olap/tablet_schema_helper.h"
+#include "olap/types.h"
+#include "util/coding.h"
+#include "util/crc32c.h"
+#include "vec/json/path_in_data.h"
+
+using namespace doris;
+using namespace doris::segment_v2;
+using namespace doris::vectorized;
+
+namespace {
+
+constexpr std::string_view kTestDir = "./ut_dir/external_col_meta_util_test";
+
+// Helper to create a clean local test directory and return absolute path + filename.
+std::string make_test_file_path(const std::string& file_name) {
+    auto fs = io::global_local_filesystem();
+    // english only in comments
+    static_cast<void>(fs->delete_directory(kTestDir));
+    CHECK(fs->create_directory(kTestDir).ok());
+    return std::string(kTestDir) + "/" + file_name;
+}
+
+// Helper to write a simple "segment-like" footer trailer. Current tests do not call
+// Segment::_parse_footer, so the exact magic value is not important as long as the
+// layout is self‑consistent.
+Status append_footer_trailer(io::FileWriter* fw, SegmentFooterPB* footer) {
+    std::string footer_buf;
+    if (!footer->SerializeToString(&footer_buf)) {
+        return Status::InternalError("failed to serialize SegmentFooterPB for test");
+    }
+    faststring fixed_buf;
+    // footer size (4 bytes)
+    put_fixed32_le(&fixed_buf, static_cast<uint32_t>(footer_buf.size()));
+    // footer checksum (4 bytes)
+    uint32_t checksum = crc32c::Value(footer_buf.data(), footer_buf.size());
+    put_fixed32_le(&fixed_buf, checksum);
+    // magic number (4 bytes)
+    fixed_buf.append("D0R1", 4);
+    std::vector<Slice> slices {Slice(footer_buf), Slice(fixed_buf)};
+    RETURN_IF_ERROR(fw->appendv(slices.data(), slices.size()));
+    return Status::OK();
+}
+
+// Helper to read SegmentFooterPB back from a segment-like file and validate checksum.
+Status read_footer_from_file(const io::FileReaderSPtr& fr, SegmentFooterPB* footer) {
+    const auto file_size = fr->size();
+    if (file_size < 12) {
+        return Status::Corruption("test segment file too small: {}", file_size);
+    }
+    uint8_t fixed_buf[12];
+    size_t bytes_read = 0;
+    RETURN_IF_ERROR(fr->read_at(file_size - 12, Slice(fixed_buf, sizeof(fixed_buf)), &bytes_read));
+    if (bytes_read != sizeof(fixed_buf)) {
+        return Status::Corruption("short read footer trailer, read={}, expect={}", bytes_read,
+                                  sizeof(fixed_buf));
+    }
+    const uint32_t footer_length = decode_fixed32_le(fixed_buf);
+    if (file_size < 12 + footer_length) {
+        return Status::Corruption("bad footer length in test segment: file_size={}, footer_len={}",
+                                  file_size, footer_length);
+    }
+    std::string footer_buf;
+    footer_buf.resize(footer_length);
+    bytes_read = 0;
+    RETURN_IF_ERROR(fr->read_at(file_size - 12 - footer_length,
+                                Slice(footer_buf.data(), footer_buf.size()), &bytes_read));
+    if (bytes_read != footer_length) {
+        return Status::Corruption("short read footer pb, read={}, expect={}", bytes_read,
+                                  footer_length);
+    }
+    const uint32_t expect_checksum = decode_fixed32_le(fixed_buf + 4);
+    const uint32_t actual_checksum = crc32c::Value(footer_buf.data(), footer_buf.size());
+    if (actual_checksum != expect_checksum) {
+        return Status::Corruption("footer checksum mismatch, actual={}, expect={}", actual_checksum,
+                                  expect_checksum);
+    }
+    if (!footer->ParseFromArray(footer_buf.data(), static_cast<int>(footer_buf.size()))) {
+        return Status::Corruption("failed to parse SegmentFooterPB from test segment");
+    }
+    return Status::OK();
+}
+
+} // namespace
+
+namespace doris {
+
+using Generator = std::function<void(size_t rid, int cid, RowCursorCell& cell)>;
+
+// Helper declarations are defined in tablet_schema_helper.{h,cpp} and
+// delete_bitmap_calculator_test.cpp.
+TabletSchemaSPtr create_schema(const std::vector<TabletColumnPtr>& columns,
+                               KeysType keys_type = UNIQUE_KEYS);
+
+void build_segment(SegmentWriterOptions opts, TabletSchemaSPtr build_schema, size_t segment_id,
+                   TabletSchemaSPtr query_schema, size_t nrows, Generator generator,
+                   std::shared_ptr<Segment>* res, std::string segment_dir);
+
+void build_segment(SegmentWriterOptions opts, TabletSchemaSPtr build_schema, size_t segment_id,
+                   TabletSchemaSPtr query_schema, size_t nrows, Generator generator,
+                   std::shared_ptr<Segment>* res, std::string segment_dir, std::string* out_path);
+
+} // namespace doris
+
+// Basic unit test for ExternalColMetaUtil pointer parsing and uid‑>col_id map.
+TEST(ExternalColMetaUtilTest, PointersAndUidMapValidation) {
+    SegmentFooterPB footer;
+    footer.set_col_meta_region_start(100U);
+    auto* e0 = footer.add_column_meta_entries();
+    e0->set_unique_id(10);
+    e0->set_length(50);
+    auto* e1 = footer.add_column_meta_entries();
+    e1->set_unique_id(20);
+    e1->set_length(60);
+
+    ExternalColMetaUtil::ExternalMetaPointers ptrs;
+    EXPECT_TRUE(ExternalColMetaUtil::parse_external_meta_pointers(footer, &ptrs).ok());
+    EXPECT_EQ(ptrs.region_start, 100U);
+    EXPECT_EQ(ptrs.num_columns, 2U);
+    EXPECT_EQ(ptrs.region_end, 210U);
+
+    std::unordered_map<int32_t, size_t> uid2colid;
+    EXPECT_TRUE(ExternalColMetaUtil::parse_uid_to_colid_map(footer, ptrs, &uid2colid).ok());
+    EXPECT_EQ(uid2colid.size(), 2U);
+    EXPECT_EQ(uid2colid[10], 0U);
+    EXPECT_EQ(uid2colid[20], 1U);
+
+    // Negative cases for pointers
+    footer.clear_column_meta_entries();
+    EXPECT_FALSE(ExternalColMetaUtil::parse_external_meta_pointers(footer, &ptrs).ok());
+    footer.add_column_meta_entries()->set_unique_id(10);
+    footer.add_column_meta_entries()->set_unique_id(20);
+    footer.mutable_column_meta_entries(0)->set_length(50);
+    footer.mutable_column_meta_entries(1)->set_length(60);
+    // zero length is illegal
+    footer.mutable_column_meta_entries(1)->set_length(0);
+    EXPECT_FALSE(ExternalColMetaUtil::parse_external_meta_pointers(footer, &ptrs).ok());
+    footer.mutable_column_meta_entries(1)->set_length(60);
+
+    // Slice validation
+    ExternalColMetaUtil::ExternalMetaPointers p;
+    p.region_start = 100;
+    p.region_end = 200;
+    EXPECT_TRUE(ExternalColMetaUtil::is_valid_meta_slice(100, 10, p));
+    EXPECT_FALSE(ExternalColMetaUtil::is_valid_meta_slice(100, 0, p));  // size == 0
+    EXPECT_FALSE(ExternalColMetaUtil::is_valid_meta_slice(50, 10, p));  // pos < region_start
+    EXPECT_FALSE(ExternalColMetaUtil::is_valid_meta_slice(195, 10, p)); // pos + size > region_end
+    // overflow guard: deliberately craft overflow
+    EXPECT_FALSE(ExternalColMetaUtil::is_valid_meta_slice(std::numeric_limits<uint64_t>::max() - 1,
+                                                          4, p));
+}
+
+// End‑to‑end test of VariantExtMetaWriter + VariantExternalMetaReader on a standalone file.
+// This does NOT involve full tablet/rowset plumbing, but exercises:
+//  - externalize_from_footer(): classification of root/non‑sparse/sparse columns
+//  - flush_to_footer(): writing IndexedColumn metas into footer.file_meta_datas
+//  - init_from_footer()/lookup_meta_by_path(): reading back subcolumn ColumnMetaPB by path
+//  - legacy unsuffixed keys: "variant_meta_keys" / "variant_meta_values"
+TEST(ExternalColMetaUtilTest, VariantExtMetaWriterAndReaderInteropAndCompatibility) {
+    // 1. Prepare a simple footer with:
+    //    - one non‑variant column (kept as is)
+    //    - one variant root (kept, path "v1")
+    //    - one non‑sparse subcolumn "v1.key0" (externalized)
+    //    - one sparse subcolumn "__DORIS_VARIANT_SPARSE__.key_sparse" (embedded into root)
+    SegmentFooterPB footer;
+    // non‑variant top level column
+    {
+        auto* col = footer.add_columns();
+        col->set_unique_id(0);
+        // use int(FieldType) to match production code
+        col->set_type(int(FieldType::OLAP_FIELD_TYPE_INT)); // simple scalar
+    }
+
+    const int32_t root_uid = 1;
+    // variant root column with path "v1"
+    ColumnMetaPB* root = footer.add_columns();
+    root->set_unique_id(root_uid);
+    root->set_type(int(FieldType::OLAP_FIELD_TYPE_VARIANT));
+    {
+        PathInData full_root_path("v1");
+        full_root_path.to_protobuf(root->mutable_column_path_info(),
+                                   /*parent_col_unique_id=*/root_uid);
+    }
+
+    // non‑sparse subcolumn: v1.key0
+    ColumnMetaPB* non_sparse = footer.add_columns();
+    non_sparse->set_unique_id(2);
+    non_sparse->set_type(int(FieldType::OLAP_FIELD_TYPE_INT));
+    {
+        PathInData full_path("v1.key0");
+        full_path.to_protobuf(non_sparse->mutable_column_path_info(),
+                              /*parent_col_unique_id=*/root_uid);
+        non_sparse->set_none_null_size(123);
+    }
+
+    // sparse subcolumn: v1.__DORIS_VARIANT_SPARSE__
+    ColumnMetaPB* sparse = footer.add_columns();
+    sparse->set_unique_id(3);
+    sparse->set_type(int(FieldType::OLAP_FIELD_TYPE_VARIANT));
+    {
+        // For sparse column, the relative path (after pop_front) should contain
+        // "__DORIS_VARIANT_SPARSE__" so that VariantExtMetaWriter treats it as sparse.
+        PathInData full_path("v1.__DORIS_VARIANT_SPARSE__");
+        full_path.to_protobuf(sparse->mutable_column_path_info(),
+                              /*parent_col_unique_id=*/root_uid);
+        // carry sparse statistics in VariantStatisticsPB
+        auto* stats = sparse->mutable_variant_statistics();
+        (*stats->mutable_sparse_column_non_null_size())["key_sparse"] = 10;
+    }
+
+    // 2. Externalize non‑sparse subcolumns into a dedicated file via VariantExtMetaWriter.
+    std::string file_path = make_test_file_path("variant_ext_meta.bin");
+    io::FileWriterPtr fw;
+    auto fs = io::global_local_filesystem();
+    Status st = fs->create_file(file_path, &fw);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    std::vector<ColumnMetaPB> out_metas;
+    {
+        VariantExtMetaWriter ext_writer(fw.get(), CompressionTypePB::LZ4);
+        st = ext_writer.externalize_from_footer(&footer, &out_metas);
+        ASSERT_TRUE(st.ok()) << st.to_json();
+    }
+
+    // Capture region start AFTER Variant Index is written
+    uint64_t meta_region_start = fw->bytes_appended();
+
+    // Manually write the Meta Region (simulating ExternalColMetaUtil::write_external_column_meta)
+    footer.clear_column_meta_entries();
+    footer.set_col_meta_region_start(meta_region_start);
+
+    for (const auto& meta : out_metas) {
+        std::string meta_bytes;
+        meta.SerializeToString(&meta_bytes);
+        ASSERT_TRUE(fw->append(Slice(meta_bytes)).ok());
+
+        auto* entry = footer.add_column_meta_entries();
+        entry->set_unique_id(meta.unique_id());
+        entry->set_length(meta_bytes.size());
+    }
+
+    // After externalize_from_footer:
+    //  - footer.columns() should contain:
+    //      * non‑variant column uid=0
+    //      * root variant column uid=1, whose children_columns includes the sparse meta
+    //  - "v1.key0" should be removed from footer.columns() and only exist in external meta.
+    ASSERT_EQ(footer.columns_size(), 2);
+    const ColumnMetaPB& kept0 = footer.columns(0);
+    const ColumnMetaPB& kept1 = footer.columns(1);
+    EXPECT_EQ(kept0.unique_id(), 0);
+    EXPECT_EQ(kept1.unique_id(), root_uid);
+    // root should have embedded sparse children
+    ASSERT_EQ(kept1.children_columns_size(), 1);
+    const ColumnMetaPB& embedded_sparse = kept1.children_columns(0);
+    EXPECT_TRUE(embedded_sparse.has_column_path_info());
+    PathInData embedded_path;
+    embedded_path.from_protobuf(embedded_sparse.column_path_info());
+    // full path should keep root prefix
+    EXPECT_EQ(embedded_path.get_path(), "v1.__DORIS_VARIANT_SPARSE__");
+    ASSERT_TRUE(embedded_sparse.has_variant_statistics());
+    EXPECT_EQ(embedded_sparse.variant_statistics().sparse_column_non_null_size().at("key_sparse"),
+              10);
+
+    // 3. Verify that ext meta keys metas were flushed into footer.file_meta_datas.
+    bool found_keys_meta = false;
+    std::string keys_meta_bytes;
+    for (const auto& m : footer.file_meta_datas()) {
+        if (m.key() == "variant_meta_keys." + std::to_string(root_uid)) {
+            found_keys_meta = true;
+            keys_meta_bytes = m.value();
+        }
+    }
+    ASSERT_TRUE(found_keys_meta);
+    // variant_meta_values should NOT be present
+
+    // 4. Finish the "segment‑like" file by appending footer + trailer, then close.
+    st = append_footer_trailer(fw.get(), &footer);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    ASSERT_TRUE(fw->close().ok());
+
+    // 5. Open a reader on the same file and build VariantExternalMetaReader from the footer.
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    st = fs->open_file(file_path, &fr, &opts);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    VariantExternalMetaReader reader_new;
+    auto footer_sp = std::make_shared<SegmentFooterPB>();
+    footer_sp->CopyFrom(footer);
+    st = reader_new.init_from_footer(footer_sp, fr, root_uid);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    ASSERT_TRUE(reader_new.available());
+
+    // 6. Lookup meta by relative path and verify content.
+    ColumnMetaPB loaded_meta;
+    st = reader_new.lookup_meta_by_path("key0", &loaded_meta);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    ASSERT_TRUE(loaded_meta.has_column_path_info());
+    PathInData loaded_path;
+    loaded_path.from_protobuf(loaded_meta.column_path_info());
+    // full path should still contain root prefix "v1.key0"
+    EXPECT_EQ(loaded_path.get_path(), "v1.key0");
+    EXPECT_EQ(loaded_meta.none_null_size(), 123);
+    EXPECT_EQ(loaded_meta.column_path_info().parrent_column_unique_id(), root_uid);
+
+    // 7. Legacy compatibility: construct a footer with unsuffixed keys:
+    //      "variant_meta_keys"
+    SegmentFooterPB legacy_footer;
+    legacy_footer.CopyFrom(footer);
+    legacy_footer.clear_file_meta_datas();
+    {
+        auto* p1 = legacy_footer.add_file_meta_datas();
+        p1->set_key("variant_meta_keys");
+        p1->set_value(keys_meta_bytes);
+    }
+
+    VariantExternalMetaReader reader_legacy;
+    auto legacy_footer_sp = std::make_shared<SegmentFooterPB>();
+    legacy_footer_sp->CopyFrom(legacy_footer);
+    st = reader_legacy.init_from_footer(legacy_footer_sp, fr, root_uid);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    ASSERT_TRUE(reader_legacy.available());
+
+    ColumnMetaPB loaded_meta_legacy;
+    st = reader_legacy.lookup_meta_by_path("key0", &loaded_meta_legacy);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    PathInData loaded_path_legacy;
+    loaded_path_legacy.from_protobuf(loaded_meta_legacy.column_path_info());
+    EXPECT_EQ(loaded_path_legacy.get_path(), "v1.key0");
+    EXPECT_EQ(loaded_meta_legacy.none_null_size(), 123);
+    EXPECT_EQ(loaded_meta_legacy.column_path_info().parrent_column_unique_id(), root_uid);
+}
+
+// End-to-end test: use ExternalColMetaUtil to write external ColumnMeta region + CMO
+// into a V3 segment-like file, then read footer back and verify:
+//  - footer.version() is V3
+//  - external meta pointers / uid->col_id map are present and valid
+//  - ColumnMetaPB can be read back through CMO by col_id
+TEST(ExternalColMetaUtilTest, WriteAndReadV3SegmentExternalMetaRoundTrip) {
+    // 1. Build a simple footer with two top-level columns so uid->col_id map is deterministic.
+    SegmentFooterPB footer;
+    {
+        auto* col0 = footer.add_columns();
+        col0->set_column_id(0);
+        col0->set_unique_id(10);
+        col0->set_type(int(FieldType::OLAP_FIELD_TYPE_INT));
+        col0->set_length(4);
+        col0->set_encoding(DEFAULT_ENCODING);
+        col0->set_compression(CompressionTypePB::LZ4);
+        col0->set_is_nullable(false);
+
+        auto* col1 = footer.add_columns();
+        col1->set_column_id(1);
+        col1->set_unique_id(20);
+        col1->set_type(int(FieldType::OLAP_FIELD_TYPE_BIGINT));
+        col1->set_length(8);
+        col1->set_encoding(DEFAULT_ENCODING);
+        col1->set_compression(CompressionTypePB::LZ4);
+        col1->set_is_nullable(true);
+    }
+
+    // Stamp V3 version like SegmentWriter::_write_footer does for external meta enabled tablets.
+    footer.set_version(segment_v2::kSegmentFooterVersionV3_ExtColMeta);
+
+    std::string file_path = make_test_file_path("v3_ext_col_meta_segment.bin");
+    auto fs = io::global_local_filesystem();
+    io::FileWriterPtr fw;
+    Status st = fs->create_file(file_path, &fw);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    // Simulate real segment layout: write some dummy data pages before external meta region
+    // so that `col_meta_region_start` is non-zero (as in production segments).
+    {
+        std::string dummy_data(16, 'X');
+        ASSERT_TRUE(fw->append(Slice(dummy_data)).ok());
+    }
+
+    // 2. Write external ColumnMeta region + CMO + uid->col_id mapping.
+    st = ExternalColMetaUtil::write_external_column_meta(
+            fw.get(), &footer, CompressionTypePB::LZ4,
+            [fw_raw = fw.get()](const std::vector<Slice>& slices) {
+                return fw_raw->appendv(slices.data(), slices.size());
+            });
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    // After write_external_column_meta, inline footer.columns() are cleared in V3 mode.
+    ASSERT_EQ(footer.columns_size(), 0);
+
+    // 3. Finish by appending footer + trailer and close writer.
+    st = append_footer_trailer(fw.get(), &footer);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    ASSERT_TRUE(fw->close().ok());
+
+    // 4. Re-open file, parse footer back from on-disk trailer, and verify footer.version() is V3.
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions opts;
+    st = fs->open_file(file_path, &fr, &opts);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    SegmentFooterPB loaded_footer;
+    st = read_footer_from_file(fr, &loaded_footer);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    EXPECT_EQ(loaded_footer.version(), segment_v2::kSegmentFooterVersionV3_ExtColMeta);
+
+    // 5. Parse external meta pointers and uid->col_id mapping from the loaded footer.
+    ExternalColMetaUtil::ExternalMetaPointers ptrs;
+    ASSERT_TRUE(ExternalColMetaUtil::parse_external_meta_pointers(loaded_footer, &ptrs).ok());
+    std::unordered_map<int32_t, size_t> uid2colid;
+    ASSERT_TRUE(ExternalColMetaUtil::parse_uid_to_colid_map(loaded_footer, ptrs, &uid2colid).ok());
+    ASSERT_EQ(uid2colid.size(), 2U);
+    EXPECT_EQ(uid2colid[10], 0U);
+    EXPECT_EQ(uid2colid[20], 1U);
+
+    // 6. Read ColumnMetaPB back from external meta region and verify it matches the
+    //    original unique_ids.
+    ColumnMetaPB meta0;
+    st = ExternalColMetaUtil::read_col_meta(fr, loaded_footer, ptrs, /*col_id=*/0, &meta0);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    EXPECT_EQ(meta0.unique_id(), 10);
+    EXPECT_EQ(meta0.column_id(), 0);
+
+    ColumnMetaPB meta1;
+    st = ExternalColMetaUtil::read_col_meta(fr, loaded_footer, ptrs, /*col_id=*/1, &meta1);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    EXPECT_EQ(meta1.unique_id(), 20);
+    EXPECT_EQ(meta1.column_id(), 1);
+}
+
+// Build a real Segment file with data rows, then verify that Segment::num_rows()
+// matches the written row count and that footer metadata is consistent with schema.
+TEST(ExternalColMetaUtilTest, BuildSegmentAndVerifyDataAndFooterMeta) {
+    // 1. Build a simple UNIQUE_KEYS tablet schema: one key column, one value column.
+    std::vector<TabletColumnPtr> columns;
+    columns.emplace_back(create_int_key(/*id=*/0));
+    columns.emplace_back(create_int_value(/*id=*/1));
+    TabletSchemaSPtr tablet_schema = create_schema(columns, UNIQUE_KEYS);
+    // Enable external ColumnMetaPB so that SegmentWriter produces a V3 footer with
+    // externalized column meta region + column_meta_entries.
+    tablet_schema->set_external_segment_meta_used_default(true);
+
+    // 2. Use existing build_segment helper (SegmentWriter-based) to write a segment file.
+    SegmentWriterOptions opts;
+    opts.enable_unique_key_merge_on_write = true;
+    const size_t nrows = 16;
+
+    auto generator = [](size_t rid, int cid, RowCursorCell& cell) {
+        cell.set_not_null();
+        // deterministic int payload: value = rid * 10 + cid
+        *reinterpret_cast<int*>(cell.mutable_cell_ptr()) = static_cast<int>(rid * 10 + cid);
+    };
+
+    std::shared_ptr<Segment> segment;
+    std::string segment_path;
+    build_segment(opts, tablet_schema,
+                  /*segment_id=*/0, tablet_schema, nrows, generator, &segment,
+                  std::string(kTestDir), &segment_path);
+
+    ASSERT_NE(segment, nullptr);
+
+    // 3. Verify basic segment data property (row count).
+    EXPECT_EQ(segment->num_rows(), nrows);
+
+    // 4. Verify external ColumnMetaPB region is actually written on disk and consistent with schema.
+    auto fs = io::global_local_filesystem();
+    io::FileReaderSPtr fr;
+    io::FileReaderOptions reader_opts;
+    Status st = fs->open_file(segment_path, &fr, &reader_opts);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+
+    SegmentFooterPB loaded_footer;
+    st = read_footer_from_file(fr, &loaded_footer);
+    ASSERT_TRUE(st.ok()) << st.to_json();
+    EXPECT_EQ(loaded_footer.version(), segment_v2::kSegmentFooterVersionV3_ExtColMeta);
+
+    ExternalColMetaUtil::ExternalMetaPointers ptrs;
+    ASSERT_TRUE(ExternalColMetaUtil::parse_external_meta_pointers(loaded_footer, &ptrs).ok());
+    ASSERT_EQ(ptrs.num_columns, tablet_schema->num_columns());
+
+    // Read back per-column meta from external region and check uid/col_id mapping.
+    for (uint32_t cid = 0; cid < ptrs.num_columns; ++cid) {
+        ColumnMetaPB meta;
+        st = ExternalColMetaUtil::read_col_meta(fr, loaded_footer, ptrs, cid, &meta);
+        ASSERT_TRUE(st.ok()) << st.to_json();
+        EXPECT_EQ(meta.unique_id(), tablet_schema->column(cid).unique_id());
+        EXPECT_EQ(meta.column_id(), cid);
+    }
+}
diff --git a/be/test/olap/rowset/segment_v2/mock/mock_segment.h b/be/test/olap/rowset/segment_v2/mock/mock_segment.h
index 7efe16e7586..9f89a084a94 100644
--- a/be/test/olap/rowset/segment_v2/mock/mock_segment.h
+++ b/be/test/olap/rowset/segment_v2/mock/mock_segment.h
@@ -52,16 +52,10 @@ public:
         _tablet_schema->_cols.push_back(std::make_shared<TabletColumn>());
         _tablet_schema->_cols.back()->set_unique_id(col_uid);
         _tablet_schema->_field_id_to_index[col_uid] = footer_ordinal;
-        _column_uid_to_footer_ordinal[col_uid] = footer_ordinal;
     }
 
     void set_footer(std::shared_ptr<SegmentFooterPB> footer) { _footer = footer; }
 
-    // Access to internal data for testing
-    const std::unordered_map<int32_t, size_t>& get_column_uid_mapping() const {
-        return _column_uid_to_footer_ordinal;
-    }
-
     std::shared_ptr<SegmentFooterPB> get_footer() const { return _footer; }
 
     std::shared_ptr<SegmentFooterPB> _footer;
diff --git a/be/test/olap/rowset/segment_v2/variant_column_writer_reader_test.cpp b/be/test/olap/rowset/segment_v2/variant_column_writer_reader_test.cpp
index 117fc0579b5..42c8a9a4bc3 100644
--- a/be/test/olap/rowset/segment_v2/variant_column_writer_reader_test.cpp
+++ b/be/test/olap/rowset/segment_v2/variant_column_writer_reader_test.cpp
@@ -16,6 +16,7 @@
 // under the License.
 
 #include "gtest/gtest.h"
+#include "olap/rowset/segment_v2/column_meta_accessor.h"
 #include "olap/rowset/segment_v2/column_reader.h"
 #include "olap/rowset/segment_v2/column_reader_cache.h"
 #include "olap/rowset/segment_v2/variant/hierarchical_data_iterator.h"
@@ -40,13 +41,16 @@ class MockColumnReaderCache : public segment_v2::ColumnReaderCache {
 public:
     MockColumnReaderCache(const SegmentFooterPB& footer, const io::FileReaderSPtr& file_reader,
                           const std::shared_ptr<TabletSchema>& tablet_schema)
-            : ColumnReaderCache(nullptr),
+            : ColumnReaderCache(nullptr, nullptr, nullptr, 0,
+                                [](std::shared_ptr<SegmentFooterPB>&, OlapReaderStatistics*) {
+                                    return Status::OK();
+                                }),
               _footer(footer),
               _file_reader(file_reader),
               _tablet_schema(tablet_schema) {}
 
     Status get_path_column_reader(
-            uint32_t col_uid, vectorized::PathInData relative_path,
+            int32_t col_uid, vectorized::PathInData relative_path,
             std::shared_ptr<segment_v2::ColumnReader>* column_reader, OlapReaderStatistics* stats,
             const SubcolumnColumnMetaInfo::Node* node_hint = nullptr) override {
         DCHECK(node_hint != nullptr);
@@ -74,6 +78,30 @@ private:
     const std::shared_ptr<TabletSchema>& _tablet_schema;
 };
 
+// Helper to create a root VariantColumnReader using ColumnMetaAccessor, which
+// hides inline vs external column meta layout (V2 vs V3).
+static Status create_variant_root_reader(const SegmentFooterPB& footer,
+                                         const io::FileReaderSPtr& file_reader,
+                                         const TabletSchemaSPtr& tablet_schema,
+                                         std::shared_ptr<segment_v2::ColumnReader>* out) {
+    segment_v2::ColumnMetaAccessor accessor;
+    RETURN_IF_ERROR(accessor.init(footer, file_reader));
+
+    segment_v2::ColumnReaderOptions opts;
+    opts.kept_in_memory = false;
+    opts.be_exec_version = BeExecVersionManager::get_newest_version();
+    opts.tablet_schema = tablet_schema;
+
+    auto variant_reader = std::make_shared<segment_v2::VariantColumnReader>();
+    int32_t root_uid = tablet_schema->column(0).unique_id();
+    // Wrap footer into shared_ptr to match VariantColumnReader::init signature.
+    auto footer_sp = std::make_shared<SegmentFooterPB>(footer);
+    RETURN_IF_ERROR(variant_reader->init(opts, &accessor, footer_sp, root_uid, footer.num_rows(),
+                                         file_reader));
+    *out = std::move(variant_reader);
+    return Status::OK();
+}
+
 class VariantColumnWriterReaderTest : public testing::Test {
 public:
     void SetUp() override {
@@ -183,6 +211,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_normal) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
 
@@ -254,10 +286,9 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_normal) {
     io::FileReaderSPtr file_reader;
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    // create root variant reader using ColumnMetaAccessor (supports inline/external meta)
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     MockColumnReaderCache column_reader_cache(footer, file_reader, _tablet_schema);
@@ -318,7 +349,7 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_normal) {
     {
         auto iter = assert_cast<HierarchicalDataIterator*>(it.get());
         std::shared_ptr<ColumnReader> column_reader1;
-        st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader1);
+        st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader1);
         EXPECT_TRUE(st.ok()) << st.msg();
         EXPECT_ANY_THROW(st = iter->seek_to_first());
         std::cout << "hier:" << iter->get_current_ordinal() << std::endl;
@@ -735,6 +766,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_advanced) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -805,10 +840,8 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_advanced) {
     io::FileReaderSPtr file_reader;
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
@@ -927,6 +960,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_sub_index) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1000,6 +1037,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_nullable) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1094,10 +1135,8 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_nullable) {
     io::FileReaderSPtr file_reader;
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
@@ -1144,6 +1183,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_data_nullable_without_finalize)
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1229,6 +1272,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_bm_with_finalize) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1316,6 +1363,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_bf_with_finalize) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1403,6 +1454,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_zm_with_finalize) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1490,6 +1545,10 @@ TEST_F(VariantColumnWriterReaderTest, test_write_inverted_with_finalize) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1576,6 +1635,10 @@ TEST_F(VariantColumnWriterReaderTest, test_no_sub_in_sparse_column) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10001;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1658,10 +1721,8 @@ TEST_F(VariantColumnWriterReaderTest, test_no_sub_in_sparse_column) {
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
-    ColumnReaderOptions reader_opts;
-    reader_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> reader;
-    st = ColumnReader::create(reader_opts, footer, 0, 1000, file_reader, &reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &reader);
     EXPECT_TRUE(st.ok()) << st.msg();
     auto variant_column_reader = assert_cast<VariantColumnReader*>(reader.get());
     EXPECT_TRUE(variant_column_reader != nullptr);
@@ -1706,6 +1767,10 @@ TEST_F(VariantColumnWriterReaderTest, test_prefix_in_sub_and_sparse) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10001;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
@@ -1800,10 +1865,8 @@ TEST_F(VariantColumnWriterReaderTest, test_prefix_in_sub_and_sparse) {
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
-    ColumnReaderOptions reader_opts;
-    reader_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> reader;
-    st = ColumnReader::create(reader_opts, footer, 0, 1000, file_reader, &reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &reader);
     EXPECT_TRUE(st.ok()) << st.msg();
     auto variant_column_reader = assert_cast<VariantColumnReader*>(reader.get());
     EXPECT_TRUE(variant_column_reader != nullptr);
@@ -1850,6 +1913,10 @@ void test_write_variant_column(StorageEngine* _engine_ref, std::string _absolute
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     EXPECT_TRUE(io::global_local_filesystem()->delete_directory(_absolute_dir).ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(_absolute_dir).ok());
@@ -1956,11 +2023,8 @@ TEST_F(VariantColumnWriterReaderTest, test_nested_subcolumn) {
     io::FileReaderSPtr file_reader;
     Status st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
@@ -2134,13 +2198,9 @@ TEST_F(VariantColumnWriterReaderTest, test_nested_iter) {
     io::FileReaderSPtr file_reader;
     Status st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-
-    MockColumnReaderCache column_reader_cache(footer, file_reader, _tablet_schema);
-
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    MockColumnReaderCache column_reader_cache(footer, file_reader, _tablet_schema);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
@@ -2290,11 +2350,8 @@ TEST_F(VariantColumnWriterReaderTest, test_nested_iter_nullable) {
     io::FileReaderSPtr file_reader;
     Status st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
@@ -2375,6 +2432,10 @@ TEST_F(VariantColumnWriterReaderTest, test_read_with_checksum) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(_tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    _tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     tablet_meta->_tablet_id = 10000;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
 
@@ -2447,10 +2508,8 @@ TEST_F(VariantColumnWriterReaderTest, test_read_with_checksum) {
     io::FileReaderSPtr file_reader;
     st = io::global_local_filesystem()->open_file(file_path, &file_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
-    ColumnReaderOptions read_opts;
-    read_opts.tablet_schema = _tablet_schema;
     std::shared_ptr<ColumnReader> column_reader;
-    st = ColumnReader::create(read_opts, footer, 0, 1000, file_reader, &column_reader);
+    st = create_variant_root_reader(footer, file_reader, _tablet_schema, &column_reader);
     EXPECT_TRUE(st.ok()) << st.msg();
 
     auto* variant_column_reader = assert_cast<VariantColumnReader*>(column_reader.get());
diff --git a/be/test/vec/common/schema_util_rowset_test.cpp b/be/test/vec/common/schema_util_rowset_test.cpp
index 7d2ac9f1912..9b681f706eb 100644
--- a/be/test/vec/common/schema_util_rowset_test.cpp
+++ b/be/test/vec/common/schema_util_rowset_test.cpp
@@ -176,7 +176,8 @@ static void fill_block_with_test_data(vectorized::Block* block, int size) {
 static int64_t inc_id = 1000;
 static RowsetWriterContext rowset_writer_context(const std::unique_ptr<DataDir>& data_dir,
                                                  const TabletSchemaSPtr& schema,
-                                                 const std::string& tablet_path) {
+                                                 const std::string& tablet_path,
+                                                 const TabletSharedPtr& tablet) {
     RowsetWriterContext context;
     RowsetId rowset_id;
     rowset_id.init(inc_id);
@@ -186,6 +187,8 @@ static RowsetWriterContext rowset_writer_context(const std::unique_ptr<DataDir>&
     context.rowset_state = VISIBLE;
     context.tablet_schema = schema;
     context.tablet_path = tablet_path;
+    context.tablet_id = tablet->tablet_id();
+    context.tablet = tablet;
     context.version = Version(inc_id, inc_id);
     context.max_rows_per_segment = 200;
     inc_id++;
@@ -220,7 +223,13 @@ TEST_F(SchemaUtilRowsetTest, check_path_stats_agg_key) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
-    string absolute_dir = _curreent_dir + std::string("/ut_dir/schema_util_rows");
+    // set seed
+    std::srand(42);
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
+    std::string absolute_dir = _curreent_dir + std::string("/ut_dir/schema_util_rows");
     EXPECT_TRUE(io::global_local_filesystem()->delete_directory(absolute_dir).ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(absolute_dir).ok());
     std::unique_ptr<DataDir> _data_dir = std::make_unique<DataDir>(*_engine_ref, absolute_dir);
@@ -236,7 +245,8 @@ TEST_F(SchemaUtilRowsetTest, check_path_stats_agg_key) {
     for (int i = 0; i < 5; i++) {
         const auto& res = RowsetFactory::create_rowset_writer(
                 *_engine_ref,
-                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path()), false);
+                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path(), _tablet),
+                false);
         EXPECT_TRUE(res.has_value()) << res.error();
         const auto& rowset_writer = res.value();
         auto rowset = create_rowset(rowset_writer, tablet_schema);
@@ -262,7 +272,13 @@ TEST_F(SchemaUtilRowsetTest, check_path_stats_agg_delete) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
-    string absolute_dir = _curreent_dir + std::string("/ut_dir/schema_util_rows1");
+    // set seed
+    std::srand(42);
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
+    std::string absolute_dir = _curreent_dir + std::string("/ut_dir/schema_util_rows1");
     EXPECT_TRUE(io::global_local_filesystem()->delete_directory(absolute_dir).ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(absolute_dir).ok());
     std::unique_ptr<DataDir> _data_dir = std::make_unique<DataDir>(*_engine_ref, absolute_dir);
@@ -278,7 +294,8 @@ TEST_F(SchemaUtilRowsetTest, check_path_stats_agg_delete) {
     for (int i = 0; i < 5; i++) {
         const auto& res = RowsetFactory::create_rowset_writer(
                 *_engine_ref,
-                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path()), false);
+                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path(), _tablet),
+                false);
         EXPECT_TRUE(res.has_value()) << res.error();
         const auto& rowset_writer = res.value();
         auto rowset = create_rowset(rowset_writer, tablet_schema);
@@ -292,7 +309,74 @@ TEST_F(SchemaUtilRowsetTest, check_path_stats_agg_delete) {
     EXPECT_FALSE(st.ok());
 }
 
-TEST_F(SchemaUtilRowsetTest, collect_path_stats_and_get_compaction_schema) {
+// Mixed-format compatibility: one tablet contains both old (inline meta) and new
+// (external column meta + variant ext meta) segments, and path stats / scanning
+// logic should work across all rowsets.
+TEST_F(SchemaUtilRowsetTest, mixed_external_segment_meta_old_new) {
+    // 1. create tablet schema (same layout as check_path_stats_agg_key)
+    TabletSchemaPB schema_pb;
+    schema_pb.set_keys_type(AGG_KEYS);
+    construct_column(schema_pb.add_column(), 0, "INT", "key", true);
+    construct_column(schema_pb.add_column(), 1, "VARIANT", "v1");
+    construct_column(schema_pb.add_column(), 2, "STRING", "v2");
+    construct_column(schema_pb.add_column(), 3, "VARIANT", "v3");
+    construct_column(schema_pb.add_column(), 4, "INT", "v4");
+    TabletSchemaSPtr tablet_schema = std::make_shared<TabletSchema>();
+    tablet_schema->init_from_pb(schema_pb);
+
+    // 2. create tablet and data dir
+    TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
+    // First write a few rowsets with external_segment_meta_used_default = false (old format)
+    tablet_schema->set_external_segment_meta_used_default(false);
+    std::string absolute_dir = _curreent_dir + std::string("/ut_dir/schema_util_rows_mixed");
+    EXPECT_TRUE(io::global_local_filesystem()->delete_directory(absolute_dir).ok());
+    EXPECT_TRUE(io::global_local_filesystem()->create_directory(absolute_dir).ok());
+    std::unique_ptr<DataDir> data_dir = std::make_unique<DataDir>(*_engine_ref, absolute_dir);
+    static_cast<void>(data_dir->update_capacity());
+    EXPECT_TRUE(data_dir->init(true).ok());
+
+    TabletSharedPtr tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, data_dir.get());
+    EXPECT_TRUE(tablet->init().ok());
+    EXPECT_TRUE(io::global_local_filesystem()->create_directory(tablet->tablet_path()).ok());
+
+    // 3. create multiple rowsets: first batch uses old footer format, second batch uses new format
+    std::vector<RowsetSharedPtr> rowsets;
+
+    auto create_and_add_rowset = [&](int /*idx*/) {
+        const auto& res = RowsetFactory::create_rowset_writer(
+                *_engine_ref,
+                rowset_writer_context(data_dir, tablet_schema, tablet->tablet_path(), tablet),
+                false);
+        EXPECT_TRUE(res.has_value()) << res.error();
+        const auto& rowset_writer = res.value();
+        auto rowset = create_rowset(rowset_writer, tablet_schema);
+        EXPECT_TRUE(tablet->add_rowset(rowset).ok());
+        rowsets.push_back(rowset);
+    };
+
+    // 3.1 write a few old-format rowsets (inline meta only)
+    for (int i = 0; i < 3; ++i) {
+        create_and_add_rowset(i);
+    }
+
+    // 3.2 flip tablet default to enable external column meta for subsequent segments
+    tablet_schema->set_external_segment_meta_used_default(true);
+
+    // 3.3 write a few new-format rowsets (external meta enabled)
+    for (int i = 0; i < 3; ++i) {
+        create_and_add_rowset(i + 3);
+    }
+
+    EXPECT_EQ(rowsets.size(), 6);
+
+    // 4. check that VariantCompactionUtil::check_path_stats works across mixed segments
+    // This will internally create Segment / ColumnReader instances and should be
+    // insensitive to whether a particular segment uses inline or external meta.
+    EXPECT_TRUE(
+            schema_util::VariantCompactionUtil::check_path_stats(rowsets, rowsets[0], tablet).ok());
+}
+
+TEST_F(SchemaUtilRowsetTest, collect_path_stats_and_get_extended_compaction_schema) {
     all_path_stats.clear();
     // 1.create tablet schema
     TabletSchemaPB schema_pb;
@@ -306,6 +390,11 @@ TEST_F(SchemaUtilRowsetTest, collect_path_stats_and_get_compaction_schema) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
+    tablet_meta->_tablet_id = 12345;
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(_tablet->tablet_path()).ok());
@@ -315,7 +404,8 @@ TEST_F(SchemaUtilRowsetTest, collect_path_stats_and_get_compaction_schema) {
     for (int i = 0; i < 5; i++) {
         const auto& res = RowsetFactory::create_rowset_writer(
                 *_engine_ref,
-                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path()), false);
+                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path(), _tablet),
+                false);
         EXPECT_TRUE(res.has_value()) << res.error();
         const auto& rowset_writer = res.value();
         auto rowset = create_rowset(rowset_writer, tablet_schema);
@@ -386,7 +476,10 @@ TEST_F(SchemaUtilRowsetTest, collect_path_stats_and_get_compaction_schema) {
         rowset_writer_context.rowset_type = BETA_ROWSET;
         rowset_writer_context.rowset_state = VISIBLE;
         rowset_writer_context.tablet_schema = tablet_schema;
-        rowset_writer_context.tablet_path = _absolute_dir + "/../";
+        rowset_writer_context.tablet_path = _tablet->tablet_path();
+        rowset_writer_context.data_dir = _data_dir.get();
+        rowset_writer_context.tablet_id = _tablet->tablet_id();
+        rowset_writer_context.tablet = _tablet;
         rowset_writer_context.version = version;
         rowset_writer_context.segments_overlap = overlap;
         rowset_writer_context.max_rows_per_segment = max_rows_per_segment;
@@ -428,6 +521,10 @@ TabletSchemaSPtr create_compaction_schema_common(StorageEngine* _engine_ref,
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     EXPECT_TRUE(io::global_local_filesystem()->delete_directory(_absolute_dir).ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(_absolute_dir).ok());
     std::unique_ptr<DataDir> _data_dir = std::make_unique<DataDir>(*_engine_ref, _absolute_dir);
@@ -444,7 +541,8 @@ TabletSchemaSPtr create_compaction_schema_common(StorageEngine* _engine_ref,
     for (int i = 0; i < 1; i++) {
         const auto& res = RowsetFactory::create_rowset_writer(
                 *_engine_ref,
-                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path()), false);
+                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path(), _tablet),
+                false);
         EXPECT_TRUE(res.has_value()) << res.error();
         const auto& rowset_writer = res.value();
         auto rowset = create_rowset(rowset_writer, tablet_schema);
@@ -551,6 +649,10 @@ TEST_F(SchemaUtilRowsetTest, typed_path_to_sparse_column) {
 
     // 2. create tablet
     TabletMetaSharedPtr tablet_meta(new TabletMeta(tablet_schema));
+    bool external_segment_meta_used_default = rand() % 2 == 0;
+    std::cout << "external_segment_meta_used_default: " << external_segment_meta_used_default
+              << std::endl;
+    tablet_schema->set_external_segment_meta_used_default(external_segment_meta_used_default);
     _tablet = std::make_shared<Tablet>(*_engine_ref, tablet_meta, _data_dir.get());
     EXPECT_TRUE(_tablet->init().ok());
     EXPECT_TRUE(io::global_local_filesystem()->create_directory(_tablet->tablet_path()).ok());
@@ -560,7 +662,8 @@ TEST_F(SchemaUtilRowsetTest, typed_path_to_sparse_column) {
     for (int i = 0; i < 5; i++) {
         const auto& res = RowsetFactory::create_rowset_writer(
                 *_engine_ref,
-                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path()), false);
+                rowset_writer_context(_data_dir, tablet_schema, _tablet->tablet_path(), _tablet),
+                false);
         EXPECT_TRUE(res.has_value()) << res.error();
         const auto& rowset_writer = res.value();
         auto rowset = create_rowset(rowset_writer, tablet_schema);
@@ -632,6 +735,9 @@ TEST_F(SchemaUtilRowsetTest, typed_path_to_sparse_column) {
         rowset_writer_context.rowset_state = VISIBLE;
         rowset_writer_context.tablet_schema = tablet_schema;
         rowset_writer_context.tablet_path = _absolute_dir + "/../";
+        rowset_writer_context.data_dir = _data_dir.get();
+        rowset_writer_context.tablet_id = _tablet->tablet_id();
+        rowset_writer_context.tablet = _tablet;
         rowset_writer_context.version = version;
         rowset_writer_context.segments_overlap = overlap;
         rowset_writer_context.max_rows_per_segment = max_rows_per_segment;
diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index 3f6577e3669..7585126aa19 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -81,7 +81,7 @@ public class Config extends ConfigBase {
             options = {"NORMAL", "ASYNC", "BRIEF"})
     public static String sys_log_mode = "ASYNC";
 
-    @ConfField(description = {"FE 在 sys_log_roll_interval (日志滚动间隔)内允许保留的最大日志文件数。"
+    @ConfField(description = {"FE 在 sys_log_roll_interval(日志滚动间隔)内允许保留的最大日志文件数。"
             + "默认值为 10,意味着在每个日志滚动周期内,系统最多会保留 10 个日志文件。",
             "This parameter defines the maximum number of FE log files that can be retained within the "
             + "sys_log_roll_interval (log roll interval). The default value is 10, which means the system"
@@ -140,7 +140,7 @@ public class Config extends ConfigBase {
     public static boolean enable_file_logger = true;
 
     @ConfField(mutable = false, masterOnly = false,
-            description = {"是否检查table锁泄漏", "Whether to check table lock leaky"})
+            description = {"是否检查 table 锁泄漏", "Whether to check table lock leaky"})
     public static boolean check_table_lock_leaky = false;
 
     @ConfField(mutable = true, masterOnly = false,
@@ -302,13 +302,13 @@ public class Config extends ConfigBase {
                     + "If there are many ReplicaWriteException in FE WARN log, you can try to increase this value"})
     public static int bdbje_replica_ack_timeout_second = 10;
 
-    @ConfField(description = {"在HA模式下,BDBJE 中保留的预留空间字节数的期望上限。非 HA 模式下无效",
+    @ConfField(description = {"在 HA 模式下,BDBJE 中保留的预留空间字节数的期望上限。非 HA 模式下无效",
             "The desired upper limit on the number of bytes of reserved space to retain "
                     + "in a replicated JE Environment. "
                     + "This parameter is ignored in a non-replicated JE Environment."})
     public static long bdbje_reserved_disk_bytes = 1 * 1024 * 1024 * 1024; // 1G
 
-    @ConfField(description = {"BDBJE 所需的空闲磁盘空间大小。如果空闲磁盘空间小于这个值,则BDBJE将无法写入。",
+    @ConfField(description = {"BDBJE 所需的空闲磁盘空间大小。如果空闲磁盘空间小于这个值,则 BDBJE 将无法写入。",
             "Amount of free disk space required by BDBJE. "
                     + "If the free disk space is less than this value, BDBJE will not be able to write."})
     public static long bdbje_free_disk_bytes = 1 * 1024 * 1024 * 1024; // 1G
@@ -321,11 +321,11 @@ public class Config extends ConfigBase {
             "Queue size to store heartbeat task in heartbeat_mgr"})
     public static int heartbeat_mgr_blocking_queue_size = 1024;
 
-    @ConfField(masterOnly = true, description = {"TabletStatMgr线程数",
+    @ConfField(masterOnly = true, description = {"TabletStatMgr 线程数",
             "Num of thread to update tablet stat"})
     public static int tablet_stat_mgr_threads_num = -1;
 
-    @ConfField(masterOnly = true, description = {"Agent任务线程池的线程数",
+    @ConfField(masterOnly = true, description = {"Agent 任务线程池的线程数",
             "Num of thread to handle agent task in agent task thread-pool"})
     public static int max_agent_task_threads_num = 4096;
 
@@ -397,11 +397,11 @@ public class Config extends ConfigBase {
             varType = VariableAnnotation.EXPERIMENTAL)
     public static boolean enable_https = false;
 
-    @ConfField(description = {"Jetty 的 acceptor 线程数。Jetty的线程架构模型很简单,分为三个线程池:acceptor、selector 和 worker。"
-            + "acceptor 负责接受新的连接,然后交给 selector 处理HTTP报文协议的解包,最后由 worker 处理请求。"
-            + "前两个线程池采用非阻塞模型,并且一个线程可以处理很多socket的读写,所以线程池的数量少。"
-            + "对于大多数项目,只需要 1-2 个 acceptor 线程,2 到 4 个就足够了。Worker 的数量取决于应用的QPS和IO事件的比例。"
-            + "越高QPS,或者IO占比越高,等待的线程越多,需要的线程总数越多。",
+    @ConfField(description = {"Jetty 的 acceptor 线程数。Jetty 的线程架构模型很简单,分为三个线程池:acceptor、selector 和 worker。"
+            + "acceptor 负责接受新的连接,然后交给 selector 处理 HTTP 报文协议的解包,最后由 worker 处理请求。"
+            + "前两个线程池采用非阻塞模型,并且一个线程可以处理很多 socket 的读写,所以线程池的数量少。"
+            + "对于大多数项目,只需要 1-2 个 acceptor 线程,2 到 4 个就足够了。Worker 的数量取决于应用的 QPS 和 IO 事件的比例。"
+            + "越高 QPS,或者 IO 占比越高,等待的线程越多,需要的线程总数越多。",
             "The number of acceptor threads for Jetty. Jetty's thread architecture model is very simple, "
                     + "divided into three thread pools: acceptor, selector and worker. "
                     + "The acceptor is responsible for accepting new connections, "
@@ -483,7 +483,7 @@ public class Config extends ConfigBase {
     @ConfField(description = {"MySQL 服务的最大任务线程数", "The max number of task threads in MySQL service"})
     public static int max_mysql_service_task_threads_num = 4096;
 
-    @ConfField(description = {"BackendServiceProxy数量, 用于池化GRPC channel",
+    @ConfField(description = {"BackendServiceProxy 数量,用于池化 GRPC channel",
             "BackendServiceProxy pool size for pooling GRPC channels."})
     public static int backend_proxy_num = 48;
 
@@ -519,7 +519,7 @@ public class Config extends ConfigBase {
     public static int publish_version_timeout_second = 30; // 30 seconds
 
     @ConfField(mutable = true, masterOnly = true, description = {"导入 Publish 阶段的等待时间,单位是秒。超过此时间,"
-            + "则只需每个tablet包含一个成功副本,则导入成功。值为 -1 时,表示无限等待。",
+            + "则只需每个 tablet 包含一个成功副本,则导入成功。值为 -1 时,表示无限等待。",
             "Waiting time for one transaction changing to \"at least one replica success\", in seconds."
             + "If time exceeds this, and for each tablet it has at least one replica publish successful, "
             + "then the load task will be successful." })
@@ -598,7 +598,12 @@ public class Config extends ConfigBase {
     public static int insert_load_default_timeout_second = 14400; // 4 hour
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "等内部攒批真正写入完成才返回;insert into和stream load默认开启攒批",
+            "在 fuzzy 测试中随机选择部分表使用 V3 storage_format(ext_meta),用于增强覆盖",
+            "Randomly use V3 storage_format (ext_meta) for some tables in fuzzy tests to increase coverage"})
+    public static boolean random_use_v3_storage_format = true;
+
+    @ConfField(mutable = true, masterOnly = true, description = {
+            "等内部攒批真正写入完成才返回;insert into 和 stream load 默认开启攒批",
             "Wait for the internal batch to be written before returning; "
                     + "insert into and stream load use group commit by default."})
     public static boolean wait_internal_group_commit_finish = false;
@@ -607,12 +612,12 @@ public class Config extends ConfigBase {
             "Default commit interval in ms for group commit"})
     public static int group_commit_interval_ms_default_value = 10000;
 
-    @ConfField(mutable = false, masterOnly = true, description = {"攒批的默认提交数据量,单位是字节,默认128M",
+    @ConfField(mutable = false, masterOnly = true, description = {"攒批的默认提交数据量,单位是字节,默认 128M",
             "Default commit data bytes for group commit"})
     public static int group_commit_data_bytes_default_value = 134217728;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "内部攒批的超时时间为table的group_commit_interval_ms的倍数",
+            "内部攒批的超时时间为 table 的 group_commit_interval_ms 的倍数",
             "The internal group commit timeout is the multiple of table's group_commit_interval_ms"})
     public static int group_commit_timeout_multipler = 10;
 
@@ -713,7 +718,7 @@ public class Config extends ConfigBase {
     public static double used_capacity_percent_max_diff = 0.30;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "设置固定的 BE 负载分数中磁盘使用率系数。BE 负载分数会综合磁盘使用率和副本数而得。有效值范围为[0, 1],"
+            "设置固定的 BE 负载分数中磁盘使用率系数。BE 负载分数会综合磁盘使用率和副本数而得。有效值范围为 [0, 1],"
                     + "当超出此范围时,则使用其他方法自动计算此系数。",
             "Sets a fixed disk usage factor in the BE load fraction. The BE load score is a combination of disk usage "
                     + "and replica count. The valid value range is [0, 1]. When it is out of this range, other "
@@ -732,13 +737,13 @@ public class Config extends ConfigBase {
                     + "and disable storage cool down function."})
     public static boolean disable_storage_medium_check = false;
 
-    @ConfField(description = {"创建表或分区时,可以指定存储介质(HDD 或 SSD)。如果未指定,"
+    @ConfField(description = {"创建表或分区时,可以指定存储介质 (HDD 或 SSD)。如果未指定,"
             + "则使用此配置指定的默认介质。",
             "When create a table(or partition), you can specify its storage medium(HDD or SSD)."})
     public static String default_storage_medium = "HDD";
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "删除数据库(表/分区)后,可以使用 RECOVER 语句恢复。此配置指定了数据的最大保留时间。"
+            "删除数据库 (表/分区) 后,可以使用 RECOVER 语句恢复。此配置指定了数据的最大保留时间。"
                     + "超过此时间,数据将被永久删除。",
             "After dropping database(table/partition), you can recover it by using RECOVER stmt.",
             "And this specifies the maximal data retention time. After time, the data will be deleted permanently."})
@@ -801,7 +806,7 @@ public class Config extends ConfigBase {
      */
     @ConfField(mutable = true, masterOnly = true) public static boolean disable_colocate_balance = false;
 
-    @ConfField(mutable = true, masterOnly = true, description = {"是否启用group间的均衡",
+    @ConfField(mutable = true, masterOnly = true, description = {"是否启用 group 间的均衡",
             "is allow colocate balance between all groups"})
     public static boolean disable_colocate_balance_between_groups = false;
 
@@ -1163,7 +1168,7 @@ public class Config extends ConfigBase {
 
     // if the number of report task in FE exceed max_report_task_num_per_rpc, then split it to multiple rpc
     @ConfField(mutable = true, masterOnly = true, description = {
-            "重新发送 agent task 时,单次 RPC 分配给每个be的任务最大个数,默认值为10000个。",
+            "重新发送 agent task 时,单次 RPC 分配给每个 be 的任务最大个数,默认值为 10000 个。",
             "The maximum number of batched tasks per RPC assigned to each BE when resending agent tasks, "
             + "the default value is 10000."
     })
@@ -1395,7 +1400,7 @@ public class Config extends ConfigBase {
             masterOnly = false,
             callbackClassString = "org.apache.doris.common.cache.NereidsSqlCacheManager$UpdateConfig",
             description = {
-                    "当前默认设置为 300,用来控制控制NereidsSqlCacheManager中sql cache过期时间,超过一段时间不访问cache会被回收",
+                    "当前默认设置为 300,用来控制控制 NereidsSqlCacheManager 中 sql cache 过期时间,超过一段时间不访问 cache 会被回收",
                     "The current default setting is 300, which is used to control the expiration time of SQL cache"
                             + "in NereidsSqlCacheManager. If the cache is not accessed for a period of time, "
                             + "it will be reclaimed"
@@ -1412,8 +1417,8 @@ public class Config extends ConfigBase {
             masterOnly = false,
             callbackClassString = "org.apache.doris.common.cache.NereidsSortedPartitionsCacheManager$UpdateConfig",
             description = {
-                "当前默认设置为 300,用来控制控制NereidsSortedPartitionsCacheManager中分区元数据缓存过期时间,"
-                    + "超过一段时间不访问cache会被回收",
+                "当前默认设置为 300,用来控制控制 NereidsSortedPartitionsCacheManager 中分区元数据缓存过期时间,"
+                    + "超过一段时间不访问 cache 会被回收",
                 "The current default setting is 300, which is used to control the expiration time of "
                     + "the partition metadata cache in NereidsSortedPartitionsCheManager. "
                     + "If the cache is not accessed for a period of time, it will be reclaimed"
@@ -1424,14 +1429,14 @@ public class Config extends ConfigBase {
     /**
      * Set the maximum number of rows that can be cached
      */
-    @ConfField(mutable = true, masterOnly = false, description = {"SQL/Partition Cache可以缓存的最大行数。",
+    @ConfField(mutable = true, masterOnly = false, description = {"SQL/Partition Cache 可以缓存的最大行数。",
         "Maximum number of rows that can be cached in SQL/Partition Cache, is 3000 by default."})
     public static int cache_result_max_row_count = 3000;
 
     /**
      * Set the maximum data size that can be cached
      */
-    @ConfField(mutable = true, masterOnly = false, description = {"SQL/Partition Cache可以缓存的最大数据大小。",
+    @ConfField(mutable = true, masterOnly = false, description = {"SQL/Partition Cache 可以缓存的最大数据大小。",
         "Maximum data size of rows that can be cached in SQL/Partition Cache, is 3000 by default."})
     public static int cache_result_max_data_size = 31457280; // 30M
 
@@ -1506,7 +1511,7 @@ public class Config extends ConfigBase {
     @ConfField
     public static boolean enable_bdbje_debug_mode = false;
 
-    @ConfField(mutable = false, masterOnly = true, description = {"是否开启debug point模式,测试使用",
+    @ConfField(mutable = false, masterOnly = true, description = {"是否开启 debug point 模式,测试使用",
             "is enable debug points, use in test."})
     public static boolean enable_debug_points = false;
 
@@ -1540,7 +1545,7 @@ public class Config extends ConfigBase {
      * sets the time without read activity before sending a keepalive ping
      * the smaller the value, the sooner the channel is unavailable, but it will increase network io
      */
-    @ConfField(description = { "设置grpc连接发送 keepalive ping 之前没有数据传输的时间。",
+    @ConfField(description = { "设置 grpc 连接发送 keepalive ping 之前没有数据传输的时间。",
             "The time without grpc read activity before sending a keepalive ping" })
     public static int grpc_keep_alive_second = 10;
 
@@ -1659,7 +1664,7 @@ public class Config extends ConfigBase {
     public static boolean enable_cloud_restore_job = false;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-        "存算分离恢复过程中,一次 create tablets rpc 创建的 tablet 数量上限,默认值为256个",
+        "存算分离恢复过程中,一次 create tablets rpc 创建的 tablet 数量上限,默认值为 256 个",
         "During the cloud restore job, the maximum number of tablets created by one "
             + "create tablets RPC, 256 by default."})
     public static int cloud_restore_create_tablet_batch_size = 256;
@@ -1852,8 +1857,8 @@ public class Config extends ConfigBase {
      */
     @ConfField(masterOnly = true, description = {"任务堆积时用于存放定时任务的队列大小", "The number of timer jobs that can be queued."})
     public static int job_dispatch_timer_job_queue_size = 1024;
-    @ConfField(masterOnly = true, description = {"一个 Job 的 task 最大的持久化数量,超过这个限制将会丢弃旧的 task 记录, 如果值 < 1, 将不会持久化。",
-            "Maximum number of persistence allowed per task in a job,exceeding which old tasks will be discarded,"
+    @ConfField(masterOnly = true, description = {"一个 Job 的 task 最大的持久化数量,超过这个限制将会丢弃旧的 task 记录,如果值 < 1, 将不会持久化。",
+            "Maximum number of persistence allowed per task in a job,exceeding which old tasks will be discarded,"
                    + "If the value is less than 1, it will not be persisted." })
     public static int max_persistence_task_count = 100;
     @ConfField(masterOnly = true, description = {"MV task 的等待队列大小,如果是负数,则会使用 1024,如果不是 2 的幂,则会自动选择一个最接近的"
@@ -1866,16 +1871,16 @@ public class Config extends ConfigBase {
             + " If the size is not a power of two, the nearest power of the size will "
             + "be automatically selected."})
     public static int insert_task_queue_size = 1024;
-    @ConfField(masterOnly = true, description = {"finished 状态的 job 最长保存时间,超过这个时间将会被删除, 单位:小时",
+    @ConfField(masterOnly = true, description = {"finished 状态的 job 最长保存时间,超过这个时间将会被删除,单位:小时",
             "The longest time to save the job in finished status, it will be deleted after this time. Unit: hour"})
     public static int finished_job_cleanup_threshold_time_hour = 24;
 
-    @ConfField(masterOnly = true, description = {"用于执行 Insert 任务的线程数,值应该大于0,否则默认为10",
+    @ConfField(masterOnly = true, description = {"用于执行 Insert 任务的线程数,值应该大于 0,否则默认为 10",
             "The number of threads used to consume Insert tasks, "
                     + "the value should be greater than 0, if it is <=0, default is 10."})
     public static int job_insert_task_consumer_thread_num = 10;
 
-    @ConfField(masterOnly = true, description = {"用于执行 MTMV 任务的线程数,值应该大于0,否则默认为10",
+    @ConfField(masterOnly = true, description = {"用于执行 MTMV 任务的线程数,值应该大于 0,否则默认为 10",
             "The number of threads used to consume mtmv tasks, "
                     + "the value should be greater than 0, if it is <=0, default is 10."})
     public static int job_mtmv_task_consumer_thread_num = 10;
@@ -1929,12 +1934,12 @@ public class Config extends ConfigBase {
     public static long query_queue_update_interval_ms = 5000;
 
     @ConfField(mutable = true, description = {
-            "当BE内存用量大于该值时,查询会进入排队逻辑,默认值为-1,代表该值不生效。取值范围0~1的小数",
+            "当 BE 内存用量大于该值时,查询会进入排队逻辑,默认值为 -1,代表该值不生效。取值范围 0~1 的小数",
             "When be memory usage bigger than this value, query could queue, "
                     + "default value is -1, means this value not work. Decimal value range from 0 to 1"})
     public static double query_queue_by_be_used_memory = -1;
 
-    @ConfField(mutable = true, description = {"基于内存反压场景FE定时拉取BE内存用量的时间间隔",
+    @ConfField(mutable = true, description = {"基于内存反压场景 FE 定时拉取 BE 内存用量的时间间隔",
             "In the scenario of memory backpressure, "
                     + "the time interval for obtaining BE memory usage at regular intervals"})
     public static long get_be_resource_usage_interval_ms = 10000;
@@ -2030,9 +2035,9 @@ public class Config extends ConfigBase {
      * And the max number of compute node is controlled by min_backend_num_for_external_table.
      * If set to false, query on external table will assign to any node.
      */
-    @ConfField(mutable = true, description = {"如果设置为true,外部表的查询将优先分配给计算节点。",
-            "并且计算节点的最大数量由min_backend_num_for_external_table控制。",
-            "如果设置为false,外部表的查询将分配给任何节点。"
+    @ConfField(mutable = true, description = {"如果设置为 true,外部表的查询将优先分配给计算节点。",
+            "并且计算节点的最大数量由 min_backend_num_for_external_table 控制。",
+            "如果设置为 false,外部表的查询将分配给任何节点。"
                     + "如果集群内没有计算节点,则该参数不生效。",
             "If set to true, query on external table will prefer to assign to compute node. "
                     + "And the max number of compute node is controlled by min_backend_num_for_external_table. "
@@ -2040,9 +2045,9 @@ public class Config extends ConfigBase {
                     + "If there is no compute node in cluster, this config takes no effect."})
     public static boolean prefer_compute_node_for_external_table = false;
 
-    @ConfField(mutable = true, description = {"只有当prefer_compute_node_for_external_table为true时生效,"
+    @ConfField(mutable = true, description = {"只有当 prefer_compute_node_for_external_table 为 true 时生效,"
             + "如果计算节点数小于这个值,外部表的查询会尝试获取一些混合节点来分配,以使节点总数达到这个值。"
-            + "如果计算节点数大于这个值,外部表的查询将只分配给计算节点。-1表示只是用当前数量的计算节点",
+            + "如果计算节点数大于这个值,外部表的查询将只分配给计算节点。-1 表示只是用当前数量的计算节点",
             "Only take effect when prefer_compute_node_for_external_table is true. "
                     + "If the compute node number is less than this value, "
                     + "query on external table will try to get some mix de to assign, "
@@ -2118,7 +2123,7 @@ public class Config extends ConfigBase {
     public static int blacklist_duration_second = 120;
 
     @ConfField(mutable = true, masterOnly = false, description = {
-            "禁止创建odbc, mysql, broker类型的外表", "Disallow the creation of odbc, mysql, broker type external tables"})
+            "禁止创建 odbc, mysql, broker 类型的外表", "Disallow the creation of odbc, mysql, broker type external tables"})
     public static boolean enable_odbc_mysql_broker_table = false;
 
     /**
@@ -2157,11 +2162,11 @@ public class Config extends ConfigBase {
             "Max cache number of database and table instance in external catalog."})
     public static long max_meta_object_cache_num = 1000;
 
-    @ConfField(description = {"Hive分区表缓存的最大数量",
+    @ConfField(description = {"Hive 分区表缓存的最大数量",
             "Max cache number of hive partition table"})
     public static long max_hive_partition_table_cache_num = 1000;
 
-    @ConfField(mutable = false, masterOnly = false, description = {"获取Hive分区值时候的最大返回数量,-1代表没有限制。",
+    @ConfField(mutable = false, masterOnly = false, description = {"获取 Hive 分区值时候的最大返回数量,-1 代表没有限制。",
             "Max number of hive partition values to return while list partitions, -1 means no limitation."})
     public static short max_hive_list_partition_num = -1;
 
@@ -2264,7 +2269,7 @@ public class Config extends ConfigBase {
      * otherwise it will throw an AnalysisException.
      */
     @ConfField(mutable = true, varType = VariableAnnotation.EXPERIMENTAL, description = {
-            "当前默认设置为 false,开启后支持使用新优化器的load语句导入数据,失败后会降级旧的load语句。",
+            "当前默认设置为 false,开启后支持使用新优化器的 load 语句导入数据,失败后会降级旧的 load 语句。",
             "Now default set to true, After this function is enabled, the load statement of "
                     + "the new optimizer can be used to import data. If this function fails, "
                     + "the old load statement will be degraded."})
@@ -2277,7 +2282,7 @@ public class Config extends ConfigBase {
             mutable = true,
             callbackClassString = "org.apache.doris.common.cache.NereidsSqlCacheManager$UpdateConfig",
             description = {
-                "当前默认设置为 100,用来控制控制NereidsSqlCacheManager管理的sql cache数量。",
+                "当前默认设置为 100,用来控制控制 NereidsSqlCacheManager 管理的 sql cache 数量。",
                 "Now default set to 100, this config is used to control the number of "
                         + "sql cache managed by NereidsSqlCacheManager"
             }
@@ -2288,7 +2293,7 @@ public class Config extends ConfigBase {
             mutable = true,
             callbackClassString = "org.apache.doris.common.cache.NereidsSortedPartitionsCacheManager$UpdateConfig",
             description = {
-                    "当前默认设置为 100,用来控制控制NereidsSortedPartitionsCacheManager中有序分区元数据的缓存个数,"
+                    "当前默认设置为 100,用来控制控制 NereidsSortedPartitionsCacheManager 中有序分区元数据的缓存个数,"
                             + "用于加速分区裁剪",
                     "The current default setting is 100, which is used to control the number of ordered "
                             + "partition metadata caches in NereidsSortedPartitionsCacheManager, "
@@ -2423,7 +2428,7 @@ public class Config extends ConfigBase {
     public static boolean enable_round_robin_create_tablet = true;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "创建分区时,总是从第一个 BE 开始创建。注意:这种方式可能造成BE不均衡",
+            "创建分区时,总是从第一个 BE 开始创建。注意:这种方式可能造成 BE 不均衡",
             "When creating tablet of a partition, always start from the first BE. "
                     + "Note: This method may cause BE imbalance"})
     public static boolean create_tablet_round_robin_from_start = false;
@@ -2486,19 +2491,19 @@ public class Config extends ConfigBase {
     public static boolean enable_query_hit_stats = false;
 
     @ConfField(mutable = true, description = {
-            "设置为 true,如果查询无法选择到健康副本时,会打印出该tablet所有副本的详细信息," + "以及不可查询的具体原因。",
+            "设置为 true,如果查询无法选择到健康副本时,会打印出该 tablet 所有副本的详细信息," + "以及不可查询的具体原因。",
             "When set to true, if a query is unable to select a healthy replica, "
                     + "the detailed information of all the replicas of the tablet,"
                     + " including the specific reason why they are unqueryable, will be printed out."})
     public static boolean show_details_for_unaccessible_tablet = true;
 
     @ConfField(mutable = false, masterOnly = false, varType = VariableAnnotation.EXPERIMENTAL, description = {
-            "是否启用binlog特性",
+            "是否启用 binlog 特性",
             "Whether to enable binlog feature"})
     public static boolean enable_feature_binlog = false;
 
     @ConfField(mutable = false, description = {
-            "是否默认为 Database/Table 启用binlog特性",
+            "是否默认为 Database/Table 启用 binlog 特性",
             "Whether to enable binlog feature for Database/Table by default"})
     public static boolean force_enable_feature_binlog = false;
 
@@ -2513,17 +2518,17 @@ public class Config extends ConfigBase {
     public static boolean disallow_create_catalog_with_resource = true;
 
     @ConfField(mutable = true, masterOnly = false, description = {
-        "Hive行数估算分区采样数",
+        "Hive 行数估算分区采样数",
         "Sample size for hive row count estimation."})
     public static int hive_stats_partition_sample_size = 30;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "启用Hive分桶表",
+            "启用 Hive 分桶表",
             "Enable external hive bucket table"})
     public static boolean enable_create_hive_bucket_table = false;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "Hive创建外部表默认指定的文件格式",
+            "Hive 创建外部表默认指定的文件格式",
             "Default hive file format for creating table."})
     public static String hive_default_file_format = "orc";
 
@@ -2571,12 +2576,12 @@ public class Config extends ConfigBase {
     public static boolean force_sample_analyze = false; // avoid full analyze for performance reason
 
     @ConfField(mutable = true, description = {
-            "Export任务允许的最大分区数量",
+            "Export 任务允许的最大分区数量",
             "The maximum number of partitions allowed by Export job"})
     public static int maximum_number_of_export_partitions = 2000;
 
     @ConfField(mutable = true, description = {
-            "Export任务允许的最大并行数",
+            "Export 任务允许的最大并行数",
             "The maximum parallelism allowed by Export job"})
     public static int maximum_parallelism_of_export_job = 50;
 
@@ -2589,20 +2594,20 @@ public class Config extends ConfigBase {
     public static boolean forbid_running_alter_job = false;
 
     @ConfField(description = {
-            "暂时性配置项,开启后会自动将所有的olap表修改为可light schema change",
+            "暂时性配置项,开启后会自动将所有的 olap 表修改为可 light schema change",
             "temporary config filed, will make all olap tables enable light schema change"
     })
     public static boolean enable_convert_light_weight_schema_change = false;
 
     @ConfField(mutable = true, masterOnly = false, description = {
-            "查询information_schema.metadata_name_ids表时,获取一个数据库中所有表用的时间",
+            "查询 information_schema.metadata_name_ids 表时,获取一个数据库中所有表用的时间",
             "When querying the information_schema.metadata_name_ids table,"
                     + " the time used to obtain all tables in one database"
     })
     public static long query_metadata_name_ids_timeout = 3;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "是否禁止LocalDeployManager删除节点",
+            "是否禁止 LocalDeployManager 删除节点",
             "Whether to disable LocalDeployManager drop node"})
     public static boolean disable_local_deploy_manager_drop_node = true;
 
@@ -2642,13 +2647,13 @@ public class Config extends ConfigBase {
     public static long analyze_record_limit = 20000;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "Auto Buckets中最小的buckets数目",
+            "Auto Buckets 中最小的 buckets 数目",
             "min buckets of auto bucket"
     })
     public static int autobucket_min_buckets = 1;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-        "Auto Buckets中最大的buckets数目",
+        "Auto Buckets 中最大的 buckets 数目",
         "max buckets of auto bucket"
     })
     public static int autobucket_max_buckets = 128;
@@ -2658,8 +2663,8 @@ public class Config extends ConfigBase {
     public static int arrow_flight_max_connections = 4096;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-        "Auto Buckets中按照partition size去估算bucket数,存算一体partition size 5G估算一个bucket,"
-            + "但存算分离下partition size 10G估算一个bucket。 若配置小于0,会在在代码中会自适应存算一体模式默认5G,在存算分离默认10G",
+        "Auto Buckets 中按照 partition size 去估算 bucket 数,存算一体 partition size 5G 估算一个 bucket,"
+            + "但存算分离下 partition size 10G 估算一个 bucket。若配置小于 0,会在在代码中会自适应存算一体模式默认 5G,在存算分离默认 10G",
         "In Auto Buckets, the number of buckets is estimated based on the partition size. "
             + "For storage and computing integration, a partition size of 5G is estimated as one bucket."
             + " but for cloud, a partition size of 10G is estimated as one bucket. "
@@ -2668,14 +2673,14 @@ public class Config extends ConfigBase {
     })
     public static int autobucket_partition_size_per_bucket_gb = -1;
 
-    @ConfField(mutable = true, masterOnly = true, description = {"Auto bucket中计算出的新的分区bucket num超过前一个分区的"
-            + "bucket num的百分比,被认为是异常case报警",
+    @ConfField(mutable = true, masterOnly = true, description = {"Auto bucket 中计算出的新的分区 bucket num 超过前一个分区的"
+            + "bucket num 的百分比,被认为是异常 case 报警",
             "The new partition bucket number calculated in the auto bucket exceeds the percentage "
             + "of the previous partition's bucket number, which is considered an abnormal case alert."})
     public static double autobucket_out_of_bounds_percent_threshold = 0.5;
 
-    @ConfField(description = {"(已弃用,被 arrow_flight_max_connection 替代) Arrow Flight Server中所有用户token的缓存上限,"
-            + "超过后LRU淘汰, arrow flight sql是无状态的协议,连接通常不会主动断开,"
+    @ConfField(description = {"(已弃用,被 arrow_flight_max_connection 替代) Arrow Flight Server 中所有用户 token 的缓存上限,"
+            + "超过后 LRU 淘汰,arrow flight sql 是无状态的协议,连接通常不会主动断开,"
             + "bearer token 从 cache 淘汰的同时会 unregister Connection.",
             "(Deprecated, replaced by arrow_flight_max_connection) The cache limit of all user tokens in "
             + "Arrow Flight Server. which will be eliminated by LRU rules after exceeding the limit, "
@@ -2683,7 +2688,7 @@ public class Config extends ConfigBase {
             + "bearer token is evict from the cache will unregister ConnectContext."})
     public static int arrow_flight_token_cache_size = 4096;
 
-    @ConfField(description = {"Arrow Flight Server中用户token的存活时间,自上次写入后过期时间,单位秒,默认值为86400,即1天",
+    @ConfField(description = {"Arrow Flight Server 中用户 token 的存活时间,自上次写入后过期时间,单位秒,默认值为 86400,即 1 天",
             "The alive time of the user token in Arrow Flight Server, expire after write, unit second,"
             + "the default value is 86400, which is 1 days"})
     public static int arrow_flight_token_alive_time_second = 86400;
@@ -2697,14 +2702,14 @@ public class Config extends ConfigBase {
     public static String mysqldb_replace_name = "mysql";
 
     @ConfField(description = {
-        "设置允许跨域访问的特定域名,默认允许任何域名跨域访问",
+        "设置允许跨域访问的特定域名,默认允许任何域名跨域访问",
         "Set the specific domain name that allows cross-domain access. "
             + "By default, any domain name is allowed cross-domain access"
     })
     public static String access_control_allowed_origin_domain = "*";
 
     @ConfField(description = {
-            "开启java_udf, 默认为true。如果该配置为false,则禁止创建和使用java_udf。在一些场景下关闭该配置可防止命令注入攻击。",
+            "开启 java_udf, 默认为 true。如果该配置为 false,则禁止创建和使用 java_udf。在一些场景下关闭该配置可防止命令注入攻击。",
             "Used to enable java_udf, default is true. if this configuration is false, creation and use of java_udf is "
                     + "disabled. in some scenarios it may be necessary to disable this configuration to prevent "
                     + "command injection attacks."
@@ -2712,7 +2717,7 @@ public class Config extends ConfigBase {
     public static boolean enable_java_udf = true;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-        "开启后,可以在导入时,利用创建的全局java_udf函数处理数据, 默认为false。",
+        "开启后,可以在导入时,利用创建的全局 java_udf 函数处理数据,默认为 false。",
         "When enabled, data can be processed using the globally created java_udf function during import."
                 + " The default setting is false."
     })
@@ -2732,8 +2737,8 @@ public class Config extends ConfigBase {
     public static boolean ignore_unknown_metadata_module = false;
 
     @ConfField(mutable = true, description = {
-            "从主节点同步image文件的超时时间,用户可根据${meta_dir}/image文件夹下面的image文件大小和节点间的网络环境调整,"
-                    + "单位为秒,默认值300",
+            "从主节点同步 image 文件的超时时间,用户可根据${meta_dir}/image 文件夹下面的 image 文件大小和节点间的网络环境调整,"
+                    + "单位为秒,默认值 300",
             "The timeout for FE Follower/Observer synchronizing an image file from the FE Master, can be adjusted by "
                     + "the user on the size of image file in the ${meta_dir}/image and the network environment between "
                     + "nodes. The default values is 300."
@@ -2775,12 +2780,12 @@ public class Config extends ConfigBase {
     @ConfField(mutable = true, masterOnly = true)
     public static int workload_group_max_num = 15;
 
-    @ConfField(description = {"查询be wal_queue 的超时阈值(ms)",
+    @ConfField(description = {"查询 be wal_queue 的超时阈值 (ms)",
             "the timeout threshold of checking wal_queue on be(ms)"})
     public static int check_wal_queue_timeout_threshold = 180000;   // 3 min
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "对于自动分区表,防止用户意外创建大量分区,每个OLAP表允许的分区数量为`max_auto_partition_num`。默认2000。",
+            "对于自动分区表,防止用户意外创建大量分区,每个 OLAP 表允许的分区数量为`max_auto_partition_num`。默认 2000。",
             "For auto-partitioned tables to prevent users from accidentally creating a large number of partitions, "
                     + "the number of partitions allowed per OLAP table is `max_auto_partition_num`. Default 2000."
     })
@@ -2801,9 +2806,9 @@ public class Config extends ConfigBase {
     public static double diagnose_balance_max_tablet_num_ratio = 1.1;
 
     @ConfField(masterOnly = true, description = {
-            "设置 root 用户初始化2阶段 SHA-1 加密密码,默认为'',即不设置 root 密码。"
+            "设置 root 用户初始化 2 阶段 SHA-1 加密密码,默认为'',即不设置 root 密码。"
                     + "后续 root 用户的 `set password` 操作会将 root 初始化密码覆盖。"
-                    + "示例:如要配置密码的明文是 `root@123`,可在Doris执行SQL `select password('root@123')` "
+                    + "示例:如要配置密码的明文是 `root@123`,可在 Doris 执行 SQL `select password('root@123')` "
                     + "获取加密密码 `*A00C34073A26B40AB4307650BFB9309D6BFA6999`",
             "Set root user initial 2-staged SHA-1 encrypted password, default as '', means no root password. "
                     + "Subsequent `set password` operations for root user will overwrite the initial root password. "
@@ -2812,25 +2817,25 @@ public class Config extends ConfigBase {
                     + "password `*A00C34073A26B40AB4307650BFB9309D6BFA6999`"})
     public static String initial_root_password = "";
 
-    @ConfField(description = {"nereids trace文件的存放路径。",
+    @ConfField(description = {"nereids trace 文件的存放路径。",
             "The path of the nereids trace file."})
     public static String nereids_trace_log_dir = System.getenv("LOG_DIR") + "/nereids_trace";
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "备份过程中,一个 upload 任务上传的快照数量上限,默认值为10个",
+            "备份过程中,一个 upload 任务上传的快照数量上限,默认值为 10 个",
             "The max number of snapshots assigned to a upload task during the backup process, the default value is 10."
     })
     public static int backup_upload_snapshot_batch_size = 10;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "恢复过程中,一个 download 任务下载的快照数量上限,默认值为10个",
+            "恢复过程中,一个 download 任务下载的快照数量上限,默认值为 10 个",
             "The max number of snapshots assigned to a download task during the restore process, "
             + "the default value is 10."
     })
     public static int restore_download_snapshot_batch_size = 10;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "备份恢复过程中,单次 RPC 分配给每个be的任务最大个数,默认值为10000个。",
+            "备份恢复过程中,单次 RPC 分配给每个 be 的任务最大个数,默认值为 10000 个。",
             "The max number of batched tasks per RPC assigned to each be during the backup/restore process, "
             + "the default value is 10000."
     })
@@ -2841,11 +2846,11 @@ public class Config extends ConfigBase {
             "The number of concurrent restore tasks per be"})
     public static int restore_task_concurrency_per_be = 5000;
 
-    @ConfField(mutable = true, description = {"执行 agent task 时,BE心跳超过多长时间,认为BE不可用",
+    @ConfField(mutable = true, description = {"执行 agent task 时,BE 心跳超过多长时间,认为 BE 不可用",
             "The time after which BE is considered unavailable if the heartbeat is not received"})
     public static int agent_task_be_unavailable_heartbeat_timeout_second = 300;
 
-    @ConfField(description = {"是否开启通过http接口获取log文件的功能",
+    @ConfField(description = {"是否开启通过 http 接口获取 log 文件的功能",
             "Whether to enable the function of getting log files through http interface"})
     public static boolean enable_get_log_file_api = false;
 
@@ -2855,20 +2860,20 @@ public class Config extends ConfigBase {
     public static boolean enable_collect_internal_query_profile = false;
 
     @ConfField(mutable = false, masterOnly = false, description = {
-        "http请求处理/api/query中sql任务的最大线程池。",
+        "http 请求处理/api/query 中 sql 任务的最大线程池。",
         "The max number work threads of http sql submitter."
     })
     public static int http_sql_submitter_max_worker_threads = 2;
 
     @ConfField(mutable = false, masterOnly = false, description = {
-        "http请求处理/api/upload任务的最大线程池。",
+        "http 请求处理/api/upload 任务的最大线程池。",
         "The max number work threads of http upload submitter."
     })
     public static int http_load_submitter_max_worker_threads = 2;
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "load label个数阈值,超过该个数后,对于已经完成导入作业或者任务,"
-            + "其label会被删除,被删除的 label 可以被重用。 值为 -1 时,表示此阈值不生效。",
+            "load label 个数阈值,超过该个数后,对于已经完成导入作业或者任务,"
+            + "其 label 会被删除,被删除的 label 可以被重用。值为 -1 时,表示此阈值不生效。",
             "The threshold of load labels' number. After this number is exceeded, "
                     + "the labels of the completed import jobs or tasks will be deleted, "
                     + "and the deleted labels can be reused. "
@@ -2890,7 +2895,7 @@ public class Config extends ConfigBase {
     @ConfField
     public static boolean ignore_bdbje_log_checksum_read = false;
 
-    @ConfField(description = {"指定 mysql登录身份认证类型",
+    @ConfField(description = {"指定 mysql 登录身份认证类型",
             "Specifies the authentication type"},
             options = {"default", "ldap"})
     public static String authentication_type = "default";
@@ -2914,8 +2919,8 @@ public class Config extends ConfigBase {
     public static String inverted_index_storage_format = "V2";
 
     @ConfField(mutable = true, masterOnly = true, description = {
-            "是否在unique表mow上开启delete语句写delete predicate。若开启,会提升delete语句的性能,"
-                    + "但delete后进行部分列更新可能会出现部分数据错误的情况。若关闭,会降低delete语句的性能来保证正确性。",
+            "是否在 unique 表 mow 上开启 delete 语句写 delete predicate。若开启,会提升 delete 语句的性能,"
+                    + "但 delete 后进行部分列更新可能会出现部分数据错误的情况。若关闭,会降低 delete 语句的性能来保证正确性。",
             "Enable the 'delete predicate' for DELETE statements. If enabled, it will enhance the performance of "
                     + "DELETE statements, but partial column updates after a DELETE may result in erroneous data. "
                     + "If disabled, it will reduce the performance of DELETE statements to ensure accuracy."
@@ -2965,7 +2970,7 @@ public class Config extends ConfigBase {
     public static int checkpoint_manual_gc_threshold = 0;
 
     @ConfField(mutable = true, description = {
-            "是否在每个请求开始之前打印一遍请求内容, 主要是query语句",
+            "是否在每个请求开始之前打印一遍请求内容,主要是 query 语句",
             "Should the request content be logged before each request starts, specifically the query statements"})
     public static boolean enable_print_request_before_execution = false;
 
@@ -2990,17 +2995,17 @@ public class Config extends ConfigBase {
     public static int profile_waiting_time_for_spill_seconds = 10;
 
     @ConfField(mutable = true, description = {
-            "是否通过检测协调者BE心跳来 abort 事务",
+            "是否通过检测协调者 BE 心跳来 abort 事务",
             "SHould abort txn by checking coorinator be heartbeat"})
     public static boolean enable_abort_txn_by_checking_coordinator_be = true;
 
     @ConfField(mutable = true, description = {
-            "是否在 schema change 过程中, 检测冲突事物并 abort 它",
+            "是否在 schema change 过程中,检测冲突事物并 abort 它",
             "SHould abort txn by checking conflick txn in schema change"})
     public static boolean enable_abort_txn_by_checking_conflict_txn = true;
 
     @ConfField(mutable = true, description = {
-            "内表自动收集时间间隔,当某一列上次收集时间距离当前时间大于该值,则会触发一次新的收集,0表示不会触发。",
+            "内表自动收集时间间隔,当某一列上次收集时间距离当前时间大于该值,则会触发一次新的收集,0 表示不会触发。",
             "Columns that have not been collected within the specified interval will trigger automatic analyze. "
                 + "0 means not trigger."
     })
@@ -3015,7 +3020,7 @@ public class Config extends ConfigBase {
     //==========================================================================
     //                    begin of cloud config
     //==========================================================================
-    @ConfField(description = {"是否启用FE 日志文件按照大小删除策略,当日志大小超过指定大小,删除相关的log。默认为按照时间策略删除",
+    @ConfField(description = {"是否启用 FE 日志文件按照大小删除策略,当日志大小超过指定大小,删除相关的 log。默认为按照时间策略删除",
         "Whether to enable the FE log file deletion policy based on size, "
             + "where logs exceeding the specified size are deleted. "
             + "It is disabled by default and follows a time-based deletion policy."},
@@ -3195,7 +3200,7 @@ public class Config extends ConfigBase {
     @ConfField(mutable = true)
     public static int mow_calculate_delete_bitmap_retry_times = 10;
 
-    @ConfField(mutable = true, description = {"指定S3 Load endpoint白名单, 举例: s3_load_endpoint_white_list=a,b,c",
+    @ConfField(mutable = true, description = {"指定 S3 Load endpoint 白名单,举例:s3_load_endpoint_white_list=a,b,c",
             "the white list for the s3 load endpoint, if it is empty, no white list will be set,"
             + "for example: s3_load_endpoint_white_list=a,b,c"})
     public static String[] s3_load_endpoint_white_list = {};
@@ -3209,7 +3214,7 @@ public class Config extends ConfigBase {
     })
     public static boolean force_azure_blob_global_endpoint = false;
 
-    @ConfField(mutable = true, description = {"指定Jdbc driver url白名单, 举例: jdbc_driver_url_white_list=a,b,c",
+    @ConfField(mutable = true, description = {"指定 Jdbc driver url 白名单,举例:jdbc_driver_url_white_list=a,b,c",
             "the white list for jdbc driver url, if it is empty, no white list will be set"
             + "for example: jdbc_driver_url_white_list=a,b,c"
     })
@@ -3261,30 +3266,30 @@ public class Config extends ConfigBase {
     public static int audit_event_log_queue_size = 250000;
 
     @ConfField(mutable = true, description = {
-            "streamload导入使用的转发策略, 可选值为public-private/public/private/direct/random-be/空",
+            "streamload 导入使用的转发策略,可选值为 public-private/public/private/direct/random-be/空",
             "streamload route policy, available options are "
             + "public-private/public/private/direct/random-be and empty string" })
     public static String streamload_redirect_policy = "";
 
     @ConfField(mutable = true, description = {
-            "存算分离模式下是否启用group commit的streamload BE转发功能。"
-                    + "解决LB随机转发导致group commit攒批失效的问题,通过BE二次转发确保同表请求到达同一BE节点。",
+            "存算分离模式下是否启用 group commit 的 streamload BE 转发功能。"
+                    + "解决 LB 随机转发导致 group commit 攒批失效的问题,通过 BE 二次转发确保同表请求到达同一 BE 节点。",
             "Whether to enable group commit streamload BE forward feature in cloud mode. "
                     + "Solves the issue where LB random forwarding breaks group commit batching "
                     + "by implementing BE-level forwarding to ensure same-table requests reach the same BE node." })
     public static boolean enable_group_commit_streamload_be_forward = false;
 
-    @ConfField(description = {"存算分离模式下建表是否检查残留recycler key, 默认true",
+    @ConfField(description = {"存算分离模式下建表是否检查残留 recycler key, 默认 true",
         "create table in cloud mode, check recycler key remained, default true"})
     public static boolean check_create_table_recycle_key_remained = true;
 
-    @ConfField(mutable = true, description = {"存算分离模式下fe向ms请求锁的过期时间,默认60s"})
+    @ConfField(mutable = true, description = {"存算分离模式下 fe 向 ms 请求锁的过期时间,默认 60s"})
     public static int delete_bitmap_lock_expiration_seconds = 60;
 
-    @ConfField(mutable = true, description = {"存算分离模式下calculate delete bitmap task 超时时间,默认60s"})
+    @ConfField(mutable = true, description = {"存算分离模式下 calculate delete bitmap task 超时时间,默认 60s"})
     public static int calculate_delete_bitmap_task_timeout_seconds = 60;
 
-    @ConfField(mutable = true, description = {"存算分离模式下commit阶段等锁超时时间,默认5s"})
+    @ConfField(mutable = true, description = {"存算分离模式下 commit 阶段等锁超时时间,默认 5s"})
     public static int try_commit_lock_timeout_seconds = 5;
 
     @ConfField(mutable = true, description = {"是否在事务提交时对所有表启用提交锁。设置为 true 时,所有表都会使用提交锁。"
@@ -3295,28 +3300,28 @@ public class Config extends ConfigBase {
             + "Default value is true." })
     public static boolean enable_commit_lock_for_all_tables = true;
 
-    @ConfField(mutable = true, description = {"存算分离模式下是否开启大事务提交,默认false"})
+    @ConfField(mutable = true, description = {"存算分离模式下是否开启大事务提交,默认 false"})
     public static boolean enable_cloud_txn_lazy_commit = false;
 
     @ConfField(mutable = true, masterOnly = true,
-            description = {"存算分离模式下,当tablet分布的be异常,是否立即映射tablet到新的be上,默认false"})
+            description = {"存算分离模式下,当 tablet 分布的 be 异常,是否立即映射 tablet 到新的 be 上,默认 false"})
     public static boolean enable_immediate_be_assign = false;
 
     @ConfField(mutable = true, masterOnly = false,
-            description = { "存算分离模式下,一个BE挂掉多长时间后,它的tablet彻底转移到其他BE上" })
+            description = { "存算分离模式下,一个 BE 挂掉多长时间后,它的 tablet 彻底转移到其他 BE 上" })
     public static int rehash_tablet_after_be_dead_seconds = 3600;
 
 
-    @ConfField(mutable = true, description = {"存算分离模式下是否启用自动启停功能,默认true",
+    @ConfField(mutable = true, description = {"存算分离模式下是否启用自动启停功能,默认 true",
         "Whether to enable the automatic start-stop feature in cloud model, default is true."})
     public static boolean enable_auto_start_for_cloud_cluster = true;
 
-    @ConfField(mutable = true, description = {"存算分离模式下自动启停等待cluster唤醒退避重试次数,默认300次大约5分钟",
+    @ConfField(mutable = true, description = {"存算分离模式下自动启停等待 cluster 唤醒退避重试次数,默认 300 次大约 5 分钟",
         "The automatic start-stop wait time for cluster wake-up backoff retry count in the cloud "
             + "model is set to 300 times, which is approximately 5 minutes by default."})
     public static int auto_start_wait_to_resume_times = 300;
 
-    @ConfField(description = {"Get tablet stat task的最大并发数。",
+    @ConfField(description = {"Get tablet stat task 的最大并发数。",
         "Maximal concurrent num of get tablet stat job."})
     public static int max_get_tablet_stat_task_threads_num = 4;
 
@@ -3328,25 +3333,25 @@ public class Config extends ConfigBase {
             "Max retry times when a schema change job fails, default is 3."})
     public static int schema_change_max_retry_time = 3;
 
-    @ConfField(mutable = true, description = {"是否允许使用ShowCacheHotSpotStmt语句",
+    @ConfField(mutable = true, description = {"是否允许使用 ShowCacheHotSpotStmt 语句",
             "Whether to enable the use of ShowCacheHotSpotStmt, default is false."})
     public static boolean enable_show_file_cache_hotspot_stmt = false;
 
-    @ConfField(mutable = true, description = {"存算分离模式下FE连接meta service的请求超时, 默认30000ms",
+    @ConfField(mutable = true, description = {"存算分离模式下 FE 连接 meta service 的请求超时,默认 30000ms",
             "Request timeout for FE connecting to meta service in cloud mode, default is 30000ms."})
     public static int meta_service_brpc_timeout_ms = 30000;
 
-    @ConfField(mutable = true, description = {"存算分离模式下FE连接meta service的连接超时,默认500ms",
+    @ConfField(mutable = true, description = {"存算分离模式下 FE 连接 meta service 的连接超时,默认 500ms",
             "Connection timeout for FE connecting to meta service in cloud mode., default is 500ms."})
     public static int meta_service_brpc_connect_timeout_ms = 500;
 
-    @ConfField(mutable = true, description = {"存算分离模式下FE请求meta service超时的重试次数,默认1次",
+    @ConfField(mutable = true, description = {"存算分离模式下 FE 请求 meta service 超时的重试次数,默认 1 次",
             "In cloud mode, the retry number when the FE requests the meta service times out is 1 by default"})
     public static int meta_service_rpc_timeout_retry_times = 1;
 
     @ConfField(mutable = true, description = {"存算分离模式下自动启停功能,对于该配置中的数据库名不进行唤醒操作,"
             + "用于内部作业的数据库,例如统计信息用到的数据库,"
-            + "举例: auto_start_ignore_db_names=__internal_schema, information_schema",
+            + "举例:auto_start_ignore_db_names=__internal_schema, information_schema",
             "In the cloud mode, the automatic start and stop ignores the DB name of the internal job,"
             + "used for databases involved in internal jobs, such as those used for statistics, "
             + "For example: auto_start_ignore_db_names=__internal_schema, information_schema"
@@ -3377,7 +3382,7 @@ public class Config extends ConfigBase {
     public static long resource_not_ready_sleep_seconds = 5;
 
     @ConfField(mutable = true, description = {
-            "设置为 true,如果查询无法选择到健康副本时,会打印出该tablet所有副本的详细信息,"})
+            "设置为 true,如果查询无法选择到健康副本时,会打印出该 tablet 所有副本的详细信息,"})
     public static boolean sql_block_rule_ignore_admin = false;
     //==========================================================================
     //                      start of lock config
@@ -3397,12 +3402,12 @@ public class Config extends ConfigBase {
         "Is metadata synchronization enabled in safe mode"})
     public static boolean meta_helper_security_mode = false;
 
-    @ConfField(description = {"用于测试,强制将所有的查询forward到master以验证forward query的行为",
+    @ConfField(description = {"用于测试,强制将所有的查询 forward 到 master 以验证 forward query 的行为",
             "For testing purposes, all queries are forcibly forwarded to the master to verify"
                     + "the behavior of forwarding queries."})
     public static boolean force_forward_all_queries = false;
 
-    @ConfField(description = {"用于禁用某些SQL,配置项为AST的class simple name列表(例如CreateRepositoryStmt,"
+    @ConfField(description = {"用于禁用某些 SQL,配置项为 AST 的 class simple name 列表 (例如 CreateRepositoryStmt,"
             + "CreatePolicyCommand),用逗号间隔开",
             "For disabling certain SQL queries, the configuration item is a list of simple class names of AST"
                     + "(for example CreateRepositoryStmt, CreatePolicyCommand), separated by commas."})
diff --git a/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java b/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java
index d5b7cd7354e..47b9fb3137d 100755
--- a/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/DorisFE.java
@@ -501,7 +501,7 @@ public class DorisFE {
             releaseFileLockAndCloseFileChannel();
             throw new RuntimeException("Try to lock process failed", e);
         }
-        throw new RuntimeException("FE process has been started,please do not start multiple FE processes at the "
+        throw new RuntimeException("FE process has been started, please do not start multiple FE processes at the "
                 + "same time");
     }
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
index 364cacab538..71b478ab12a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java
@@ -1395,6 +1395,11 @@ public class SchemaChangeHandler extends AlterHandler {
 
         TStorageFormat storageFormat = PropertyAnalyzer.analyzeStorageFormat(propertyMap);
 
+        // if not explicitly specified in ALTER statement, use the current storage format of the table
+        if (!propertyMap.containsKey("storage_format")) {
+            storageFormat = olapTable.getStorageFormat();
+        }
+
         // property store_row_column && row_store_columns
         // eg. "store_row_column" = "true"
         // eg. "row_store_columns" = "k1, k2"
@@ -1662,14 +1667,7 @@ public class SchemaChangeHandler extends AlterHandler {
         schemaChangeJob.setStoreRowColumnInfo(hasRowStoreChanged, storeRowColumn, rsColumns);
         schemaChangeJob.setEnableUniqueKeySkipBitmapInfo(enableUniqueKeySkipBitmap,
                 hasEnableUniqueKeySkipBitmapChanged);
-
-        // If StorageFormat is set to TStorageFormat.V2
-        // which will create tablet with preferred_rowset_type set to BETA
-        // for both base table and rollup index
-        if (hasIndexChange) {
-            // only V2 support index, so if there is index changed, storage format must be V2
-            storageFormat = TStorageFormat.V2;
-        }
+        // set storage format of schema change job
         schemaChangeJob.setStorageFormat(storageFormat);
 
         // the following operations are done outside the 'for indices' loop
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
index e50a40304be..3d971971dea 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java
@@ -1160,11 +1160,14 @@ public class PropertyAnalyzer {
             return TStorageFormat.V2;
         }
 
-        if (storageFormat.equalsIgnoreCase("v1")) {
+        if (storageFormat.equalsIgnoreCase("V1")) {
             throw new AnalysisException("Storage format V1 has been deprecated since version 0.14, "
                     + "please use V2 instead");
-        } else if (storageFormat.equalsIgnoreCase("v2")) {
+        } else if (storageFormat.equalsIgnoreCase("V2")) {
             return TStorageFormat.V2;
+        } else if (storageFormat.equalsIgnoreCase("V3")) {
+            // V3 (V2 + external column meta feature)
+            return TStorageFormat.V3;
         } else if (storageFormat.equalsIgnoreCase("default")) {
             return TStorageFormat.V2;
         } else {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
index a9fa6d42df0..664d6305457 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java
@@ -79,6 +79,7 @@ import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.util.TypeCoercionUtils;
 import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
 import org.apache.doris.thrift.TInvertedIndexFileStorageFormat;
 
 import com.google.common.base.Preconditions;
@@ -88,6 +89,8 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -104,7 +107,6 @@ import java.util.stream.Collectors;
  * table info in creating table.
  */
 public class CreateTableInfo {
-
     public static final String ENGINE_OLAP = "olap";
     public static final String ENGINE_JDBC = "jdbc";
     public static final String ENGINE_ELASTICSEARCH = "elasticsearch";
@@ -113,6 +115,8 @@ public class CreateTableInfo {
     public static final String ENGINE_BROKER = "broker";
     public static final String ENGINE_HIVE = "hive";
     public static final String ENGINE_ICEBERG = "iceberg";
+
+    private static final Logger LOG = LogManager.getLogger(CreateTableInfo.class);
     private static final ImmutableSet<AggregateType> GENERATED_COLUMN_ALLOW_AGG_TYPE =
             ImmutableSet.of(AggregateType.REPLACE, AggregateType.REPLACE_IF_NOT_NULL);
 
@@ -382,6 +386,19 @@ public class CreateTableInfo {
         if (engineName.equalsIgnoreCase(ENGINE_OLAP)) {
             boolean enableDuplicateWithoutKeysByDefault = false;
             properties = PropertyAnalyzer.getInstance().rewriteOlapProperties(ctlName, dbName, properties);
+
+            // In fuzzy tests, randomly set storage_format=V3 (ext_meta) for some tables.
+            SessionVariable sv = ctx.getSessionVariable();
+            boolean randomUseV3 =
+                    sv != null && sv.randomUseV3StorageFormat && Config.use_fuzzy_session_variable;
+            if (randomUseV3
+                    && properties != null
+                    && !properties.containsKey(PropertyAnalyzer.PROPERTIES_STORAGE_FORMAT)) {
+                properties.put(PropertyAnalyzer.PROPERTIES_STORAGE_FORMAT, "V3");
+                LOG.info("Randomly set storage_format=V3 for table {}.{} in fuzzy mode (session={})",
+                         dbName, tableName,
+                         ctx != null ? ctx.getSessionId() : "<null>");
+            }
             try {
                 if (properties != null) {
                     enableDuplicateWithoutKeysByDefault =
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index bc0c440bfec..ec1c67d1ee8 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -2635,6 +2635,15 @@ public class SessionVariable implements Serializable, Writable {
     )
     public int defaultVariantMaxSparseColumnStatisticsSize = 10000;
 
+    @VariableMgr.VarAttr(
+            name = "random_use_v3_storage_format",
+            fuzzy = true,
+            description = {
+                    "In fuzzy tests, randomly use V3 storage_format (ext_meta) for some tables.",
+                    "Only takes effect when user does not explicitly specify storage_format."}
+    )
+    public boolean randomUseV3StorageFormat = false;
+
     // If this fe is in fuzzy mode, then will use initFuzzyModeVariables to generate some variables,
     // not the default value set in the code.
     @SuppressWarnings("checkstyle:Indentation")
@@ -2656,6 +2665,7 @@ public class SessionVariable implements Serializable, Writable {
         this.enableShareHashTableForBroadcastJoin = random.nextBoolean();
         this.defaultVariantMaxSubcolumnsCount = random.nextInt(10);
         this.defaultEnableTypedPathsToSparse = random.nextBoolean();
+        this.randomUseV3StorageFormat = random.nextBoolean();
         int randomInt = random.nextInt(4);
         if (randomInt % 2 == 0) {
             this.rewriteOrToInPredicateThreshold = 100000;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java b/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
index a5b4ef1a1c3..352db545a66 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/task/CreateReplicaTask.java
@@ -362,7 +362,6 @@ public class CreateReplicaTask extends AgentTask {
                 }
             }
             tSchema.setIndexes(tIndexes);
-            storageFormat = TStorageFormat.V2;
         }
 
         if (bfColumns != null) {
diff --git a/gensrc/proto/olap_file.proto b/gensrc/proto/olap_file.proto
index 1e97d5ad476..6ace01a9257 100644
--- a/gensrc/proto/olap_file.proto
+++ b/gensrc/proto/olap_file.proto
@@ -427,6 +427,9 @@ message TabletSchemaPB {
     optional int32 skip_bitmap_col_idx = 29 [default = -1];
     optional int64 storage_page_size = 30 [default=65536];
     optional int64 storage_dict_page_size = 31 [default=262144];
+
+    // Whether new segments of this tablet use externalized ColumnMetaPB layout (CMO)
+    optional bool is_external_segment_meta_used_default = 32 [default = false];
 }
 
 message TabletSchemaCloudPB {
@@ -463,6 +466,9 @@ message TabletSchemaCloudPB {
     optional int64 storage_page_size = 31 [default=65536];
     optional int64 storage_dict_page_size = 32 [default=262144];
 
+    // Whether new segments of this tablet use externalized ColumnMetaPB layout (CMO)
+    optional bool is_external_segment_meta_used_default = 33 [default = false];
+
     optional bool is_dynamic_schema = 100 [default=false];
 }
 
diff --git a/gensrc/proto/segment_v2.proto b/gensrc/proto/segment_v2.proto
index d2f2199e5ba..e902a5989e1 100644
--- a/gensrc/proto/segment_v2.proto
+++ b/gensrc/proto/segment_v2.proto
@@ -209,6 +209,16 @@ message ColumnMetaPB {
     optional uint64 none_null_size = 23 [default = 0];
 }
 
+// External column meta entry describing one top-level column's externalized
+// ColumnMetaPB in the Column Meta Region.
+message ColumnMetaEntryPB {
+    // unique column id (top-level only, including variant roots)
+    optional int32 unique_id = 1;
+    // serialized length in bytes of this column's ColumnMetaPB in the
+    // external Column Meta Region.
+    optional uint32 length = 2;
+}
+
 message PrimaryKeyIndexMetaPB {
     // primary key index
     optional IndexedColumnMetaPB primary_key_index = 1;
@@ -235,6 +245,14 @@ message SegmentFooterPB {
 
     // Primary key index meta
     optional PrimaryKeyIndexMetaPB primary_key_index_meta = 10;
+
+    // Externalized ColumnMetaPB layout (optional; presence indicates v2 meta layout available)
+    // Start offset of the contiguous per-column ColumnMetaPB region (absolute file offset).
+    optional uint64 col_meta_region_start = 11;
+    // Per-column external meta entries, in col_id order. Each entry contains the top-level
+    // unique id and the serialized length (in bytes) of its ColumnMetaPB in the external
+    // Column Meta Region.
+    repeated ColumnMetaEntryPB column_meta_entries = 12;
 }
 
 message BTreeMetaPB {
diff --git a/gensrc/thrift/AgentService.thrift b/gensrc/thrift/AgentService.thrift
index 91658f929ea..d1130637846 100644
--- a/gensrc/thrift/AgentService.thrift
+++ b/gensrc/thrift/AgentService.thrift
@@ -57,9 +57,11 @@ struct TTabletSchema {
 // V1 for Segment-V1
 // V2 for Segment-V2
 enum TStorageFormat {
-    DEFAULT,
-    V1,
-    V2
+    DEFAULT = 0,
+    V1 = 1,
+    V2 = 2,
+    // V3 stands externalized column meta (CMO)
+    V3 = 3
 }
 
 enum TEncryptionAlgorithm {
diff --git a/regression-test/data/variant_p0/ext_meta/test_storage_format_v2_1.out b/regression-test/data/variant_p0/ext_meta/test_storage_format_v2_1.out
new file mode 100644
index 00000000000..a04a08cbf67
--- /dev/null
+++ b/regression-test/data/variant_p0/ext_meta/test_storage_format_v2_1.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !sql1 --
+1	1
+2	10
+
+-- !sql2 --
+1	2
+2	20
+
+-- !sql3 --
+2	40
+
+-- !sql4 --
+3	500
+
+-- !sql5 --
+1	100
+
+-- !sql6 --
+1	value
+
diff --git a/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_concurrent.out b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_concurrent.out
new file mode 100644
index 00000000000..293c7c5371d
--- /dev/null
+++ b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_concurrent.out
@@ -0,0 +1,29 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !concurrent_1 --
+20
+
+-- !concurrent_2 --
+20
+
+-- !write_read_1 --
+16
+
+-- !write_read_2 --
+10
+
+-- !write_read_3 --
+6
+
+-- !stress_1 --
+15
+
+-- !stress_2 --
+0	0
+1	1
+2	2
+3	3
+4	4
+
+-- !cache_1 --
+5
+
diff --git a/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_edge_cases.out b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_edge_cases.out
new file mode 100644
index 00000000000..b95f6f8f269
--- /dev/null
+++ b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_edge_cases.out
@@ -0,0 +1,174 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !empty_1 --
+1	\N
+2	\N
+3	\N
+
+-- !empty_2 --
+
+-- !empty_3 --
+1
+2
+3
+
+-- !many_1 --
+0	0
+1	0
+2	0
+3	0
+4	0
+5	0
+6	0
+7	0
+8	0
+9	0
+
+-- !many_2 --
+5	50
+6	50
+7	50
+8	50
+9	50
+
+-- !many_3 --
+9	99
+
+-- !many_4 --
+10
+
+-- !mixed_1 --
+1	1
+2	10
+3	100
+4	1000
+
+-- !mixed_2 --
+1	2
+
+-- !mixed_3 --
+2	3
+
+-- !mixed_4 --
+3	4
+
+-- !mixed_5 --
+4	5
+
+-- !mixed_after_compact_1 --
+1	1
+2	10
+3	100
+4	1000
+
+-- !mixed_after_compact_2 --
+4
+
+-- !nested_1 --
+1	value
+2	value2
+3	\N
+4	\N
+
+-- !nested_2 --
+2	123
+
+-- !nested_3 --
+1
+2
+
+-- !nested_4 --
+1
+2
+
+-- !array_1 --
+1	[1, 2, 3]
+2	[10, 20, 30, 40]
+3	\N
+4	\N
+
+-- !array_2 --
+1
+2
+
+-- !evolution_1 --
+1	1
+2	2
+3	3
+4	\N
+
+-- !evolution_2 --
+1	\N
+2	2
+3	3
+4	\N
+
+-- !evolution_3 --
+1	\N
+2	\N
+3	3
+4	\N
+
+-- !evolution_4 --
+1	\N
+2	\N
+3	\N
+4	10
+
+-- !evolution_5 --
+1	\N
+2	\N
+3	\N
+4	20
+
+-- !special_1 --
+1	1
+
+-- !special_2 --
+2	2
+
+-- !special_3 --
+3	3
+
+-- !null_1 --
+1	\N
+2	1
+3	\N
+
+-- !null_2 --
+3
+
+-- !null_3 --
+1
+3
+
+-- !null_4 --
+2
+
+-- !large_1 --
+1	10000
+2	5
+
+-- !large_2 --
+1
+
+-- !toggle_1 --
+1	1
+2	2
+3	3
+
+-- !toggle_2 --
+2	2
+3	3
+
+-- !toggle_3 --
+3	3
+
+-- !toggle_after_compact_1 --
+1	1
+2	2
+3	3
+
+-- !toggle_after_compact_2 --
+2	2
+3	3
+
diff --git a/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_integration.out b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_integration.out
new file mode 100644
index 00000000000..0926c7eb167
--- /dev/null
+++ b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_integration.out
@@ -0,0 +1,120 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !inverted_1 --
+1	Alice Smith
+3	Charlie Smith
+
+-- !inverted_2 --
+1	Alice Smith
+
+-- !inverted_3 --
+2	30
+3	35
+
+-- !inverted_after_compact_1 --
+1	Alice Smith
+3	Charlie Smith
+
+-- !inverted_after_compact_2 --
+1
+
+-- !parent_idx_1 --
+1	Hello World
+3	World News
+
+-- !parent_idx_2 --
+1	This is a test
+2	Another test here
+
+-- !parent_idx_3 --
+1
+
+-- !multi_var_1 --
+1	1	10
+2	100	1000
+3	\N	\N
+
+-- !multi_var_2 --
+1	2
+
+-- !multi_var_3 --
+1	20
+
+-- !multi_var_4 --
+3	4	40
+
+-- !multi_var_after_compact_1 --
+1	1	10
+2	100	1000
+3	\N	\N
+
+-- !multi_var_after_compact_2 --
+3
+
+-- !agg_1 --
+A	300
+B	400
+
+-- !agg_2 --
+A	1.5
+B	2
+
+-- !agg_3 --
+2
+
+-- !join_1 --
+1	Alice	Engineering
+2	Bob	Sales
+3	Charlie	Engineering
+
+-- !join_2 --
+2
+
+-- !union_1 --
+1	table1	100
+2	table1	200
+3	table2	150
+4	table2	250
+
+-- !union_2 --
+table1	2
+table2	2
+
+-- !subquery_1 --
+6	60	A
+7	70	A
+8	80	A
+9	90	A
+10	100	A
+
+-- !subquery_2 --
+A	5
+B	1
+
+-- !having_1 --
+A	600
+
+-- !order_limit_1 --
+1	19	item_1
+2	18	item_2
+3	17	item_3
+4	16	item_4
+5	15	item_5
+
+-- !order_limit_2 --
+9	item_9
+8	item_8
+7	item_7
+
+-- !distinct_1 --
+A
+B
+C
+
+-- !distinct_2 --
+100
+150
+200
+
+-- !distinct_3 --
+3
+
diff --git a/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_with_sparse.out b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_with_sparse.out
new file mode 100644
index 00000000000..d5f580cabc9
--- /dev/null
+++ b/regression-test/data/variant_p0/ext_meta/test_variant_external_meta_with_sparse.out
@@ -0,0 +1,197 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !sparse_1 --
+1	1
+2	10
+3	100
+
+-- !sparse_2 --
+1	2
+2	20
+
+-- !sparse_3 --
+1	3
+2	30
+
+-- !sparse_4 --
+2	40
+
+-- !sparse_5 --
+3	500
+
+-- !sparse_6 --
+3	600
+
+-- !sparse_7 --
+3	700
+
+-- !sparse_after_compact_1 --
+1	1
+2	10
+3	100
+
+-- !sparse_after_compact_2 --
+2	40
+
+-- !sparse_after_compact_3 --
+3	700
+
+-- !bucket_1 --
+0	0
+1	1
+2	2
+3	3
+4	4
+5	5
+6	6
+7	7
+8	8
+9	9
+
+-- !bucket_2 --
+0	0
+1	10
+2	20
+3	30
+4	40
+5	50
+6	60
+7	70
+8	80
+9	90
+
+-- !bucket_3 --
+0	0
+
+-- !bucket_4 --
+5	503
+
+-- !bucket_5 --
+10
+
+-- !bucket_after_compact_1 --
+0	0
+1	1
+2	2
+3	3
+4	4
+5	5
+6	6
+7	7
+8	8
+9	9
+
+-- !bucket_after_compact_2 --
+0	0
+
+-- !mixed_1 --
+1	1
+2	10
+3	100
+
+-- !mixed_2 --
+1	5
+2	50
+
+-- !mixed_3 --
+2	60
+
+-- !mixed_4 --
+3	200
+
+-- !mixed_5 --
+1	\N
+2	\N
+3	\N
+
+-- !threshold_1 --
+1	1
+2	10
+3	100
+4	1000
+
+-- !threshold_2 --
+1	2
+2	20
+3	200
+
+-- !threshold_3 --
+2	30
+3	300
+
+-- !threshold_4 --
+3	400
+
+-- !threshold_5 --
+4	5000
+
+-- !threshold_after_compact_1 --
+1	1
+2	10
+3	100
+4	1000
+
+-- !threshold_after_compact_2 --
+3	400
+
+-- !threshold_after_compact_3 --
+4	5000
+
+-- !toggle_sparse_1 --
+1	1
+2	10
+3	100
+
+-- !toggle_sparse_2 --
+1	2
+2	20
+
+-- !toggle_sparse_3 --
+1	3
+2	30
+
+-- !toggle_sparse_4 --
+1	4
+
+-- !toggle_sparse_5 --
+2	50
+
+-- !toggle_sparse_6 --
+3	600
+
+-- !toggle_sparse_after_compact_1 --
+1	1
+2	10
+3	100
+
+-- !toggle_sparse_after_compact_2 --
+1	4
+
+-- !toggle_sparse_after_compact_3 --
+3
+
+-- !high_cardinality_1 --
+0	0
+1	1
+2	2
+3	3
+4	4
+
+-- !high_cardinality_2 --
+0	0
+
+-- !high_cardinality_3 --
+10	1005
+
+-- !high_cardinality_4 --
+20
+
+-- !high_cardinality_after_compact_1 --
+0	0
+1	1
+2	2
+3	3
+4	4
+
+-- !high_cardinality_after_compact_2 --
+20
+
diff --git a/regression-test/data/variant_p0/query_subcolumns.out b/regression-test/data/variant_p0/query_subcolumns.out
new file mode 100644
index 00000000000..1abc0a027c9
--- /dev/null
+++ b/regression-test/data/variant_p0/query_subcolumns.out
@@ -0,0 +1,211 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
+-- !sql --
+1
+1
+1
+1
+
+-- !sql --
+2
+2
+2
+
+-- !sql --
+3
+3
+
+-- !sql --
+4
+
+-- !sql --
+hello, world
+
diff --git a/regression-test/suites/variant_github_events_nonConcurrent_p2/load.groovy b/regression-test/suites/variant_github_events_nonConcurrent_p2/load.groovy
index 09cb8b8a43e..3d21a05a9ab 100644
--- a/regression-test/suites/variant_github_events_nonConcurrent_p2/load.groovy
+++ b/regression-test/suites/variant_github_events_nonConcurrent_p2/load.groovy
@@ -152,6 +152,20 @@ suite("regression_test_variant_github_events_p2", "nonConcurrent,p2"){
     qt_sql """select cast(v["payload"]["pull_request"]["additions"] as int)  from github_events where v["repo"]["name"] match 'xpressengine' order by 1;"""
     qt_sql """select count()  from github_events where v["repo"]["name"] match 'apache' order by 1;"""
 
+    sql """ALTER TABLE github_events SET("bloom_filter_columns" = "k,v")"""
+
+    waitForSchemaChangeDone {
+        sql """SHOW ALTER TABLE COLUMN WHERE IndexName='github_events' ORDER BY createtime DESC LIMIT 1"""
+        time 600
+    }
+
+    sql """ALTER TABLE github_events2 SET("bloom_filter_columns" = "k,v")"""
+
+    waitForSchemaChangeDone {
+        sql """SHOW ALTER TABLE COLUMN WHERE IndexName='github_events' ORDER BY createtime DESC LIMIT 1"""
+        time 600
+    }
+
     // specify schema
     // sql "alter table github_events2 modify column v variant<`payload.comment.id`:int,`payload.commits.url`:text,`payload.forkee.has_pages`:tinyint>"
     // load_json_data.call("github_events2", """${getS3Url() + '/regression/gharchive.m/2022-11-07-23.json'}""")
diff --git a/regression-test/suites/variant_p0/agg.groovy b/regression-test/suites/variant_p0/agg.groovy
index 97999a122ff..ef9420c84d1 100644
--- a/regression-test/suites/variant_p0/agg.groovy
+++ b/regression-test/suites/variant_p0/agg.groovy
@@ -16,23 +16,24 @@
 // under the License.
 
 suite("regression_test_variant_agg"){
-    sql """DROP TABLE IF EXISTS var_agg"""
+    // sql """DROP TABLE IF EXISTS var_agg"""
 
-    int max_subcolumns_count = Math.floor(Math.random() * 10) + 1
-    def var = "variant<properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")> replace"
-    if (max_subcolumns_count % 2 == 0) {
-        var = "variant <'d' : int, 'b.f' : int, 'xxxx' : string, 'point' : bigint, properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")> replace"
-    }
-    sql """
-        CREATE TABLE IF NOT EXISTS var_agg (
-                k bigint,
-                v ${var},
-                s bigint sum
-            )
-            AGGREGATE KEY(`k`)
-            DISTRIBUTED BY HASH(k) BUCKETS 4
-            properties("replication_num" = "1", "disable_auto_compaction" = "true");
-    """
+    // int max_subcolumns_count = Math.floor(Math.random() * 10) + 1
+    // def var = "variant<properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")> replace"
+    // if (max_subcolumns_count % 2 == 0) {
+    //     var = "variant <'d' : int, 'b.f' : int, 'xxxx' : string, 'point' : bigint, properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")> replace"
+    // }
+    // sql """
+    //     CREATE TABLE IF NOT EXISTS var_agg (
+    //             k bigint,
+    //             v ${var},
+    //             s bigint sum
+    //         )
+    //         AGGREGATE KEY(`k`)
+    //         DISTRIBUTED BY HASH(k) BUCKETS 4
+    //         properties("replication_num" = "1", "disable_auto_compaction" = "true");
+    // """
+sql "alter table var_agg add column s bigint sum;"
     sql """insert into var_agg values (1,  '[1]', 1),(1,  '{"a" : 1}', 1);"""
     sql """insert into var_agg values (2,  '[2]', 2),(1,  '{"a" : [[[1]]]}', 2);"""
     sql """insert into var_agg values (3,  '3', 3),(1,  '{"a" : 1}', 3), (1,  '{"a" : [1]}', 3);"""
diff --git a/regression-test/suites/variant_p0/ext_meta/test_storage_format_v2_1.groovy b/regression-test/suites/variant_p0/ext_meta/test_storage_format_v2_1.groovy
new file mode 100644
index 00000000000..d7fe1e48e3a
--- /dev/null
+++ b/regression-test/suites/variant_p0/ext_meta/test_storage_format_v2_1.groovy
@@ -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.
+
+suite("test_storage_format_v2_1") {
+    def tableName = "test_storage_format_v2_1_table"
+    
+    // Test 1: Create table with storage_format = V2.1
+    sql "DROP TABLE IF EXISTS ${tableName}"
+    sql """
+        CREATE TABLE ${tableName} (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "storage_format" = "V3"
+        );
+    """
+    
+    // Insert some data
+    sql """insert into ${tableName} values (1, '{"a": 1, "b": 2, "c": 3}')"""
+    sql """insert into ${tableName} values (2, '{"a": 10, "b": 20, "d": 40}')"""
+    sql """insert into ${tableName} values (3, '{"e": 500, "f": 600}')"""
+    
+    // Query to verify data is correct
+    qt_sql1 "select k, v['a'] from ${tableName} where cast(v['a'] as int) is not null order by k"
+    qt_sql2 "select k, v['b'] from ${tableName} where cast(v['b'] as int) is not null order by k"
+    qt_sql3 "select k, v['d'] from ${tableName} where cast(v['d'] as int) is not null order by k"
+    qt_sql4 "select k, v['e'] from ${tableName} where cast(v['e'] as int) is not null order by k"
+    
+    // Verify table properties
+    def result = sql "SHOW CREATE TABLE ${tableName}"
+    logger.info("Show create table result: ${result}")
+    assertTrue(result[0][1].contains("V3"), 
+               "Table should be created with storage_format V3")
+    
+    // Cleanup
+    sql "DROP TABLE IF EXISTS ${tableName}"
+    
+    // Test 2: Create table without specifying storage_format (should default to V2)
+    def tableName2 = "test_storage_format_default"
+    sql "DROP TABLE IF EXISTS ${tableName2}"
+    sql """
+        CREATE TABLE ${tableName2} (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1"
+        );
+    """
+    
+    sql """insert into ${tableName2} values (1, '{"x": 100}')"""
+    qt_sql5 "select k, v['x'] from ${tableName2} order by k"
+    
+    sql "DROP TABLE IF EXISTS ${tableName2}"
+    
+    // Test 3: Verify that storage_format is case-insensitive
+    def tableName3 = "test_storage_format_case_insensitive"
+    sql "DROP TABLE IF EXISTS ${tableName3}"
+    sql """
+        CREATE TABLE ${tableName3} (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "storage_format" = "V3"
+        );
+    """
+    
+    sql """insert into ${tableName3} values (1, '{"test": "value"}')"""
+    qt_sql6 "select k, v['test'] from ${tableName3} order by k"
+    
+    sql "DROP TABLE IF EXISTS ${tableName3}"
+}
+
+
diff --git a/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_concurrent.groovy b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_concurrent.groovy
new file mode 100644
index 00000000000..8ba7c427d35
--- /dev/null
+++ b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_concurrent.groovy
@@ -0,0 +1,307 @@
+// 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.
+
+suite("test_variant_external_meta_concurrent", "nonConcurrent") {
+    def set_be_config = { key, value ->
+        String backend_id;
+        def backendId_to_backendIP = [:]
+        def backendId_to_backendHttpPort = [:]
+        getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
+
+        backend_id = backendId_to_backendIP.keySet()[0]
+        def (code, out, err) = update_be_config(backendId_to_backendIP.get(backend_id), 
+                                                backendId_to_backendHttpPort.get(backend_id), 
+                                                key, value)
+        logger.info("update config: code=" + code + ", out=" + out + ", err=" + err)
+    }
+
+    // Test 1: Concurrent reads of external meta
+    sql "DROP TABLE IF EXISTS test_concurrent_read"
+    sql """
+        CREATE TABLE test_concurrent_read (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 4
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Insert data with many subcolumns to stress test external meta loading
+    for (int i = 0; i < 20; i++) {
+        def fields = []
+        for (int j = 0; j < 50; j++) {
+            fields.add("\"field_${j}\": ${i * 100 + j}")
+        }
+        def json = "{" + fields.join(", ") + "}"
+        sql """insert into test_concurrent_read values (${i}, '${json}')"""
+    }
+    
+    // Run concurrent queries that will trigger external meta loading
+    def threads = []
+    def results = [].asSynchronized()
+    
+    for (int t = 0; t < 5; t++) {
+        def threadId = t
+        threads.add(Thread.start {
+            try {
+                // Each thread queries different fields
+                def fieldIdx = threadId * 10
+                def result = sql """
+                    select count(*) from test_concurrent_read 
+                    where cast(v['field_${fieldIdx}'] as int) is not null
+                """
+                results.add([threadId: threadId, success: true, result: result])
+            } catch (Exception e) {
+                results.add([threadId: threadId, success: false, error: e.message])
+                logger.error("Thread ${threadId} failed: ${e.message}")
+            }
+        })
+    }
+    
+    threads.each { it.join() }
+    
+    // Verify all threads succeeded
+    def failures = results.findAll { !it.success }
+    if (!failures.isEmpty()) {
+        logger.error("Concurrent read test failed: ${failures}")
+        throw new Exception("Concurrent read test had failures: ${failures}")
+    }
+    
+    qt_concurrent_1 "select count(*) from test_concurrent_read"
+    qt_concurrent_2 "select count(distinct k) from test_concurrent_read"
+
+    // Test 2: Multiple tables with external meta
+    for (int tableIdx = 0; tableIdx < 5; tableIdx++) {
+        def tableName = "test_multi_table_${tableIdx}"
+        sql "DROP TABLE IF EXISTS ${tableName}"
+        sql """
+            CREATE TABLE ${tableName} (
+                k bigint,
+                v variant
+            )
+            DUPLICATE KEY(`k`)
+            DISTRIBUTED BY HASH(k) BUCKETS 1
+            properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+        """
+        
+        sql """insert into ${tableName} values (1, '{"table_${tableIdx}": ${tableIdx}}')"""
+        sql """insert into ${tableName} values (2, '{"table_${tableIdx}": ${tableIdx * 10}}')"""
+    }
+    
+    // Query all tables concurrently
+    threads = []
+    results = [].asSynchronized()
+    
+    for (int t = 0; t < 5; t++) {
+        def tableIdx = t
+        def tableName = "test_multi_table_${tableIdx}"
+        threads.add(Thread.start {
+            try {
+                def result = sql """
+                    select k, v['table_${tableIdx}'] from ${tableName} order by k
+                """
+                results.add([tableIdx: tableIdx, success: true, rowCount: result.size()])
+            } catch (Exception e) {
+                results.add([tableIdx: tableIdx, success: false, error: e.message])
+                logger.error("Table ${tableIdx} query failed: ${e.message}")
+            }
+        })
+    }
+    
+    threads.each { it.join() }
+    
+    failures = results.findAll { !it.success }
+    if (!failures.isEmpty()) {
+        throw new Exception("Multi-table concurrent test had failures: ${failures}")
+    }
+
+    // Test 3: Concurrent writes and reads
+    sql "DROP TABLE IF EXISTS test_concurrent_write_read"
+    sql """
+        CREATE TABLE test_concurrent_write_read (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 4
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Initial data
+    for (int i = 0; i < 10; i++) {
+        sql """insert into test_concurrent_write_read values (${i}, '{"initial": ${i}}')"""
+    }
+    
+    threads = []
+    results = [].asSynchronized()
+    
+    // Start reader threads
+    for (int t = 0; t < 3; t++) {
+        def threadId = t
+        threads.add(Thread.start {
+            try {
+                for (int i = 0; i < 5; i++) {
+                    def result = sql """select count(*) from test_concurrent_write_read"""
+                    Thread.sleep(100)  // Small delay between reads
+                }
+                results.add([type: 'reader', threadId: threadId, success: true])
+            } catch (Exception e) {
+                results.add([type: 'reader', threadId: threadId, success: false, error: e.message])
+            }
+        })
+    }
+    
+    // Start writer threads
+    for (int t = 0; t < 2; t++) {
+        def threadId = t
+        threads.add(Thread.start {
+            try {
+                for (int i = 0; i < 3; i++) {
+                    def k = 100 + threadId * 10 + i
+                    sql """insert into test_concurrent_write_read values (${k}, '{"writer": ${threadId}}')"""
+                    Thread.sleep(150)
+                }
+                results.add([type: 'writer', threadId: threadId, success: true])
+            } catch (Exception e) {
+                results.add([type: 'writer', threadId: threadId, success: false, error: e.message])
+            }
+        })
+    }
+    
+    threads.each { it.join() }
+    
+    failures = results.findAll { !it.success }
+    if (!failures.isEmpty()) {
+        throw new Exception("Concurrent write-read test had failures: ${failures}")
+    }
+    
+    qt_write_read_1 "select count(*) from test_concurrent_write_read"
+    qt_write_read_2 "select count(*) from test_concurrent_write_read where cast(v['initial'] as int) is not null"
+    qt_write_read_3 "select count(*) from test_concurrent_write_read where cast(v['writer'] as int) is not null"
+
+    // Test 4: Stress test with frequent compaction triggers
+    sql "DROP TABLE IF EXISTS test_compaction_stress"
+    sql """
+        CREATE TABLE test_compaction_stress (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 2
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Insert multiple small segments
+    for (int i = 0; i < 15; i++) {
+        sql """insert into test_compaction_stress values (${i}, '{"segment": ${i}, "value": ${i * 100}}')"""
+    }
+    
+    // Trigger compaction while querying
+    threads = []
+    results = [].asSynchronized()
+    
+    // Query thread
+    threads.add(Thread.start {
+        try {
+            for (int i = 0; i < 10; i++) {
+                def result = sql """select count(*) from test_compaction_stress"""
+                Thread.sleep(200)
+            }
+            results.add([type: 'query', success: true])
+        } catch (Exception e) {
+            results.add([type: 'query', success: false, error: e.message])
+        }
+    })
+    
+    // Give query thread a head start
+    Thread.sleep(100)
+    
+    // Compaction thread
+    threads.add(Thread.start {
+        try {
+            trigger_and_wait_compaction("test_compaction_stress", "full")
+            results.add([type: 'compaction', success: true])
+        } catch (Exception e) {
+            results.add([type: 'compaction', success: false, error: e.message])
+        }
+    })
+    
+    threads.each { it.join() }
+    
+    failures = results.findAll { !it.success }
+    if (!failures.isEmpty()) {
+        throw new Exception("Compaction stress test had failures: ${failures}")
+    }
+    
+    qt_stress_1 "select count(*) from test_compaction_stress"
+    qt_stress_2 "select k, v['segment'] from test_compaction_stress order by k limit 5"
+
+    // Test 5: Cache behavior - repeated queries should hit cache
+    sql "DROP TABLE IF EXISTS test_cache_behavior"
+    sql """
+        CREATE TABLE test_cache_behavior (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Insert data with many subcolumns
+    def fields = []
+    for (int i = 0; i < 100; i++) {
+        fields.add("\"cache_field_${i}\": ${i}")
+    }
+    def json = "{" + fields.join(", ") + "}"
+    
+    for (int i = 0; i < 5; i++) {
+        sql """insert into test_cache_behavior values (${i}, '${json}')"""
+    }
+    
+    // First query - should load external meta
+    def startTime = System.currentTimeMillis()
+    sql """select k, v['cache_field_0'], v['cache_field_50'], v['cache_field_99'] 
+           from test_cache_behavior order by k"""
+    def firstQueryTime = System.currentTimeMillis() - startTime
+    
+    // Subsequent queries - should use cached meta (should be faster)
+    startTime = System.currentTimeMillis()
+    for (int i = 0; i < 5; i++) {
+        sql """select k, v['cache_field_0'], v['cache_field_50'], v['cache_field_99'] 
+               from test_cache_behavior order by k"""
+    }
+    def cachedQueriesTime = System.currentTimeMillis() - startTime
+    def avgCachedTime = cachedQueriesTime / 5
+    
+    logger.info("First query time: ${firstQueryTime}ms, Avg cached query time: ${avgCachedTime}ms")
+    
+    qt_cache_1 "select count(*) from test_cache_behavior"
+
+    // Cleanup
+    sql "DROP TABLE IF EXISTS test_concurrent_read"
+    for (int i = 0; i < 5; i++) {
+        sql "DROP TABLE IF EXISTS test_multi_table_${i}"
+    }
+    sql "DROP TABLE IF EXISTS test_concurrent_write_read"
+    sql "DROP TABLE IF EXISTS test_compaction_stress"
+    sql "DROP TABLE IF EXISTS test_cache_behavior"
+    
+}
+
+
diff --git a/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_edge_cases.groovy b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_edge_cases.groovy
new file mode 100644
index 00000000000..029200519e9
--- /dev/null
+++ b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_edge_cases.groovy
@@ -0,0 +1,296 @@
+// 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.
+
+suite("test_variant_external_meta_edge_cases", "nonConcurrent") {
+    def set_be_config = { key, value ->
+        String backend_id;
+        def backendId_to_backendIP = [:]
+        def backendId_to_backendHttpPort = [:]
+        getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
+
+        backend_id = backendId_to_backendIP.keySet()[0]
+        def (code, out, err) = update_be_config(backendId_to_backendIP.get(backend_id), 
+                                                backendId_to_backendHttpPort.get(backend_id), 
+                                                key, value)
+        logger.info("update config: code=" + code + ", out=" + out + ", err=" + err)
+    }
+
+    // Test 1: Empty subcolumns (only root variant column)
+    sql "DROP TABLE IF EXISTS test_empty_subcolumns"
+    sql """
+        CREATE TABLE test_empty_subcolumns (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_empty_subcolumns values (1, '{}')"""
+    sql """insert into test_empty_subcolumns values (2, 'null')"""
+    sql """insert into test_empty_subcolumns values (3, '[]')"""
+    
+    qt_empty_1 "select k, cast(v as string) from test_empty_subcolumns order by k"
+    qt_empty_2 "select k from test_empty_subcolumns where v is null order by k"
+    qt_empty_3 "select k from test_empty_subcolumns where v is not null order by k"
+
+    // Test 2: Many subcolumns (test performance and correctness)
+    sql "DROP TABLE IF EXISTS test_many_subcolumns"
+    sql """
+        CREATE TABLE test_many_subcolumns (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Generate JSON with 100 subcolumns
+    def generateLargeJson = { int numFields ->
+        def fields = []
+        for (int i = 0; i < numFields; i++) {
+            fields.add("\"field_${i}\": ${i}")
+        }
+        return "{" + fields.join(", ") + "}"
+    }
+    
+    // external meta enabled by table property
+    // Insert rows with different numbers of subcolumns
+    for (int row = 0; row < 10; row++) {
+        int numFields = (row + 1) * 10  // 10, 20, 30, ..., 100 fields
+        def json = generateLargeJson(numFields)
+        sql """insert into test_many_subcolumns values (${row}, '${json}')"""
+    }
+    
+    qt_many_1 "select k, v['field_0'] from test_many_subcolumns order by k"
+    qt_many_2 "select k, v['field_50'] from test_many_subcolumns where cast(v['field_50'] as int) is not null order by k"
+    qt_many_3 "select k, v['field_99'] from test_many_subcolumns where cast(v['field_99'] as int) is not null order by k"
+    qt_many_4 "select count(*) from test_many_subcolumns"
+
+    // Test 3: Mixed old and new format segments
+    sql "DROP TABLE IF EXISTS test_mixed_format"
+    sql """
+        CREATE TABLE test_mixed_format (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Write with old format (V2)
+    sql """insert into test_mixed_format values (1, '{"a": 1, "b": 2}')"""
+    sql """insert into test_mixed_format values (2, '{"a": 10, "c": 3}')"""
+    
+    // Switch to new format (V2.1) and write more rows
+    sql """insert into test_mixed_format values (3, '{"a": 100, "d": 4}')"""
+    sql """insert into test_mixed_format values (4, '{"a": 1000, "e": 5}')"""
+    
+    // Query should work across all segments
+    qt_mixed_1 "select k, v['a'] from test_mixed_format order by k"
+    qt_mixed_2 "select k, v['b'] from test_mixed_format where cast(v['b'] as int) is not null order by k"
+    qt_mixed_3 "select k, v['c'] from test_mixed_format where cast(v['c'] as int) is not null order by k"
+    qt_mixed_4 "select k, v['d'] from test_mixed_format where cast(v['d'] as int) is not null order by k"
+    qt_mixed_5 "select k, v['e'] from test_mixed_format where cast(v['e'] as int) is not null order by k"
+    
+    // Trigger compaction and verify
+    trigger_and_wait_compaction("test_mixed_format", "full")
+    
+    qt_mixed_after_compact_1 "select k, v['a'] from test_mixed_format order by k"
+    qt_mixed_after_compact_2 "select count(distinct k) from test_mixed_format"
+
+    // Test 4: Nested structures with external meta
+    sql "DROP TABLE IF EXISTS test_nested_external"
+    sql """
+        CREATE TABLE test_nested_external (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_nested_external values (1, '{"nested": {"level1": {"level2": "value"}}}')"""
+    sql """insert into test_nested_external values (2, '{"nested": {"level1": {"level2": "value2", "level2_b": 123}}}')"""
+    sql """insert into test_nested_external values (3, '{"nested": {"level1": null}}')"""
+    sql """insert into test_nested_external values (4, '{"nested": null}')"""
+    
+    qt_nested_1 "select k, v['nested']['level1']['level2'] from test_nested_external order by k"
+    qt_nested_2 "select k, v['nested']['level1']['level2_b'] from test_nested_external where cast(v['nested']['level1']['level2_b'] as int) is not null order by k"
+    qt_nested_3 "select k from test_nested_external where v['nested'] is not null order by k"
+    qt_nested_4 "select k from test_nested_external where v['nested']['level1'] is not null order by k"
+
+    // Test 5: Array types with external meta
+    sql "DROP TABLE IF EXISTS test_array_external"
+    sql """
+        CREATE TABLE test_array_external (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_array_external values (1, '{"arr": [1, 2, 3]}')"""
+    sql """insert into test_array_external values (2, '{"arr": [10, 20, 30, 40]}')"""
+    sql """insert into test_array_external values (3, '{"arr": []}')"""
+    sql """insert into test_array_external values (4, '{"arr": null}')"""
+    
+    qt_array_1 "select k, v['arr'] from test_array_external order by k"
+    qt_array_2 "select k from test_array_external where v['arr'] is not null order by k"
+
+    // Test 6: Schema evolution - adding subcolumns over time
+    sql "DROP TABLE IF EXISTS test_schema_evolution"
+    sql """
+        CREATE TABLE test_schema_evolution (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_schema_evolution values (1, '{"a": 1}')"""
+    
+    // Segment 2: add field 'b'
+    sql """insert into test_schema_evolution values (2, '{"a": 2, "b": 2}')"""
+    
+    // Segment 3: add field 'c'
+    sql """insert into test_schema_evolution values (3, '{"a": 3, "b": 3, "c": 3}')"""
+    
+    // Segment 4: completely different schema
+    sql """insert into test_schema_evolution values (4, '{"x": 10, "y": 20}')"""
+    
+    qt_evolution_1 "select k, v['a'] from test_schema_evolution order by k"
+    qt_evolution_2 "select k, v['b'] from test_schema_evolution order by k"
+    qt_evolution_3 "select k, v['c'] from test_schema_evolution order by k"
+    qt_evolution_4 "select k, v['x'] from test_schema_evolution order by k"
+    qt_evolution_5 "select k, v['y'] from test_schema_evolution order by k"
+
+    // Test 7: Special characters in field names
+    sql "DROP TABLE IF EXISTS test_special_chars"
+    sql """
+        CREATE TABLE test_special_chars (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_special_chars values (1, '{"field-with-dash": 1}')"""
+    sql """insert into test_special_chars values (2, '{"field.with.dot": 2}')"""
+    sql """insert into test_special_chars values (3, '{"field_with_underscore": 3}')"""
+    
+    qt_special_1 "select k, v['field-with-dash'] from test_special_chars where cast(v['field-with-dash'] as int) is not null order by k"
+    qt_special_2 "select k, v['field.with.dot'] from test_special_chars where cast(v['field.with.dot'] as int) is not null order by k"
+    qt_special_3 "select k, v['field_with_underscore'] from test_special_chars where cast(v['field_with_underscore'] as int) is not null order by k"
+
+    // Test 8: NULL handling
+    sql "DROP TABLE IF EXISTS test_null_handling"
+    sql """
+        CREATE TABLE test_null_handling (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_null_handling values (1, '{"a": null}')"""
+    sql """insert into test_null_handling values (2, '{"a": 1}')"""
+    sql """insert into test_null_handling values (3, null)"""
+    
+    qt_null_1 "select k, v['a'] from test_null_handling order by k"
+    qt_null_2 "select k from test_null_handling where v is null order by k"
+    qt_null_3 "select k from test_null_handling where v['a'] is null order by k"
+    qt_null_4 "select k from test_null_handling where v['a'] is not null order by k"
+
+    // Test 9: Large string values
+    sql "DROP TABLE IF EXISTS test_large_strings"
+    sql """
+        CREATE TABLE test_large_strings (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    def largeString = "x" * 10000  // 10KB string
+    sql """insert into test_large_strings values (1, '{"large_field": "${largeString}"}')"""
+    sql """insert into test_large_strings values (2, '{"large_field": "small"}')"""
+    
+    qt_large_1 "select k, length(cast(v['large_field'] as string)) from test_large_strings order by k"
+    qt_large_2 "select k from test_large_strings where length(cast(v['large_field'] as string)) > 100 order by k"
+
+    // Test 10: Config toggle during operation
+    sql "DROP TABLE IF EXISTS test_config_toggle"
+    sql """
+        CREATE TABLE test_config_toggle (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Start with new format (V2.1)
+    sql """insert into test_config_toggle values (1, '{"a": 1}')"""
+    
+    // Switch to legacy (V2)
+    sql """insert into test_config_toggle values (2, '{"a": 2, "b": 2}')"""
+    
+    // Enable again (V2.1)
+    sql """insert into test_config_toggle values (3, '{"a": 3, "b": 3, "c": 3}')"""
+    
+    // All queries should work regardless of config state
+    qt_toggle_1 "select k, v['a'] from test_config_toggle order by k"
+    qt_toggle_2 "select k, v['b'] from test_config_toggle where cast(v['b'] as int) is not null order by k"
+    qt_toggle_3 "select k, v['c'] from test_config_toggle where cast(v['c'] as int) is not null order by k"
+    
+    // Compact and verify
+    trigger_and_wait_compaction("test_config_toggle", "full")
+    
+    qt_toggle_after_compact_1 "select k, v['a'] from test_config_toggle order by k"
+    qt_toggle_after_compact_2 "select k, v['b'] from test_config_toggle where cast(v['b'] as int) is not null order by k"
+
+    // Cleanup
+    sql "DROP TABLE IF EXISTS test_empty_subcolumns"
+    sql "DROP TABLE IF EXISTS test_many_subcolumns"
+    sql "DROP TABLE IF EXISTS test_mixed_format"
+    sql "DROP TABLE IF EXISTS test_nested_external"
+    sql "DROP TABLE IF EXISTS test_array_external"
+    sql "DROP TABLE IF EXISTS test_schema_evolution"
+    sql "DROP TABLE IF EXISTS test_special_chars"
+    sql "DROP TABLE IF EXISTS test_null_handling"
+    sql "DROP TABLE IF EXISTS test_large_strings"
+    sql "DROP TABLE IF EXISTS test_config_toggle"
+    
+}
+
+
diff --git a/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_integration.groovy b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_integration.groovy
new file mode 100644
index 00000000000..fa68799b041
--- /dev/null
+++ b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_integration.groovy
@@ -0,0 +1,355 @@
+// 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.
+
+suite("test_variant_external_meta_integration", "nonConcurrent") {
+    def set_be_config = { key, value ->
+        String backend_id;
+        def backendId_to_backendIP = [:]
+        def backendId_to_backendHttpPort = [:]
+        getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
+
+        backend_id = backendId_to_backendIP.keySet()[0]
+        def (code, out, err) = update_be_config(backendId_to_backendIP.get(backend_id), 
+                                                backendId_to_backendHttpPort.get(backend_id), 
+                                                key, value)
+        logger.info("update config: code=" + code + ", out=" + out + ", err=" + err)
+    }
+
+    sql "set enable_match_without_inverted_index = false"
+    sql "set enable_common_expr_pushdown_for_inverted_index = true"
+    sql "set enable_common_expr_pushdown = true"
+
+    // Test 1: External meta with inverted index on extracted columns
+    sql "DROP TABLE IF EXISTS test_inverted_index_extracted"
+    sql """
+        CREATE TABLE test_inverted_index_extracted (
+            k bigint,
+            v variant<'name':string, 'age':int>,
+            INDEX idx_v (v) USING INVERTED PROPERTIES("parser" = "english")
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_inverted_index_extracted values (1, '{"name": "Alice Smith", "age": 25}')"""
+    sql """insert into test_inverted_index_extracted values (2, '{"name": "Bob Johnson", "age": 30}')"""
+    sql """insert into test_inverted_index_extracted values (3, '{"name": "Charlie Smith", "age": 35}')"""
+    
+    // Test inverted index on name field (should work with external meta)
+    qt_inverted_1 "select k, v['name'] from test_inverted_index_extracted where v['name'] match 'Smith' order by k"
+    qt_inverted_2 "select k, v['name'] from test_inverted_index_extracted where v['name'] match 'Alice' order by k"
+    qt_inverted_3 "select k, v['age'] from test_inverted_index_extracted where cast(v['age'] as int) > 25 order by k"
+    
+    // After compaction, inverted index should still work
+    trigger_and_wait_compaction("test_inverted_index_extracted", "full")
+    
+    qt_inverted_after_compact_1 "select k, v['name'] from test_inverted_index_extracted where v['name'] match 'Smith' order by k"
+    qt_inverted_after_compact_2 "select count(*) from test_inverted_index_extracted where v['name'] match 'Johnson'"
+
+    // Test 2: External meta with parent column inverted index
+    sql "DROP TABLE IF EXISTS test_parent_index"
+    sql """
+        CREATE TABLE test_parent_index (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "5")>,
+            INDEX idx_v (v) USING INVERTED PROPERTIES("parser" = "english")
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_parent_index values (1, '{"title": "Hello World", "content": "This is a test"}')"""
+    sql """insert into test_parent_index values (2, '{"title": "Test Document", "content": "Another test here"}')"""
+    sql """insert into test_parent_index values (3, '{"title": "World News", "content": "Breaking news today"}')"""
+    
+    // Query with inherited inverted index
+    qt_parent_idx_1 "select k, v['title'] from test_parent_index where v['title'] match 'World' order by k"
+    qt_parent_idx_2 "select k, v['content'] from test_parent_index where v['content'] match 'test' order by k"
+    qt_parent_idx_3 "select count(*) from test_parent_index where v['title'] match 'Document'"
+
+    // Test 3: Multiple variant columns with external meta
+    sql "DROP TABLE IF EXISTS test_multiple_variants"
+    sql """
+        CREATE TABLE test_multiple_variants (
+            k bigint,
+            v1 variant,
+            v2 variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_multiple_variants values (1, '{"a": 1, "b": 2}', '{"x": 10, "y": 20}')"""
+    sql """insert into test_multiple_variants values (2, '{"a": 100, "c": 300}', '{"x": 1000, "z": 3000}')"""
+    sql """insert into test_multiple_variants values (3, '{"d": 4}', '{"w": 40}')"""
+    
+    // Both variant columns should use external meta independently
+    qt_multi_var_1 "select k, v1['a'], v2['x'] from test_multiple_variants order by k"
+    qt_multi_var_2 "select k, v1['b'] from test_multiple_variants where cast(v1['b'] as int) is not null order by k"
+    qt_multi_var_3 "select k, v2['y'] from test_multiple_variants where cast(v2['y'] as int) is not null order by k"
+    qt_multi_var_4 "select k, v1['d'], v2['w'] from test_multiple_variants where cast(v1['d'] as int) is not null order by k"
+    
+    trigger_and_wait_compaction("test_multiple_variants", "full")
+    
+    qt_multi_var_after_compact_1 "select k, v1['a'], v2['x'] from test_multiple_variants order by k"
+    qt_multi_var_after_compact_2 "select count(*) from test_multiple_variants"
+
+    // Test 4: External meta with aggregation functions
+    sql "DROP TABLE IF EXISTS test_aggregation"
+    sql """
+        CREATE TABLE test_aggregation (
+            k bigint,
+            category varchar(30),
+            v variant
+        )
+        DUPLICATE KEY(`k`, `category`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_aggregation values (1, 'A', '{"value": 100, "count": 1}')"""
+    sql """insert into test_aggregation values (2, 'A', '{"value": 200, "count": 2}')"""
+    sql """insert into test_aggregation values (3, 'B', '{"value": 150, "count": 1}')"""
+    sql """insert into test_aggregation values (4, 'B', '{"value": 250, "count": 3}')"""
+    
+    qt_agg_1 "select category, sum(cast(v['value'] as int)) from test_aggregation group by category order by category"
+    qt_agg_2 "select category, avg(cast(v['count'] as double)) from test_aggregation group by category order by category"
+    qt_agg_3 "select count(distinct category) from test_aggregation where cast(v['value'] as int) > 100"
+
+    // Test 5: External meta with JOIN operations
+    sql "DROP TABLE IF EXISTS test_join_left"
+    sql "DROP TABLE IF EXISTS test_join_right"
+    
+    sql """
+        CREATE TABLE test_join_left (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """
+        CREATE TABLE test_join_right (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_join_left values (1, '{"name": "Alice", "dept_id": 10}')"""
+    sql """insert into test_join_left values (2, '{"name": "Bob", "dept_id": 20}')"""
+    sql """insert into test_join_left values (3, '{"name": "Charlie", "dept_id": 10}')"""
+    
+    sql """insert into test_join_right values (10, '{"dept_name": "Engineering", "location": "Building A"}')"""
+    sql """insert into test_join_right values (20, '{"dept_name": "Sales", "location": "Building B"}')"""
+    
+    qt_join_1 """
+        select l.k, l.v['name'], r.v['dept_name']
+        from test_join_left l 
+        join test_join_right r on cast(l.v['dept_id'] as int) = r.k
+        order by l.k
+    """
+    
+    qt_join_2 """
+        select count(*) 
+        from test_join_left l 
+        join test_join_right r on cast(l.v['dept_id'] as int) = r.k
+        where r.v['location'] = 'Building A'
+    """
+
+    // Test 6: External meta with UNION operations
+    sql "DROP TABLE IF EXISTS test_union_1"
+    sql "DROP TABLE IF EXISTS test_union_2"
+    
+    sql """
+        CREATE TABLE test_union_1 (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """
+        CREATE TABLE test_union_2 (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_union_1 values (1, '{"source": "table1", "value": 100}')"""
+    sql """insert into test_union_1 values (2, '{"source": "table1", "value": 200}')"""
+    
+    sql """insert into test_union_2 values (3, '{"source": "table2", "value": 150}')"""
+    sql """insert into test_union_2 values (4, '{"source": "table2", "value": 250}')"""
+    
+    qt_union_1 """
+        select k, v['source'], v['value'] from test_union_1
+        union all
+        select k, v['source'], v['value'] from test_union_2
+        order by k
+    """
+    
+    qt_union_2 """
+        select cast(v['source'] as string) as src, count(*) 
+        from (
+            select v from test_union_1
+            union all
+            select v from test_union_2
+        ) t
+        group by src
+        order by src
+    """
+
+    // Test 7: External meta with subquery and WHERE clause
+    sql "DROP TABLE IF EXISTS test_subquery"
+    sql """
+        CREATE TABLE test_subquery (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    for (int i = 1; i <= 10; i++) {
+        sql """insert into test_subquery values (${i}, '{"score": ${i * 10}, "grade": "${i > 5 ? 'A' : 'B'}"}')"""
+    }
+    
+    qt_subquery_1 """
+        select k, v['score'], v['grade']
+        from test_subquery
+        where cast(v['score'] as int) > (select avg(cast(v['score'] as int)) from test_subquery)
+        order by k
+    """
+    
+    qt_subquery_2 """
+        select cast(v['grade'] as string) as grade, count(*) as cnt
+        from test_subquery
+        where k in (select k from test_subquery where cast(v['score'] as int) >= 50)
+        group by grade
+        order by grade
+    """
+
+    // Test 8: External meta with HAVING clause
+    sql "DROP TABLE IF EXISTS test_having"
+    sql """
+        CREATE TABLE test_having (
+            k bigint,
+            category varchar(30),
+            v variant
+        )
+        DUPLICATE KEY(`k`, `category`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_having values (1, 'A', '{"amount": 100}')"""
+    sql """insert into test_having values (2, 'A', '{"amount": 200}')"""
+    sql """insert into test_having values (3, 'A', '{"amount": 300}')"""
+    sql """insert into test_having values (4, 'B', '{"amount": 50}')"""
+    sql """insert into test_having values (5, 'B', '{"amount": 60}')"""
+    
+    qt_having_1 """
+        select category, sum(cast(v['amount'] as int)) as total
+        from test_having
+        group by category
+        having total > 150
+        order by category
+    """
+
+    // Test 9: External meta with ORDER BY and LIMIT on variant fields
+    sql "DROP TABLE IF EXISTS test_order_limit"
+    sql """
+        CREATE TABLE test_order_limit (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    for (int i = 1; i <= 20; i++) {
+        sql """insert into test_order_limit values (${i}, '{"priority": ${20 - i}, "name": "item_${i}"}')"""
+    }
+    
+    qt_order_limit_1 """
+        select k, v['priority'], v['name']
+        from test_order_limit
+        order by cast(v['priority'] as int) desc
+        limit 5
+    """
+    
+    qt_order_limit_2 """
+        select k, v['name']
+        from test_order_limit
+        where cast(v['priority'] as int) > 10
+        order by cast(v['priority'] as int)
+        limit 3
+    """
+
+    // Test 10: External meta with DISTINCT on variant fields
+    sql "DROP TABLE IF EXISTS test_distinct"
+    sql """
+        CREATE TABLE test_distinct (
+            k bigint,
+            v variant
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "false", "storage_format" = "V3");
+    """
+    
+    sql """insert into test_distinct values (1, '{"category": "A", "value": 100}')"""
+    sql """insert into test_distinct values (2, '{"category": "B", "value": 200}')"""
+    sql """insert into test_distinct values (3, '{"category": "A", "value": 150}')"""
+    sql """insert into test_distinct values (4, '{"category": "C", "value": 100}')"""
+    
+    qt_distinct_1 "select distinct cast(v['category'] as string) from test_distinct order by cast(v['category'] as string)"
+    qt_distinct_2 "select distinct cast(v['value'] as int) from test_distinct order by cast(v['value'] as int)"
+    qt_distinct_3 "select count(distinct cast(v['category'] as string)) from test_distinct"
+
+    // Cleanup
+    sql "DROP TABLE IF EXISTS test_inverted_index_extracted"
+    sql "DROP TABLE IF EXISTS test_parent_index"
+    sql "DROP TABLE IF EXISTS test_multiple_variants"
+    sql "DROP TABLE IF EXISTS test_aggregation"
+    sql "DROP TABLE IF EXISTS test_join_left"
+    sql "DROP TABLE IF EXISTS test_join_right"
+    sql "DROP TABLE IF EXISTS test_union_1"
+    sql "DROP TABLE IF EXISTS test_union_2"
+    sql "DROP TABLE IF EXISTS test_subquery"
+    sql "DROP TABLE IF EXISTS test_having"
+    sql "DROP TABLE IF EXISTS test_order_limit"
+    sql "DROP TABLE IF EXISTS test_distinct"
+    
+}
+
+
diff --git a/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_with_sparse.groovy b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_with_sparse.groovy
new file mode 100644
index 00000000000..676366758b1
--- /dev/null
+++ b/regression-test/suites/variant_p0/ext_meta/test_variant_external_meta_with_sparse.groovy
@@ -0,0 +1,251 @@
+// 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.
+
+suite("test_variant_external_meta_with_sparse", "nonConcurrent") {
+    def set_be_config = { key, value ->
+        String backend_id;
+        def backendId_to_backendIP = [:]
+        def backendId_to_backendHttpPort = [:]
+        getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
+
+        backend_id = backendId_to_backendIP.keySet()[0]
+        def (code, out, err) = update_be_config(backendId_to_backendIP.get(backend_id), 
+                                                backendId_to_backendHttpPort.get(backend_id), 
+                                                key, value)
+        logger.info("update config: code=" + code + ", out=" + out + ", err=" + err)
+    }
+
+    // Test 1: Sparse columns should be embedded in root's children_columns
+    sql "DROP TABLE IF EXISTS test_sparse_embedded"
+    sql """
+        CREATE TABLE test_sparse_embedded (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "3")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Insert data that will exceed subcolumn limit, creating sparse columns
+    sql """insert into test_sparse_embedded values (1, '{"a": 1, "b": 2, "c": 3}')"""
+    sql """insert into test_sparse_embedded values (2, '{"a": 10, "b": 20, "c": 30, "d": 40}')"""
+    sql """insert into test_sparse_embedded values (3, '{"a": 100, "e": 500, "f": 600, "g": 700}')"""
+    
+    // Query extracted columns (should be in external meta)
+    qt_sparse_1 "select k, v['a'] from test_sparse_embedded order by k"
+    qt_sparse_2 "select k, v['b'] from test_sparse_embedded where cast(v['b'] as int) is not null order by k"
+    qt_sparse_3 "select k, v['c'] from test_sparse_embedded where cast(v['c'] as int) is not null order by k"
+    
+    // Query sparse columns (should be embedded in root's children_columns)
+    qt_sparse_4 "select k, v['d'] from test_sparse_embedded where cast(v['d'] as int) is not null order by k"
+    qt_sparse_5 "select k, v['e'] from test_sparse_embedded where cast(v['e'] as int) is not null order by k"
+    qt_sparse_6 "select k, v['f'] from test_sparse_embedded where cast(v['f'] as int) is not null order by k"
+    qt_sparse_7 "select k, v['g'] from test_sparse_embedded where cast(v['g'] as int) is not null order by k"
+    
+    // Compaction should handle both extracted and sparse columns correctly
+    trigger_and_wait_compaction("test_sparse_embedded", "full")
+    
+    qt_sparse_after_compact_1 "select k, v['a'] from test_sparse_embedded order by k"
+    qt_sparse_after_compact_2 "select k, v['d'] from test_sparse_embedded where cast(v['d'] as int) is not null order by k"
+    qt_sparse_after_compact_3 "select k, v['g'] from test_sparse_embedded where cast(v['g'] as int) is not null order by k"
+
+    // Test 2: Bucketed sparse columns with external meta
+    sql "DROP TABLE IF EXISTS test_bucket_sparse"
+    sql """
+        CREATE TABLE test_bucket_sparse (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "2")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Create multiple rows that will generate bucketed sparse columns
+    for (int i = 0; i < 10; i++) {
+        def fields = ["\"extracted_1\": ${i}", "\"extracted_2\": ${i * 10}"]
+        // Add varying sparse fields
+        for (int j = 0; j < 5; j++) {
+            fields.add("\"sparse_${i}_${j}\": ${i * 100 + j}")
+        }
+        def json = "{" + fields.join(", ") + "}"
+        sql """insert into test_bucket_sparse values (${i}, '${json}')"""
+    }
+    
+    // Query extracted columns
+    qt_bucket_1 "select k, v['extracted_1'] from test_bucket_sparse order by k"
+    qt_bucket_2 "select k, v['extracted_2'] from test_bucket_sparse order by k"
+    
+    // Query sparse columns from different rows
+    qt_bucket_3 "select k, v['sparse_0_0'] from test_bucket_sparse where cast(v['sparse_0_0'] as int) is not null order by k"
+    qt_bucket_4 "select k, v['sparse_5_3'] from test_bucket_sparse where cast(v['sparse_5_3'] as int) is not null order by k"
+    qt_bucket_5 "select count(*) from test_bucket_sparse"
+    
+    // Compact and verify buckets are handled correctly
+    trigger_and_wait_compaction("test_bucket_sparse", "full")
+    
+    qt_bucket_after_compact_1 "select k, v['extracted_1'] from test_bucket_sparse order by k"
+    qt_bucket_after_compact_2 "select k, v['sparse_0_0'] from test_bucket_sparse where cast(v['sparse_0_0'] as int) is not null order by k"
+
+    // Test 3: Mixed extracted, sparse, and non-existent columns
+    sql "DROP TABLE IF EXISTS test_mixed_column_types"
+    sql """
+        CREATE TABLE test_mixed_column_types (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "5")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Row 1: Only extracted columns
+    sql """insert into test_mixed_column_types values (1, '{"e1": 1, "e2": 2, "e3": 3, "e4": 4, "e5": 5}')"""
+    
+    // Row 2: Extracted + sparse
+    sql """insert into test_mixed_column_types values (2, '{"e1": 10, "e2": 20, "e3": 30, "e4": 40, "e5": 50, "s1": 60, "s2": 70}')"""
+    
+    // Row 3: Different set of fields
+    sql """insert into test_mixed_column_types values (3, '{"e1": 100, "new1": 200, "new2": 300}')"""
+    
+    // Query all types
+    qt_mixed_1 "select k, v['e1'] from test_mixed_column_types order by k"  // Exists in all rows (extracted)
+    qt_mixed_2 "select k, v['e5'] from test_mixed_column_types where cast(v['e5'] as int) is not null order by k"  // Exists in row 1,2 (extracted)
+    qt_mixed_3 "select k, v['s1'] from test_mixed_column_types where cast(v['s1'] as int) is not null order by k"  // Exists in row 2 (sparse)
+    qt_mixed_4 "select k, v['new1'] from test_mixed_column_types where cast(v['new1'] as int) is not null order by k"  // Exists in row 3 (sparse or extracted)
+    qt_mixed_5 "select k, v['non_existent'] from test_mixed_column_types order by k"  // Doesn't exist anywhere
+
+    // Test 4: Transition from extracted to sparse
+    sql "DROP TABLE IF EXISTS test_extraction_threshold"
+    sql """
+        CREATE TABLE test_extraction_threshold (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "3")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Segment 1: Within extraction limit
+    sql """insert into test_extraction_threshold values (1, '{"field_a": 1, "field_b": 2}')"""
+    sql """insert into test_extraction_threshold values (2, '{"field_a": 10, "field_b": 20, "field_c": 30}')"""
+    
+    // Segment 2: Exceeds limit, triggers sparse columns
+    sql """insert into test_extraction_threshold values (3, '{"field_a": 100, "field_b": 200, "field_c": 300, "field_d": 400}')"""
+    sql """insert into test_extraction_threshold values (4, '{"field_a": 1000, "field_e": 5000}')"""
+    
+    // All fields should be queryable
+    qt_threshold_1 "select k, v['field_a'] from test_extraction_threshold order by k"
+    qt_threshold_2 "select k, v['field_b'] from test_extraction_threshold where cast(v['field_b'] as int) is not null order by k"
+    qt_threshold_3 "select k, v['field_c'] from test_extraction_threshold where cast(v['field_c'] as int) is not null order by k"
+    qt_threshold_4 "select k, v['field_d'] from test_extraction_threshold where cast(v['field_d'] as int) is not null order by k"
+    qt_threshold_5 "select k, v['field_e'] from test_extraction_threshold where cast(v['field_e'] as int) is not null order by k"
+    
+    // After compaction, schema should be merged correctly
+    trigger_and_wait_compaction("test_extraction_threshold", "full")
+    
+    qt_threshold_after_compact_1 "select k, v['field_a'] from test_extraction_threshold order by k"
+    qt_threshold_after_compact_2 "select k, v['field_d'] from test_extraction_threshold where cast(v['field_d'] as int) is not null order by k"
+    qt_threshold_after_compact_3 "select k, v['field_e'] from test_extraction_threshold where cast(v['field_e'] as int) is not null order by k"
+
+    // Test 5: Sparse columns with config toggle
+    sql "DROP TABLE IF EXISTS test_sparse_config_toggle"
+    sql """
+        CREATE TABLE test_sparse_config_toggle (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "2")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Write with external meta enabled (V2.1)
+    sql """insert into test_sparse_config_toggle values (1, '{"a": 1, "b": 2, "c": 3, "d": 4}')"""
+    
+    // Switch to legacy V2
+    sql """insert into test_sparse_config_toggle values (2, '{"a": 10, "b": 20, "c": 30, "e": 50}')"""
+    
+    // Enable external meta again (V2.1)
+    sql """insert into test_sparse_config_toggle values (3, '{"a": 100, "f": 600, "g": 700}')"""
+    
+    // All queries should work regardless of which format was used
+    qt_toggle_sparse_1 "select k, v['a'] from test_sparse_config_toggle order by k"
+    qt_toggle_sparse_2 "select k, v['b'] from test_sparse_config_toggle where cast(v['b'] as int) is not null order by k"
+    qt_toggle_sparse_3 "select k, v['c'] from test_sparse_config_toggle where cast(v['c'] as int) is not null order by k"
+    qt_toggle_sparse_4 "select k, v['d'] from test_sparse_config_toggle where cast(v['d'] as int) is not null order by k"
+    qt_toggle_sparse_5 "select k, v['e'] from test_sparse_config_toggle where cast(v['e'] as int) is not null order by k"
+    qt_toggle_sparse_6 "select k, v['f'] from test_sparse_config_toggle where cast(v['f'] as int) is not null order by k"
+    
+    // Compact
+    trigger_and_wait_compaction("test_sparse_config_toggle", "full")
+    
+    qt_toggle_sparse_after_compact_1 "select k, v['a'] from test_sparse_config_toggle order by k"
+    qt_toggle_sparse_after_compact_2 "select k, v['d'] from test_sparse_config_toggle where cast(v['d'] as int) is not null order by k"
+    qt_toggle_sparse_after_compact_3 "select count(*) from test_sparse_config_toggle"
+
+    // Test 6: Very high cardinality sparse columns
+    sql "DROP TABLE IF EXISTS test_high_cardinality_sparse"
+    sql """
+        CREATE TABLE test_high_cardinality_sparse (
+            k bigint,
+            v variant<properties("variant_max_subcolumns_count" = "1")>
+        )
+        DUPLICATE KEY(`k`)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+    """
+    
+    // Each row has one extracted column and many unique sparse columns
+    for (int i = 0; i < 20; i++) {
+        def fields = ["\"common\": ${i}"]
+        // Each row has unique sparse fields
+        for (int j = 0; j < 10; j++) {
+            fields.add("\"unique_${i}_${j}\": ${i * 100 + j}")
+        }
+        def json = "{" + fields.join(", ") + "}"
+        sql """insert into test_high_cardinality_sparse values (${i}, '${json}')"""
+    }
+    
+    // Query common extracted field
+    qt_high_cardinality_1 "select k, v['common'] from test_high_cardinality_sparse order by k limit 5"
+    
+    // Query sparse fields (each exists in only one row)
+    qt_high_cardinality_2 "select k, v['unique_0_0'] from test_high_cardinality_sparse where cast(v['unique_0_0'] as int) is not null order by k"
+    qt_high_cardinality_3 "select k, v['unique_10_5'] from test_high_cardinality_sparse where cast(v['unique_10_5'] as int) is not null order by k"
+    qt_high_cardinality_4 "select count(*) from test_high_cardinality_sparse"
+    
+    // Compaction with high cardinality sparse columns
+    trigger_and_wait_compaction("test_high_cardinality_sparse", "full")
+    
+    qt_high_cardinality_after_compact_1 "select k, v['common'] from test_high_cardinality_sparse order by k limit 5"
+    qt_high_cardinality_after_compact_2 "select count(*) from test_high_cardinality_sparse"
+
+    // Cleanup
+    sql "DROP TABLE IF EXISTS test_sparse_embedded"
+    sql "DROP TABLE IF EXISTS test_bucket_sparse"
+    sql "DROP TABLE IF EXISTS test_mixed_column_types"
+    sql "DROP TABLE IF EXISTS test_extraction_threshold"
+    sql "DROP TABLE IF EXISTS test_sparse_config_toggle"
+    sql "DROP TABLE IF EXISTS test_high_cardinality_sparse"
+    
+    
+}
+
+
diff --git a/regression-test/suites/variant_p0/predefine/test_variant_compaction_with_sparse_limit.groovy b/regression-test/suites/variant_p0/predefine/test_variant_compaction_with_sparse_limit.groovy
index d47c486047e..5b129ac1363 100644
--- a/regression-test/suites/variant_p0/predefine/test_variant_compaction_with_sparse_limit.groovy
+++ b/regression-test/suites/variant_p0/predefine/test_variant_compaction_with_sparse_limit.groovy
@@ -130,8 +130,8 @@ suite("test_compaction_variant_predefine_with_sparse_limit", "nonConcurrent") {
             order_qt_select "select * from ${tableName} order by k, cast(v as string) limit 5;"
         }
     } finally {
-        sql "DROP TABLE IF EXISTS simple_variant_DUPLICATE"
-        sql "DROP TABLE IF EXISTS simple_variant_UNIQUE"
-        sql "DROP TABLE IF EXISTS simple_variant_AGGREGATE"
+        // sql "DROP TABLE IF EXISTS simple_variant_DUPLICATE"
+        // sql "DROP TABLE IF EXISTS simple_variant_UNIQUE"
+        // sql "DROP TABLE IF EXISTS simple_variant_AGGREGATE"
     }
 }
diff --git a/regression-test/suites/variant_p0/query_subcolumns.groovy b/regression-test/suites/variant_p0/query_subcolumns.groovy
new file mode 100644
index 00000000000..5590bcaab6a
--- /dev/null
+++ b/regression-test/suites/variant_p0/query_subcolumns.groovy
@@ -0,0 +1,94 @@
+// 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.
+
+suite("regression_test_query_subcolumns", "nonConcurrent"){
+
+    def set_be_config = { key, value ->
+        String backend_id;
+        def backendId_to_backendIP = [:]
+        def backendId_to_backendHttpPort = [:]
+        getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort);
+
+        backend_id = backendId_to_backendIP.keySet()[0]
+        def (code, out, err) = update_be_config(backendId_to_backendIP.get(backend_id), backendId_to_backendHttpPort.get(backend_id), key, value)
+        logger.info("update config: code=" + code + ", out=" + out + ", err=" + err)
+    }
+    sql """DROP TABLE IF EXISTS query_subcolumns"""
+
+    // query must use inverted index for match operator
+    sql """ set enable_common_expr_pushdown = true; """
+    sql "set enable_match_without_inverted_index = false"
+    for (int i = 0; i < 5; i++) {
+        int max_subcolumns_count = Math.floor(Math.random() * 10)
+        // int max_subcolumns_count =1 
+        def var = """variant<properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")>,
+                    INDEX idx_v (v) USING INVERTED PROPERTIES("parser" = "english") """
+        if (max_subcolumns_count % 2 == 0) {
+            var = """variant <'d' : int, 'b' : int, 'e' : string, properties(\"variant_max_subcolumns_count\" = \"${max_subcolumns_count}\")>,
+                    INDEX idx_v_e (v) USING INVERTED PROPERTIES("field_pattern" = "e", "parser" = "english", "support_phrase" = "true"),
+                    INDEX idx_v (v) USING INVERTED PROPERTIES("parser" = "english") """
+        }
+        sql "DROP TABLE IF EXISTS query_subcolumns"
+        sql """
+            CREATE TABLE IF NOT EXISTS query_subcolumns (
+                    k bigint,
+                    v ${var}
+                )
+                DUPLICATE KEY(`k`)
+                DISTRIBUTED BY HASH(k) BUCKETS 1
+                properties("replication_num" = "1", "disable_auto_compaction" = "true", "storage_format" = "V3");
+        """
+        sql """insert into query_subcolumns values (1, '{"a" : 1, "b" : "2"}')"""
+        // legacy V2 write
+        sql """insert into query_subcolumns values (2, '{"a" : 1, "b" : "2", "c" : 3}')"""
+        // switch to V2.1
+        sql """insert into query_subcolumns values (3, '{"a" : 1, "b" : "2", "c" : 3, "d" : 4}')"""
+        // keep V2.1
+        sql """insert into query_subcolumns values (4, '{"a" : 1}')"""
+        // switch back to V2
+        sql """insert into query_subcolumns values (5, '{"e" : "hello, world"}')"""
+        // and again V2.1
+        sql """insert into query_subcolumns values (6, '{"f" : "make it work"}')"""
+
+        qt_sql "select v['a'] from query_subcolumns where cast(v['a'] as int) is not null order by k"
+        qt_sql "select v['b'] from query_subcolumns where cast(v['b'] as int) is not null order by k"
+        qt_sql "select v['c'] from query_subcolumns where cast(v['c'] as int) is not null order by k"
+        qt_sql "select v['d'] from query_subcolumns where cast(v['d'] as int) is not null order by k"
+        qt_sql "select v['e'] from query_subcolumns where v['e'] like 'hello%' order by k"
+        if (max_subcolumns_count >= 6) {
+            sql "select v['f'] from query_subcolumns where v['f'] match 'make' order by k"
+        }
+
+        // if (max_subcolumns_count % 2) {
+        //     // sql """alter table query_subcolumns set ("storage_format" = "V3")"""
+        // } else {
+        //     // sql """alter table query_subcolumns set ("storage_format" = "V2")"""
+        // }
+
+        // triger compaction
+        trigger_and_wait_compaction("query_subcolumns", "full")        
+
+        qt_sql "select v['a'] from query_subcolumns where cast(v['a'] as int) is not null order by k"
+        qt_sql "select v['b'] from query_subcolumns where cast(v['b'] as int) is not null order by k"
+        qt_sql "select v['c'] from query_subcolumns where cast(v['c'] as int) is not null order by k"
+        qt_sql "select v['d'] from query_subcolumns where cast(v['d'] as int) is not null order by k"
+        qt_sql "select v['e'] from query_subcolumns where v['e'] like 'hello%' order by k"
+        if (max_subcolumns_count >= 6) {
+            sql "select v['f'] from query_subcolumns where v['f'] match 'make' order by k"
+        }
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org