This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 00537142bf8 branch-4.2: [improvement](variant) Reduce Variant array
shredding CPU during import #67983 (#68296)
00537142bf8 is described below
commit 00537142bf8e9aedfcaec90eff6792c997eab9b6
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 21 17:07:53 2026 +0800
branch-4.2: [improvement](variant) Reduce Variant array shredding CPU
during import #67983 (#68296)
Cherry-picked from #67983
Co-authored-by: lihangyu <[email protected]>
Co-authored-by: Claude <[email protected]>
---
.../segment/variant/v2/variant_path_builder.cpp | 58 ++++++++++++++------
.../variant/variant_column_writer_reader_test.cpp | 64 ++++++++++++++++++++++
2 files changed, 104 insertions(+), 18 deletions(-)
diff --git a/be/src/storage/segment/variant/v2/variant_path_builder.cpp
b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
index ea609d3bdac..efb68f5f632 100644
--- a/be/src/storage/segment/variant/v2/variant_path_builder.cpp
+++ b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
@@ -215,8 +215,16 @@ const DataTypePtr& cached_decimal_type(uint32_t scale) {
return types[scale];
}
-DataTypePtr infer_type(VariantRef value, const DataTypePtr& reusable_type =
nullptr) {
- const ValueKind kind = value_kind(value);
+const DataTypePtr& array_element_type(const DataTypeArray& array) {
+ // DataTypeArray always wraps its element in Nullable. Borrow the element
instead of copying it
+ // through remove_nullable(): element types are process-wide statics
shared by concurrent
+ // flushes, so each shared_ptr copy is a contended reference-count update.
+ return assert_cast<const
DataTypeNullable&>(*array.get_nested_type()).get_nested_type();
+}
+
+// Every scalar storage type is a process-wide static, so it is returned by
reference.
+const DataTypePtr& infer_scalar_type(VariantRef value, ValueKind kind) {
+ DORIS_CHECK(kind != ValueKind::ARRAY);
switch (kind) {
case ValueKind::NULL_VALUE:
return nothing_type();
@@ -272,8 +280,18 @@ DataTypePtr infer_type(VariantRef value, const
DataTypePtr& reusable_type = null
case ValueKind::ARRAY:
break;
}
+ __builtin_unreachable();
+}
+
+DataTypePtr infer_type(VariantRef value, const DataTypePtr& reusable_type =
nullptr) {
+ const ValueKind kind = value_kind(value);
+ if (kind != ValueKind::ARRAY) {
+ return infer_scalar_type(value, kind);
+ }
- DataTypePtr element_type;
+ // Borrow the static element types and only resolve a common type when
elements differ.
+ DataTypePtr promoted_element;
+ const DataTypePtr* element_type = nullptr;
const uint32_t element_count = value.num_elements();
for (uint32_t index = 0; index < element_count; ++index) {
const VariantRef element = value.array_at(index);
@@ -283,17 +301,18 @@ DataTypePtr infer_type(VariantRef value, const
DataTypePtr& reusable_type = null
element.basic_type() == VariantBasicType::OBJECT)) {
return jsonb_type();
}
- DataTypePtr inferred = infer_type(element);
+ const DataTypePtr& inferred = infer_scalar_type(element, element_kind);
if (inferred->get_primitive_type() == INVALID_TYPE) {
continue;
}
- element_type = element_type == nullptr ? std::move(inferred)
- :
path_least_common_type(element_type, inferred);
- }
-
- if (element_type == nullptr) {
- element_type = nothing_type();
+ if (element_type == nullptr) {
+ element_type = &inferred;
+ } else if (element_type->get() != inferred.get()) {
+ promoted_element = path_least_common_type(*element_type, inferred);
+ element_type = &promoted_element;
+ }
}
+ const DataTypePtr& resolved_element = element_type == nullptr ?
nothing_type() : *element_type;
// A path commonly sees the same ARRAY element type on every row. Reuse
the builder's
// DataTypeArray in that case instead of allocating a temporary shared_ptr
per value. The
@@ -302,17 +321,20 @@ DataTypePtr infer_type(VariantRef value, const
DataTypePtr& reusable_type = null
if (const auto* reusable_array =
reusable_type == nullptr ? nullptr
: typeid_cast<const
DataTypeArray*>(reusable_type.get())) {
- const DataTypePtr& reusable_element =
reusable_array->get_nested_type();
- if (reusable_element.get() == element_type.get() ||
- reusable_element->equals(*element_type)) {
+ // Inferred element types are never nullable, so compare against the
unwrapped element;
+ // otherwise every ARRAY value would miss the equality check and pay
+ // get_least_supertype_jsonb() only to rebuild the same type.
+ const DataTypePtr& reusable_element =
array_element_type(*reusable_array);
+ if (reusable_element.get() == resolved_element.get() ||
+ reusable_element->equals(*resolved_element)) {
return reusable_type;
}
- DataTypePtr common_element = path_least_common_type(reusable_element,
element_type);
+ DataTypePtr common_element = path_least_common_type(reusable_element,
resolved_element);
if (reusable_element->equals(*common_element)) {
return reusable_type;
}
}
- return std::make_shared<DataTypeArray>(element_type);
+ return std::make_shared<DataTypeArray>(resolved_element);
}
bool is_small_or_regular_integer(PrimitiveType type) {
@@ -458,8 +480,8 @@ bool value_is_representable(VariantRef value, const
DataTypePtr& target_type) {
if (kind != ValueKind::ARRAY) {
return false;
}
- const DataTypePtr element_type =
- remove_nullable(assert_cast<const
DataTypeArray&>(*target_type).get_nested_type());
+ const DataTypePtr& element_type =
+ array_element_type(assert_cast<const
DataTypeArray&>(*target_type));
const uint32_t count = value.num_elements();
for (uint32_t index = 0; index < count; ++index) {
const VariantRef element = value.array_at(index);
@@ -769,7 +791,7 @@ void append_array(VariantRef value, const DataTypePtr&
target_type, IColumn* tar
const auto& array_type = assert_cast<const DataTypeArray&>(*target_type);
auto& array = assert_cast<ColumnArray&>(*target);
auto& elements = assert_cast<ColumnNullable&>(array.get_data());
- const DataTypePtr element_type =
remove_nullable(array_type.get_nested_type());
+ const DataTypePtr& element_type = array_element_type(array_type);
// infer_type() made the first borrowed pass. Revisit the encoded children
only after path type
// promotion is complete, appending directly without an owning recursive
scratch tree.
const uint32_t count = value.num_elements();
diff --git a/be/test/storage/variant/variant_column_writer_reader_test.cpp
b/be/test/storage/variant/variant_column_writer_reader_test.cpp
index fd9cf208648..15530dd6a58 100644
--- a/be/test/storage/variant/variant_column_writer_reader_test.cpp
+++ b/be/test/storage/variant/variant_column_writer_reader_test.cpp
@@ -28,6 +28,7 @@
#include <thread>
#include "common/config.h"
+#include "core/column/column_array.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.h"
@@ -809,6 +810,69 @@ TEST(VariantPathBuilderTest,
PreservesIncomingArrayWhenInferredDecimalPromotionO
"[9999999999999999999999999999999999999.9]");
}
+TEST(VariantPathBuilderTest, ArrayPathReusesElementTypeAcrossRows) {
+ VariantBatchBuilder value_builder;
+ const auto append_array = [&](auto&& fill) {
+ auto row = value_builder.begin_row();
+ auto array = row.start_array();
+ fill(row);
+ array.finish();
+ row.finish();
+ };
+ append_array([](auto& row) {
+ row.add_float(1.0F);
+ row.add_float(2.0F);
+ });
+ append_array([](auto& row) {
+ row.add_float(3.0F);
+ row.add_null();
+ });
+ append_array([](auto& row) { row.add_null(); });
+ append_array([](auto& row) { row.add_double(4.5); });
+ append_array([](auto& row) { row.add_float(5.0F); });
+ VariantBatchBuilder values = value_builder.finish_batch();
+
+ const auto element_primitive = [](const DataTypePtr& type) {
+ const DataTypePtr array = remove_nullable(type);
+ return remove_nullable(assert_cast<const
DataTypeArray&>(*array).get_nested_type())
+ ->get_primitive_type();
+ };
+ segment_v2::VariantPathBuilder builder(PathInData("metric"));
+ // FLOAT arrays, arrays with null elements, and all-null arrays share the
first row's type.
+ for (size_t row = 0; row < 3; ++row) {
+ ASSERT_TRUE(builder.append(values.value_at(row), row).ok());
+ EXPECT_EQ(builder.promotion_count(), 0) << "row=" << row;
+ ASSERT_EQ(element_primitive(builder.type()), TYPE_FLOAT) << "row=" <<
row;
+ }
+ ASSERT_TRUE(builder.append(values.value_at(3), 3).ok());
+ EXPECT_EQ(builder.promotion_count(), 1);
+ ASSERT_EQ(element_primitive(builder.type()), TYPE_DOUBLE);
+ // A narrower FLOAT element after promotion reuses the promoted DOUBLE
array type.
+ ASSERT_TRUE(builder.append(values.value_at(4), 4).ok());
+ EXPECT_EQ(builder.promotion_count(), 1);
+ ASSERT_EQ(element_primitive(builder.type()), TYPE_DOUBLE);
+
+ ColumnPtr materialized;
+ ASSERT_TRUE(builder.materialize(&materialized).ok());
+ const auto& array = assert_cast<const ColumnArray&>(
+ assert_cast<const
ColumnNullable&>(*materialized).get_nested_column());
+ const auto& elements = assert_cast<const
ColumnNullable&>(array.get_data());
+ const auto& doubles =
+ assert_cast<const
ColumnFloat64&>(elements.get_nested_column()).get_data();
+ const std::vector<std::optional<double>> expected {1.0, 2.0, 3.0,
std::nullopt,
+ std::nullopt, 4.5, 5.0};
+ EXPECT_EQ(std::vector<uint64_t>(array.get_offsets().begin(),
array.get_offsets().end()),
+ (std::vector<uint64_t> {2, 4, 5, 6, 7}));
+ ASSERT_EQ(elements.size(), expected.size());
+ for (size_t index = 0; index < expected.size(); ++index) {
+ SCOPED_TRACE(testing::Message() << "element=" << index);
+ EXPECT_EQ(elements.is_null_at(index), !expected[index].has_value());
+ if (expected[index].has_value()) {
+ EXPECT_DOUBLE_EQ(doubles[index], *expected[index]);
+ }
+ }
+}
+
TEST(VariantPathBuilderTest,
StringifiesArrayWithoutTreatingExistingNullAsCastFailure) {
VariantBatchBuilder value_builder;
auto row = value_builder.begin_row();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]