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

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


The following commit(s) were added to refs/heads/master by this push:
     new 1cd984aa5f3 [opt](build) Cut three more waves of hot include edges in 
the BE header graph (#66672)
1cd984aa5f3 is described below

commit 1cd984aa5f3e54417aad9832e59e41f63dcc8b8d
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Aug 13 11:54:55 2026 +0800

    [opt](build) Cut three more waves of hot include edges in the BE header 
graph (#66672)
    
    > Split out of **https://github.com/apache/doris/pull/66510**, which
    carries the whole
    > BE build-time batch. This PR continues the header-closure surgery
    merged as
    > **#66400** with three more waves, and is independent of the rest of
    that batch —
    > it can be reviewed and merged on its own.
    
    ### What problem does this PR solve?
    
    Related PR: #66400, #66510
    
    Problem Summary:
    
    Three waves of include-edge surgery in the BE header graph, each in the
    same
    "seed first, then cut" shape as #66400: one purely additive commit that
    outlines
    the inline bodies forcing the instantiation and seeds the direct
    includes the cut
    will expose, then one commit that deletes the edges and pins them with
    new
    `build-support/check-header-deps.py` rules.
    
    #### Wave 1 — three hot edges (`Prepare cutting three hot edges` + `Cut
    three hot include edges`)
    
    | edge cut | why it cost so much | how it is made removable |
    |---|---|---|
    | `core/pod_array.h` → `runtime/thread_context.h` | **dead include**
    left over from the PODArray memory-tracking experiment (#50549); the
    tracking logic has since moved into `Allocator` and `pod_array.h` names
    no `thread_context` symbol | just delete it — 203 TUs stop seeing
    `thread_context.h` and 195 of them stop seeing `exec_env.h` (1.37 MB/TU
    differential payload) |
    | `core/column/column.h` → `exec/sort/hybrid_sorter.h` | a `core` →
    `exec` layering violation reaching **808 TUs** | `HybridSorter` only
    appears in virtual signatures, so a forward declaration covers it; the
    `BE_TEST`-only `get_permutation_default` body (it constructs a
    `HybridSorter` by value) moves to `column.cpp` |
    | `core/wide_integer_impl.h` → `boost/multiprecision` (unconditional) |
    4.27 MB of preprocessed closure in **1169 TUs** on every platform
    without an 80-bit long double | the from-double members stay inline (and
    `constexpr`) only where `LDBL_MANT_DIG == 64`; elsewhere they compile
    once in a new `core/wide_integer_from_double.cpp` with explicit
    instantiations for `integer<128\|256, signed\|unsigned>`. They were
    never `constexpr` on those platforms, so no constant evaluation is lost
    |
    
    Preprocessed closure of `column.h`: **16.72 MB → 10.15 MB (-39%)**.
    
    #### Wave 2 — two instantiation amplifiers: RLE and FMT_COMPILE
    
    Both parquet `decoder.h` trees included `util/rle_encoding.h` only
    because inline
    `BaseDictDecoder` bodies made ~530 TUs instantiate the
    `RleBatchDecoder<uint32_t>` → `GetLiteralValues` → `UnpackBatch` →
    `UnpackValues`
    chain — measured at **0.43 CPU s per TU, 231 CPU s total** — on top of
    reparsing
    the 2038-line rle/bit-stream/bit-packing family each time. The eight
    per-page /
    per-batch methods plus the ctor and dtor move out of line, so the
    `unique_ptr<RleBatchDecoder<uint32_t>>` member only needs the complete
    type in
    `decoder.cpp`. Everything moved is dispatched through the vtable at
    every call
    site already, so no generated call changes; the real decode TUs keep
    including
    `rle_encoding.h` directly and inline the chain exactly as before, and
    the
    per-value-hot `LevelDecoder::get_next` path is deliberately untouched.
    
    `core/uint24.h` and `core/value/large_int_value.h` pushed their
    `FMT_COMPILE`
    formatter instantiations (the `"{:04d}-{:02d}-{:02d}"` date formatter is
    the
    single biggest fmt instantiation in the tree, plus the int128
    formatters) into
    ~1150 TUs, **53.5 CPU s**. Those bodies move to `.cpp` files and both
    headers
    drop `<fmt/compile.h>` / `<fmt/format.h>`. They return `std::string` and
    are
    allocation-dominated, so the now-outlined call is noise.
    
    #### Wave 3 — the DataVariants amplifier behind `dependency.h`
    
    `exec/pipeline/dependency.h` and `exec/pipeline/rec_cte_shared_state.h`
    hold
    non-template **in-class inline** bodies — SharedState constructors,
    destructors,
    close paths and three `std::visit` dispatches — that name the full
    `AggregatedDataVariants` / `JoinDataVariants` / `SetDataVariants` /
    `DistinctDataVariants` surface. Such bodies are semantically analyzed
    **when the
    header is parsed**, not when they are called, so every one of the ~128
    TUs that
    transitively include `dependency.h` instantiated that whole surface at a
    flat
    **~0.85 CPU s per TU (~130 CPU s)** — and 105 of those TUs never touch a
    variant.
    
    All of it is per-query setup/teardown, so it moves to `dependency.cpp`
    and a new
    `rec_cte_shared_state.cpp`. With the bodies gone the headers can drop:
    
    - `exec/common/agg_utils.h`, `set_utils.h`, `distinct_agg_utils.h` —
    forward declarations suffice;
    - `exec/common/join_utils.h` → the new light
    `exec/common/join_op_utils.h` (JoinOpVariants and the `AsofIndexGroup`
    family split out of `join_utils.h`, which re-exports them;
    `dependency.h` holds these **by value**, and the new header depends only
    on thrift enums, pdqsort and std containers — not on the hash tables);
    - `exec/operator/join/process_hash_table_probe.h` — dead include;
    - `util/brpc_closure.h` — dead include, and the sole route carrying
    `query_context.h`, `thread_context.h` and `service/brpc.h` into ~100
    pipeline TUs (1.36 MB/TU);
    - `<concurrentqueue.h>` — dead include (152 KB third-party single
    header);
    - `util/brpc_client_cache.h` from `rec_cte_shared_state.h` — the rpc
    send bodies live in the `.cpp` now.
    
    `BucketedAggSharedState::init_instances` also becomes a non-template
    taking
    `std::function`: non-dependent constructs in a member-template body are
    checked
    at definition time, so the old inline template forced the destructor of
    `unique_ptr<BucketedAggDataVariants>` on every includer despite never
    being
    called there.
    
    #### One CI-critical commit rides along
    
    `Opt wide_integer_from_double.cpp out of the PCH` must ship **in this
    PR**, not as
    a follow-up. Upstream's clang toolchain defaults to `ENABLE_PCH=ON`
    (`be/CMakeLists.txt`), and `pch.h` transitively includes
    `wide_integer_impl.h`,
    whose include guard is then already consumed when the impl TU is
    compiled — the
    explicit instantiations would find no definition. It is the only file in
    the tree
    with this interaction, but splitting the two commits apart would leave a
    state
    that fails the clang CI lane.
    
    ### Benefit
    
    Compile-time only; no runtime behavior change.
    
    #### Head-to-head on exactly this PR's content
    
    Cold, cache-free BE builds of this PR's merge base (`c29075a7e10`) and
    its head
    (`03129023ee5`), run back-to-back on the same machine with the repo's
    own
    `--compile-bench` harness — dedicated always-cold build dirs, ccache
    disabled,
    `-j10`, `ENABLE_PCH=ON`, and both runs started from the same cooled
    state
    (load1 2.1 vs 2.3) so neither side pays for the other's heat.
    
    | metric | before | after | delta |
    |---|---|---|---|
    | **build phase wall** | 14m50s | **14m14s** | **-36.3 s (-4.1%)** |
    | **Σ TU wall time** (parallelism-independent) | 2h19m | **2h13m** |
    **-6 min (-4.3%)** |
    | Σ TU cpu (user+sys) | 2h18m | 2h12m | -6 min |
    | effective parallelism | 9.4× | 9.4× | identical ⇒ clean attribution |
    | per-file wall | — | — | **197 improved / 12 regressed / 3 new** |
    
    The improvements land exactly on the predicted targets —
    `pipeline_fragment_context.cpp`
    -6.0 s, `rec_cte_anchor_sink_operator.cpp` -5.0 s, `operator.cpp` -3.7
    s,
    `data_queue.cpp` -3.0 s (all W3 `dependency.h` consumers), and
    `byte_array_dict_decoder.cpp` -2.5 s (the W2 decoder edge).
    
    Of the 12 regressions, **`dependency.cpp` +2.6 s is by design**: that is
    the TU the
    SharedState bodies were moved *into*, so it pays once for what ~128 TUs
    stop paying.
    The rest sit in the noise floor of a `-j10` run, and there is direct
    evidence for
    that floor: `gensrc/build/gen_cpp/cloud.pb.cc`, a generated protobuf TU
    this PR
    cannot touch, moved **+2.3 s** between the two runs.
    
    **Why no end-to-end total-wall number is quoted:** the two trees
    differed *outside*
    the build phase — the baseline tree skipped the contrib submodule step
    while the PR
    tree re-fetched it (+58.1 s), and its gensrc was already generated (-5.6
    s). Those
    phases are not compilation, so only the build-phase and Σ-TU figures
    above are
    attributable to the change.
    
    **One caveat worth stating:** these numbers are with `ENABLE_PCH=ON`. A
    PCH already
    amortizes exactly the kind of shared headers this PR is cutting, so it
    masks part of
    the win — and the upstream compile lane builds *without* a PCH (its
    command line
    carries no `-include-pch`). The effect there should be larger, not
    smaller.
    
    #### Per-wave numbers from the development branch
    
    | wave | effect | measurement |
    |---|---|---|
    | W1 | **-24.3 s** wall, `be/src` CPU **-3.6%** | same-codebase A/B on
    the batch branch |
    | W2 | **-125 s / -6.9%** (30m24s → 28m19s) | same-codebase A/B,
    back-to-back |
    | W3 | ~130 CPU s of parse-time instantiation removed (~0.85 CPU s ×
    ~128 TUs), plus 1.36 MB/TU × ~100 TUs of dead brpc payload | P3.5
    research traces (per-TU CPU, not end-to-end wall) |
    
    These were taken on the batch branch at `-j5`/`-j6` against an older
    base, so they
    do not add up to the head-to-head figure above; they are included
    because they
    attribute the win to each wave separately.
    
    ### Risk and verification
    
    - **Header sweeps.** Every wave was validated with a full
    `-fsyntax-only` sweep over the natural (no-PCH) include closure: W1 1358
    TUs with only the 4 known pre-existing failures, W2 **1364/1364 clean**,
    W3 **0 failing of 1365**.
    - **BE unit tests.** The first sweeps covered `be/src` only, so a full
    `ninja -k 0` over 2422 targets was run to reach the 1024 `be/test` TUs;
    it surfaced 8 failing TUs in 4 families, all repaired in `Repair the BE
    UT build after the include-edge cuts`. **Nothing there restores a cut
    edge and no production header gains an include.** Two of those repairs
    fix a latent defect that predates this PR: `BaseDictDecoder`'s
    defaulted-in-class constructor odr-uses
    `~unique_ptr<RleBatchDecoder<uint32_t>>` in every TU constructing a
    derived decoder, so the header's own claim that the complete type is
    only needed in `decoder.cpp` did not hold — in **both** trees, though
    only the `format/` one had a test reaching it.
    - **Rebased onto current master (`c29075a7e10`) and rebuilt from
    scratch** in a clean worktree, macOS/arm64 + clang 20, `ENABLE_PCH=ON`
    (upstream's clang default, so the PCH opt-out above is exercised):
    **8554/8554 ninja edges, zero failures, `doris_be` links.**
    `compile_commands.json` confirms `wide_integer_from_double.cpp` is the
    one first-party TU compiled without the PCH.
    - **BE unit tests rebuilt on the rebased tree: all 9602 objects compile,
    zero failures**, and the final link resolves every symbol (0 undefined).
    Two pre-existing macOS-only obstacles were hit on the way and are called
    out under *Proactive disclosure*.
    - **Natural-closure sweep (`syntax_sweep.py --no-pch`, the gate added in
    #66616): 0 failing TUs of 1380.** This is the gate that matters for a PR
    that cuts include edges — a normal build with `ENABLE_PCH=ON` cannot see
    a missing include that the PCH happens to supply, which is precisely how
    the `<ranges>` breakage in the last commit reached CI before it reached
    me.
    - **Runtime cost of outlining.** Everything moved out of line is either
    per-query setup/teardown (SharedState ctor/dtor/close), or a
    per-page/per-batch method already reached through a vtable. No per-value
    or per-row hot path was outlined.
    - **Guard rules.** `check-header-deps.py` gains rules pinning
    `pod_array.h !-> thread_context.h`, `column.h !-> exec/sort/`, and both
    `decoder.h` headers `!-> util/rle_encoding.h` (19 rules total, all
    passing).
    
    ### Proactive disclosure
    
    - **One commit here is unrelated to include edges: `Make
    hierarchical_data_iterator_test compile on macOS arm64`.**
    `std::min(*rows, ROWS - current_ordinal)` cannot deduce `_Tp` where
    `size_t` is `unsigned long` and `ordinal_t` (`uint64_t`) is `unsigned
    long long`, which is the case on macOS/arm64 but not on Linux — so CI is
    green while `be/test` does not build on macOS at all. It arrived with
    #66204. It is fixed here because this branch is verified on macOS and
    the break blocks that verification; happy to split it out if a reviewer
    prefers.
    - **The macOS Debug UT link has outgrown the Mach-O format**,
    independently of this PR: `section __debug_names's file offset exceeds
    4GB`. Master's own test growth is what crossed the line — the same
    worktree layout linked fine on 2026-08-10 at 7.6 GB of test debug info,
    and master is now at 8.0 GB across 10 more test TUs, while this PR adds
    one `#include` to each of 5 test files. Omitting the debug map
    (`-Wl,-S`) links the binary cleanly with zero undefined symbols, which
    is how the link was verified here. Worth someone's attention as a
    separate issue.
    - **Cross-platform is the blind spot.** All measurements and all sweeps
    ran on macOS/arm64 + clang 20. Nothing here is platform-specific by
    construction, but the Linux gcc/clang lanes are covered only by upstream
    CI, not by any local gate — please give those two lanes a look.
    - **Two cuts have no scanner rule.** The boost/multiprecision edge is
    preprocessor-gated and `check-header-deps.py` is preprocessor-blind (it
    would flag the impl TU's gated include); the macro structure is
    self-guarding instead — breaking it fails the impl TU's build. The fmt
    cuts are likewise only noted in a comment, since the scanner follows
    quoted project includes.
    - **Three test files carry `clang-format off/on` around their
    includes.** `asof_join_test` and the two `fix_length_dict_decoder` tests
    are include-order-sensitive: the supplying include has to come *before*
    the header under test (ADL cannot reach the global `pdqsort` from
    `std::vector`'s iterators; a `unique_ptr<RleBatchDecoder<uint32_t>>`
    dereference depends on no template parameter, so it binds where the
    template is parsed). Without the marker the formatter sorts the include
    back and breaks the build. The cost was deliberately kept in the tests
    rather than paid by a production header.
    - **`DISALLOW_COPY_AND_ASSIGN` in `storage/olap_define.h` loses its
    trailing semicolon.** `butil/macros.h` defines the same macro without
    one and wins under `#ifndef` in TUs that see butil first, so the two
    expansions have to stay call-site compatible. All 45 call sites in the
    tree already write the `;`.
---
 be/src/cloud/cloud_meta_mgr.cpp                    |   1 +
 be/src/cloud/cloud_meta_mgr.h                      |   1 +
 be/src/core/CMakeLists.txt                         |   7 +
 be/src/core/column/column.cpp                      |   9 +
 be/src/core/column/column.h                        |   9 +-
 be/src/core/column/column_const.h                  |   1 +
 be/src/core/column/column_decimal.cpp              |  28 +++
 be/src/core/column/column_decimal.h                |  26 +--
 be/src/core/pod_array.h                            |   1 -
 .../core/{value/large_int_value.cpp => uint24.cpp} |  34 +---
 be/src/core/uint24.h                               |  13 +-
 be/src/core/value/large_int_value.cpp              |  15 ++
 be/src/core/value/large_int_value.h                |  16 +-
 be/src/core/wide_integer_from_double.cpp           |  50 +++++
 be/src/core/wide_integer_impl.h                    |  32 ++-
 be/src/exec/common/join_op_utils.h                 | 157 ++++++++++++++
 be/src/exec/common/join_utils.h                    | 122 +----------
 be/src/exec/exchange/local_exchanger.h             |   2 +
 be/src/exec/operator/aggregation_sink_operator.h   |   1 +
 .../exec/operator/aggregation_source_operator.cpp  |   1 +
 .../operator/bucketed_aggregation_sink_operator.h  |   1 +
 .../bucketed_aggregation_source_operator.cpp       |   1 +
 be/src/exec/operator/hashjoin_build_sink.cpp       |   1 +
 be/src/exec/operator/hashjoin_probe_operator.cpp   |   1 +
 be/src/exec/operator/hashjoin_probe_operator.h     |   1 +
 .../exec/operator/join/process_hash_table_probe.h  |   1 +
 .../operator/join/process_hash_table_probe_impl.h  |   1 +
 be/src/exec/operator/join_build_sink_operator.h    |   1 +
 be/src/exec/operator/materialization_opertor.h     |   1 +
 .../partitioned_aggregation_source_operator.cpp    |   1 +
 be/src/exec/operator/rec_cte_source_operator.cpp   |   2 +
 be/src/exec/operator/set_probe_sink_operator.cpp   |   1 +
 be/src/exec/operator/set_sink_operator.cpp         |   1 +
 be/src/exec/operator/set_source_operator.cpp       |   1 +
 .../exec/operator/streaming_aggregation_operator.h |   1 +
 be/src/exec/pipeline/dependency.cpp                | 132 ++++++++++++
 be/src/exec/pipeline/dependency.h                  | 174 +++++-----------
 be/src/exec/pipeline/rec_cte_shared_state.cpp      | 157 ++++++++++++++
 be/src/exec/pipeline/rec_cte_shared_state.h        | 141 ++-----------
 be/src/format/parquet/decoder.cpp                  |  23 +++
 be/src/format/parquet/decoder.h                    |  28 +--
 be/src/format_v2/native/native_reader.h            |   1 +
 be/src/format_v2/parquet/reader/native/decoder.cpp | 219 ++++++++++++++++++++
 be/src/format_v2/parquet/reader/native/decoder.h   | 225 ++-------------------
 be/src/storage/compaction/compaction.cpp           |   1 +
 be/src/storage/index/ann/ann_index_writer.h        |  11 +
 be/src/storage/index/index_file_writer.h           |  11 +
 .../index/inverted/inverted_index_searcher.h       |  11 +
 be/src/storage/index/inverted/query/query.h        |  11 +
 .../index/inverted/similarity/bm25_similarity.cpp  |   2 -
 be/src/storage/olap_define.h                       |   7 +-
 be/src/storage/tablet/base_tablet.cpp              |   1 +
 be/test/core/block/block_test.cpp                  |   1 +
 be/test/core/column/column_variant_v2_test.cpp     |   1 +
 be/test/exec/operator/asof_join_test.cpp           |   7 +
 .../fix_length_dict_decoder_empty_dict_test.cpp    |   9 +
 .../parquet/fix_length_dict_decoder_test.cpp       |   9 +
 .../segment/hierarchical_data_iterator_test.cpp    |   6 +-
 build-support/check-header-deps.py                 | 102 ++++++++++
 59 files changed, 1153 insertions(+), 679 deletions(-)

diff --git a/be/src/cloud/cloud_meta_mgr.cpp b/be/src/cloud/cloud_meta_mgr.cpp
index 0a2ebe46635..7a8b2ee8ff2 100644
--- a/be/src/cloud/cloud_meta_mgr.cpp
+++ b/be/src/cloud/cloud_meta_mgr.cpp
@@ -37,6 +37,7 @@
 #include <memory>
 #include <mutex>
 #include <random>
+#include <ranges>
 #include <shared_mutex>
 #include <string>
 #include <type_traits>
diff --git a/be/src/cloud/cloud_meta_mgr.h b/be/src/cloud/cloud_meta_mgr.h
index 4b3f3bed044..1bcfbe69a81 100644
--- a/be/src/cloud/cloud_meta_mgr.h
+++ b/be/src/cloud/cloud_meta_mgr.h
@@ -20,6 +20,7 @@
 
 #include <future>
 #include <memory>
+#include <ranges>
 #include <string>
 #include <tuple>
 #include <variant>
diff --git a/be/src/core/CMakeLists.txt b/be/src/core/CMakeLists.txt
index 84de7cd3a9d..348a375eef0 100644
--- a/be/src/core/CMakeLists.txt
+++ b/be/src/core/CMakeLists.txt
@@ -25,3 +25,10 @@ file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS *.cpp)
 add_library(Core STATIC ${SRC_FILES})
 
 pch_reuse(Core)
+# wide_integer_from_double.cpp defines DORIS_WIDE_INTEGER_FROM_DOUBLE_IMPL_TU
+# before including wide_integer_impl.h to compile the from-double bodies 
exactly
+# once. The PCH already contains that header (via storage/olap_common.h) in
+# declaration-only mode, and its pre-spent include guard would leave the 
explicit
+# instantiations below without definitions. Opt this single TU out of the PCH.
+set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/wide_integer_from_double.cpp
+                            PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
diff --git a/be/src/core/column/column.cpp b/be/src/core/column/column.cpp
index ce49646b839..46471fac2ac 100644
--- a/be/src/core/column/column.cpp
+++ b/be/src/core/column/column.cpp
@@ -23,6 +23,7 @@
 #include "core/column/column_const.h"
 #include "core/column/column_nullable.h"
 #include "core/data_type/data_type.h"
+#include "exec/sort/hybrid_sorter.h"
 #include "exec/sort/sort_block.h"
 #include "util/simd/bits.h"
 
@@ -242,4 +243,12 @@ void IColumn::check_const_only_in_top_level() const {
     for_each_subcolumn(throw_if_const);
 }
 
+#ifdef BE_TEST
+void IColumn::get_permutation_default(bool reverse, size_t limit, int 
nan_direction_hint,
+                                      Permutation& res) const {
+    HybridSorter sorter;
+    get_permutation(reverse, limit, nan_direction_hint, sorter, res);
+}
+#endif
+
 } // namespace doris
diff --git a/be/src/core/column/column.h b/be/src/core/column/column.h
index 9a6f3d05d02..b3545309cbc 100644
--- a/be/src/core/column/column.h
+++ b/be/src/core/column/column.h
@@ -36,7 +36,6 @@
 #include "core/string_ref.h"
 #include "core/typeid_cast.h"
 #include "core/types.h"
-#include "exec/sort/hybrid_sorter.h"
 #include "storage/olap_common.h"
 
 namespace doris {
@@ -47,6 +46,7 @@ namespace doris {
 
 class Arena;
 class ColumnSorter;
+class HybridSorter;
 
 using EqualFlags = std::vector<uint8_t>;
 using EqualRange = std::pair<int, int>;
@@ -514,11 +514,10 @@ public:
     }
 
 #ifdef BE_TEST
+    // Defined in column.cpp: constructs a HybridSorter, which is only
+    // forward-declared here to keep exec/sort out of column.h's closure.
     void get_permutation_default(bool reverse, size_t limit, int 
nan_direction_hint,
-                                 Permutation& res) const {
-        HybridSorter sorter;
-        get_permutation(reverse, limit, nan_direction_hint, sorter, res);
-    }
+                                 Permutation& res) const;
 #endif
 
     /** Split column to smaller columns. Each value goes to column index, 
selected by corresponding element of 'selector'.
diff --git a/be/src/core/column/column_const.h 
b/be/src/core/column/column_const.h
index 7c38fd2cbbe..44b1f9d477a 100644
--- a/be/src/core/column/column_const.h
+++ b/be/src/core/column/column_const.h
@@ -27,6 +27,7 @@
 #include <cstdint>
 #include <functional>
 #include <initializer_list>
+#include <span>
 #include <string>
 #include <type_traits>
 #include <utility>
diff --git a/be/src/core/column/column_decimal.cpp 
b/be/src/core/column/column_decimal.cpp
index eed01445bf8..fed2fe04c3a 100644
--- a/be/src/core/column/column_decimal.cpp
+++ b/be/src/core/column/column_decimal.cpp
@@ -34,6 +34,7 @@
 #include "core/value/decimalv2_value.h"
 #include "exec/common/int_exp.h"
 #include "exec/common/sip_hash.h"
+#include "exec/sort/hybrid_sorter.h"
 #include "exec/sort/sort_block.h"
 #include "util/hash_util.hpp"
 #include "util/simd/bits.h"
@@ -305,6 +306,33 @@ Field ColumnDecimal<T>::operator[](size_t n) const {
     return Field::create_field<T>(*(typename 
PrimitiveTypeTraits<T>::CppType*)(&data[n]));
 }
 
+template <PrimitiveType T>
+template <typename U>
+void ColumnDecimal<T>::permutation(bool reverse, size_t limit, HybridSorter& 
sorter,
+                                   PaddedPODArray<U>& res) const {
+    size_t s = data.size();
+    res.resize(s);
+    for (U i = 0; i < s; ++i) res[i] = i;
+
+    auto sort_end = res.end();
+    if (limit && static_cast<double>(limit) < static_cast<double>(s) / 8.0) {
+        sort_end = res.begin() + limit;
+        if (reverse)
+            std::partial_sort(res.begin(), sort_end, res.end(),
+                              [this](size_t a, size_t b) { return data[a] > 
data[b]; });
+        else
+            std::partial_sort(res.begin(), sort_end, res.end(),
+                              [this](size_t a, size_t b) { return data[a] < 
data[b]; });
+    } else {
+        if (reverse)
+            sorter.sort(res.begin(), res.end(),
+                        [this](size_t a, size_t b) { return data[a] > data[b]; 
});
+        else
+            sorter.sort(res.begin(), res.end(),
+                        [this](size_t a, size_t b) { return data[a] < data[b]; 
});
+    }
+}
+
 template <PrimitiveType T>
 void ColumnDecimal<T>::get_permutation(bool reverse, size_t limit, int, 
HybridSorter& sorter,
                                        IColumn::Permutation& res) const {
diff --git a/be/src/core/column/column_decimal.h 
b/be/src/core/column/column_decimal.h
index 0f2d96f7e34..9d6ec34c8b8 100644
--- a/be/src/core/column/column_decimal.h
+++ b/be/src/core/column/column_decimal.h
@@ -285,31 +285,11 @@ public:
 protected:
     Container data;
     UInt32 scale;
+    // Defined in column_decimal.cpp (its only caller is get_permutation 
there):
+    // the body dereferences HybridSorter, which is forward-declared in 
column.h.
     template <typename U>
     void permutation(bool reverse, size_t limit, HybridSorter& sorter,
-                     PaddedPODArray<U>& res) const {
-        size_t s = data.size();
-        res.resize(s);
-        for (U i = 0; i < s; ++i) res[i] = i;
-
-        auto sort_end = res.end();
-        if (limit && static_cast<double>(limit) < static_cast<double>(s) / 
8.0) {
-            sort_end = res.begin() + limit;
-            if (reverse)
-                std::partial_sort(res.begin(), sort_end, res.end(),
-                                  [this](size_t a, size_t b) { return data[a] 
> data[b]; });
-            else
-                std::partial_sort(res.begin(), sort_end, res.end(),
-                                  [this](size_t a, size_t b) { return data[a] 
< data[b]; });
-        } else {
-            if (reverse)
-                sorter.sort(res.begin(), res.end(),
-                            [this](size_t a, size_t b) { return data[a] > 
data[b]; });
-            else
-                sorter.sort(res.begin(), res.end(),
-                            [this](size_t a, size_t b) { return data[a] < 
data[b]; });
-        }
-    }
+                     PaddedPODArray<U>& res) const;
 
     void ALWAYS_INLINE decimalv2_do_crc(size_t i, uint32_t& hash) const {
         const auto& dec_val = (const DecimalV2Value&)data[i];
diff --git a/be/src/core/pod_array.h b/be/src/core/pod_array.h
index 2ca12dc49ed..4ce168b4858 100644
--- a/be/src/core/pod_array.h
+++ b/be/src/core/pod_array.h
@@ -35,7 +35,6 @@
 #include "common/compiler_util.h" // IWYU pragma: keep
 #include "core/allocator.h"       // IWYU pragma: keep
 #include "core/memcpy_small.h"
-#include "runtime/thread_context.h"
 
 #ifndef NDEBUG
 #include <sys/mman.h>
diff --git a/be/src/core/value/large_int_value.cpp b/be/src/core/uint24.cpp
similarity index 50%
copy from be/src/core/value/large_int_value.cpp
copy to be/src/core/uint24.cpp
index 4fac5285265..7688a02edcf 100644
--- a/be/src/core/value/large_int_value.cpp
+++ b/be/src/core/uint24.cpp
@@ -15,36 +15,20 @@
 // specific language governing permissions and limitations
 // under the License.
 
-#include "core/value/large_int_value.h"
+#include "core/uint24.h"
 
-#include <string>
-
-#include "util/string_parser.hpp"
+#include <fmt/compile.h>
+#include <fmt/format.h>
 
 namespace doris {
 
-std::ostream& operator<<(std::ostream& os, __int128 const& value) {
-    std::ostream::sentry s(os);
-    if (s) {
-        std::string value_str = fmt::format("{}", value);
-        if (os.rdbuf()->sputn(value_str.data(), value_str.size()) != 
value_str.size()) {
-            os.setstate(std::ios_base::badbit);
-        }
-    }
-    return os;
-}
+std::string uint24_t::to_string() const {
+    int value = *reinterpret_cast<const uint24_t*>(data);
+    int mday = value & 31;
+    int mon = value >> 5 & 15;
+    int year = value >> 9;
 
-std::istream& operator>>(std::istream& is, __int128& value) {
-    std::string str;
-    is >> str;
-    StringParser::ParseResult result;
-    value = StringParser::string_to_int<__int128>(str.c_str(), str.size(), 
&result);
-    if (result != StringParser::PARSE_SUCCESS) {
-        is.setstate(std::ios_base::failbit);
-    }
-    return is;
+    return fmt::format(FMT_COMPILE("{:04d}-{:02d}-{:02d}"), year, mon, mday);
 }
 
 } // namespace doris
-
-/* vim: set expandtab ts=4 sw=4 sts=4 tw=100: */
diff --git a/be/src/core/uint24.h b/be/src/core/uint24.h
index f2fc77ef7ab..a98144591ae 100644
--- a/be/src/core/uint24.h
+++ b/be/src/core/uint24.h
@@ -17,8 +17,6 @@
 
 #pragma once
 
-#include <fmt/compile.h>
-
 #include <cstdint>
 #include <cstring>
 #include <iostream>
@@ -128,14 +126,9 @@ public:
         return 0;
     }
 
