Skip to content

Commit 5e7a0f2

Browse files
feat: topk in group by in counts api (#1702)
current: counts api returns the date binned result for each distinct value for the field(s) present in group by if the field used is high cardinal, the response will be bloated Prism uses this response to build a chart that might crash in render change: add optional topk param to group by default to 10 api returns topk results for the group by field(s)
1 parent e178599 commit 5e7a0f2

1 file changed

Lines changed: 157 additions & 11 deletions

File tree

src/query/mod.rs

Lines changed: 157 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type BoxedBatchStream = SendableRecordBatchStream;
8080

8181
/// Result type returned by query execution: either collected batches or a streaming adapter, plus field names.
8282
type QueryResult = Result<(Either<Vec<RecordBatch>, BoxedBatchStream>, Vec<String>), ExecuteError>;
83+
const DEFAULT_COUNTS_TOP_K: usize = 10;
8384

8485
// pub static QUERY_SESSION: Lazy<SessionContext> =
8586
// Lazy::new(|| Query::create_session_context(PARSEABLE.storage()));
@@ -495,6 +496,9 @@ pub struct CountConditions {
495496
pub conditions: Option<Conditions>,
496497
/// GroupBy columns
497498
pub group_by: Option<Vec<String>>,
499+
/// Optional number of top group values to return.
500+
#[serde(alias = "topk", alias = "top_k")]
501+
pub top_k: Option<usize>,
498502
}
499503

500504
/// Request for counts, received from API/SQL query.
@@ -513,6 +517,10 @@ pub struct CountsRequest {
513517
pub conditions: Option<CountConditions>,
514518
}
515519

520+
fn quote_identifier(value: &str) -> String {
521+
format!("\"{}\"", value.replace('"', "\"\""))
522+
}
523+
516524
impl CountsRequest {
517525
/// This function is supposed to read maninfest files for the given stream,
518526
/// get the sum of `num_rows` between the `startTime` and `endTime`,
@@ -652,17 +660,19 @@ impl CountsRequest {
652660
let time_range = TimeRange::parse_human_time(&self.start_time, &self.end_time)?;
653661

654662
let table_name = &self.stream;
663+
let table_ref = quote_identifier(table_name);
664+
let time_column_ref = format!("{}.{}", table_ref, quote_identifier(&time_column));
655665
let start_time_col_name = "_bin_start_time_";
656666
let end_time_col_name = "_bin_end_time_";
657667
let bin_interval = count_api_bin_interval(&time_range.start, &time_range.end);
658668
let date_bin = format!(
659-
"CAST(DATE_BIN('{bin_interval}', \"{table_name}\".\"{time_column}\", TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') AS TEXT) as {start_time_col_name}, DATE_BIN('{bin_interval}', \"{table_name}\".\"{time_column}\", TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') + INTERVAL '{bin_interval}' as {end_time_col_name}"
669+
"CAST(DATE_BIN('{bin_interval}', {time_column_ref}, TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') AS TEXT) as {start_time_col_name}, DATE_BIN('{bin_interval}', {time_column_ref}, TIMESTAMP '{DATE_BIN_EPOCH_ANCHOR}') + INTERVAL '{bin_interval}' as {end_time_col_name}"
660670
);
661671

662672
let group_by_cols = count_conditions
663673
.group_by
664674
.as_ref()
665-
.map(|cols| cols.iter().map(|c| format!("\"{c}\"")).collect::<Vec<_>>())
675+
.map(|cols| cols.iter().map(|c| quote_identifier(c)).collect::<Vec<_>>())
666676
.unwrap_or_default();
667677

668678
let group_clause = if group_by_cols.is_empty() {
@@ -671,17 +681,47 @@ impl CountsRequest {
671681
format!(", {}", group_by_cols.join(", "))
672682
};
673683

674-
let query = if let Some(conditions) = &count_conditions.conditions {
684+
let where_clause = if let Some(conditions) = &count_conditions.conditions {
675685
let f = get_filter_string(conditions).map_err(QueryError::CustomError)?;
676-
format!(
677-
"SELECT {date_bin}{group_clause}, COUNT(*) as count FROM \"{table_name}\" WHERE {} GROUP BY {end_time_col_name},{start_time_col_name}{group_clause} ORDER BY {end_time_col_name}{group_clause}",
678-
f
679-
)
686+
format!(" WHERE {f}")
680687
} else {
681-
format!(
682-
"SELECT {date_bin}{group_clause}, COUNT(*) as count FROM \"{table_name}\" GROUP BY {end_time_col_name},{start_time_col_name}{group_clause} ORDER BY {end_time_col_name}{group_clause}",
683-
)
688+
String::default()
684689
};
690+
691+
let query = format!(
692+
"SELECT {date_bin}{group_clause}, COUNT(*) as count FROM {table_ref}{where_clause} GROUP BY {end_time_col_name},{start_time_col_name}{group_clause} ORDER BY {end_time_col_name}{group_clause}",
693+
);
694+
695+
if group_by_cols.is_empty() {
696+
return Ok(query);
697+
}
698+
699+
let top_k = count_conditions.top_k.unwrap_or(DEFAULT_COUNTS_TOP_K);
700+
701+
if top_k == 0 {
702+
return Err(QueryError::CustomError(
703+
"topK must be greater than 0".to_string(),
704+
));
705+
}
706+
707+
let top_group_cols = group_by_cols.join(", ");
708+
let top_group_join = group_by_cols
709+
.iter()
710+
.map(|col| format!("(gc.{col} = tg.{col} OR (gc.{col} IS NULL AND tg.{col} IS NULL))"))
711+
.join(" AND ");
712+
let top_group_select = group_by_cols
713+
.iter()
714+
.map(|col| format!(", gc.{col} AS {col}"))
715+
.join("");
716+
let top_group_order = group_by_cols
717+
.iter()
718+
.map(|col| format!(", gc.{col}"))
719+
.join("");
720+
721+
let query = format!(
722+
"WITH grouped_counts AS (SELECT {date_bin}{group_clause}, COUNT(*) as count FROM {table_ref}{where_clause} GROUP BY {end_time_col_name},{start_time_col_name}{group_clause}), top_groups AS (SELECT {top_group_cols} FROM grouped_counts GROUP BY {top_group_cols} ORDER BY SUM(\"count\") DESC LIMIT {top_k}) SELECT gc.{start_time_col_name} AS {start_time_col_name}, gc.{end_time_col_name} AS {end_time_col_name}{top_group_select}, gc.\"count\" AS count FROM grouped_counts gc INNER JOIN top_groups tg ON {top_group_join} ORDER BY gc.{end_time_col_name}{top_group_order}",
723+
);
724+
685725
Ok(query)
686726
}
687727
}
@@ -1016,7 +1056,113 @@ impl PartitionedMetricMonitor {
10161056
mod tests {
10171057
use serde_json::json;
10181058

1019-
use crate::query::flatten_objects_for_count;
1059+
use crate::query::{
1060+
CountConditions, CountsRequest, flatten_objects_for_count, resolve_stream_names,
1061+
};
1062+
1063+
#[test]
1064+
fn test_count_conditions_accepts_top_k() {
1065+
let conditions: CountConditions = serde_json::from_value(json!({
1066+
"groupBy": ["host"],
1067+
"topK": 5
1068+
}))
1069+
.unwrap();
1070+
assert_eq!(conditions.top_k, Some(5));
1071+
1072+
let conditions: CountConditions = serde_json::from_value(json!({
1073+
"groupBy": ["host"],
1074+
"topk": 3
1075+
}))
1076+
.unwrap();
1077+
assert_eq!(conditions.top_k, Some(3));
1078+
}
1079+
1080+
#[tokio::test]
1081+
async fn test_counts_sql_applies_top_k_to_group_by() {
1082+
let request = CountsRequest {
1083+
stream: "logs".to_string(),
1084+
start_time: "2024-01-01T00:00:00Z".to_string(),
1085+
end_time: "2024-01-01T01:00:00Z".to_string(),
1086+
num_bins: None,
1087+
conditions: Some(CountConditions {
1088+
conditions: None,
1089+
group_by: Some(vec!["host".to_string(), "service.name".to_string()]),
1090+
top_k: Some(5),
1091+
}),
1092+
};
1093+
1094+
let sql = request.get_df_sql("p_timestamp".to_string()).await.unwrap();
1095+
1096+
assert!(sql.starts_with("WITH grouped_counts AS"));
1097+
assert!(sql.contains("SELECT \"host\", \"service.name\" FROM grouped_counts"));
1098+
assert!(sql.contains("GROUP BY \"host\", \"service.name\""));
1099+
assert!(sql.contains("ORDER BY SUM(\"count\") DESC LIMIT 5"));
1100+
assert!(sql.contains(
1101+
"(gc.\"host\" = tg.\"host\" OR (gc.\"host\" IS NULL AND tg.\"host\" IS NULL))"
1102+
));
1103+
assert!(sql.contains("ORDER BY gc._bin_end_time_, gc.\"host\", gc.\"service.name\""));
1104+
}
1105+
1106+
#[tokio::test]
1107+
async fn test_counts_sql_defaults_top_k_for_group_by() {
1108+
let request = CountsRequest {
1109+
stream: "logs".to_string(),
1110+
start_time: "2024-01-01T00:00:00Z".to_string(),
1111+
end_time: "2024-01-01T01:00:00Z".to_string(),
1112+
num_bins: None,
1113+
conditions: Some(CountConditions {
1114+
conditions: None,
1115+
group_by: Some(vec!["host".to_string()]),
1116+
top_k: None,
1117+
}),
1118+
};
1119+
1120+
let sql = request.get_df_sql("p_timestamp".to_string()).await.unwrap();
1121+
1122+
assert!(sql.starts_with("WITH grouped_counts AS"));
1123+
assert!(sql.contains("ORDER BY SUM(\"count\") DESC LIMIT 10"));
1124+
}
1125+
1126+
#[tokio::test]
1127+
async fn test_counts_top_k_sql_resolves_source_stream() {
1128+
let request = CountsRequest {
1129+
stream: "logs".to_string(),
1130+
start_time: "2024-01-01T00:00:00Z".to_string(),
1131+
end_time: "2024-01-01T01:00:00Z".to_string(),
1132+
num_bins: None,
1133+
conditions: Some(CountConditions {
1134+
conditions: None,
1135+
group_by: Some(vec!["host".to_string()]),
1136+
top_k: Some(5),
1137+
}),
1138+
};
1139+
1140+
let sql = request.get_df_sql("p_timestamp".to_string()).await.unwrap();
1141+
let streams = resolve_stream_names(&sql).unwrap();
1142+
1143+
assert_eq!(streams, vec!["logs"]);
1144+
}
1145+
1146+
#[tokio::test]
1147+
async fn test_counts_sql_rejects_zero_top_k_for_group_by() {
1148+
let request = CountsRequest {
1149+
stream: "logs".to_string(),
1150+
start_time: "2024-01-01T00:00:00Z".to_string(),
1151+
end_time: "2024-01-01T01:00:00Z".to_string(),
1152+
num_bins: None,
1153+
conditions: Some(CountConditions {
1154+
conditions: None,
1155+
group_by: Some(vec!["host".to_string()]),
1156+
top_k: Some(0),
1157+
}),
1158+
};
1159+
1160+
let err = request
1161+
.get_df_sql("p_timestamp".to_string())
1162+
.await
1163+
.unwrap_err();
1164+
assert!(err.to_string().contains("topK must be greater than 0"));
1165+
}
10201166

10211167
#[test]
10221168
fn test_flat_simple() {

0 commit comments

Comments
 (0)