Skip to content

Commit 6ba9f6c

Browse files
authored
feat(shredding): improve shared-shredding adaptive width restore & refactor write (alibaba#377)
1 parent 445d4e6 commit 6ba9f6c

51 files changed

Lines changed: 2137 additions & 587 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/paimon/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,24 @@ set(PAIMON_CORE_SRCS
220220
core/io/data_file_meta.cpp
221221
core/io/data_file_meta_serializer.cpp
222222
core/io/data_file_path_factory.cpp
223+
core/io/append_data_file_writer_factory.cpp
224+
core/io/blob_data_file_writer_factory.cpp
225+
core/io/data_file_writer_factory.cpp
223226
core/io/data_file_writer.cpp
224227
core/io/field_mapping_reader.cpp
225228
core/io/complete_row_tracking_fields_reader.cpp
226229
core/io/file_index_evaluator.cpp
227230
core/io/key_value_data_file_record_reader.cpp
231+
core/io/key_value_data_file_writer_factory.cpp
228232
core/io/key_value_data_file_writer.cpp
229233
core/io/key_value_in_memory_record_reader.cpp
230234
core/io/merged_key_value_record_reader.cpp
231235
core/io/key_value_meta_projection_consumer.cpp
232236
core/io/key_value_projection_consumer.cpp
233237
core/io/key_value_projection_reader.cpp
238+
core/io/map_shared_shredding_core_utils.cpp
239+
core/io/shredding_append_data_file_writer_factory.cpp
240+
core/io/shredding_key_value_data_file_writer_factory.cpp
234241
core/io/external_storage_blob_writer.cpp
235242
core/io/multiple_blob_file_writer.cpp
236243
core/io/rolling_blob_file_writer.cpp

src/paimon/common/data/shredding/map_shared_shredding_context.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "paimon/common/data/shredding/map_shared_shredding_context.h"
1818

1919
#include <algorithm>
20+
#include <cmath>
2021

2122
namespace paimon {
2223

@@ -32,8 +33,8 @@ std::map<std::string, int32_t> MapSharedShreddingContext::ComputeNextK() const {
3233
// First file — no history, use K_max.
3334
result[field_name] = k_max;
3435
} else {
35-
int32_t window_max = ComputeWindowMax(it->second);
36-
result[field_name] = std::max(1, std::min(window_max, k_max));
36+
int32_t adaptive_width = ComputeAdaptiveWidth(it->second);
37+
result[field_name] = std::max(1, std::min(adaptive_width, k_max));
3738
}
3839
}
3940
return result;
@@ -57,12 +58,27 @@ std::vector<std::string> MapSharedShreddingContext::GetShreddingColumnNames() co
5758
return names;
5859
}
5960

60-
int32_t MapSharedShreddingContext::ComputeWindowMax(const std::vector<int32_t>& values) {
61+
int32_t MapSharedShreddingContext::ComputeAdaptiveWidth(const std::vector<int32_t>& values) {
6162
if (values.empty()) {
6263
return 0;
6364
}
64-
// TODO(xinyu.lxy): support P99
65-
return *std::max_element(values.begin(), values.end());
65+
66+
std::vector<int32_t> sorted_values(values.begin(), values.end());
67+
std::sort(sorted_values.begin(), sorted_values.end());
68+
69+
int32_t max_width = sorted_values.back();
70+
auto percentile_rank = static_cast<int64_t>(std::ceil(kPercentileRatio * sorted_values.size()));
71+
percentile_rank = std::clamp<int64_t>(percentile_rank, 1, sorted_values.size());
72+
int32_t percentile_width = sorted_values[percentile_rank - 1];
73+
74+
// Use P90 to ignore far outliers, but keep max when it is close enough to normal rows.
75+
auto relative_close_threshold = static_cast<int32_t>(
76+
std::ceil(static_cast<double>(percentile_width) * kMaxCloseRelativeRatio));
77+
if (max_width - percentile_width <= kMaxCloseAbsoluteSlack ||
78+
max_width <= relative_close_threshold) {
79+
return max_width;
80+
}
81+
return percentile_width;
6682
}
6783

6884
} // namespace paimon

src/paimon/common/data/shredding/map_shared_shredding_context.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ namespace paimon {
3030
/// values to support adaptive K sizing across files.
3131
///
3232
/// - First file: K = K_max (no history).
33-
/// - Subsequent files: K = min(max(recent_max_row_widths), K_max).
33+
/// - Subsequent files: K = min(adaptive_width(recent_max_row_widths), K_max).
3434
class MapSharedShreddingContext {
3535
public:
3636
/// @param column_to_k_max Map from field name to its K_max (from options).
@@ -50,9 +50,12 @@ class MapSharedShreddingContext {
5050
std::vector<std::string> GetShreddingColumnNames() const;
5151

5252
private:
53-
static constexpr int32_t kWindowSize = 100;
53+
static constexpr int32_t kWindowSize = 20;
54+
static constexpr double kPercentileRatio = 0.90;
55+
static constexpr int32_t kMaxCloseAbsoluteSlack = 4;
56+
static constexpr double kMaxCloseRelativeRatio = 1.25;
5457

55-
static int32_t ComputeWindowMax(const std::vector<int32_t>& values);
58+
static int32_t ComputeAdaptiveWidth(const std::vector<int32_t>& values);
5659

5760
/// K_max per shared-shredding field, from options.
5861
std::map<std::string, int32_t> column_to_k_max_;

src/paimon/common/data/shredding/map_shared_shredding_context_test.cpp

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ TEST(MapSharedShreddingContextTest, FirstFileUsesKMax) {
3737

3838
TEST(MapSharedShreddingContextTest, AdaptKAfterOneFile) {
3939
// After reporting stats from one file, K should adapt to
40-
// min(max_row_width, K_max).
40+
// min(adaptive width, K_max).
4141
std::map<std::string, int32_t> field_to_k_max = {{"m", 10}};
4242
MapSharedShreddingContext context(field_to_k_max);
4343

@@ -64,20 +64,71 @@ TEST(MapSharedShreddingContextTest, AdaptKCappedByKMax) {
6464
ASSERT_EQ(5, next_k.at("m"));
6565
}
6666

67-
TEST(MapSharedShreddingContextTest, WindowMaxTracksLargest) {
68-
// K should use the max of all recent max_row_widths within the window.
67+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWhenSamplesAreClose) {
68+
// Small sample windows still use max because max and P90 are close.
6969
std::map<std::string, int32_t> field_to_k_max = {{"m", 20}};
7070
MapSharedShreddingContext context(field_to_k_max);
7171

7272
context.ReportFileStats("m", 3);
7373
context.ReportFileStats("m", 7);
7474
context.ReportFileStats("m", 5);
7575

76-
// max of {3, 7, 5} = 7, capped by K_max=20 → K=7.
7776
auto next_k = context.ComputeNextK();
7877
ASSERT_EQ(7, next_k.at("m"));
7978
}
8079

80+
TEST(MapSharedShreddingContextTest, WindowP90IgnoresSingleFarOutlier) {
81+
std::map<std::string, int32_t> field_to_k_max = {{"m", 2000}};
82+
MapSharedShreddingContext context(field_to_k_max);
83+
84+
for (int32_t i = 0; i < 19; ++i) {
85+
context.ReportFileStats("m", 3);
86+
}
87+
context.ReportFileStats("m", 1000);
88+
89+
auto next_k = context.ComputeNextK();
90+
ASSERT_EQ(3, next_k.at("m"));
91+
}
92+
93+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWithinAbsoluteSlack) {
94+
std::map<std::string, int32_t> field_to_k_max = {{"m", 20}};
95+
MapSharedShreddingContext context(field_to_k_max);
96+
97+
for (int32_t i = 0; i < 19; ++i) {
98+
context.ReportFileStats("m", 3);
99+
}
100+
context.ReportFileStats("m", 7);
101+
102+
auto next_k = context.ComputeNextK();
103+
ASSERT_EQ(7, next_k.at("m"));
104+
}
105+
106+
TEST(MapSharedShreddingContextTest, WindowP90UsesMaxWithinRelativeSlack) {
107+
std::map<std::string, int32_t> field_to_k_max = {{"m", 200}};
108+
MapSharedShreddingContext context(field_to_k_max);
109+
110+
for (int32_t i = 0; i < 19; ++i) {
111+
context.ReportFileStats("m", 100);
112+
}
113+
context.ReportFileStats("m", 125);
114+
115+
auto next_k = context.ComputeNextK();
116+
ASSERT_EQ(125, next_k.at("m"));
117+
}
118+
119+
TEST(MapSharedShreddingContextTest, WindowP90IgnoresMaxBeyondBothSlacks) {
120+
std::map<std::string, int32_t> field_to_k_max = {{"m", 200}};
121+
MapSharedShreddingContext context(field_to_k_max);
122+
123+
for (int32_t i = 0; i < 19; ++i) {
124+
context.ReportFileStats("m", 100);
125+
}
126+
context.ReportFileStats("m", 130);
127+
128+
auto next_k = context.ComputeNextK();
129+
ASSERT_EQ(100, next_k.at("m"));
130+
}
131+
81132
TEST(MapSharedShreddingContextTest, MultipleColumnsIndependent) {
82133
// Each field adapts independently.
83134
std::map<std::string, int32_t> field_to_k_max = {{"tags", 10}, {"attrs", 6}};
@@ -101,8 +152,7 @@ TEST(MapSharedShreddingContextTest, MultipleColumnsIndependent) {
101152
context.ReportFileStats("attrs", 6);
102153

103154
auto k3 = context.ComputeNextK();
104-
// tags: max(4,8)=8, capped by 10 → 8
105-
// attrs: max(2,6)=6, capped by 6 → 6
155+
// tags and attrs are close sample windows, so adaptive width keeps max.
106156
ASSERT_EQ(8, k3.at("tags"));
107157
ASSERT_EQ(6, k3.at("attrs"));
108158
}
@@ -116,7 +166,7 @@ TEST(MapSharedShreddingContextTest, GetShreddingColumnNames) {
116166
}
117167

118168
TEST(MapSharedShreddingContextTest, SlidingWindowEvictsOldEntries) {
119-
// The window size is 100. After filling 100 entries, adding one more
169+
// The window size is 20. After filling 20 entries, adding one more
120170
// should evict the oldest. Verify that the evicted value no longer
121171
// affects ComputeNextK.
122172
std::map<std::string, int32_t> field_to_k_max = {{"m", 256}};
@@ -125,19 +175,19 @@ TEST(MapSharedShreddingContextTest, SlidingWindowEvictsOldEntries) {
125175
// Insert a large value as the first entry.
126176
context.ReportFileStats("m", 200);
127177

128-
// Fill the remaining 99 slots with small values.
129-
for (int i = 0; i < 99; ++i) {
178+
// Fill the remaining 19 slots with small values.
179+
for (int32_t i = 0; i < 19; ++i) {
130180
context.ReportFileStats("m", 3);
131181
}
132182

133-
// Window = [200, 3, 3, ..., 3] (100 entries). Max = 200.
183+
// Window = [200, 3, 3, ..., 3] (20 entries). P90 = 3, max is a far outlier.
134184
auto k_before = context.ComputeNextK();
135-
ASSERT_EQ(200, k_before.at("m"));
185+
ASSERT_EQ(3, k_before.at("m"));
136186

137187
// Push one more — evicts the 200.
138188
context.ReportFileStats("m", 5);
139189

140-
// Window = [3, 3, ..., 3, 5] (100 entries). Max = 5.
190+
// Window = [3, 3, ..., 3, 5] (20 entries). Max is within the absolute slack.
141191
auto k_after = context.ComputeNextK();
142192
ASSERT_EQ(5, k_after.at("m"));
143193
}

src/paimon/common/data/shredding/map_shared_shredding_utils.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Result<std::shared_ptr<arrow::Schema>> MapSharedShreddingUtils::LogicalToPhysica
139139
auto value_type = map_type->item_type();
140140
bool value_nullable = map_type->item_field()->nullable();
141141
auto physical_type = BuildPhysicalStructType(value_type, it->second, value_nullable);
142-
auto physical_field = arrow::field(field->name(), physical_type, field->nullable());
142+
auto physical_field = field->WithType(physical_type);
143143
physical_fields.push_back(physical_field);
144144
} else {
145145
physical_fields.push_back(field);
@@ -451,6 +451,10 @@ MapSharedShreddingUtils::BuildMetadataFinalizer(
451451
arrow::FieldVector updated_fields = physical_schema->fields();
452452
for (const std::string& field_name : shredding_field_names) {
453453
int32_t col_index = physical_schema->GetFieldIndex(field_name);
454+
if (col_index < 0) {
455+
return Status::Invalid(fmt::format(
456+
"Shared-shredding field '{}' not found in physical schema.", field_name));
457+
}
454458
const auto& field = physical_schema->field(col_index);
455459
auto metadata = field->metadata() ? field->metadata()->Copy()
456460
: std::make_shared<arrow::KeyValueMetadata>();

src/paimon/common/data/shredding/map_shared_shredding_utils_test.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNullable) {
138138
ASSERT_OK_AND_ASSIGN(
139139
auto physical, MapSharedShreddingUtils::LogicalToPhysicalSchema(schema_nullable, col_map));
140140
auto struct_type = physical->field(0)->type();
141+
ASSERT_TRUE(struct_type->field(0)->nullable());
141142
ASSERT_TRUE(struct_type->field(1)->nullable());
142143
ASSERT_TRUE(struct_type->field(2)->nullable());
143144

@@ -152,6 +153,21 @@ TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNullable) {
152153
ASSERT_FALSE(struct_type2->field(2)->nullable());
153154
}
154155

156+
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaPreservesFieldMetadata) {
157+
auto metadata = std::make_shared<arrow::KeyValueMetadata>();
158+
metadata->Append("paimon.field.id", "7");
159+
metadata->Append("description", "original map field");
160+
auto map_type = arrow::map(arrow::utf8(), arrow::int64());
161+
auto schema = arrow::schema({arrow::field("m", map_type, false, metadata)});
162+
std::map<std::string, int32_t> col_map = {{"m", 2}};
163+
164+
ASSERT_OK_AND_ASSIGN(auto physical_schema,
165+
MapSharedShreddingUtils::LogicalToPhysicalSchema(schema, col_map));
166+
167+
ASSERT_FALSE(physical_schema->field(0)->nullable());
168+
ASSERT_TRUE(physical_schema->field(0)->metadata()->Equals(*metadata));
169+
}
170+
155171
TEST(MapSharedShreddingUtilsTest, LogicalToPhysicalSchemaNoShreddingColumns) {
156172
auto schema = arrow::schema({
157173
arrow::field("id", arrow::int32()),

src/paimon/common/data/shredding/shared_shredding_file_reader_test.cpp

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ class SharedShreddingFileReaderTest : public ::testing::Test {
166166
return reader_builder->Build(input_stream).value();
167167
}
168168

169+
Result<std::unique_ptr<AppendOnlyWriter>> CreateAppendOnlyWriter(
170+
const CoreOptions& core_options, int64_t schema_id,
171+
const std::shared_ptr<arrow::Schema>& logical_schema,
172+
const std::optional<std::vector<std::string>>& write_cols, int64_t max_sequence_number,
173+
const std::shared_ptr<DataFilePathFactory>& path_factory,
174+
const std::shared_ptr<CompactManager>& compact_manager) const {
175+
PAIMON_ASSIGN_OR_RAISE(
176+
std::shared_ptr<MapSharedShreddingContext> shredding_context,
177+
MapSharedShreddingUtils::CreateShreddingContext(logical_schema, core_options));
178+
return std::make_unique<AppendOnlyWriter>(core_options, schema_id, logical_schema,
179+
write_cols, max_sequence_number, path_factory,
180+
compact_manager, shredding_context, pool_);
181+
}
182+
169183
private:
170184
std::shared_ptr<MemoryPool> pool_;
171185
std::shared_ptr<arrow::Schema> logical_schema_ = arrow::schema({
@@ -473,9 +487,9 @@ TEST_F(SharedShreddingFileReaderTest, TestOrcDictionaryEncodedStringValue) {
473487
auto compact_manager = std::make_shared<NoopCompactManager>();
474488
ASSERT_OK_AND_ASSIGN(
475489
auto writer,
476-
AppendOnlyWriter::Create(core_options, /*schema_id=*/0, logical_schema,
477-
/*write_cols=*/std::nullopt,
478-
/*max_sequence_number=*/-1, path_factory, compact_manager, pool_));
490+
CreateAppendOnlyWriter(core_options, /*schema_id=*/0, logical_schema,
491+
/*write_cols=*/std::nullopt,
492+
/*max_sequence_number=*/-1, path_factory, compact_manager));
479493
auto batch = CreateBatch(logical_schema, R"([
480494
[1, [["a", "red"], ["b", "blue"]]],
481495
[2, [["c", "green"], ["a", "red"], ["b", "blue"]]],
@@ -530,9 +544,9 @@ TEST_F(SharedShreddingFileReaderTest, TestReadsRealFormatFile) {
530544
auto compact_manager = std::make_shared<NoopCompactManager>();
531545
ASSERT_OK_AND_ASSIGN(
532546
auto writer,
533-
AppendOnlyWriter::Create(core_options, /*schema_id=*/0, logical_schema_,
534-
/*write_cols=*/std::nullopt,
535-
/*max_sequence_number=*/-1, path_factory, compact_manager, pool_));
547+
CreateAppendOnlyWriter(core_options, /*schema_id=*/0, logical_schema_,
548+
/*write_cols=*/std::nullopt,
549+
/*max_sequence_number=*/-1, path_factory, compact_manager));
536550
auto batch = CreateBatch(logical_schema_, R"([
537551
[1, [["a", 1], ["b", 2]]],
538552
[2, [["c", 3], ["a", 4], ["b", 5]]],

0 commit comments

Comments
 (0)