Skip to content

Commit 1440600

Browse files
authored
feat(storage): adapt string stats length for common prefixes (#20199)
fix(storage): adapt string stats prefix length
1 parent 7117b1f commit 1440600

5 files changed

Lines changed: 182 additions & 100 deletions

File tree

src/query/storages/fuse/src/io/write/stream/column_statistics_builder.rs

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ use databend_common_expression::with_number_type;
4747
use databend_storages_common_table_meta::meta::ColumnStatistics;
4848
use enum_dispatch::enum_dispatch;
4949

50-
use crate::statistics::STATS_STRING_PREFIX_LEN;
51-
use crate::statistics::Trim;
52-
use crate::statistics::trim_string_max_with_len;
53-
use crate::statistics::trim_string_min_with_len;
50+
use crate::statistics::trim_column_min_max;
5451

5552
pub type CommonBuilder<T> = GenericColumnStatisticsBuilder<T, CommonAdapter>;
5653
pub type DecimalBuilder<T> = GenericColumnStatisticsBuilder<T, DecimalAdapter>;
@@ -105,7 +102,7 @@ where
105102

106103
pub fn create_column_stats_builder(
107104
data_type: &DataType,
108-
string_col_len: usize,
105+
string_col_len: Option<usize>,
109106
) -> ColumnStatisticsBuilder {
110107
let inner_type = data_type.remove_nullable();
111108
macro_rules! match_number_type_create {
@@ -226,7 +223,7 @@ where
226223
null_count: usize,
227224
in_memory_size: usize,
228225
data_type: DataType,
229-
string_col_len: usize,
226+
string_col_len: Option<usize>,
230227

231228
_phantom: PhantomData<(T, A)>,
232229
}
@@ -245,12 +242,12 @@ where
245242
null_count: 0,
246243
in_memory_size: 0,
247244
data_type,
248-
string_col_len: STATS_STRING_PREFIX_LEN,
245+
string_col_len: None,
249246
_phantom: PhantomData,
250247
}
251248
}
252249

