Skip to content

Commit c67cf42

Browse files
committed
[improvement](be) Optimize flat Variant assembly for sparse rows
### What problem does this PR solve? Issue Number: None Related PR: None Problem Summary: VariantAssembler scanned every flat materialized path for every row even when only a small fraction of paths were present. Build a batch-local active-slot index for the narrow whole-root, flat, non-conflicting layout and keep all other layouts on the existing generic path. On the fixed 10 GB workload with 2000 materialized paths and 20 values per row, the wall median decreases from 77.7376 to 75.0729 seconds and BE CPU decreases from 245.04 to 227.51 core-seconds, improvements of 3.43% and 7.15% respectively. ### Release note None ### Check List (For Author) - Test: Unit Test and manual performance validation - ASAN BE UT: VariantAssemblerLegacyTest.*, 33/33 passed - Performance benchmark: one warmup and three trials on the fixed 10 GB sparse workload - Behavior changed: No - Does this need documentation: No
1 parent fc3235c commit c67cf42

3 files changed

Lines changed: 180 additions & 7 deletions

File tree

be/src/storage/segment/variant/v2/variant_assembler.cpp

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "core/column/column_vector.h"
3030
#include "core/column/variant_column_utils.h"
3131
#include "core/column/variant_v2/column_variant_v2.h"
32+
#include "core/data_type/data_type_nullable.h"
3233
#include "core/value/variant/variant_batch_builder.h"
3334
#include "core/value/variant/variant_parquet_encoding.h"
3435
#include "exprs/function/parse/variant_jsonb_parse.h"
@@ -298,6 +299,89 @@ struct PreparedHierarchicalBatch {
298299
DorisVector<variant_assembler_detail::PreparedMaterializedColumn> materialized;
299300
};
300301