-    std::string to_string() const {
-        int value = *reinterpret_cast<const uint24_t*>(data);
-        int mday = value & 31;
-        int mon = value >> 5 & 15;
-        int year = value >> 9;
-
-        return fmt::format(FMT_COMPILE("{:04d}-{:02d}-{:02d}"), year, mon, 
mday);
-    }
+    // Defined in uint24.cpp: the FMT_COMPILE format machinery is expensive to
+    // instantiate and this header is included nearly everywhere.
+    std::string to_string() const;
 
     const uint8_t* get_data() const { return data; }
 
diff --git a/be/src/core/value/large_int_value.cpp 
b/be/src/core/value/large_int_value.cpp
index 4fac5285265..37f64df2f02 100644
--- a/be/src/core/value/large_int_value.cpp
+++ b/be/src/core/value/large_int_value.cpp
@@ -17,12 +17,27 @@
 
 #include "core/value/large_int_value.h"
 
+#include <fmt/compile.h>
+#include <fmt/format.h>
+
 #include <string>
 
 #include "util/string_parser.hpp"
 
 namespace doris {
 
+int64_t LargeIntValue::to_buffer(__int128 value, char* buffer) {
+    return fmt::format_to(buffer, FMT_COMPILE("{}"), value) - buffer;
+}
+
+std::string LargeIntValue::to_string(__int128 value) {
+    return fmt::format(FMT_COMPILE("{}"), value);
+}
+
+std::string LargeIntValue::to_string(__uint128_t value) {
+    return fmt::format(FMT_COMPILE("{}"), value);
+}
+
 std::ostream& operator<<(std::ostream& os, __int128 const& value) {
     std::ostream::sentry s(os);
     if (s) {
diff --git a/be/src/core/value/large_int_value.h 
b/be/src/core/value/large_int_value.h
index c65879eb95a..9a2a4878401 100644
--- a/be/src/core/value/large_int_value.h
+++ b/be/src/core/value/large_int_value.h
@@ -16,8 +16,6 @@
 // under the License.
 
 #pragma once
-#include <fmt/compile.h>
-#include <fmt/format.h>
 #include <stdint.h>
 
 #include <cstddef>
@@ -31,14 +29,12 @@ inline const __int128 MIN_INT128 = ((__int128)0x01 << 127);
 
 class LargeIntValue {
 public:
-    static int64_t to_buffer(__int128 value, char* buffer) {
-        return fmt::format_to(buffer, FMT_COMPILE("{}"), value) - buffer;
-    }
-
-    static std::string to_string(__int128 value) { return 
fmt::format(FMT_COMPILE("{}"), value); }
-    static std::string to_string(__uint128_t value) {
-        return fmt::format(FMT_COMPILE("{}"), value);
-    }
+    // Defined in large_int_value.cpp: instantiating the FMT_COMPILE int128
+    // formatter in every includer of this header is expensive.
+    static int64_t to_buffer(__int128 value, char* buffer);
+
+    static std::string to_string(__int128 value);
+    static std::string to_string(__uint128_t value);
 };
 
 std::ostream& operator<<(std::ostream& os, __int128 const& value);
diff --git a/be/src/core/wide_integer_from_double.cpp 
b/be/src/core/wide_integer_from_double.cpp
new file mode 100644
index 00000000000..e0f8b480f39
--- /dev/null
+++ b/be/src/core/wide_integer_from_double.cpp
@@ -0,0 +1,50 @@
+// 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.
+
+// On platforms whose long double lacks an 80-bit mantissa (LDBL_MANT_DIG != 
64),
+// wide_integer_impl.h emulates the from-double intermediate type with
+// boost::multiprecision::cpp_bin_float_double_extended. Making that include 
part
+// of every TU's closure costs ~4.3MB of preprocessed text; instead, ordinary 
TUs
+// only see declarations of set_multiplier / wide_integer_from_builtin(double)
+// and this TU compiles the bodies once. The macro below switches
+// wide_integer_impl.h into "definitions inline here" mode.
+#define DORIS_WIDE_INTEGER_FROM_DOUBLE_IMPL_TU
+
+#include "core/wide_integer.h"
+
+// The `defined(...)` term below is what marks the macro above as used, and it 
has to stay the
+// left operand: the preprocessor short-circuits `&&`, so a false left term 
would skip it and the
+// definition would read as an unused macro (-Werror,-Wunused-macros). That 
happens on exactly the
+// platforms this file exists for the inverse of -- where the 80-bit long 
double exists,
+// wide_integer_impl.h takes its `#if (LDBL_MANT_DIG == 64)` branch and never 
evaluates the
+// `#elif defined(...)` that would otherwise consume the macro. Reading it 
here is also honest:
+// these instantiations exist precisely because this TU is the impl TU.
+#if defined(DORIS_WIDE_INTEGER_FROM_DOUBLE_IMPL_TU) && !(LDBL_MANT_DIG == 64)
+
+// Every wide::integer specialization used by Doris (see core/extended_types.h)
+// gets its from-double conversion emitted here. A new specialization that is
+// constructed from double elsewhere will fail at link time — add it below.
+template void wide::integer<128, signed>::_impl::wide_integer_from_builtin(
+        wide::integer<128, signed>&, double) noexcept;
+template void wide::integer<128, unsigned>::_impl::wide_integer_from_builtin(
+        wide::integer<128, unsigned>&, double) noexcept;
+template void wide::integer<256, signed>::_impl::wide_integer_from_builtin(
+        wide::integer<256, signed>&, double) noexcept;
+template void wide::integer<256, unsigned>::_impl::wide_integer_from_builtin(
+        wide::integer<256, unsigned>&, double) noexcept;
+
+#endif // !(LDBL_MANT_DIG == 64)
diff --git a/be/src/core/wide_integer_impl.h b/be/src/core/wide_integer_impl.h
index a5ad6ae041b..a6e9ecf8032 100644
--- a/be/src/core/wide_integer_impl.h
+++ b/be/src/core/wide_integer_impl.h
@@ -23,7 +23,6 @@
 // and modified by Doris
 #pragma once
 
-#include <boost/math/special_functions/fpclassify.hpp>
 #include <cassert>
 #include <cfloat>
 #include <cmath>
@@ -37,13 +36,28 @@
 
 /// Use same extended double for all platforms
 #if (LDBL_MANT_DIG == 64)
+#include <boost/math/special_functions/fpclassify.hpp>
+
 #define CONSTEXPR_FROM_DOUBLE constexpr
+#define DORIS_WIDE_FROM_DOUBLE_INLINE 1
 using FromDoubleIntermediateType = long double;
-#else
+#elif defined(DORIS_WIDE_INTEGER_FROM_DOUBLE_IMPL_TU)
+#include <boost/math/special_functions/fpclassify.hpp>
 #include <boost/multiprecision/cpp_bin_float.hpp>
 /// `wide_integer_from_builtin` can't be constexpr with non-literal 
`cpp_bin_float_double_extended`
 #define CONSTEXPR_FROM_DOUBLE
+#define DORIS_WIDE_FROM_DOUBLE_INLINE 1
 using FromDoubleIntermediateType = 
boost::multiprecision::cpp_bin_float_double_extended;
+#else
+/// Platforms without an 80-bit long double emulate the intermediate type with
+/// boost::multiprecision, which drags ~4.3MB of preprocessed closure into 
every
+/// TU. Ordinary TUs therefore see only declarations of the two from-double
+/// members below; the bodies are compiled once in
+/// core/wide_integer_from_double.cpp (which defines the *_IMPL_TU macro above
+/// and emits explicit instantiations). wide_integer_from_builtin(double) was
+/// never constexpr on these platforms, so no constant evaluation is lost.
+#define CONSTEXPR_FROM_DOUBLE
+#define DORIS_WIDE_FROM_DOUBLE_INLINE 0
 #endif
 
 namespace wide {
@@ -308,7 +322,11 @@ struct integer<Bits, Signed>::_impl {
      * a_(n - 1) = a_n * max_int + b2, a_n <= max_int <- base case.
      */
     template <class T>
-    constexpr static void set_multiplier(integer<Bits, Signed>& self, T t) 
noexcept {
+    CONSTEXPR_FROM_DOUBLE static void set_multiplier(integer<Bits, Signed>& 
self, T t) noexcept
+#if !DORIS_WIDE_FROM_DOUBLE_INLINE
+            ;
+#else
+    {
         constexpr uint64_t max_int = std::numeric_limits<uint64_t>::max();
         static_assert(std::is_same_v<T, double> || std::is_same_v<T, 
FromDoubleIntermediateType>);
         /// Implementation specific behaviour on overflow (if we don't check 
here, stack overflow will triggered in bigint_cast).
@@ -343,9 +361,14 @@ struct integer<Bits, Signed>::_impl {
         self += static_cast<uint64_t>(t - floor(static_cast<double>(alpha)) *
                                                   static_cast<T>(max_int)); // 
+= b_i
     }
+#endif
 
     CONSTEXPR_FROM_DOUBLE static void wide_integer_from_builtin(integer<Bits, 
Signed>& self,
-                                                                double rhs) 
noexcept {
+                                                                double rhs) 
noexcept
+#if !DORIS_WIDE_FROM_DOUBLE_INLINE
+            ;
+#else
+    {
         constexpr int64_t max_int = std::numeric_limits<int64_t>::max();
         constexpr int64_t min_int = std::numeric_limits<int64_t>::lowest();
 
@@ -374,6 +397,7 @@ struct integer<Bits, Signed>::_impl {
             self = -self;
         }
     }
+#endif
 
     template <size_t Bits2, typename Signed2>
     constexpr static void wide_integer_from_wide_integer(
diff --git a/be/src/exec/common/join_op_utils.h 
b/be/src/exec/common/join_op_utils.h
new file mode 100644
index 00000000000..7b6636f88dc
--- /dev/null
+++ b/be/src/exec/common/join_op_utils.h
@@ -0,0 +1,157 @@
+// 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
+
+// Lightweight join-op level types split out of join_utils.h so that
+// exec/pipeline/dependency.h (which holds JoinOpVariants / AsofIndexVariant
+// members by value) does not have to see the hash-table machinery.
+// Everything here depends only on thrift enums and std containers.
+
+#include <gen_cpp/PlanNodes_types.h>
+#include <pdqsort.h>
+
+#include <cstdint>
+#include <variant>
+#include <vector>
+
+#include "common/compiler_util.h"
+
+namespace doris {
+
+using JoinOpVariants =
+        std::variant<std::integral_constant<TJoinOp::type, 
TJoinOp::INNER_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_SEMI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_ANTI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_OUTER_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::FULL_OUTER_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_OUTER_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::CROSS_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_SEMI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_ANTI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::NULL_AWARE_LEFT_ANTI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::NULL_AWARE_LEFT_SEMI_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::ASOF_LEFT_INNER_JOIN>,
+                     std::integral_constant<TJoinOp::type, 
TJoinOp::ASOF_LEFT_OUTER_JOIN>>;
+
+inline bool is_asof_join(TJoinOp::type join_op) {
+    return join_op == TJoinOp::ASOF_LEFT_INNER_JOIN || join_op == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
+}
+
+template <int JoinOpType>
+inline constexpr bool is_asof_join_op_v =
+        JoinOpType == TJoinOp::ASOF_LEFT_INNER_JOIN || JoinOpType == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
+
+template <int JoinOpType>
+inline constexpr bool is_asof_outer_join_op_v = JoinOpType == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
+
+// ASOF JOIN index with inline values for cache-friendly branchless binary 
search.
+// IntType is the integer representation of the ASOF column value:
+//   uint32_t for DateV2, uint64_t for DateTimeV2 and TimestampTZ.
+// Rows are sorted by asof_value during build, then materialized into SoA 
arrays
+// so probe-side binary search only touches the ASOF values hot path.
+template <typename IntType>
+struct AsofIndexGroup {
+    using int_type = IntType;
+
+    struct Entry {
+        IntType asof_value;
+        uint32_t row_index; // 1-based, 0 = invalid/padding
+    };
+
+    std::vector<Entry> entries;
+    std::vector<IntType> asof_values;
+    std::vector<uint32_t> row_indexes;
+
+    void add_row(IntType value, uint32_t row_idx) { entries.push_back({value, 
row_idx}); }
+
+    void sort_and_finalize() {
+        if (entries.empty()) {
+            return;
+        }
+        if (entries.size() > 1) {
+            pdqsort(entries.begin(), entries.end(),
+                    [](const Entry& a, const Entry& b) { return a.asof_value < 
b.asof_value; });
+        }
+
+        asof_values.resize(entries.size());
+        row_indexes.resize(entries.size());
+        for (size_t i = 0; i < entries.size(); ++i) {
+            asof_values[i] = entries[i].asof_value;
+            row_indexes[i] = entries[i].row_index;
+        }
+
+        std::vector<Entry>().swap(entries);
+    }
+
+    const IntType* values_data() const { return asof_values.data(); }
+
+    // Branchless lower_bound: first i where asof_values[i] >= target
+    ALWAYS_INLINE size_t lower_bound(IntType target) const {
+        size_t lo = 0, n = asof_values.size();
+        while (n > 1) {
+            size_t half = n / 2;
+            lo += half * (asof_values[lo + half] < target);
+            n -= half;
+        }
+        if (lo < asof_values.size()) {
+            lo += (asof_values[lo] < target);
+        }
+        return lo;
+    }
+
+    // Branchless upper_bound: first i where asof_values[i] > target
+    ALWAYS_INLINE size_t upper_bound(IntType target) const {
+        size_t lo = 0, n = asof_values.size();
+        while (n > 1) {
+            size_t half = n / 2;
+            lo += half * (asof_values[lo + half] <= target);
+            n -= half;
+        }
+        if (lo < asof_values.size()) {
+            lo += (asof_values[lo] <= target);
+        }
+        return lo;
+    }
+
+    // Semantics by (is_greater, is_strict):
+    //   (true,  false): probe >= build  ->  find largest  build value <= probe
+    //   (true,  true):  probe >  build  ->  find largest  build value <  probe
+    //   (false, false): probe <= build  ->  find smallest build value >= probe
+    //   (false, true):  probe <  build  ->  find smallest build value >  probe
+    // Returns the build row index of the best match, or 0 if no match.
+    template <bool IsGreater, bool IsStrict>
+    ALWAYS_INLINE uint32_t find_best_match(IntType probe_value) const {
+        if (asof_values.empty()) {
+            return 0;
+        }
+        if constexpr (IsGreater) {
+            size_t pos = IsStrict ? lower_bound(probe_value) : 
upper_bound(probe_value);
+            return pos > 0 ? row_indexes[pos - 1] : 0;
+        } else {
+            size_t pos = IsStrict ? upper_bound(probe_value) : 
lower_bound(probe_value);
+            return pos < asof_values.size() ? row_indexes[pos] : 0;
+        }
+    }
+};
+
+// Type-erased container for all ASOF index groups.
+// DateV2 -> uint32_t, DateTimeV2/TimestampTZ -> uint64_t.
+using AsofIndexVariant = std::variant<std::monostate, 
std::vector<AsofIndexGroup<uint32_t>>,
+                                      std::vector<AsofIndexGroup<uint64_t>>>;
+
+} // namespace doris
diff --git a/be/src/exec/common/join_utils.h b/be/src/exec/common/join_utils.h
index 7482c6fbdae..fc0ae935adc 100644
--- a/be/src/exec/common/join_utils.h
+++ b/be/src/exec/common/join_utils.h
@@ -24,6 +24,7 @@
 #include "exec/common/hash_table/hash_key_type.h"
 #include "exec/common/hash_table/hash_map_context.h"
 #include "exec/common/hash_table/join_hash_table.h"
+#include "exec/common/join_op_utils.h" // IWYU pragma: export 
(JoinOpVariants/AsofIndexGroup moved there)
 
 namespace doris {
 
@@ -45,32 +46,6 @@ decltype(auto) asof_column_dispatch(const IColumn* col, 
Func&& func) {
         return std::forward<Func>(func)(col);
     }
 }
-using JoinOpVariants =
-        std::variant<std::integral_constant<TJoinOp::type, 
TJoinOp::INNER_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_SEMI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_ANTI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::LEFT_OUTER_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::FULL_OUTER_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_OUTER_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::CROSS_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_SEMI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::RIGHT_ANTI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::NULL_AWARE_LEFT_ANTI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::NULL_AWARE_LEFT_SEMI_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::ASOF_LEFT_INNER_JOIN>,
-                     std::integral_constant<TJoinOp::type, 
TJoinOp::ASOF_LEFT_OUTER_JOIN>>;
-
-inline bool is_asof_join(TJoinOp::type join_op) {
-    return join_op == TJoinOp::ASOF_LEFT_INNER_JOIN || join_op == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
-}
-
-template <int JoinOpType>
-inline constexpr bool is_asof_join_op_v =
-        JoinOpType == TJoinOp::ASOF_LEFT_INNER_JOIN || JoinOpType == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
-
-template <int JoinOpType>
-inline constexpr bool is_asof_outer_join_op_v = JoinOpType == 
TJoinOp::ASOF_LEFT_OUTER_JOIN;
-
 template <class T>
 using PrimaryTypeHashTableContext = MethodOneNumber<T, JoinHashMap<T, 
HashCRC32Return32<T>, false>>;
 
@@ -232,99 +207,4 @@ inline void try_convert_to_direct_mapping(
     primary_to_direct_mapping(context, key_columns, variant_ptrs);
 }
 
-// ASOF JOIN index with inline values for cache-friendly branchless binary 
search.
-// IntType is the integer representation of the ASOF column value:
-//   uint32_t for DateV2, uint64_t for DateTimeV2 and TimestampTZ.
-// Rows are sorted by asof_value during build, then materialized into SoA 
arrays
-// so probe-side binary search only touches the ASOF values hot path.
-template <typename IntType>
-struct AsofIndexGroup {
-    using int_type = IntType;
-
-    struct Entry {
-        IntType asof_value;
-        uint32_t row_index; // 1-based, 0 = invalid/padding
-    };
-
-    std::vector<Entry> entries;
-    std::vector<IntType> asof_values;
-    std::vector<uint32_t> row_indexes;
-
-    void add_row(IntType value, uint32_t row_idx) { entries.push_back({value, 
row_idx}); }
-
-    void sort_and_finalize() {
-        if (entries.empty()) {
-            return;
-        }
-        if (entries.size() > 1) {
-            pdqsort(entries.begin(), entries.end(),
-                    [](const Entry& a, const Entry& b) { return a.asof_value < 
b.asof_value; });
-        }
-
-        asof_values.resize(entries.size());
-        row_indexes.resize(entries.size());
-        for (size_t i = 0; i < entries.size(); ++i) {
-            asof_values[i] = entries[i].asof_value;
-            row_indexes[i] = entries[i].row_index;
-        }
-
-        std::vector<Entry>().swap(entries);
-    }
-
-    const IntType* values_data() const { return asof_values.data(); }
-
-    // Branchless lower_bound: first i where asof_values[i] >= target
-    ALWAYS_INLINE size_t lower_bound(IntType target) const {
-        size_t lo = 0, n = asof_values.size();
-        while (n > 1) {
-            size_t half = n / 2;
-            lo += half * (asof_values[lo + half] < target);
-            n -= half;
-        }
-        if (lo < asof_values.size()) {
-            lo += (asof_values[lo] < target);
-        }
-        return lo;
-    }
-
-    // Branchless upper_bound: first i where asof_values[i] > target
-    ALWAYS_INLINE size_t upper_bound(IntType target) const {
-        size_t lo = 0, n = asof_values.size();
-        while (n > 1) {
-            size_t half = n / 2;
-            lo += half * (asof_values[lo + half] <= target);
-            n -= half;
-        }
-        if (lo < asof_values.size()) {
-            lo += (asof_values[lo] <= target);
-        }
-        return lo;
-    }
-
-    // Semantics by (is_greater, is_strict):
-    //   (true,  false): probe >= build  ->  find largest  build value <= probe
-    //   (true,  true):  probe >  build  ->  find largest  build value <  probe
-    //   (false, false): probe <= build  ->  find smallest build value >= probe
-    //   (false, true):  probe <  build  ->  find smallest build value >  probe
-    // Returns the build row index of the best match, or 0 if no match.
-    template <bool IsGreater, bool IsStrict>
-    ALWAYS_INLINE uint32_t find_best_match(IntType probe_value) const {
-        if (asof_values.empty()) {
-            return 0;
-        }
-        if constexpr (IsGreater) {
-            size_t pos = IsStrict ? lower_bound(probe_value) : 
upper_bound(probe_value);
-            return pos > 0 ? row_indexes[pos - 1] : 0;
-        } else {
-            size_t pos = IsStrict ? upper_bound(probe_value) : 
lower_bound(probe_value);
-            return pos < asof_values.size() ? row_indexes[pos] : 0;
-        }
-    }
-};
-
-// Type-erased container for all ASOF index groups.
-// DateV2 -> uint32_t, DateTimeV2/TimestampTZ -> uint64_t.
-using AsofIndexVariant = std::variant<std::monostate, 
std::vector<AsofIndexGroup<uint32_t>>,
-                                      std::vector<AsofIndexGroup<uint64_t>>>;
-
 } // namespace doris
diff --git a/be/src/exec/exchange/local_exchanger.h 
b/be/src/exec/exchange/local_exchanger.h
index e0a821a3643..f1bb9f11052 100644
--- a/be/src/exec/exchange/local_exchanger.h
+++ b/be/src/exec/exchange/local_exchanger.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+#include <concurrentqueue.h>
+
 #include "exec/operator/operator.h"
 #include "exec/pipeline/dependency.h"
 
diff --git a/be/src/exec/operator/aggregation_sink_operator.h 
b/be/src/exec/operator/aggregation_sink_operator.h
index 50b32ecb364..28f1845e3c9 100644
--- a/be/src/exec/operator/aggregation_sink_operator.h
+++ b/be/src/exec/operator/aggregation_sink_operator.h
@@ -19,6 +19,7 @@
 
 #include <stdint.h>
 
+#include "exec/common/agg_utils.h"
 #include "exec/operator/operator.h"
 #include "runtime/exec_env.h"
 #include "runtime/runtime_profile.h"
diff --git a/be/src/exec/operator/aggregation_source_operator.cpp 
b/be/src/exec/operator/aggregation_source_operator.cpp
index f90ebd9bb47..f474c09b6b1 100644
--- a/be/src/exec/operator/aggregation_source_operator.cpp
+++ b/be/src/exec/operator/aggregation_source_operator.cpp
@@ -22,6 +22,7 @@
 
 #include "common/exception.h"
 #include "core/column/column_fixed_length_object.h"
+#include "exec/common/agg_utils.h"
 #include "exec/operator/operator.h"
 #include "exprs/vectorized_agg_fn.h"
 #include "exprs/vexpr_fwd.h"
diff --git a/be/src/exec/operator/bucketed_aggregation_sink_operator.h 
b/be/src/exec/operator/bucketed_aggregation_sink_operator.h
index 891977b226c..5c56b65efd1 100644
--- a/be/src/exec/operator/bucketed_aggregation_sink_operator.h
+++ b/be/src/exec/operator/bucketed_aggregation_sink_operator.h
@@ -19,6 +19,7 @@
 
 #include <stdint.h>
 
+#include "exec/common/agg_utils.h"
 #include "exec/operator/operator.h"
 #include "runtime/exec_env.h"
 #include "runtime/runtime_profile.h"
diff --git a/be/src/exec/operator/bucketed_aggregation_source_operator.cpp 
b/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
index ba0a1b650bc..a9ff2db49c4 100644
--- a/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
+++ b/be/src/exec/operator/bucketed_aggregation_source_operator.cpp
@@ -22,6 +22,7 @@
 
 #include "common/exception.h"
 #include "core/column/column_vector.h"
+#include "exec/common/agg_utils.h"
 #include "exec/common/hash_table/hash.h"
 #include "exec/common/util.hpp"
 #include "exec/operator/operator.h"
diff --git a/be/src/exec/operator/hashjoin_build_sink.cpp 
b/be/src/exec/operator/hashjoin_build_sink.cpp
index 55716412701..2d0aea94ffc 100644
--- a/be/src/exec/operator/hashjoin_build_sink.cpp
+++ b/be/src/exec/operator/hashjoin_build_sink.cpp
@@ -25,6 +25,7 @@
 #include "core/column/column_const.h"
 #include "core/column/column_nullable.h"
 #include "core/data_type/data_type_nullable.h"
+#include "exec/common/hash_table/hash_map_util.h"
 #include "exec/common/template_helpers.hpp"
 #include "exec/operator/hashjoin_probe_operator.h"
 #include "exec/operator/operator.h"
diff --git a/be/src/exec/operator/hashjoin_probe_operator.cpp 
b/be/src/exec/operator/hashjoin_probe_operator.cpp
index 44d9e806672..4214928af4b 100644
--- a/be/src/exec/operator/hashjoin_probe_operator.cpp
+++ b/be/src/exec/operator/hashjoin_probe_operator.cpp
@@ -28,6 +28,7 @@
 #include "core/column/column_const.h"
 #include "core/column/column_nullable.h"
 #include "core/data_type/data_type_nullable.h"
+#include "exec/common/join_utils.h"
 #include "exec/operator/operator.h"
 #include "runtime/descriptors.h"
 #include "util/uid_util.h"
diff --git a/be/src/exec/operator/hashjoin_probe_operator.h 
b/be/src/exec/operator/hashjoin_probe_operator.h
index 371a43fe1eb..2542efe9c88 100644
--- a/be/src/exec/operator/hashjoin_probe_operator.h
+++ b/be/src/exec/operator/hashjoin_probe_operator.h
@@ -20,6 +20,7 @@
 
 #include "common/be_mock_util.h"
 #include "common/status.h"
+#include "exec/operator/join/process_hash_table_probe.h"
 #include "exec/operator/join_probe_operator.h"
 #include "exec/operator/operator.h"
 
diff --git a/be/src/exec/operator/join/process_hash_table_probe.h 
b/be/src/exec/operator/join/process_hash_table_probe.h
index f17e49d448f..f39e865f8cd 100644
--- a/be/src/exec/operator/join/process_hash_table_probe.h
+++ b/be/src/exec/operator/join/process_hash_table_probe.h
@@ -23,6 +23,7 @@
 #include "core/column/column.h"
 #include "core/column/column_vector.h"
 #include "core/custom_allocator.h"
+#include "runtime/runtime_profile.h"
 
 namespace doris {
 class Block;
diff --git a/be/src/exec/operator/join/process_hash_table_probe_impl.h 
b/be/src/exec/operator/join/process_hash_table_probe_impl.h
index 03401a089f3..e3713f8a00f 100644
--- a/be/src/exec/operator/join/process_hash_table_probe_impl.h
+++ b/be/src/exec/operator/join/process_hash_table_probe_impl.h
@@ -25,6 +25,7 @@
 #include "core/column/column_filter_helper.h"
 #include "core/column/column_nullable.h"
 #include "core/data_type/data_type_number.h" // IWYU pragma: keep
+#include "exec/common/join_utils.h"
 #include "exec/operator/hashjoin_probe_operator.h"
 #include "exec/operator/join/process_hash_table_probe.h"
 #include "exprs/vexpr_context.h"
diff --git a/be/src/exec/operator/join_build_sink_operator.h 
b/be/src/exec/operator/join_build_sink_operator.h
index 006d6c1f551..9fc27e471bc 100644
--- a/be/src/exec/operator/join_build_sink_operator.h
+++ b/be/src/exec/operator/join_build_sink_operator.h
@@ -17,6 +17,7 @@
 
 #pragma once
 
+#include "exec/common/join_utils.h"
 #include "exec/operator/operator.h"
 
 namespace doris {
diff --git a/be/src/exec/operator/materialization_opertor.h 
b/be/src/exec/operator/materialization_opertor.h
index bc6618b1bb0..7dfdf69f823 100644
--- a/be/src/exec/operator/materialization_opertor.h
+++ b/be/src/exec/operator/materialization_opertor.h
@@ -25,6 +25,7 @@
 
 #include "common/status.h"
 #include "exec/operator/operator.h"
+#include "service/brpc.h" // IWYU pragma: keep (brpc::Controller member below)
 
 namespace doris {
 class RuntimeState;
diff --git a/be/src/exec/operator/partitioned_aggregation_source_operator.cpp 
b/be/src/exec/operator/partitioned_aggregation_source_operator.cpp
index 14acb727052..58018b6a529 100644
--- a/be/src/exec/operator/partitioned_aggregation_source_operator.cpp
+++ b/be/src/exec/operator/partitioned_aggregation_source_operator.cpp
@@ -25,6 +25,7 @@
 #include "common/exception.h"
 #include "common/logging.h"
 #include "common/status.h"
+#include "exec/common/agg_utils.h"
 #include "exec/operator/aggregation_source_operator.h"
 #include "exec/operator/operator.h"
 #include "exec/operator/spill_utils.h"
diff --git a/be/src/exec/operator/rec_cte_source_operator.cpp 
b/be/src/exec/operator/rec_cte_source_operator.cpp
index 1c32983b22a..63331bbdfbc 100644
--- a/be/src/exec/operator/rec_cte_source_operator.cpp
+++ b/be/src/exec/operator/rec_cte_source_operator.cpp
@@ -17,6 +17,8 @@
 
 #include "exec/operator/rec_cte_source_operator.h"
 
+#include "exec/common/distinct_agg_utils.h"
+
 namespace doris {
 
 RecCTESourceLocalState::RecCTESourceLocalState(RuntimeState* state, 
OperatorXBase* parent)
diff --git a/be/src/exec/operator/set_probe_sink_operator.cpp 
b/be/src/exec/operator/set_probe_sink_operator.cpp
index c3fb08a3725..4d77bd22bc8 100644
--- a/be/src/exec/operator/set_probe_sink_operator.cpp
+++ b/be/src/exec/operator/set_probe_sink_operator.cpp
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include "exec/common/hash_table/hash_table_set_probe.h"
+#include "exec/common/set_utils.h"
 #include "exec/operator/operator.h"
 #include "exec/pipeline/pipeline_task.h"
 
diff --git a/be/src/exec/operator/set_sink_operator.cpp 
b/be/src/exec/operator/set_sink_operator.cpp
index 614a45afbc8..b90c12bb994 100644
--- a/be/src/exec/operator/set_sink_operator.cpp
+++ b/be/src/exec/operator/set_sink_operator.cpp
@@ -21,6 +21,7 @@
 
 #include "core/block/materialize_block.h"
 #include "exec/common/hash_table/hash_table_set_build.h"
+#include "exec/common/set_utils.h"
 #include "exec/operator/operator.h"
 
 namespace doris {
diff --git a/be/src/exec/operator/set_source_operator.cpp 
b/be/src/exec/operator/set_source_operator.cpp
index 63afdae0814..7f6394e20e6 100644
--- a/be/src/exec/operator/set_source_operator.cpp
+++ b/be/src/exec/operator/set_source_operator.cpp
@@ -21,6 +21,7 @@
 #include <type_traits>
 
 #include "common/status.h"
+#include "exec/common/set_utils.h"
 #include "exec/operator/operator.h"
 #include "runtime/runtime_profile.h"
 
diff --git a/be/src/exec/operator/streaming_aggregation_operator.h 
b/be/src/exec/operator/streaming_aggregation_operator.h
index b23c79477a1..1c57d0ca33f 100644
--- a/be/src/exec/operator/streaming_aggregation_operator.h
+++ b/be/src/exec/operator/streaming_aggregation_operator.h
@@ -23,6 +23,7 @@
 
 #include "common/status.h"
 #include "core/block/block.h"
+#include "exec/common/agg_utils.h"
 #include "exec/operator/operator.h"
 #include "runtime/runtime_profile.h"
 
diff --git a/be/src/exec/pipeline/dependency.cpp 
b/be/src/exec/pipeline/dependency.cpp
index 3f50bb6e977..f808e6b10e8 100644
--- a/be/src/exec/pipeline/dependency.cpp
+++ b/be/src/exec/pipeline/dependency.cpp
@@ -21,6 +21,11 @@
 #include <mutex>
 
 #include "common/logging.h"
+#include "exec/common/agg_utils.h"
+#include "exec/common/hash_table/hash_map_util.h"
+#include "exec/common/join_utils.h"
+#include "exec/common/set_utils.h"
+#include "exec/common/template_helpers.hpp"
 #include "exec/operator/multi_cast_data_streamer.h"
 #include "exec/pipeline/pipeline_fragment_context.h"
 #include "exec/pipeline/pipeline_task.h"
@@ -197,6 +202,133 @@ LocalExchangeSharedState::LocalExchangeSharedState(int 
num_instances) {
     mem_counters.resize(num_instances, nullptr);
 }
 
+AggSharedState::AggSharedState() {
+    agg_data = std::make_unique<AggregatedDataVariants>();
+}
+
+AggSharedState::~AggSharedState() {
+    if (!probe_expr_ctxs.empty()) {
+        _close_with_serialized_key();
+    } else {
+        _close_without_key();
+    }
+}
+
+void AggSharedState::_close_with_serialized_key() {
+    std::visit(Overload {[&](std::monostate& arg) -> void {
+                             // Do nothing
+                         },
+                         [&](auto& agg_method) -> void {
+                             if (use_simple_count) {
+                                 // Inline count: mapped slots hold UInt64,
+                                 // not real agg state pointers. Skip destroy.
+                                 return;
+                             }
+                             auto& data = *agg_method.hash_table;
+                             data.for_each_mapped([&](auto& mapped) {
+                                 if (mapped) {
+                                     _destroy_agg_status(mapped);
+                                     mapped = nullptr;
+                                 }
+                             });
+                             if (data.has_null_key_data()) {
+                                 _destroy_agg_status(
+                                         data.template 
get_null_key_data<AggregateDataPtr>());
+                             }
+                         }},
+               agg_data->method_variant);
+}
+
+void AggSharedState::_close_without_key() {
+    //because prepare maybe failed, and couldn't create agg data.
+    //but finally call close to destory agg data, if agg data has bitmapValue
+    //will be core dump, it's not initialized
+    if (agg_data_created_without_key) {
+        _destroy_agg_status(agg_data->without_key);
+        agg_data_created_without_key = false;
+    }
+}
+
+BucketedAggSharedState::PerInstanceData::PerInstanceData() : 
arena(std::make_unique<Arena>()) {
+    bucket_agg_data.resize(BUCKETED_AGG_NUM_BUCKETS);
+    for (auto& p : bucket_agg_data) {
+        p = std::make_unique<BucketedAggDataVariants>();
+    }
+}
+
+BucketedAggSharedState::~BucketedAggSharedState() {
+    _close();
+}
+
+Status BucketedAggSharedState::init_instances(int num_instances,
+                                              const std::function<Status()>& 
metadata_init) {
+    std::call_once(_init_once, [&]() {
+        num_sink_instances = num_instances;
+        per_instance_data.resize(num_instances);
+        sink_finished = std::make_unique<std::atomic<bool>[]>(num_instances);
+        for (int i = 0; i < num_instances; ++i) {
+            sink_finished[i].store(false, std::memory_order_relaxed);
+        }
+        for (auto& bs : bucket_states) {
+            bs.merged_instances.resize(num_instances, false);
+        }
+        _init_status = metadata_init();
+    });
+    return _init_status;
+}
+
+void BucketedAggSharedState::_close() {
+    for (auto& inst : per_instance_data) {
+        for (auto& bucket_data : inst.bucket_agg_data) {
+            _close_one_agg_data(*bucket_data);
+        }
+    }
+}
+
+void BucketedAggSharedState::_close_one_agg_data(BucketedAggDataVariants& 
agg_data) {
+    std::visit(Overload {[&](std::monostate& arg) -> void {
+                             // Do nothing
+                         },
+                         [&](auto& agg_method) -> void {
+                             if (use_simple_count) {
+                                 // simple_count: mapped slots hold UInt64 
counters,
+                                 // not real agg state pointers. Skip destroy.
+                                 return;
+                             }
+                             auto& data = *agg_method.hash_table;
+                             data.for_each_mapped([&](auto& mapped) {
+                                 if (mapped) {
+                                     _destroy_agg_status(mapped);
+                                     mapped = nullptr;
+                                 }
+                             });
+                             if constexpr 
(std::is_assignable_v<decltype(data.has_null_key_data()),
+                                                                bool>) {
+                                 if (data.has_null_key_data()) {
+                                     _destroy_agg_status(
+                                             data.template 
get_null_key_data<AggregateDataPtr>());
+                                 }
+                             }
+                         }},
+               agg_data.method_variant);
+}
+
+HashJoinSharedState::HashJoinSharedState() {
+    hash_table_variant_vector.push_back(std::make_shared<JoinDataVariants>());
+}
+
+HashJoinSharedState::HashJoinSharedState(int num_instances) {
+    source_deps.resize(num_instances, nullptr);
+    hash_table_variant_vector.resize(num_instances, nullptr);
+    for (int i = 0; i < num_instances; i++) {
+        hash_table_variant_vector[i] = std::make_shared<JoinDataVariants>();
+    }
+}
+
+SetSharedState::SetSharedState() : 
hash_table_variants(std::make_unique<SetDataVariants>()) {}
+
+SetSharedState::~SetSharedState() = default;
+
 MutableColumns AggSharedState::_get_keys_hash_table() {
     return std::visit(
             Overload {[&](std::monostate& arg) {
diff --git a/be/src/exec/pipeline/dependency.h 
b/be/src/exec/pipeline/dependency.h
index a8b7ad4402b..53f9ed9281b 100644
--- a/be/src/exec/pipeline/dependency.h
+++ b/be/src/exec/pipeline/dependency.h
@@ -22,7 +22,6 @@
 #include <sys/_types/_u_int.h>
 #endif
 
-#include <concurrentqueue.h>
 #include <gen_cpp/Partitions_types.h>
 #include <gen_cpp/internal_service.pb.h>
 #include <sqltypes.h>
@@ -30,31 +29,44 @@
 #include <atomic>
 #include <condition_variable>
 #include <functional>
+#include <list>
 #include <memory>
 #include <mutex>
+#include <queue>
+#include <set>
 #include <thread>
 #include <utility>
 
+#include "common/cast_set.h"
 #include "common/config.h"
+#include "common/exception.h"
+#include "common/factory_creator.h"
 #include "common/logging.h"
 #include "common/thread_safety_annotations.h"
+#include "core/arena.h"
 #include "core/block/block.h"
 #include "core/types.h"
-#include "exec/common/agg_utils.h"
-#include "exec/common/join_utils.h"
-#include "exec/common/set_utils.h"
+#include "exec/common/join_op_utils.h"
 #include "exec/operator/data_queue.h"
-#include "exec/operator/join/process_hash_table_probe.h"
 #include "exec/sort/partition_sorter.h"
 #include "exec/sort/sorter.h"
 #include "exec/spill/spill_file.h"
+#include "exprs/vexpr_fwd.h"
 #include "runtime/runtime_profile_counter_names.h"
-#include "util/brpc_closure.h"
 #include "util/stack_util.h"
+#include "util/stopwatch.hpp"
 
 namespace doris {
 class AggFnEvaluator;
 class VSlotRef;
+// Heavy hash-table variant machinery (exec/common/*_utils.h) is referenced
+// through pointers only; keep it out of this header's parse cost.
+struct AggregatedDataVariants;
+struct AggregateDataContainer;
+struct BucketedAggDataVariants;
+struct JoinDataVariants;
+struct SetDataVariants;
+using AggregateDataPtr = char*;
 } // namespace doris
 
 namespace doris {
@@ -293,14 +305,11 @@ struct RuntimeFilterTimerQueue {
 struct AggSharedState : public BasicSharedState {
     ENABLE_FACTORY_CREATOR(AggSharedState)
 public:
-    AggSharedState() { agg_data = std::make_unique<AggregatedDataVariants>(); }
-    ~AggSharedState() override {
-        if (!probe_expr_ctxs.empty()) {
-            _close_with_serialized_key();
-        } else {
-            _close_without_key();
-        }
-    }
+    // Defined in dependency.cpp: the bodies touch AggregatedDataVariants'
+    // full variant machinery, which every includer would otherwise
+    // instantiate at parse time.
+    AggSharedState();
+    ~AggSharedState() override;
 
     Status reset_hash_table();
 
@@ -312,7 +321,7 @@ public:
     // 2nd phase: is_merge=false, maybe have multiple exprs.
     static int get_slot_column_id(const AggFnEvaluator* evaluator);
 
-    AggregatedDataVariantsUPtr agg_data = nullptr;
+    std::unique_ptr<AggregatedDataVariants> agg_data;
     std::unique_ptr<AggregateDataContainer> aggregate_data_container;
     std::vector<AggFnEvaluator*> aggregate_evaluators;
     // group by k1,k2
@@ -323,7 +332,7 @@ public:
     size_t total_size_of_aggregate_states = 0;
     size_t align_aggregate_states = 1;
     /// The offset to the n-th aggregate function in a row of aggregate 
functions.
-    Sizes offsets_of_aggregate_states;
+    std::vector<size_t> offsets_of_aggregate_states;
     std::vector<size_t> make_nullable_keys;
 
     bool agg_data_created_without_key = false;
@@ -398,40 +407,8 @@ public:
 private:
     MutableColumns _get_keys_hash_table();
 
-    void _close_with_serialized_key() {
-        std::visit(Overload {[&](std::monostate& arg) -> void {
-                                 // Do nothing
-                             },
-                             [&](auto& agg_method) -> void {
-                                 if (use_simple_count) {
-                                     // Inline count: mapped slots hold UInt64,
-                                     // not real agg state pointers. Skip 
destroy.
-                                     return;
-                                 }
-                                 auto& data = *agg_method.hash_table;
-                                 data.for_each_mapped([&](auto& mapped) {
-                                     if (mapped) {
-                                         _destroy_agg_status(mapped);
-                                         mapped = nullptr;
-                                     }
-                                 });
-                                 if (data.has_null_key_data()) {
-                                     _destroy_agg_status(
-                                             data.template 
get_null_key_data<AggregateDataPtr>());
-                                 }
-                             }},
-                   agg_data->method_variant);
-    }
-
-    void _close_without_key() {
-        //because prepare maybe failed, and couldn't create agg data.
-        //but finally call close to destory agg data, if agg data has 
bitmapValue
-        //will be core dump, it's not initialized
-        if (agg_data_created_without_key) {
-            _destroy_agg_status(agg_data->without_key);
-            agg_data_created_without_key = false;
-        }
-    }
+    void _close_with_serialized_key();
+    void _close_without_key();
     void _destroy_agg_status(AggregateDataPtr data);
 };
 
@@ -460,22 +437,17 @@ struct BucketedAggSharedState : public BasicSharedState {
     ENABLE_FACTORY_CREATOR(BucketedAggSharedState)
 public:
     BucketedAggSharedState() = default;
-    ~BucketedAggSharedState() override { _close(); }
+    ~BucketedAggSharedState() override; // defined in dependency.cpp (variant 
machinery)
 
     /// Per-instance data. One per sink pipeline instance.
     /// Each instance has 256 bucket hash tables + 1 shared arena.
     struct PerInstanceData {
         /// 256 per-bucket hash tables. Each bucket has its own 
BucketedAggDataVariants.
         /// Uses PHHashMap<StringRef> for string keys instead of StringHashMap.
-        std::vector<BucketedAggDataVariantsUPtr> bucket_agg_data;
-        ArenaUPtr arena;
+        std::vector<std::unique_ptr<BucketedAggDataVariants>> bucket_agg_data;
+        std::unique_ptr<Arena> arena;
 
-        PerInstanceData() : arena(std::make_unique<Arena>()) {
-            bucket_agg_data.resize(BUCKETED_AGG_NUM_BUCKETS);
-            for (auto& p : bucket_agg_data) {
-                p = std::make_unique<BucketedAggDataVariants>();
-            }
-        }
+        PerInstanceData(); // defined in dependency.cpp (variant machinery)
     };
 
     /// Per-bucket merge state for pipelined source-side processing.
@@ -514,7 +486,7 @@ public:
     VExprContextSPtrs probe_expr_ctxs;
     size_t total_size_of_aggregate_states = 0;
     size_t align_aggregate_states = 1;
-    Sizes offsets_of_aggregate_states;
+    std::vector<size_t> offsets_of_aggregate_states;
     std::vector<size_t> make_nullable_keys;
 
     std::atomic<size_t> input_num_rows {0};
@@ -542,64 +514,17 @@ public:
     /// The callback runs exactly once (under std::call_once), must return 
Status,
     /// and should populate shared metadata like probe_expr_ctxs, 
aggregate_evaluators, etc.
     /// All threads observe the same init status via _init_status.
-    template <typename Func>
-    Status init_instances(int num_instances, Func&& metadata_init) {
-        std::call_once(_init_once, [&]() {
-            num_sink_instances = num_instances;
-            per_instance_data.resize(num_instances);
-            sink_finished = 
std::make_unique<std::atomic<bool>[]>(num_instances);
-            for (int i = 0; i < num_instances; ++i) {
-                sink_finished[i].store(false, std::memory_order_relaxed);
-            }
-            for (auto& bs : bucket_states) {
-                bs.merged_instances.resize(num_instances, false);
-            }
-            _init_status = std::forward<Func>(metadata_init)();
-        });
-        return _init_status;
-    }
+    /// Once-per-query cold path; defined in dependency.cpp so the body (which
+    /// materializes the per-bucket BucketedAggDataVariants storage) stays out
+    /// of every includer's parse.
+    Status init_instances(int num_instances, const std::function<Status()>& 
metadata_init);
 
 private:
     std::once_flag _init_once;
     Status _init_status;
 
-    void _close() {
-        for (auto& inst : per_instance_data) {
-            for (auto& bucket_data : inst.bucket_agg_data) {
-                _close_one_agg_data(*bucket_data);
-            }
-        }
-    }
-
-    void _close_one_agg_data(BucketedAggDataVariants& agg_data) {
-        std::visit(
-                Overload {[&](std::monostate& arg) -> void {
-                              // Do nothing
-                          },
-                          [&](auto& agg_method) -> void {
-                              if (use_simple_count) {
-                                  // simple_count: mapped slots hold UInt64 
counters,
-                                  // not real agg state pointers. Skip destroy.
-                                  return;
-                              }
-                              auto& data = *agg_method.hash_table;
-                              data.for_each_mapped([&](auto& mapped) {
-                                  if (mapped) {
-                                      _destroy_agg_status(mapped);
-                                      mapped = nullptr;
-                                  }
-                              });
-                              if constexpr 
(std::is_assignable_v<decltype(data.has_null_key_data()),
-                                                                 bool>) {
-                                  if (data.has_null_key_data()) {
-                                      _destroy_agg_status(
-                                              data.template 
get_null_key_data<AggregateDataPtr>());
-                                  }
-                              }
-                          }},
-                agg_data.method_variant);
-    }
-
+    void _close();
+    void _close_one_agg_data(BucketedAggDataVariants& agg_data);
     void _destroy_agg_status(AggregateDataPtr data);
 };
 
@@ -712,16 +637,9 @@ struct JoinSharedState : public BasicSharedState {
 
 struct HashJoinSharedState : public JoinSharedState {
     ENABLE_FACTORY_CREATOR(HashJoinSharedState)
-    HashJoinSharedState() {
-        
hash_table_variant_vector.push_back(std::make_shared<JoinDataVariants>());
-    }
-    HashJoinSharedState(int num_instances) {
-        source_deps.resize(num_instances, nullptr);
-        hash_table_variant_vector.resize(num_instances, nullptr);
-        for (int i = 0; i < num_instances; i++) {
-            hash_table_variant_vector[i] = 
std::make_shared<JoinDataVariants>();
-        }
-    }
+    // Defined in dependency.cpp (they materialize JoinDataVariants).
+    HashJoinSharedState();
+    HashJoinSharedState(int num_instances);
     std::shared_ptr<Arena> arena = std::make_shared<Arena>();
 
     const std::vector<TupleDescriptor*> build_side_child_desc;
@@ -794,6 +712,11 @@ public:
 struct SetSharedState : public BasicSharedState {
     ENABLE_FACTORY_CREATOR(SetSharedState)
 public:
+    // Defined in dependency.cpp: constructing/destroying SetDataVariants
+    // needs the full variant machinery.
+    SetSharedState();
+    ~SetSharedState() override;
+
     /// default init
     Block build_block; // build to source
     //record element size in hashtable
@@ -804,9 +727,8 @@ public:
 
     //// shared static states (shared, decided in prepare/open...)
 
-    /// init in setup_local_state
-    std::unique_ptr<SetDataVariants> hash_table_variants =
-            std::make_unique<SetDataVariants>(); // the real data HERE.
+    /// init in setup_local_state (allocated in the constructor)
+    std::unique_ptr<SetDataVariants> hash_table_variants; // the real data 
HERE.
     std::vector<bool> build_not_ignore_null;
 
     // The SET operator's child might have different nullable attributes.
diff --git a/be/src/exec/pipeline/rec_cte_shared_state.cpp 
b/be/src/exec/pipeline/rec_cte_shared_state.cpp
new file mode 100644
index 00000000000..b0c41fd86ae
--- /dev/null
+++ b/be/src/exec/pipeline/rec_cte_shared_state.cpp
@@ -0,0 +1,157 @@
+// 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 "exec/pipeline/rec_cte_shared_state.h"
+
+#include <gen_cpp/internal_service.pb.h>
+
+#include "exec/common/distinct_agg_utils.h"
+#include "exec/common/template_helpers.hpp"
+#include "runtime/exec_env.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_state.h"
+#include "util/brpc_client_cache.h"
+#include "util/uid_util.h"
+
+namespace doris {
+
+RecCTESharedState::RecCTESharedState() = default;
+
+RecCTESharedState::~RecCTESharedState() = default;
+
+Status RecCTESharedState::emplace_block(RuntimeState* state, Block&& block) {
+    if (agg_data) {
+        auto num_rows = uint32_t(block.rows());
+        ColumnRawPtrs raw_columns;
+        std::vector<ColumnPtr> columns = block.get_columns_and_convert();
+        for (auto& col : columns) {
+            raw_columns.push_back(col.get());
+        }
+
+        std::visit(Overload {[&](std::monostate& arg) -> void {
+                                 throw 
doris::Exception(ErrorCode::INTERNAL_ERROR,
+                                                        "uninited hash table");
+                             },
+                             [&](auto& agg_method) -> void {
+                                 SCOPED_TIMER(hash_table_compute_timer);
+                                 using HashMethodType = 
std::decay_t<decltype(agg_method)>;
+                                 using AggState = typename 
HashMethodType::State;
+
+                                 AggState agg_state(raw_columns);
+                                 agg_method.init_serialized_keys(raw_columns, 
num_rows);
+                                 distinct_row.clear();
+
+                                 size_t row = 0;
+                                 auto creator = [&](const auto& ctor, auto& 
key, auto& origin) {
+                                     HashMethodType::try_presis_key(key, 
origin, arena);
+                                     ctor(key);
+                                     distinct_row.push_back(row);
+                                 };
+                                 auto creator_for_null_key = [&]() { 
distinct_row.push_back(row); };
+
+                                 SCOPED_TIMER(hash_table_emplace_timer);
+                                 lazy_emplace_batch_void(agg_method, 
agg_state, num_rows, creator,
+                                                         creator_for_null_key,
+                                                         [&](uint32_t r) { row 
= r; });
+                                 COUNTER_UPDATE(hash_table_input_counter, 
num_rows);
+                             }},
+                   agg_data->method_variant);
+
+        if (distinct_row.size() == block.rows()) {
+            blocks.emplace_back(std::move(block));
+        } else if (!distinct_row.empty()) {
+            auto distinct_block = MutableBlock(block.clone_empty());
+            RETURN_IF_ERROR(block.append_to_block_by_selector(&distinct_block, 
distinct_row));
+            blocks.emplace_back(distinct_block.to_block());
+        }
+    } else {
+        blocks.emplace_back(std::move(block));
+    }
+    return Status::OK();
+}
+
+PTransmitRecCTEBlockParams RecCTESharedState::build_basic_param(RuntimeState* 
state,
+                                                                const 
TRecCTETarget& target) const {
+    PTransmitRecCTEBlockParams request;
+    request.set_node_id(target.node_id);
+    
request.mutable_query_id()->CopyFrom(UniqueId(state->query_id()).to_proto());
+    request.mutable_fragment_instance_id()->CopyFrom(
+            UniqueId(target.fragment_instance_id).to_proto());
+    return request;
+}
+
+Status RecCTESharedState::send_data_to_targets(RuntimeState* state, size_t 
round_offset) const {
+    if (targets.size() == 0) {
+        return Status::OK();
+    }
+    int send_multi_blocks_byte_size = 
state->query_options().exchange_multi_blocks_byte_size;
+    int block_number_per_target =
+            int(blocks.size() - round_offset + targets.size() - 1) / 
targets.size();
+    for (auto target : targets) {
+        auto stub = 
state->get_query_ctx()->exec_env()->brpc_internal_client_cache()->get_client(
+                target.addr);
+        if (!stub) {
+            return Status::InternalError(fmt::format("Get rpc stub failed, 
host={}, port={}",
+                                                     target.addr.hostname, 
target.addr.port));
+        }
+
+        // send blocks
+        int step = block_number_per_target;
+        while (round_offset < blocks.size() && step > 0) {
+            PTransmitRecCTEBlockParams request = build_basic_param(state, 
target);
+            auto current_bytes = 0;
+            while (round_offset < blocks.size() && step > 0 &&
+                   current_bytes < send_multi_blocks_byte_size) {
+                auto* pblock = request.add_blocks();
+                size_t uncompressed_bytes = 0;
+                size_t compressed_bytes = 0;
+                int64_t compress_time;
+                RETURN_IF_ERROR(blocks[round_offset].serialize(
+                        state->be_exec_version(), pblock, &uncompressed_bytes, 
&compressed_bytes,
+                        &compress_time, 
state->fragement_transmission_compression_type()));
+                round_offset++;
+                step--;
+                current_bytes += compressed_bytes;
+            }
+            request.set_eos(false);
+
+            PTransmitRecCTEBlockResult result;
+            brpc::Controller controller;
+            controller.set_timeout_ms(
+                    
get_execution_rpc_timeout_ms(state->get_query_ctx()->execution_timeout()));
+
+            stub->transmit_rec_cte_block(&controller, &request, &result, 
brpc::DoNothing());
+            brpc::Join(controller.call_id());
+            RETURN_IF_ERROR(Status::create(result.status()));
+        }
+
+        // send eos
+        {
+            PTransmitRecCTEBlockParams request = build_basic_param(state, 
target);
+            request.set_eos(true);
+
+            PTransmitRecCTEBlockResult result;
+            brpc::Controller controller;
+            stub->transmit_rec_cte_block(&controller, &request, &result, 
brpc::DoNothing());
+            brpc::Join(controller.call_id());
+            RETURN_IF_ERROR(Status::create(result.status()));
+        }
+    }
+    return Status::OK();
+}
+
+} // namespace doris
diff --git a/be/src/exec/pipeline/rec_cte_shared_state.h 
b/be/src/exec/pipeline/rec_cte_shared_state.h
index 527f98e1f0a..fe87f63e271 100644
--- a/be/src/exec/pipeline/rec_cte_shared_state.h
+++ b/be/src/exec/pipeline/rec_cte_shared_state.h
@@ -17,13 +17,18 @@
 
 #pragma once
 
-#include "exec/common/distinct_agg_utils.h"
 #include "exec/pipeline/dependency.h"
-#include "util/brpc_client_cache.h"
 
 namespace doris {
 
+struct DistinctDataVariants;
+
 struct RecCTESharedState : public BasicSharedState {
+    // Defined in rec_cte_shared_state.cpp: DistinctDataVariants carries the
+    // full hash-table variant machinery.
+    RecCTESharedState();
+    ~RecCTESharedState() override;
+
     std::vector<TRecCTETarget> targets;
     std::vector<Block> blocks;
     IColumn::Selector distinct_row;
@@ -34,7 +39,11 @@ struct RecCTESharedState : public BasicSharedState {
     RuntimeProfile::Counter* hash_table_emplace_timer = nullptr;
     RuntimeProfile::Counter* hash_table_input_counter = nullptr;
 
-    std::unique_ptr<DistinctDataVariants> agg_data = nullptr;
+    // No `= nullptr` initializer: a default member initializer makes GCC
+    // instantiate ~unique_ptr<DistinctDataVariants> in every TU that merely 
sees
+    // this class, which needs the complete type. The defaulted constructor in
+    // rec_cte_shared_state.cpp already leaves this null.
+    std::unique_ptr<DistinctDataVariants> agg_data;
 
     int current_round = 0;
     int last_round_offset = 0;
@@ -47,130 +56,14 @@ struct RecCTESharedState : public BasicSharedState {
         }
     }
 
-    Status emplace_block(RuntimeState* state, Block&& block) {
-        if (agg_data) {
-            auto num_rows = uint32_t(block.rows());
-            ColumnRawPtrs raw_columns;
-            std::vector<ColumnPtr> columns = block.get_columns_and_convert();
-            for (auto& col : columns) {
-                raw_columns.push_back(col.get());
-            }
-
-            std::visit(Overload {[&](std::monostate& arg) -> void {
-                                     throw 
doris::Exception(ErrorCode::INTERNAL_ERROR,
-                                                            "uninited hash 
table");
-                                 },
-                                 [&](auto& agg_method) -> void {
-                                     SCOPED_TIMER(hash_table_compute_timer);
-                                     using HashMethodType = 
std::decay_t<decltype(agg_method)>;
-                                     using AggState = typename 
HashMethodType::State;
-
-                                     AggState agg_state(raw_columns);
-                                     
agg_method.init_serialized_keys(raw_columns, num_rows);
-                                     distinct_row.clear();
-
-                                     size_t row = 0;
-                                     auto creator = [&](const auto& ctor, 
auto& key, auto& origin) {
-                                         HashMethodType::try_presis_key(key, 
origin, arena);
-                                         ctor(key);
-                                         distinct_row.push_back(row);
-                                     };
-                                     auto creator_for_null_key = [&]() {
-                                         distinct_row.push_back(row);
-                                     };
-
-                                     SCOPED_TIMER(hash_table_emplace_timer);
-                                     lazy_emplace_batch_void(agg_method, 
agg_state, num_rows,
-                                                             creator, 
creator_for_null_key,
-                                                             [&](uint32_t r) { 
row = r; });
-                                     COUNTER_UPDATE(hash_table_input_counter, 
num_rows);
-                                 }},
-                       agg_data->method_variant);
-
-            if (distinct_row.size() == block.rows()) {
-                blocks.emplace_back(std::move(block));
-            } else if (!distinct_row.empty()) {
-                auto distinct_block = MutableBlock(block.clone_empty());
-                
RETURN_IF_ERROR(block.append_to_block_by_selector(&distinct_block, 
distinct_row));
-                blocks.emplace_back(distinct_block.to_block());
-            }
-        } else {
-            blocks.emplace_back(std::move(block));
-        }
-        return Status::OK();
-    }
+    // Bodies live in rec_cte_shared_state.cpp: they touch the distinct hash
+    // table variant machinery and the brpc client stack.
+    Status emplace_block(RuntimeState* state, Block&& block);
 
     PTransmitRecCTEBlockParams build_basic_param(RuntimeState* state,
-                                                 const TRecCTETarget& target) 
const {
-        PTransmitRecCTEBlockParams request;
-        request.set_node_id(target.node_id);
-        
request.mutable_query_id()->CopyFrom(UniqueId(state->query_id()).to_proto());
-        request.mutable_fragment_instance_id()->CopyFrom(
-                UniqueId(target.fragment_instance_id).to_proto());
-        return request;
-    }
-
-    Status send_data_to_targets(RuntimeState* state, size_t round_offset) 
const {
-        if (targets.size() == 0) {
-            return Status::OK();
-        }
-        int send_multi_blocks_byte_size = 
state->query_options().exchange_multi_blocks_byte_size;
-        int block_number_per_target =
-                int(blocks.size() - round_offset + targets.size() - 1) / 
targets.size();
-        for (auto target : targets) {
-            auto stub =
-                    
state->get_query_ctx()->exec_env()->brpc_internal_client_cache()->get_client(
-                            target.addr);
-            if (!stub) {
-                return Status::InternalError(fmt::format("Get rpc stub failed, 
host={}, port={}",
-                                                         target.addr.hostname, 
target.addr.port));
-            }
-
-            // send blocks
-            int step = block_number_per_target;
-            while (round_offset < blocks.size() && step > 0) {
-                PTransmitRecCTEBlockParams request = build_basic_param(state, 
target);
-                auto current_bytes = 0;
-                while (round_offset < blocks.size() && step > 0 &&
-                       current_bytes < send_multi_blocks_byte_size) {
-                    auto* pblock = request.add_blocks();
-                    size_t uncompressed_bytes = 0;
-                    size_t compressed_bytes = 0;
-                    int64_t compress_time;
-                    RETURN_IF_ERROR(blocks[round_offset].serialize(
-                            state->be_exec_version(), pblock, 
&uncompressed_bytes,
-                            &compressed_bytes, &compress_time,
-                            state->fragement_transmission_compression_type()));
-                    round_offset++;
-                    step--;
-                    current_bytes += compressed_bytes;
-                }
-                request.set_eos(false);
-
-                PTransmitRecCTEBlockResult result;
-                brpc::Controller controller;
-                controller.set_timeout_ms(
-                        
get_execution_rpc_timeout_ms(state->get_query_ctx()->execution_timeout()));
+                                                 const TRecCTETarget& target) 
const;
 
-                stub->transmit_rec_cte_block(&controller, &request, &result, 
brpc::DoNothing());
-                brpc::Join(controller.call_id());
-                RETURN_IF_ERROR(Status::create(result.status()));
-            }
-
-            // send eos
-            {
-                PTransmitRecCTEBlockParams request = build_basic_param(state, 
target);
-                request.set_eos(true);
-
-                PTransmitRecCTEBlockResult result;
-                brpc::Controller controller;
-                stub->transmit_rec_cte_block(&controller, &request, &result, 
brpc::DoNothing());
-                brpc::Join(controller.call_id());
-                RETURN_IF_ERROR(Status::create(result.status()));
-            }
-        }
-        return Status::OK();
-    }
+    Status send_data_to_targets(RuntimeState* state, size_t round_offset) 
const;
 };
 
 } // namespace doris
diff --git a/be/src/format/parquet/decoder.cpp 
b/be/src/format/parquet/decoder.cpp
index f97ff464b80..016d69b4d12 100644
--- a/be/src/format/parquet/decoder.cpp
+++ b/be/src/format/parquet/decoder.cpp
@@ -20,6 +20,7 @@
 #include <cctz/time_zone.h>
 #include <gen_cpp/parquet_types.h>
 
+#include "common/cast_set.h"
 #include "format/parquet/bool_plain_decoder.h"
 #include "format/parquet/bool_rle_decoder.h"
 #include "format/parquet/byte_array_dict_decoder.h"
@@ -28,8 +29,30 @@
 #include "format/parquet/delta_bit_pack_decoder.h"
 #include "format/parquet/fix_length_dict_decoder.hpp"
 #include "format/parquet/fix_length_plain_decoder.h"
+#include "util/rle_encoding.h"
 
 namespace doris {
+
+BaseDictDecoder::BaseDictDecoder() = default;
+
+BaseDictDecoder::~BaseDictDecoder() = default;
+
+Status BaseDictDecoder::set_data(Slice* data) {
+    _data = data;
+    _offset = 0;
+    uint8_t bit_width = *data->data;
+    _index_batch_decoder = std::make_unique<RleBatchDecoder<uint32_t>>(
+            reinterpret_cast<uint8_t*>(data->data) + 1, 
static_cast<int>(data->size) - 1,
+            bit_width);
+    return Status::OK();
+}
+
+Status BaseDictDecoder::skip_values(size_t num_values) {
+    _indexes.resize(num_values);
+    _index_batch_decoder->GetBatch(_indexes.data(), 
cast_set<uint32_t>(num_values));
+    return Status::OK();
+}
+
 Status Decoder::get_decoder(tparquet::Type::type type, 
tparquet::Encoding::type encoding,
                             std::unique_ptr<Decoder>& decoder) {
     switch (encoding) {
diff --git a/be/src/format/parquet/decoder.h b/be/src/format/parquet/decoder.h
index c936e5e042f..cf1f3c81adf 100644
--- a/be/src/format/parquet/decoder.h
+++ b/be/src/format/parquet/decoder.h
@@ -38,13 +38,14 @@
 #include "core/pod_array_fwd.h"
 #include "core/types.h"
 #include "format/parquet/parquet_common.h"
-#include "util/rle_encoding.h"
 #include "util/slice.h"
 
 namespace doris {
 template <typename T>
 class ColumnStr;
 using ColumnString = ColumnStr<UInt32>;
+template <typename T>
+class RleBatchDecoder;
 
 class Decoder {
 public:
@@ -93,19 +94,16 @@ protected:
 
 class BaseDictDecoder : public Decoder {
 public:
-    BaseDictDecoder() = default;
-    ~BaseDictDecoder() override = default;
+    // Out-of-line: member unique_ptr<RleBatchDecoder<uint32_t>> only needs the
+    // complete type where the ctor/dtor/set_data/skip_values are defined 
(decoder.cpp),
+    // keeping the costly RLE template machinery out of every includer of this 
header.
+    // The ctor counts: a defaulted-in-class one would be defined in every TU 
that
+    // constructs a derived decoder, and it odr-uses the member's destructor.
+    BaseDictDecoder();
+    ~BaseDictDecoder() override;
 
     // Set the data to be decoded
-    Status set_data(Slice* data) override {
-        _data = data;
-        _offset = 0;
-        uint8_t bit_width = *data->data;
-        _index_batch_decoder = std::make_unique<RleBatchDecoder<uint32_t>>(
-                reinterpret_cast<uint8_t*>(data->data) + 1, 
static_cast<int>(data->size) - 1,
-                bit_width);
-        return Status::OK();
-    }
+    Status set_data(Slice* data) override;
 
 protected:
     /**
@@ -146,11 +144,7 @@ protected:
         return Status::OK();
     }
 
-    Status skip_values(size_t num_values) override {
-        _indexes.resize(num_values);
-        _index_batch_decoder->GetBatch(_indexes.data(), 
cast_set<uint32_t>(num_values));
-        return Status::OK();
-    }
+    Status skip_values(size_t num_values) override;
 
     // For dictionary encoding
     DorisUniqueBufferPtr<uint8_t> _dict;
diff --git a/be/src/format_v2/native/native_reader.h 
b/be/src/format_v2/native/native_reader.h
index 15a52fe6f8b..8460a2faec5 100644
--- a/be/src/format_v2/native/native_reader.h
+++ b/be/src/format_v2/native/native_reader.h
@@ -25,6 +25,7 @@
 #include <vector>
 
 #include "format_v2/file_reader.h"
+#include "runtime/runtime_profile.h"
 
 namespace doris::format::native {
 
diff --git a/be/src/format_v2/parquet/reader/native/decoder.cpp 
b/be/src/format_v2/parquet/reader/native/decoder.cpp
index c068cafaf0d..845c9deacd7 100644
--- a/be/src/format_v2/parquet/reader/native/decoder.cpp
+++ b/be/src/format_v2/parquet/reader/native/decoder.cpp
@@ -20,6 +20,7 @@
 #include <cctz/time_zone.h>
 #include <gen_cpp/parquet_types.h>
 
+#include "common/cast_set.h"
 #include "format_v2/parquet/reader/native/bool_plain_decoder.h"
 #include "format_v2/parquet/reader/native/bool_rle_decoder.h"
 #include "format_v2/parquet/reader/native/byte_array_dict_decoder.h"
@@ -28,8 +29,226 @@
 #include "format_v2/parquet/reader/native/delta_bit_pack_decoder.h"
 #include "format_v2/parquet/reader/native/fix_length_dict_decoder.hpp"
 #include "format_v2/parquet/reader/native/fix_length_plain_decoder.h"
+#include "util/rle_encoding.h"
 
 namespace doris::format::parquet::native {
+
+BaseDictDecoder::BaseDictDecoder() = default;
+
+BaseDictDecoder::~BaseDictDecoder() = default;
+
+Status BaseDictDecoder::set_data(Slice* data) {
+    if (UNLIKELY(data == nullptr || data->size == 0)) {
+        return Status::Corruption("Parquet dictionary index stream is empty");
+    }
+    _data = data;
+    _offset = 0;
+    uint8_t bit_width = *data->data;
+    // Dictionary indices are uint32_t; wider external widths make repeated 
runs overwrite the
+    // decoder's four-byte state before any dictionary-bound check can run.
+    if (UNLIKELY(bit_width > 32)) {
+        return Status::Corruption("Parquet dictionary index bit width {} 
exceeds 32", bit_width);
+    }
+    _index_batch_decoder = std::make_unique<RleBatchDecoder<uint32_t>>(
+            reinterpret_cast<uint8_t*>(data->data) + 1, 
static_cast<int>(data->size) - 1,
+            bit_width);
+    return Status::OK();
+}
+
+Status BaseDictDecoder::decode_dictionary_indices(size_t num_values,
+                                                  std::vector<uint32_t>* 
indices) {
+    DORIS_CHECK(indices != nullptr);
+    indices->resize(num_values);
+    const auto decoded =
+            _index_batch_decoder->GetBatch(indices->data(), 
cast_set<uint32_t>(num_values));
+    if (UNLIKELY(decoded != num_values)) {
+        return Status::IOError("Can't read enough Parquet dictionary indices");
+    }
+    const size_t num_dictionary_values = dictionary_size();
+    if (UNLIKELY(!dictionary_indices_in_bounds(indices->data(), num_values,
+                                               num_dictionary_values))) {
+        // The SIMD common path only computes a bound; recover the exact 
corrupt row for the
+        // diagnostic after the batch has already been proven invalid.
+        for (size_t row = 0; row < num_values; ++row) {
+            if ((*indices)[row] < num_dictionary_values) {
+                continue;
+            }
+            return Status::Corruption(
+                    "Parquet dictionary index {} at row {} exceeds dictionary 
size {}",
+                    (*indices)[row], row, num_dictionary_values);
+        }
+    }
+    return Status::OK();
+}
+
+Status BaseDictDecoder::decode_selected_dictionary_indices(const 
ParquetSelection& selection,
+                                                           
std::vector<uint32_t>* indices) {
+    DORIS_CHECK(indices != nullptr);
+    const size_t num_dictionary_values = dictionary_size();
+    if (_is_fragmented_selection(selection)) {
+        RETURN_IF_ERROR(_decode_fragmented_selection(selection, 
num_dictionary_values));
+        indices->assign(_skip_indices.begin(), _skip_indices.begin() + 
selection.selected_values);
+        return Status::OK();
+    }
+    indices->resize(selection.selected_values);
+    size_t cursor = 0;
+    size_t output = 0;
+    for (const auto& range : selection.ranges) {
+        DORIS_CHECK(range.first >= cursor);
+        RETURN_IF_ERROR(
+                _decode_and_validate_skipped(range.first - cursor, cursor, 
num_dictionary_values));
+        const auto decoded = _index_batch_decoder->GetBatch(indices->data() + 
output,
+                                                            
cast_set<uint32_t>(range.count));
+        if (UNLIKELY(decoded != range.count)) {
+            return Status::IOError("Can't read enough Parquet dictionary 
indices");
+        }
+        if (UNLIKELY(!dictionary_indices_in_bounds(indices->data() + output, 
range.count,
+                                                   num_dictionary_values))) {
+            for (size_t row = 0; row < range.count; ++row) {
+                if ((*indices)[output + row] < num_dictionary_values) {
+                    continue;
+                }
+                return Status::Corruption(
+                        "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
+                        (*indices)[output + row], range.first + row, 
num_dictionary_values);
+            }
+        }
+        output += range.count;
+        cursor = range.first + range.count;
+    }
+    DORIS_CHECK(cursor <= selection.total_values);
+    RETURN_IF_ERROR(_decode_and_validate_skipped(selection.total_values - 
cursor, cursor,
+                                                 num_dictionary_values));
+    DORIS_CHECK_EQ(output, selection.selected_values);
+    return Status::OK();
+}
+
+Status BaseDictDecoder::_decode_fragmented_selection(const ParquetSelection& 
selection,
+                                                     size_t 
num_dictionary_values) {
+    // Decode and validate the page batch once when predicate survivors 
alternate in tiny runs.
+    // Walking each range separately turns one RLE batch into millions of 
decoder calls for
+    // low-cardinality predicates such as TPC-DS quantity buckets.
+    _skip_indices.resize(selection.total_values);
+    const auto decoded = _index_batch_decoder->GetBatch(_skip_indices.data(),
+                                                        
cast_set<uint32_t>(selection.total_values));
+    if (UNLIKELY(decoded != selection.total_values)) {
+        return Status::IOError("Can't read enough Parquet dictionary indices");
+    }
+    if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), 
selection.total_values,
+                                               num_dictionary_values))) {
+        for (size_t row = 0; row < selection.total_values; ++row) {
+            if (_skip_indices[row] < num_dictionary_values) {
+                continue;
+            }
+            return Status::Corruption(
+                    "Parquet dictionary index {} at row {} exceeds dictionary 
size {}",
+                    _skip_indices[row], row, num_dictionary_values);
+        }
+    }
+    size_t output = 0;
+    constexpr size_t MAX_INLINE_COPY_VALUES = 4;
+    for (const auto& range : selection.ranges) {
+        DORIS_CHECK(range.first + range.count <= selection.total_values);
+        // Alternating predicates mostly produce one-row spans; inline tiny 
forward copies so
+        // range compaction does not replace decoder calls with equally 
numerous libc calls.
+        if (range.count <= MAX_INLINE_COPY_VALUES) {
+            for (size_t row = 0; row < range.count; ++row) {
+                _skip_indices[output + row] = _skip_indices[range.first + row];
+            }
+        } else {
+            memmove(_skip_indices.data() + output, _skip_indices.data() + 
range.first,
+                    range.count * sizeof(uint32_t));
+        }
+        output += range.count;
+    }
+    DORIS_CHECK_EQ(output, selection.selected_values);
+    return Status::OK();
+}
+
+Status BaseDictDecoder::_decode_and_validate_skipped(size_t num_values, size_t 
row_offset,
+                                                     size_t 
num_dictionary_values) {
+    constexpr size_t kSkipBatchSize = 4096;
+    // Skipped dictionary ids are still external input and must be 
bounds-checked, but keeping
+    // only one bounded gap buffer avoids the page-sized scratch used by 
sparse selections.
+    _skip_indices.resize(std::min(num_values, kSkipBatchSize));
+    size_t skipped_values = 0;
+    while (skipped_values < num_values) {
+        const size_t batch_size = std::min(num_values - skipped_values, 
kSkipBatchSize);
+        const auto skipped = 
_index_batch_decoder->GetBatch(_skip_indices.data(),
+                                                            
static_cast<uint32_t>(batch_size));
+        if (UNLIKELY(skipped != batch_size)) {
+            return Status::IOError(
+                    "Can't skip enough Parquet dictionary indices at row {}: 
{} of {}",
+                    row_offset + skipped_values, skipped, batch_size);
+        }
+        // Filter gaps may be huge RLE runs; validate them in bounded 
SIMD-sized batches.
+        if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), 
batch_size,
+                                                   num_dictionary_values))) {
+            for (size_t row = 0; row < batch_size; ++row) {
+                if (_skip_indices[row] < num_dictionary_values) {
+                    continue;
+                }
+                return Status::Corruption(
+                        "Parquet dictionary index {} at skipped row {} exceeds 
dictionary "
+                        "size {}",
+                        _skip_indices[row], row_offset + skipped_values + row,
+                        num_dictionary_values);
+            }
+        }
+        skipped_values += batch_size;
+    }
+    return Status::OK();
+}
+
+Status BaseDictDecoder::_decode_dictionary_values(size_t num_values, size_t 
row_offset,
+                                                  size_t num_dictionary_values,
+                                                  
ParquetDictionaryValueConsumer& consumer) {
+    constexpr size_t kLiteralBatchSize = 1024;
+    size_t decoded_values = 0;
+    while (decoded_values < num_values) {
+        const int32_t repeats = _index_batch_decoder->NextNumRepeats();
+        if (repeats > 0) {
+            const size_t run = std::min<size_t>(repeats, num_values - 
decoded_values);
+            const uint32_t index = 
_index_batch_decoder->GetRepeatedValue(cast_set<int32_t>(run));
+            if (UNLIKELY(static_cast<size_t>(index) >= num_dictionary_values)) 
{
+                return Status::Corruption(
+                        "Parquet dictionary index {} at row {} exceeds 
dictionary size {}", index,
+                        row_offset + decoded_values, num_dictionary_values);
+            }
+            RETURN_IF_ERROR(consumer.consume_repeated(index, run));
+            decoded_values += run;
+            continue;
+        }
+
+        const int32_t literals = _index_batch_decoder->NextNumLiterals();
+        if (UNLIKELY(literals == 0)) {
+            return Status::IOError("Can't read enough Parquet dictionary 
indices");
+        }
+        const size_t batch = std::min(
+                {static_cast<size_t>(literals), num_values - decoded_values, 
kLiteralBatchSize});
+        _skip_indices.resize(batch);
+        if 
(UNLIKELY(!_index_batch_decoder->GetLiteralValues(cast_set<int32_t>(batch),
+                                                             
_skip_indices.data()))) {
+            return Status::IOError("Can't read enough Parquet dictionary 
indices");
+        }
+        if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), batch,
+                                                   num_dictionary_values))) {
+            for (size_t row = 0; row < batch; ++row) {
+                if (_skip_indices[row] < num_dictionary_values) {
+                    continue;
+                }
+                return Status::Corruption(
+                        "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
+                        _skip_indices[row], row_offset + decoded_values + row,
+                        num_dictionary_values);
+            }
+        }
+        RETURN_IF_ERROR(consumer.consume_indices(_skip_indices.data(), batch));
+        decoded_values += batch;
+    }
+    return Status::OK();
+}
+
 namespace {
 Status unsupported_type(tparquet::Type::type type, tparquet::Encoding::type 
encoding) {
     return Status::InternalError("Unsupported type {}(encoding={}) in parquet 
decoder",
diff --git a/be/src/format_v2/parquet/reader/native/decoder.h 
b/be/src/format_v2/parquet/reader/native/decoder.h
index 628a34ac976..873a7ab9b02 100644
--- a/be/src/format_v2/parquet/reader/native/decoder.h
+++ b/be/src/format_v2/parquet/reader/native/decoder.h
@@ -37,9 +37,13 @@
 #include "core/custom_allocator.h"
 #include "core/data_type_serde/parquet_decode_source.h"
 #include "core/types.h"
-#include "util/rle_encoding.h"
 #include "util/slice.h"
 
+namespace doris {
+template <typename T>
+class RleBatchDecoder;
+} // namespace doris
+
 namespace doris::format::parquet::native {
 
 inline bool dictionary_indices_in_bounds(const uint32_t* indices, size_t count,
@@ -127,99 +131,25 @@ protected:
 
 class BaseDictDecoder : public Decoder {
 public:
-    BaseDictDecoder() = default;
-    ~BaseDictDecoder() override = default;
+    // Out-of-line together with every member that dereferences 
_index_batch_decoder:
+    // keeping the RleBatchDecoder<uint32_t> machinery out of this header 
stops every
+    // includer from instantiating the RLE decode chain; the complete type 
lives only
+    // in decoder.cpp. All of these are per-page/per-batch virtual calls. The 
ctor is
+    // out of line for the same reason: a defaulted-in-class one would be 
defined in
+    // every TU that constructs a derived decoder, and it odr-uses the 
member's dtor.
+    BaseDictDecoder();
+    ~BaseDictDecoder() override;
 
     // Set the data to be decoded
-    Status set_data(Slice* data) override {
-        if (UNLIKELY(data == nullptr || data->size == 0)) {
-            return Status::Corruption("Parquet dictionary index stream is 
empty");
-        }
-        _data = data;
-        _offset = 0;
-        uint8_t bit_width = *data->data;
-        // Dictionary indices are uint32_t; wider external widths make 
repeated runs overwrite the
-        // decoder's four-byte state before any dictionary-bound check can run.
-        if (UNLIKELY(bit_width > 32)) {
-            return Status::Corruption("Parquet dictionary index bit width {} 
exceeds 32",
-                                      bit_width);
-        }
-        _index_batch_decoder = std::make_unique<RleBatchDecoder<uint32_t>>(
-                reinterpret_cast<uint8_t*>(data->data) + 1, 
static_cast<int>(data->size) - 1,
-                bit_width);
-        return Status::OK();
-    }
+    Status set_data(Slice* data) override;
 
     bool has_dictionary() const override { return true; }
     uint64_t dictionary_generation() const override { return 
_dictionary_generation; }
 
-    Status decode_dictionary_indices(size_t num_values, std::vector<uint32_t>* 
indices) override {
-        DORIS_CHECK(indices != nullptr);
-        indices->resize(num_values);
-        const auto decoded =
-                _index_batch_decoder->GetBatch(indices->data(), 
cast_set<uint32_t>(num_values));
-        if (UNLIKELY(decoded != num_values)) {
-            return Status::IOError("Can't read enough Parquet dictionary 
indices");
-        }
-        const size_t num_dictionary_values = dictionary_size();
-        if (UNLIKELY(!dictionary_indices_in_bounds(indices->data(), num_values,
-                                                   num_dictionary_values))) {
-            // The SIMD common path only computes a bound; recover the exact 
corrupt row for the
-            // diagnostic after the batch has already been proven invalid.
-            for (size_t row = 0; row < num_values; ++row) {
-                if ((*indices)[row] < num_dictionary_values) {
-                    continue;
-                }
-                return Status::Corruption(
-                        "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
-                        (*indices)[row], row, num_dictionary_values);
-            }
-        }
-        return Status::OK();
-    }
+    Status decode_dictionary_indices(size_t num_values, std::vector<uint32_t>* 
indices) override;
 
     Status decode_selected_dictionary_indices(const ParquetSelection& 
selection,
-                                              std::vector<uint32_t>* indices) 
override {
-        DORIS_CHECK(indices != nullptr);
-        const size_t num_dictionary_values = dictionary_size();
-        if (_is_fragmented_selection(selection)) {
-            RETURN_IF_ERROR(_decode_fragmented_selection(selection, 
num_dictionary_values));
-            indices->assign(_skip_indices.begin(),
-                            _skip_indices.begin() + selection.selected_values);
-            return Status::OK();
-        }
-        indices->resize(selection.selected_values);
-        size_t cursor = 0;
-        size_t output = 0;
-        for (const auto& range : selection.ranges) {
-            DORIS_CHECK(range.first >= cursor);
-            RETURN_IF_ERROR(_decode_and_validate_skipped(range.first - cursor, 
cursor,
-                                                         
num_dictionary_values));
-            const auto decoded = 
_index_batch_decoder->GetBatch(indices->data() + output,
-                                                                
cast_set<uint32_t>(range.count));
-            if (UNLIKELY(decoded != range.count)) {
-                return Status::IOError("Can't read enough Parquet dictionary 
indices");
-            }
-            if (UNLIKELY(!dictionary_indices_in_bounds(indices->data() + 
output, range.count,
-                                                       
num_dictionary_values))) {
-                for (size_t row = 0; row < range.count; ++row) {
-                    if ((*indices)[output + row] < num_dictionary_values) {
-                        continue;
-                    }
-                    return Status::Corruption(
-                            "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
-                            (*indices)[output + row], range.first + row, 
num_dictionary_values);
-                }
-            }
-            output += range.count;
-            cursor = range.first + range.count;
-        }
-        DORIS_CHECK(cursor <= selection.total_values);
-        RETURN_IF_ERROR(_decode_and_validate_skipped(selection.total_values - 
cursor, cursor,
-                                                     num_dictionary_values));
-        DORIS_CHECK_EQ(output, selection.selected_values);
-        return Status::OK();
-    }
+                                              std::vector<uint32_t>* indices) 
override;
 
     Status decode_dictionary_values(size_t num_values,
                                     ParquetDictionaryValueConsumer& consumer) 
override {
@@ -266,135 +196,18 @@ protected:
     }
 
     Status _decode_fragmented_selection(const ParquetSelection& selection,
-                                        size_t num_dictionary_values) {
-        // Decode and validate the page batch once when predicate survivors 
alternate in tiny runs.
-        // Walking each range separately turns one RLE batch into millions of 
decoder calls for
-        // low-cardinality predicates such as TPC-DS quantity buckets.
-        _skip_indices.resize(selection.total_values);
-        const auto decoded = _index_batch_decoder->GetBatch(
-                _skip_indices.data(), 
cast_set<uint32_t>(selection.total_values));
-        if (UNLIKELY(decoded != selection.total_values)) {
-            return Status::IOError("Can't read enough Parquet dictionary 
indices");
-        }
-        if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), 
selection.total_values,
-                                                   num_dictionary_values))) {
-            for (size_t row = 0; row < selection.total_values; ++row) {
-                if (_skip_indices[row] < num_dictionary_values) {
-                    continue;
-                }
-                return Status::Corruption(
-                        "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
-                        _skip_indices[row], row, num_dictionary_values);
-            }
-        }
-        size_t output = 0;
-        constexpr size_t MAX_INLINE_COPY_VALUES = 4;
-        for (const auto& range : selection.ranges) {
-            DORIS_CHECK(range.first + range.count <= selection.total_values);
-            // Alternating predicates mostly produce one-row spans; inline 
tiny forward copies so
-            // range compaction does not replace decoder calls with equally 
numerous libc calls.
-            if (range.count <= MAX_INLINE_COPY_VALUES) {
-                for (size_t row = 0; row < range.count; ++row) {
-                    _skip_indices[output + row] = _skip_indices[range.first + 
row];
-                }
-            } else {
-                memmove(_skip_indices.data() + output, _skip_indices.data() + 
range.first,
-                        range.count * sizeof(uint32_t));
-            }
-            output += range.count;
-        }
-        DORIS_CHECK_EQ(output, selection.selected_values);
-        return Status::OK();
-    }
+                                        size_t num_dictionary_values);
 
     Status skip_values(size_t num_values) override {
         return _decode_and_validate_skipped(num_values, 0, dictionary_size());
     }
 
     Status _decode_and_validate_skipped(size_t num_values, size_t row_offset,
-                                        size_t num_dictionary_values) {
-        constexpr size_t kSkipBatchSize = 4096;
-        // Skipped dictionary ids are still external input and must be 
bounds-checked, but keeping
-        // only one bounded gap buffer avoids the page-sized scratch used by 
sparse selections.
-        _skip_indices.resize(std::min(num_values, kSkipBatchSize));
-        size_t skipped_values = 0;
-        while (skipped_values < num_values) {
-            const size_t batch_size = std::min(num_values - skipped_values, 
kSkipBatchSize);
-            const auto skipped = 
_index_batch_decoder->GetBatch(_skip_indices.data(),
-                                                                
static_cast<uint32_t>(batch_size));
-            if (UNLIKELY(skipped != batch_size)) {
-                return Status::IOError(
-                        "Can't skip enough Parquet dictionary indices at row 
{}: {} of {}",
-                        row_offset + skipped_values, skipped, batch_size);
-            }
-            // Filter gaps may be huge RLE runs; validate them in bounded 
SIMD-sized batches.
-            if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), 
batch_size,
-                                                       
num_dictionary_values))) {
-                for (size_t row = 0; row < batch_size; ++row) {
-                    if (_skip_indices[row] < num_dictionary_values) {
-                        continue;
-                    }
-                    return Status::Corruption(
-                            "Parquet dictionary index {} at skipped row {} 
exceeds dictionary "
-                            "size {}",
-                            _skip_indices[row], row_offset + skipped_values + 
row,
-                            num_dictionary_values);
-                }
-            }
-            skipped_values += batch_size;
-        }
-        return Status::OK();
-    }
+                                        size_t num_dictionary_values);
 
     Status _decode_dictionary_values(size_t num_values, size_t row_offset,
                                      size_t num_dictionary_values,
-                                     ParquetDictionaryValueConsumer& consumer) 
{
-        constexpr size_t kLiteralBatchSize = 1024;
-        size_t decoded_values = 0;
-        while (decoded_values < num_values) {
-            const int32_t repeats = _index_batch_decoder->NextNumRepeats();
-            if (repeats > 0) {
-                const size_t run = std::min<size_t>(repeats, num_values - 
decoded_values);
-                const uint32_t index =
-                        
_index_batch_decoder->GetRepeatedValue(cast_set<int32_t>(run));
-                if (UNLIKELY(static_cast<size_t>(index) >= 
num_dictionary_values)) {
-                    return Status::Corruption(
-                            "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
-                            index, row_offset + decoded_values, 
num_dictionary_values);
-                }
-                RETURN_IF_ERROR(consumer.consume_repeated(index, run));
-                decoded_values += run;
-                continue;
-            }
-
-            const int32_t literals = _index_batch_decoder->NextNumLiterals();
-            if (UNLIKELY(literals == 0)) {
-                return Status::IOError("Can't read enough Parquet dictionary 
indices");
-            }
-            const size_t batch = std::min({static_cast<size_t>(literals),
-                                           num_values - decoded_values, 
kLiteralBatchSize});
-            _skip_indices.resize(batch);
-            if 
(UNLIKELY(!_index_batch_decoder->GetLiteralValues(cast_set<int32_t>(batch),
-                                                                 
_skip_indices.data()))) {
-                return Status::IOError("Can't read enough Parquet dictionary 
indices");
-            }
-            if (UNLIKELY(!dictionary_indices_in_bounds(_skip_indices.data(), 
batch,
-                                                       
num_dictionary_values))) {
-                for (size_t row = 0; row < batch; ++row) {
-                    if (_skip_indices[row] < num_dictionary_values) {
-                        continue;
-                    }
-                    return Status::Corruption(
-                            "Parquet dictionary index {} at row {} exceeds 
dictionary size {}",
-                            _skip_indices[row], row_offset + decoded_values + 
row,
-                            num_dictionary_values);
-                }
-            }
-            RETURN_IF_ERROR(consumer.consume_indices(_skip_indices.data(), 
batch));
-            decoded_values += batch;
-        }
-        return Status::OK();
-    }
+                                     ParquetDictionaryValueConsumer& consumer);
 
     // For dictionary encoding
     DorisUniqueBufferPtr<uint8_t> _dict;
diff --git a/be/src/storage/compaction/compaction.cpp 
b/be/src/storage/compaction/compaction.cpp
index 46dc32d2bcd..86578831740 100644
--- a/be/src/storage/compaction/compaction.cpp
+++ b/be/src/storage/compaction/compaction.cpp
@@ -32,6 +32,7 @@
 #include <nlohmann/json.hpp>
 #include <numeric>
 #include <ostream>
+#include <ranges>
 #include <set>
 #include <shared_mutex>
 #include <utility>
diff --git a/be/src/storage/index/ann/ann_index_writer.h 
b/be/src/storage/index/ann/ann_index_writer.h
index 67061bef921..913a4e33084 100644
--- a/be/src/storage/index/ann/ann_index_writer.h
+++ b/be/src/storage/index/ann/ann_index_writer.h
@@ -17,9 +17,20 @@
 
 #pragma once
 
+// CLucene is third-party code and is not clean under -Wconversion (which
+// -Wshorten-64-to-32 belongs to). Whether its first expansion lands inside
+// someone else's suppressed region depends on include order, so suppress it
+// deliberately here (same pattern as inverted_index_common_impl.h).
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wconversion"
+#endif
 #include <CLucene.h> // IWYU pragma: keep
 #include <CLucene/analysis/LanguageBasedAnalyzer.h>
 #include <CLucene/util/bkd/bkd_writer.h>
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
 #include <glog/logging.h>
 
 #include <cstdint>
diff --git a/be/src/storage/index/index_file_writer.h 
b/be/src/storage/index/index_file_writer.h
index a303de8b68c..35e8904b837 100644
--- a/be/src/storage/index/index_file_writer.h
+++ b/be/src/storage/index/index_file_writer.h
@@ -17,8 +17,19 @@
 
 #pragma once
 
+// CLucene is third-party code and is not clean under -Wconversion (which
+// -Wshorten-64-to-32 belongs to). Whether its first expansion lands inside
+// someone else's suppressed region depends on include order, so suppress it
+// deliberately here (same pattern as inverted_index_common_impl.h).
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wconversion"
+#endif
 #include <CLucene.h> // IWYU pragma: keep
 #include <CLucene/store/IndexInput.h>
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
 #include <gen_cpp/olap_common.pb.h>
 #include <gen_cpp/olap_file.pb.h>
 
diff --git a/be/src/storage/index/inverted/inverted_index_searcher.h 
b/be/src/storage/index/inverted/inverted_index_searcher.h
index 63009a6e2ee..97e3c2dd189 100644
--- a/be/src/storage/index/inverted/inverted_index_searcher.h
+++ b/be/src/storage/index/inverted/inverted_index_searcher.h
@@ -21,10 +21,21 @@
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wshadow-field"
 #endif
+// CLucene is third-party code and is not clean under -Wconversion (which
+// -Wshorten-64-to-32 belongs to). Whether its first expansion lands inside
+// someone else's suppressed region depends on include order, so suppress it
+// deliberately here (same pattern as inverted_index_common_impl.h).
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wconversion"
+#endif
 #include <CLucene.h> // IWYU pragma: keep
 #ifdef __clang__
 #pragma clang diagnostic pop
 #endif
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
 
 #include <memory>
 #include <optional>
diff --git a/be/src/storage/index/inverted/query/query.h 
b/be/src/storage/index/inverted/query/query.h
index 4f985736a3b..a64d9034f55 100644
--- a/be/src/storage/index/inverted/query/query.h
+++ b/be/src/storage/index/inverted/query/query.h
@@ -17,9 +17,20 @@
 
 #pragma once
 
+// CLucene is third-party code and is not clean under -Wconversion (which
+// -Wshorten-64-to-32 belongs to). Whether its first expansion lands inside
+// someone else's suppressed region depends on include order, so suppress it
+// deliberately here (same pattern as inverted_index_common_impl.h).
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wconversion"
+#endif
 #include <CLucene.h> // IWYU pragma: keep
 #include <CLucene/index/IndexReader.h>
 #include <CLucene/index/Term.h>
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
 #include <gen_cpp/PaloInternalService_types.h>
 
 #include <memory>
diff --git a/be/src/storage/index/inverted/similarity/bm25_similarity.cpp 
b/be/src/storage/index/inverted/similarity/bm25_similarity.cpp
index b973243c489..2ab946a8791 100644
--- a/be/src/storage/index/inverted/similarity/bm25_similarity.cpp
+++ b/be/src/storage/index/inverted/similarity/bm25_similarity.cpp
@@ -21,8 +21,6 @@
 
 namespace doris::segment_v2 {
 
-using namespace inverted_index;
-
 const int32_t BM25Similarity::MAX_INT32 = std::numeric_limits<int32_t>::max();
 const uint32_t BM25Similarity::MAX_INT4 = 
long_to_int4(static_cast<uint64_t>(MAX_INT32));
 const int32_t BM25Similarity::NUM_FREE_VALUES = 255 - 
static_cast<int>(MAX_INT4);
diff --git a/be/src/storage/olap_define.h b/be/src/storage/olap_define.h
index 5131c51ca01..2f40e8d6ab1 100644
--- a/be/src/storage/olap_define.h
+++ b/be/src/storage/olap_define.h
@@ -157,11 +157,14 @@ const std::string ROWSET_ID_PREFIX = "s_";
 const std::string REMOTE_ROWSET_GC_PREFIX = "gc_";
 const std::string REMOTE_TABLET_GC_PREFIX = "tgc_";
 
-// Declare copy constructor and equal operator as private
+// Declare copy constructor and equal operator as private.
+// No trailing semicolon: call sites write `DISALLOW_COPY_AND_ASSIGN(X);`, same
+// contract as butil/macros.h's definition of the same macro (which wins in TUs
+// that include butil first — keep the two expansions call-site compatible).
 #ifndef DISALLOW_COPY_AND_ASSIGN
 #define DISALLOW_COPY_AND_ASSIGN(type_t) \
     type_t& operator=(const type_t&);    \
-    type_t(const type_t&);
+    type_t(const type_t&)
 #endif
 
 #define SAFE_DELETE(ptr)      \
diff --git a/be/src/storage/tablet/base_tablet.cpp 
b/be/src/storage/tablet/base_tablet.cpp
index aab47367c7d..ec16d69bf57 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -26,6 +26,7 @@
 #include <cstdint>
 #include <iterator>
 #include <random>
+#include <ranges>
 #include <shared_mutex>
 
 #include "cloud/cloud_tablet.h"
diff --git a/be/test/core/block/block_test.cpp 
b/be/test/core/block/block_test.cpp
index 2a3f07e01c5..566eaccc982 100644
--- a/be/test/core/block/block_test.cpp
+++ b/be/test/core/block/block_test.cpp
@@ -65,6 +65,7 @@
 #include "exec/common/sip_hash.h"
 #include "runtime/descriptor_helper.h"
 #include "runtime/descriptors.h"
+#include "runtime/runtime_profile.h"
 #include "testutil/column_helper.h"
 #include "util/debug_points.h"
 #include "util/defer_op.h"
diff --git a/be/test/core/column/column_variant_v2_test.cpp 
b/be/test/core/column/column_variant_v2_test.cpp
index efee3303224..91656d4b526 100644
--- a/be/test/core/column/column_variant_v2_test.cpp
+++ b/be/test/core/column/column_variant_v2_test.cpp
@@ -61,6 +61,7 @@
 #include "exec/common/hash_table/hash_map_context.h"
 #include "exec/common/hash_table/string_hash_map.h"
 #include "exec/common/sip_hash.h"
+#include "exec/sort/hybrid_sorter.h"
 #include "exprs/function/parse/variant_jsonb_parse.h"
 #include "exprs/function/parse/variant_string_parse.h"
 #include "runtime/memory/mem_tracker.h"
diff --git a/be/test/exec/operator/asof_join_test.cpp 
b/be/test/exec/operator/asof_join_test.cpp
index 8b31897b7de..accf929fd5c 100644
--- a/be/test/exec/operator/asof_join_test.cpp
+++ b/be/test/exec/operator/asof_join_test.cpp
@@ -16,6 +16,13 @@
 // under the License.
 
 #include <gtest/gtest.h>
+// Must precede exec/common/join_utils.h below. 
AsofIndexGroup::sort_and_finalize calls the
+// global pdqsort, which ADL cannot reach from std::vector's iterators, so the 
name has to be
+// visible where the template is defined rather than where it is instantiated. 
join_utils.h
+// deliberately does not include it: core/column/column.h stopped pulling 
exec/sort/hybrid_sorter.h
+// (pdqsort + timsort) out of 808 TUs, and production TUs reach join_utils.h 
through headers that
+// already supply pdqsort. Only this test pays for it.
+#include <pdqsort.h>
 
 #include <initializer_list>
 #include <type_traits>
diff --git a/be/test/format/parquet/fix_length_dict_decoder_empty_dict_test.cpp 
b/be/test/format/parquet/fix_length_dict_decoder_empty_dict_test.cpp
index e901507b0c3..4eb7180d70e 100644
--- a/be/test/format/parquet/fix_length_dict_decoder_empty_dict_test.cpp
+++ b/be/test/format/parquet/fix_length_dict_decoder_empty_dict_test.cpp
@@ -18,7 +18,16 @@
 #include <gtest/gtest.h>
 
 #include "core/column/column_vector.h"
+// Must precede the header below. FixLengthDictDecoder::_decode_values 
dereferences the inherited
+// unique_ptr<RleBatchDecoder<uint32_t>>, and that type depends on no template 
parameter, so the
+// call is bound when the template is parsed rather than when it is 
instantiated. decoder.h only
+// forward-declares RleBatchDecoder to keep the RLE machinery out of its ~530 
includers, so this TU
+// supplies the complete type up front. Only this test pays for it.
+// clang-format off
+#include "util/rle_encoding.h" // IWYU pragma: keep
+
 #include "format/parquet/fix_length_dict_decoder.hpp"
+// clang-format on
 
 namespace doris {
 
diff --git a/be/test/format/parquet/fix_length_dict_decoder_test.cpp 
b/be/test/format/parquet/fix_length_dict_decoder_test.cpp
index 5c8854b665b..22015555abd 100644
--- a/be/test/format/parquet/fix_length_dict_decoder_test.cpp
+++ b/be/test/format/parquet/fix_length_dict_decoder_test.cpp
@@ -15,7 +15,16 @@
 // specific language governing permissions and limitations
 // under the License.
 
+// Must precede the header under test. FixLengthDictDecoder::_decode_values 
dereferences the
+// inherited unique_ptr<RleBatchDecoder<uint32_t>>, and that type depends on 
no template parameter,
+// so the call is bound when the template is parsed rather than when it is 
instantiated. decoder.h
+// only forward-declares RleBatchDecoder to keep the RLE machinery out of its 
~530 includers, so
+// this TU supplies the complete type up front. Only this test pays for it.
+// clang-format off
+#include "util/rle_encoding.h" // IWYU pragma: keep
+
 #include "format/parquet/fix_length_dict_decoder.hpp"
+// clang-format on
 
 #include <gtest/gtest.h>
 
diff --git a/be/test/storage/segment/hierarchical_data_iterator_test.cpp 
b/be/test/storage/segment/hierarchical_data_iterator_test.cpp
index 69619726e4f..b69789d02c5 100644
--- a/be/test/storage/segment/hierarchical_data_iterator_test.cpp
+++ b/be/test/storage/segment/hierarchical_data_iterator_test.cpp
@@ -190,7 +190,7 @@ public:
             return Status::InvalidArgument("JSONB destination is not a string 
column");
         }
 
-        const size_t produced = std::min(*rows, ROWS - 
_state->current_ordinal);
+        const size_t produced = std::min<size_t>(*rows, ROWS - 
_state->current_ordinal);
         auto serde = std::make_shared<doris::DataTypeJsonb>()->get_serde();
         doris::DataTypeSerDe::FormatOptions options;
         for (size_t row = 0; row < produced; ++row) {
@@ -241,7 +241,7 @@ public:
             return Status::InvalidArgument("sparse destination is not a map");
         }
 
-        const size_t produced = std::min(*rows, ROWS - 
_state->current_ordinal);
+        const size_t produced = std::min<size_t>(*rows, ROWS - 
_state->current_ordinal);
         auto& keys = assert_cast<ColumnString&>(map->get_keys());
         auto& values = assert_cast<ColumnString&>(map->get_values());
         auto& map_offsets = map->get_offsets();
@@ -297,7 +297,7 @@ public:
             return Status::InvalidArgument("JSONB sparse destination is not a 
map");
         }
 
-        const size_t produced = std::min(*rows, ROWS - _current_ordinal);
+        const size_t produced = std::min<size_t>(*rows, ROWS - 
_current_ordinal);
         auto& keys = assert_cast<ColumnString&>(map->get_keys());
         auto& values = assert_cast<ColumnString&>(map->get_values());
         auto& offsets = map->get_offsets();
diff --git a/build-support/check-header-deps.py 
b/build-support/check-header-deps.py
index c91a3f25b4e..7161b2e15fd 100755
--- a/build-support/check-header-deps.py
+++ b/build-support/check-header-deps.py
@@ -197,8 +197,110 @@ RULES = [
         "thrift and forward-declared); this was a dead include spreading 
PBlock "
         "and segment_v2.pb.h to ~595 TUs through thread_context.h",
     ),
+    (
+        "core/pod_array.h",
+        "runtime/thread_context.h",
+        set(),
+        "dead include left over from the PODArray memory-tracking experiment "
+        "(#50549); the tracking logic since moved into Allocator and 
pod_array.h "
+        "references no thread_context symbol, yet the edge dragged 
thread_context, "
+        "exec_env.h and mem_tracker_limiter.h into 203 TUs of core/",
+    ),
+    (
+        "core/column/column.h",
+        "exec/sort/",
+        set(),
+        "core must not depend on the exec sort machinery: column.h only names "
+        "HybridSorter in virtual signatures (forward-declared, bodies in "
+        "column.cpp); the old hybrid_sorter.h include was a layering violation 
"
+        "that pushed pdqsort/timsort into 808 TUs "
+        "(exec/common/endian.h still rides in via storage/olap_common.h -> "
+        "util/hash_util.hpp, a separate pre-existing wart)",
+    ),
+    (
+        "format/parquet/decoder.h",
+        "util/rle_encoding.h",
+        set(),
+        "BaseDictDecoder holds RleBatchDecoder<uint32_t> behind a unique_ptr "
+        "(forward-declared; the dtor and every member that dereferences it are 
"
+        "defined in decoder.cpp); the old include made every one of ~530 TUs "
+        "that transitively see a parquet decoder instantiate the whole "
+        "RLE/BitPacking decode chain at ~0.4 CPU s each",
+    ),
+    (
+        "format_v2/parquet/reader/native/decoder.h",
+        "util/rle_encoding.h",
+        set(),
+        "same contract as format/parquet/decoder.h: the dictionary index "
+        "decoder is forward-declared and only decoder.cpp needs the complete "
+        "RleBatchDecoder type; keeping rle_encoding.h (and the unrolled "
+        "bit_packing.inline.h it carries) out of this header keeps the RLE "
+        "instantiation chain out of the native-reader include tree",
+    ),
+    (
+        "exec/pipeline/dependency.h",
+        "exec/common/hash_table/",
+        {
+            # Declarations-only phmap forward header (the sanctioned way 
through
+            # the barrier; its name predates the *_fwd.h convention).
+            "exec/common/hash_table/phmap_fwd_decl.h",
+        },
+        "the SharedState classes hold every DataVariants behind unique_ptr/"
+        "shared_ptr with ctors/dtors/close bodies defined in dependency.cpp; "
+        "any path back into the hash-table machinery re-instantiates the "
+        "Agg/Join/Set variant surface (~0.85 CPU s) in each of the ~128 TUs "
+        "that include dependency.h transitively",
+    ),
+    (
+        "exec/pipeline/dependency.h",
+        "exec/operator/join/process_hash_table_probe.h",
+        set(),
+        "dead include: dependency.h references no ProcessHashTableProbe "
+        "symbol; the probe machinery belongs to the hash-join TUs that "
+        "include process_hash_table_probe_impl.h",
+    ),
+    (
+        "exec/pipeline/dependency.h",
+        "util/brpc_closure.h",
+        set(),
+        "dead include: dependency.h references no brpc symbol, yet this edge "
+        "carried runtime/query_context.h, runtime/thread_context.h and "
+        "service/brpc.h (1.36 MB of preprocessed payload) into ~100 TUs "
+        "whose only other route to them was this header",
+    ),
+    (
+        "exec/pipeline/rec_cte_shared_state.h",
+        "exec/common/hash_table/",
+        {
+            "exec/common/hash_table/phmap_fwd_decl.h",
+        },
+        "DistinctDataVariants is forward-declared and only touched in "
+        "rec_cte_shared_state.cpp (emplace_block's std::visit); the distinct "
+        "hash-table family must not ride the rec_cte operator headers into "
+        "the pipeline registry TUs",
+    ),
+    (
+        "exec/pipeline/rec_cte_shared_state.h",
+        "util/brpc_client_cache.h",
+        set(),
+        "send_data_to_targets/build_basic_param bodies live in "
+        "rec_cte_shared_state.cpp; the brpc client stack must not ride a "
+        "SharedState header",
+    ),
 ]
 
+# Not expressible as RULES entries (the scanner only follows quoted project
+# includes and <gen_cpp/...>): core/uint24.h and core/value/large_int_value.h
+# must not regain <fmt/compile.h> / <fmt/format.h>. Their to_string/to_buffer
+# bodies live in the matching .cpp files precisely so the FMT_COMPILE formatter
+# templates (53.5 CPU s over ~1150 TUs for the uint24 date format alone) are
+# instantiated once instead of in every includer.
+#
+# Likewise <concurrentqueue.h> must not return to exec/pipeline/dependency.h:
+# it was a dead 152 KB third-party include there; the moodycamel users
+# (local_exchanger.h, scanner_context.h, async_result_writer.h) include it
+# themselves.
+
 # Forward-declaration headers are the sanctioned way through a barrier: they 
carry
 # declarations only, so they cost nothing to include.
 FWD_SUFFIX = "_fwd.h"


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

Reply via email to