253-
fn create_with_string_len(data_type: DataType, string_col_len: usize) -> Self {
250+
fn create_with_string_len(data_type: DataType, string_col_len: Option<usize>) -> Self {
254251
Self {
255252
min: None,
256253
max: None,
@@ -351,47 +348,25 @@ where
351348
}
352349

353350
fn finalize(self) -> Result<ColumnStatistics> {
354-
let string_col_len = self.string_col_len;
355-
let min = if let Some(v) = self.min {
351+
let raw_min = if let Some(v) = self.min {
356352
let v = A::value_to_scalar(v);
357-
let scalar = T::upcast_scalar_with_type(v, &self.data_type);
358-
match scalar {
359-
Scalar::String(s) => trim_string_min_with_len(s, string_col_len)
360-
.map(Scalar::String)
361-
.unwrap_or(Scalar::Null),
362-
other => other.trim_min().unwrap_or(Scalar::Null),
363-
}
353+
T::upcast_scalar_with_type(v, &self.data_type)
364354
} else {
365355
Scalar::Null
366356
};
367-
let max = if let Some(v) = self.max {
357+
let raw_max = if let Some(v) = self.max {
368358
let v = A::value_to_scalar(v);
369-
let scalar = T::upcast_scalar_with_type(v, &self.data_type);
370-
match scalar {
371-
Scalar::String(s) => {
372-
if let Some(v) = trim_string_max_with_len(s, string_col_len) {
373-
Scalar::String(v)
374-
} else {
375-
return Err(ErrorCode::Internal(
376-
"Unable to trim string: first chars are all replacement_point"
377-
.to_string(),
378-
));
379-
}
380-
}
381-
other => {
382-
if let Some(v) = other.trim_max() {
383-
v
384-
} else {
385-
return Err(ErrorCode::Internal(
386-
"Unable to trim string: first 16 chars are all replacement_point"
387-
.to_string(),
388-
));
389-
}
390-
}
391-
}
359+
T::upcast_scalar_with_type(v, &self.data_type)
392360
} else {
393361
Scalar::Null
394362
};
363+
let (min, max) =
364+
trim_column_min_max(raw_min, raw_max, self.string_col_len).ok_or_else(|| {
365+
ErrorCode::Internal(
366+
"Unable to trim string: retained prefix is at the end of the Unicode range"
367+
.to_string(),
368+
)
369+
})?;
395370

396371
Ok(ColumnStatistics::new(
397372
min,

src/query/storages/fuse/src/io/write/stream/column_statistics_state.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ impl ColumnStatisticsState {
4545
let col_stats = stats_columns
4646
.iter()
4747
.map(|(col_id, data_type)| {
48-
let string_len = col_stats_truncate_lens
49-
.get(col_id)
50-
.copied()
51-
.unwrap_or(crate::statistics::STATS_STRING_PREFIX_LEN);
48+
let string_len = col_stats_truncate_lens.get(col_id).copied();
5249
(*col_id, create_column_stats_builder(data_type, string_len))
5350
})
5451
.collect();
@@ -146,6 +143,7 @@ mod tests {
146143
use databend_storages_common_index::RangeIndex;
147144

148145
use super::*;
146+
use crate::statistics::END_OF_UNICODE_RANGE;
149147
use crate::statistics::gen_columns_statistics;
150148

151149
#[test]
@@ -198,4 +196,33 @@ mod tests {
198196
assert_eq!(stats_0, stats_1);
199197
Ok(())
200198
}
199+
200+
#[test]
201+
fn test_column_stats_state_adaptive_string_prefix() -> Result<()> {
202+
let field = TableField::new("a", TableDataType::String);
203+
let schema = Arc::new(TableSchema::new(vec![field]));
204+
let prefix = "abcdefghijklmnop";
205+
let min = format!("{prefix}a-min-suffix");
206+
let max = format!("{prefix}z-max-suffix");
207+
let block = DataBlock::new_from_columns(vec![StringType::from_data(vec![
208+
min.as_str(),
209+
max.as_str(),
210+
])]);
211+
let stats_columns = vec![(0, DataType::String)];
212+
213+
let mut state = ColumnStatisticsState::new(&stats_columns, &[], &BTreeMap::new());
214+
state.add_block(&schema, &block)?;
215+
let stats = state.finalize(HashMap::new())?;
216+
let col_stats = stats.get(&0).unwrap();
217+
218+
assert_eq!(
219+
col_stats.min(),
220+
&databend_common_expression::Scalar::String(format!("{prefix}a"))
221+
);
222+
assert_eq!(
223+
col_stats.max(),
224+
&databend_common_expression::Scalar::String(format!("{prefix}{END_OF_UNICODE_RANGE}"))
225+
);
226+
Ok(())
227+
}
201228
}

src/query/storages/fuse/src/statistics/column_statistic.rs

Lines changed: 103 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,19 @@ pub fn gen_columns_statistics(
6868
let (distinct_of_values, unset_bits) =
6969
if s == Scalar::Null { (0, rows) } else { (1, 0) };
7070

71+
let Some((min, max)) = trim_column_min_max(
72+
s.clone(),
73+
s.clone(),
74+
col_stats_truncate_lens.get(&column_id).copied(),
75+
) else {
76+
continue;
77+
};
78+
7179
// when we read it back from parquet, it is a Column instead of Scalar
7280
let in_memory_size = s.as_ref().estimated_scalar_repeat_size(rows, &data_type);
7381
let col_stats = ColumnStatistics::new(
74-
s.clone(),
75-
s.clone(),
82+
min,
83+
max,
7684
unset_bits as u64,
7785
in_memory_size as u64,
7886
Some(distinct_of_values),
@@ -82,56 +90,27 @@ pub fn gen_columns_statistics(
8290
}
8391
Value::Column(col) => {
8492
// later, during the evaluation of expressions, name of field does not matter
85-
let mut min = Scalar::Null;
86-
let mut max = Scalar::Null;
87-
88-
if col.len() > 0 {
93+
let (min, max) = if col.len() > 0 {
8994
let (mins, _) = eval_aggr("min", vec![], &[col.clone().into()], rows, vec![])?;
9095
let (maxs, _) = eval_aggr("max", vec![], &[col.clone().into()], rows, vec![])?;
9196

92-
let truncate_len = col_stats_truncate_lens
93-
.get(&column_id)
94-
.copied()
95-
.unwrap_or(STATS_STRING_PREFIX_LEN);
96-
97-
if mins.len() > 0 {
98-
min = if let Some(v) = mins.index(0) {
99-
let owned = v.to_owned();
100-
let trimmed = match owned {
101-
Scalar::String(s) => {
102-
trim_string_min_with_len(s, truncate_len).map(Scalar::String)
103-
}
104-
other => other.trim_min(),
105-
};
106-
if let Some(v) = trimmed {
107-
v
108-
} else {
109-
continue;
110-
}
111-
} else {
112-
continue;
113-
}
114-
}
115-
116-
if maxs.len() > 0 {
117-
max = if let Some(v) = maxs.index(0) {
118-
let owned = v.to_owned();
119-
let trimmed = match owned {
120-
Scalar::String(s) => {
121-
trim_string_max_with_len(s, truncate_len).map(Scalar::String)
122-
}
123-
other => other.trim_max(),
124-
};
125-
if let Some(v) = trimmed {
126-
v
127-
} else {
128-
continue;
129-
}
130-
} else {
131-
continue;
132-
}
133-
}
134-
}
97+
let Some(raw_min) = mins.index(0).map(|v| v.to_owned()) else {
98+
continue;
99+
};
100+
let Some(raw_max) = maxs.index(0).map(|v| v.to_owned()) else {
101+
continue;
102+
};
103+
let Some(min_max) = trim_column_min_max(
104+
raw_min,
105+
raw_max,
106+
col_stats_truncate_lens.get(&column_id).copied(),
107+
) else {
108+
continue;
109+
};
110+
min_max
111+
} else {
112+
(Scalar::Null, Scalar::Null)
113+
};
135114

136115
let (is_all_null, bitmap) = col.validity();
137116
let unset_bits = match (is_all_null, bitmap) {
@@ -190,6 +169,43 @@ pub trait Trim: Sized {
190169

191170
pub const END_OF_UNICODE_RANGE: char = '\u{10FFFF}';
192171
pub const STATS_STRING_PREFIX_LEN: usize = 16;
172+
const MAX_AUTO_STATS_STRING_PREFIX_LEN: usize = 32;
173+
const MAX_AUTO_STATS_STRING_COMMON_PREFIX_LEN: usize = MAX_AUTO_STATS_STRING_PREFIX_LEN - 1;
174+
175+
fn auto_stats_string_prefix_len(min: &str, max: &str) -> usize {
176+
let common_prefix_len = min
177+
.chars()
178+
.zip(max.chars())
179+
.take(MAX_AUTO_STATS_STRING_COMMON_PREFIX_LEN)
180+
.take_while(|(min_char, max_char)| min_char == max_char)
181+
.count();
182+
183+
if common_prefix_len < STATS_STRING_PREFIX_LEN {
184+
STATS_STRING_PREFIX_LEN
185+
} else {
186+
common_prefix_len
187+
.saturating_add(1)
188+
.min(MAX_AUTO_STATS_STRING_PREFIX_LEN)
189+
}
190+
}
191+
192+
pub(crate) fn trim_column_min_max(
193+
min: Scalar,
194+
max: Scalar,
195+
string_truncate_len: Option<usize>,
196+
) -> Option<(Scalar, Scalar)> {
197+
match (min, max) {
198+
(Scalar::String(min), Scalar::String(max)) => {
199+
let truncate_len =
200+
string_truncate_len.unwrap_or_else(|| auto_stats_string_prefix_len(&min, &max));
201+
Some((
202+
Scalar::String(trim_string_min_with_len(min, truncate_len)?),
203+
Scalar::String(trim_string_max_with_len(max, truncate_len)?),
204+
))
205+
}
206+
(min, max) => Some((min.trim_min()?, max.trim_max()?)),
207+
}
208+
}
193209

194210
impl Trim for Scalar {
195211
fn trim_min(self) -> Option<Self> {
@@ -317,10 +333,48 @@ pub fn trim_string_max_with_len(s: String, len: usize) -> Option<String> {
317333
mod tests {
318334
use databend_common_expression::Scalar;
319335

336+
use super::MAX_AUTO_STATS_STRING_COMMON_PREFIX_LEN;
337+
use super::MAX_AUTO_STATS_STRING_PREFIX_LEN;
338+
use super::auto_stats_string_prefix_len;
320339
use crate::statistics::END_OF_UNICODE_RANGE;
321340
use crate::statistics::STATS_STRING_PREFIX_LEN;
322341
use crate::statistics::Trim;
323342

343+
#[test]
344+
fn test_auto_stats_string_prefix_len() {
345+
assert_eq!(
346+
auto_stats_string_prefix_len("abcdefghijklmnoa", "abcdefghijklmnoz"),
347+
STATS_STRING_PREFIX_LEN
348+
);
349+
350+
let prefix = "abcdefghijklmnop";
351+
assert_eq!(
352+
auto_stats_string_prefix_len(
353+
&format!("{prefix}a-min-suffix"),
354+
&format!("{prefix}z-max-suffix"),
355+
),
356+
STATS_STRING_PREFIX_LEN + 1
357+
);
358+
359+
let long_prefix = "a".repeat(MAX_AUTO_STATS_STRING_COMMON_PREFIX_LEN);
360+
assert_eq!(
361+
auto_stats_string_prefix_len(
362+
&format!("{long_prefix}x-min"),
363+
&format!("{long_prefix}y-max"),
364+
),
365+
MAX_AUTO_STATS_STRING_PREFIX_LEN
366+
);
367+
368+
let capped_prefix = "好".repeat(MAX_AUTO_STATS_STRING_PREFIX_LEN);
369+
assert_eq!(
370+
auto_stats_string_prefix_len(
371+
&format!("{capped_prefix}甲"),
372+
&format!("{capped_prefix}乙"),
373+
),
374+
MAX_AUTO_STATS_STRING_PREFIX_LEN
375+
);
376+
}
377+
324378
#[test]
325379
fn test_trim_max() {
326380
{

src/query/storages/fuse/src/statistics/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub use column_statistic::Trim;
4141
pub use column_statistic::calc_column_distinct_of_values;
4242
pub use column_statistic::gen_columns_statistics;
4343
pub use column_statistic::scalar_min_max;
44+
pub(crate) use column_statistic::trim_column_min_max;
4445
pub use column_statistic::trim_string_max_with_len;
4546
pub use column_statistic::trim_string_min_with_len;
4647
pub use reducers::merge_statistics;

0 commit comments

Comments
 (0)