302+
bool can_assemble_flat_materialized(StorageMapKind storage_map_kind, const PathInData& requested,
303+
std::span<const MaterializedSlot> materialized_slots) {
304+
if (storage_map_kind != StorageMapKind::NONE || !requested.empty() ||
305+
materialized_slots.empty()) {
306+
return false;
307+
}
308+
for (size_t index = 0; index < materialized_slots.size(); ++index) {
309+
const MaterializedSlot& slot = materialized_slots[index];
310+
if (slot.relative_path.get_parts().size() != 1 ||
311+
remove_nullable(slot.type)->get_primitive_type() == TYPE_ARRAY ||
312+
(index != 0 && slot.relative_path == materialized_slots[index - 1].relative_path)) {
313+
return false;
314+
}
315+
}
316+
return true;
317+
}
318+
319+
bool has_only_empty_root_payload(const PreparedHierarchicalBatch& batch, size_t rows) {
320+
if (batch.root_values == nullptr) {
321+
return true;
322+
}
323+
for (size_t row = 0; row < rows; ++row) {
324+
if ((batch.root_nulls == nullptr || batch.root_nulls[row] == 0) &&
325+
batch.root_values->get_data_at(row).size != 0) {
326+
return false;
327+
}
328+
}
329+
return true;
330+
}
331+
332+
template <typename Visitor>
333+
void visit_visible_scalar_rows(const variant_assembler_detail::PreparedMaterializedColumn& column,
334+
size_t rows, Visitor&& visitor) {
335+
DCHECK_NE(column.primitive, TYPE_ARRAY);
336+
if (column.nulls == nullptr) {
337+
for (size_t row = 0; row < rows; ++row) {
338+
visitor(row);
339+
}
340+
return;
341+
}
342+
const uint8_t* current = column.nulls;
343+
const uint8_t* end = current + rows;
344+
while (current != end) {
345+
const auto* visible = static_cast<const uint8_t*>(std::memchr(current, 0, end - current));
346+
if (visible == nullptr) {
347+
return;
348+
}
349+
visitor(static_cast<size_t>(visible - column.nulls));
350+
current = visible + 1;
351+
}
352+
}
353+
354+
struct ActiveMaterializedRows {
355+
DorisVector<size_t> offsets;
356+
DorisVector<size_t> slots;
357+
};
358+
359+
ActiveMaterializedRows index_active_materialized_rows(const PreparedHierarchicalBatch& batch,
360+
size_t rows) {
361+
ActiveMaterializedRows result;
362+
result.offsets.resize(rows + 1);
363+
for (const auto& column : batch.materialized) {
364+
visit_visible_scalar_rows(column, rows, [&](size_t row) {
365+
if (batch.root_nulls == nullptr || batch.root_nulls[row] == 0) {
366+
++result.offsets[row + 1];
367+
}
368+
});
369+
}
370+
for (size_t row = 0; row < rows; ++row) {
371+
result.offsets[row + 1] += result.offsets[row];
372+
}
373+
result.slots.resize(result.offsets.back());
374+
DorisVector<size_t> positions = result.offsets;
375+
for (size_t slot = 0; slot < batch.materialized.size(); ++slot) {
376+
visit_visible_scalar_rows(batch.materialized[slot], rows, [&](size_t row) {
377+
if (batch.root_nulls == nullptr || batch.root_nulls[row] == 0) {
378+
result.slots[positions[row]++] = slot;
379+
}
380+
});
381+
}
382+
return result;
383+
}
384+
301385
bool has_materialized_value(
302386
std::span<const variant_assembler_detail::PreparedMaterializedColumn> materialized,
303387
std::span<const MaterializedSlot> materialized_slots, size_t row,
@@ -557,14 +641,49 @@ Status assemble_hierarchical_row(StorageMapKind storage_map_kind, bool has_root,
557641
return Status::OK();
558642
}
559643

644+
Status assemble_flat_materialized(std::span<const MaterializedSlot> materialized_slots,
645+
const PreparedHierarchicalBatch& batch, size_t rows,
646+
ColumnNullable::MutablePtr* output) {
647+
ActiveMaterializedRows active = index_active_materialized_rows(batch, rows);
648+
VariantBatchBuilder builder({.rows = rows, .metadata_keys = materialized_slots.size()});
649+
auto outer = ColumnUInt8::create();
650+
outer->reserve(rows);
651+
for (size_t row_index = 0; row_index < rows; ++row_index) {
652+
auto row = builder.begin_row();
653+
if (batch.root_nulls != nullptr && batch.root_nulls[row_index] != 0) {
654+
outer->insert_value(1);
655+
row.add_null();
656+
row.finish();
657+
continue;
658+
}
659+
auto object = row.start_object();
660+
for (size_t active_index = active.offsets[row_index];
661+
active_index < active.offsets[row_index + 1]; ++active_index) {
662+
const size_t slot_index = active.slots[active_index];
663+
const std::string& path = materialized_slots[slot_index].relative_path.get_path();
664+
object.add_key({path.data(), path.size()});
665+
RETURN_IF_ERROR(variant_assembler_detail::append_materialized_value(
666+
batch.materialized[slot_index], row_index, row, 1));
667+
}
668+
object.finish();
669+
outer->insert_value(0);
670+
row.finish();
671+
}
672+
publish_encoded(&builder, std::move(outer), output);
673+
return Status::OK();
674+
}
675+
560676
Status assemble_hierarchical(StorageMapKind storage_map_kind, bool has_root,
561677
const PathInData& requested,
562678
std::span<const MaterializedSlot> materialized_slots,
563-
const VariantAssemblerBatchView& batch,
679+
bool can_assemble_flat, const VariantAssemblerBatchView& batch,
564680
ColumnNullable::MutablePtr* output) {
565681
StorageMapRowCursor map_cursor;
566682
PreparedHierarchicalBatch prepared = prepare_hierarchical_batch(
567683
storage_map_kind, has_root, materialized_slots, batch, &map_cursor);
684+
if (can_assemble_flat && has_only_empty_root_payload(prepared, batch.num_rows)) {
685+
return assemble_flat_materialized(materialized_slots, prepared, batch.num_rows, output);
686+
}
568687
VariantBatchBuilder builder(
569688
{.rows = batch.num_rows, .metadata_keys = materialized_slots.size() + 8});
570689
auto outer = ColumnUInt8::create();
@@ -652,26 +771,31 @@ Result<std::unique_ptr<VariantAssembler>> VariantAssembler::create(
652771
VariantAssemblerOptions options) {
653772
RETURN_IF_ERROR_RESULT(check_options(options));
654773
DorisVector<MaterializedSlot> materialized = build_materialized_slots(options);
774+
const bool can_assemble_flat = can_assemble_flat_materialized(
775+
options.storage_map_kind, options.requested_path, materialized);
655776
return std::unique_ptr<VariantAssembler>(
656777
new VariantAssembler(options.storage_map_kind, options.has_root, options.requested_path,
657-
std::move(materialized)));
778+
std::move(materialized), can_assemble_flat));
658779
}
659780

660781
VariantAssembler::VariantAssembler(
661782
StorageMapKind storage_map_kind, bool has_root, const PathInData& requested,
662-
DorisVector<variant_assembler_detail::MaterializedSlot> materialized)
783+
DorisVector<variant_assembler_detail::MaterializedSlot> materialized,
784+
bool can_assemble_flat_materialized)
663785
: _storage_map_kind(storage_map_kind),
664786
_has_root(has_root),
665787
_requested(requested),
666-
_materialized(std::move(materialized)) {}
788+
_materialized(std::move(materialized)),
789+
_can_assemble_flat_materialized(can_assemble_flat_materialized) {}
667790

668791
Status VariantAssembler::assemble(const VariantAssemblerBatchView& batch,
669792
ColumnNullable::MutablePtr* output) const {
670793
DORIS_CHECK(output != nullptr);
671794
try {
672795
ColumnNullable::MutablePtr result;
673-
const Status status = assemble_hierarchical(_storage_map_kind, _has_root, _requested,
674-
_materialized, batch, &result);
796+
const Status status =
797+
assemble_hierarchical(_storage_map_kind, _has_root, _requested, _materialized,
798+
_can_assemble_flat_materialized, batch, &result);
675799
if (!status.ok()) {
676800
return status;
677801
}

be/src/storage/segment/variant/v2/variant_assembler.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,14 @@ class VariantAssembler final {
9494

9595
private:
9696
VariantAssembler(StorageMapKind storage_map_kind, bool has_root, const PathInData& requested,
97-
DorisVector<variant_assembler_detail::MaterializedSlot> materialized);
97+
DorisVector<variant_assembler_detail::MaterializedSlot> materialized,
98+
bool can_assemble_flat_materialized);
9899

99100
StorageMapKind _storage_map_kind;
100101
bool _has_root;
101102
PathInData _requested;
102103
DorisVector<variant_assembler_detail::MaterializedSlot> _materialized;
104+
bool _can_assemble_flat_materialized;
103105
};
104106

105107
} // namespace segment_v2::variant_v2

be/test/storage/segment/variant_assembler_legacy_test.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <gtest/gtest.h>
1919

2020
#include <array>
21+
#include <initializer_list>
2122
#include <memory>
2223
#include <span>
2324
#include <string>
@@ -862,6 +863,52 @@ TEST(VariantAssemblerLegacyTest, UnsortedMaterializedPathsKeepSourceColumns) {
862863
EXPECT_EQ(json_at(assembled_values(output), 0), R"({"a":20,"m":{"child":30},"z":10})");
863864
}
864865

866+
TEST(VariantAssemblerLegacyTest, FlatMaterializedSkipsNullSlotsAndKeepsOuterNull) {
867+
auto make_nulls = [](std::initializer_list<uint8_t> values) {
868+
auto column = ColumnUInt8::create();
869+
for (uint8_t value : values) {
870+
column->insert_value(value);
871+
}
872+
return column;
873+
};
874+
auto z_values = ColumnInt32::create();
875+
auto a_values = ColumnInt32::create();
876+
for (int32_t value : {10, 20, 30, 40}) {
877+
z_values->insert_value(value);
878+
}
879+
for (int32_t value : {1, 2, 3, 4}) {
880+
a_values->insert_value(value);
881+
}
882+
auto z = ColumnNullable::create(std::move(z_values), make_nulls({1, 0, 1, 0}));
883+
auto a = ColumnNullable::create(std::move(a_values), make_nulls({0, 1, 1, 0}));
884+
885+
auto root_values = ColumnString::create();
886+
root_values->insert_many_defaults(4);
887+
auto root = ColumnNullable::create(std::move(root_values), make_nulls({0, 0, 0, 1}));
888+
889+
const auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
890+
VariantAssemblerOptions options;
891+
options.has_root = true;
892+
options.materialized_paths = {
893+
{.path = PathInData("z"), .type = nullable_int},
894+
{.path = PathInData("a"), .type = nullable_int},
895+
};
896+
auto assembler = create_assembler(std::move(options));
897+
const std::array<const IColumn*, 2> materialized {z.get(), a.get()};
898+
VariantAssemblerBatchView batch;
899+
batch.num_rows = 4;
900+
batch.root_jsonb = root.get();
901+
batch.materialized_columns = materialized;
902+
903+
ColumnNullable::MutablePtr output;
904+
ASSERT_TRUE(assembler->assemble(batch, &output).ok());
905+
const std::array<std::string_view, 4> expected {R"({"a":1})", R"({"z":20})", "{}", "null"};
906+
for (size_t row = 0; row < expected.size(); ++row) {
907+
EXPECT_EQ(json_at(assembled_values(output), row), expected[row]) << "row=" << row;
908+
}
909+
EXPECT_EQ(output->get_null_map_data(), (PaddedPODArray<uint8_t> {0, 0, 0, 1}));
910+
}
911+
865912
TEST(VariantAssemblerLegacyTest, RootSidecarYieldsToVisibleHierarchicalStreams) {
866913
const auto int_type = std::make_shared<DataTypeInt32>();
867914
const std::string sparse_cell = fixed_storage_cell<int32_t>(FieldType::OLAP_FIELD_TYPE_INT, 20);

0 commit comments

Comments
 (0)