From 428cd8137d14215989b11f46ff98f188d214dea5 Mon Sep 17 00:00:00 2001 From: Ruihang Xia Date: Fri, 5 Jun 2026 16:44:50 +0800 Subject: [PATCH] feat: identify noneffective binary modifiers (#8230) * perf(promql): use tsid for full-label modifier joins * fix(promql): collect narrow range joins by tsid * verify on for both sides Signed-off-by: Ruihang Xia * check if tsid exists Signed-off-by: Ruihang Xia * update sqlness Signed-off-by: Ruihang Xia * correct behavior Signed-off-by: Ruihang Xia --------- Signed-off-by: Ruihang Xia --- .../src/optimizer/promql_tsid_narrow_join.rs | 61 ++- src/query/src/promql/planner.rs | 478 +++++++++++++++--- .../distributed/flow-tql/tsid_on_phy.result | 46 +- .../standalone/common/promql/label.result | 35 +- .../promql/tsid_binary_join_regression.result | 88 +++- .../promql/tsid_binary_join_regression.sql | 35 ++ 6 files changed, 624 insertions(+), 119 deletions(-) diff --git a/src/query/src/optimizer/promql_tsid_narrow_join.rs b/src/query/src/optimizer/promql_tsid_narrow_join.rs index 419415662e..6dd0b5e4bb 100644 --- a/src/query/src/optimizer/promql_tsid_narrow_join.rs +++ b/src/query/src/optimizer/promql_tsid_narrow_join.rs @@ -82,20 +82,19 @@ impl PromqlTsidNarrowJoin { } fn is_promql_value_tsid_time_schema(schema: &SchemaRef) -> bool { - let mut has_value = false; + let mut value_columns = 0; let mut has_tsid = false; let mut has_time = false; for field in schema.fields() { match field.name().as_str() { - "greptime_value" => has_value = true, DATA_SCHEMA_TSID_COLUMN_NAME => has_tsid = true, _ if matches!(field.data_type(), DataType::Timestamp(_, _)) => has_time = true, - _ => return false, + _ => value_columns += 1, } } - has_value && has_tsid && has_time + value_columns == 1 && has_tsid && has_time } fn joins_on_tsid_and_time(hash_join: &HashJoinExec) -> bool { @@ -214,6 +213,60 @@ mod tests { ); } + #[test] + fn chooses_collect_left_for_computed_narrow_value_column() { + let left = Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ + Field::new("prom_rate(greptime_value)", DataType::Float64, true), + Field::new(DATA_SCHEMA_TSID_COLUMN_NAME, DataType::UInt64, false), + Field::new( + "greptime_timestamp", + DataType::Timestamp(TimeUnit::Millisecond, None), + false, + ), + ])))) as Arc; + let right = Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ + Field::new("greptime_value", DataType::Float64, true), + Field::new("host", DataType::Utf8, true), + Field::new(DATA_SCHEMA_TSID_COLUMN_NAME, DataType::UInt64, false), + Field::new( + "greptime_timestamp", + DataType::Timestamp(TimeUnit::Millisecond, None), + false, + ), + ])))) as Arc; + let on = vec![ + ( + Arc::new(Column::new(DATA_SCHEMA_TSID_COLUMN_NAME, 1)) as Arc, + Arc::new(Column::new(DATA_SCHEMA_TSID_COLUMN_NAME, 2)) as Arc, + ), + ( + Arc::new(Column::new("greptime_timestamp", 2)) as Arc, + Arc::new(Column::new("greptime_timestamp", 3)) as Arc, + ), + ]; + let join = Arc::new( + HashJoinExec::try_new( + left, + right, + on, + None, + &JoinType::Inner, + Some(vec![0, 3, 4, 5, 6]), + PartitionMode::Partitioned, + NullEquality::NullEqualsNull, + false, + ) + .unwrap(), + ) as Arc; + + let optimized = PromqlTsidNarrowJoin + .optimize(join, &ConfigOptions::default()) + .unwrap(); + let optimized_join = optimized.as_any().downcast_ref::().unwrap(); + + assert_eq!(optimized_join.partition_mode(), &PartitionMode::CollectLeft); + } + #[test] fn keeps_partitioned_join_when_left_side_carries_labels() { let left = Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ diff --git a/src/query/src/promql/planner.rs b/src/query/src/promql/planner.rs index 46740a8b36..f361d4d51e 100644 --- a/src/query/src/promql/planner.rs +++ b/src/query/src/promql/planner.rs @@ -876,14 +876,14 @@ impl PromPlanner { ) -> Result { let only_join_time_index = first_leaf.ctx.tag_columns.is_empty() || right_leaf.ctx.tag_columns.is_empty(); - let (mut left_keys, mut right_keys) = Self::binary_join_key_columns( - &left, - &right_leaf.plan, + let (mut left_keys, mut right_keys, force_empty_join) = self.binary_join_key_columns( + left.schema(), + right_leaf.plan.schema(), &first_leaf.ctx, &right_leaf.ctx, only_join_time_index, &None, - ); + )?; if let (Some(left_time_index_column), Some(right_time_index_column)) = ( first_leaf.ctx.time_index_column.clone(), @@ -907,7 +907,7 @@ impl PromPlanner { .map(|name| Column::new(Some(right_leaf.alias.clone()), name)) .collect::>(), ), - None, + force_empty_join.then_some(lit(false)), NullEquality::NullEqualsNull, ) .context(DataFusionPlanningSnafu)? @@ -1250,6 +1250,14 @@ impl PromPlanner { } let (output_field_columns, field_columns) = Self::align_binary_field_columns(&left_field_columns, &right_field_columns); + let left_aligned_field_columns = field_columns + .iter() + .map(|(left_col_name, _)| (*left_col_name).clone()) + .collect::>(); + let right_aligned_field_columns = field_columns + .iter() + .map(|(_, right_col_name)| (*right_col_name).clone()) + .collect::>(); // PromQL binary arithmetic only combines the shared prefix of value columns. // Keep the output field count aligned with that zipped prefix so planning // remains stable even when the two sides have uneven multi-field schemas. @@ -1267,6 +1275,8 @@ impl PromPlanner { // under this case we only join on time index left_context.tag_columns.is_empty() || right_context.tag_columns.is_empty(), modifier, + &left_context, + &right_context, )?; let join_plan_schema = join_plan.schema().clone(); @@ -1300,14 +1310,21 @@ impl PromPlanner { // So we filter on the join result and then project only the side that should // be preserved according to PromQL semantics. let filtered = self.filter_on_field_column(join_plan, bin_expr_builder)?; - let (project_table_ref, project_context) = + let (project_table_ref, mut project_context, project_field_columns) = match (lhs.value_type(), rhs.value_type()) { - (ValueType::Scalar, ValueType::Vector) => { - (&right_table_ref, &right_context) - } - _ => (&left_table_ref, &left_context), + (ValueType::Scalar, ValueType::Vector) => ( + &right_table_ref, + right_context.clone(), + right_aligned_field_columns, + ), + _ => ( + &left_table_ref, + left_context.clone(), + left_aligned_field_columns, + ), }; - self.project_binary_join_side(filtered, project_table_ref, project_context) + project_context.field_columns = project_field_columns; + self.project_binary_join_side(filtered, project_table_ref, &project_context) } else { self.projection_for_each_field_column(join_plan, bin_expr_builder) } @@ -1600,6 +1617,11 @@ impl PromPlanner { self.create_function_expr(func, args.literals.clone(), query_engine_state)?; func_exprs.insert(0, self.create_time_index_column_expr()?); func_exprs.extend_from_slice(&self.create_tag_column_exprs()?); + if let Some(tsid_col) = + Self::optional_tsid_projection(input.schema(), None, self.ctx.use_tsid) + { + func_exprs.push(tsid_col); + } let builder = LogicalPlanBuilder::from(input) .project(func_exprs) @@ -2848,6 +2870,7 @@ impl PromPlanner { } "label_join" => { + self.ctx.use_tsid = false; let (concat_expr, dst_label) = Self::build_concat_labels_expr( &mut other_input_exprs, &self.ctx, @@ -2871,6 +2894,7 @@ impl PromPlanner { ScalarFunc::GeneratedExpr } "label_replace" => { + self.ctx.use_tsid = false; if let Some((replace_expr, dst_label)) = self .build_regexp_replace_label_expr(&mut other_input_exprs, query_engine_state)? { @@ -3978,20 +4002,26 @@ impl PromPlanner { } fn binary_join_key_columns( - left: &LogicalPlan, - right: &LogicalPlan, - left_ctx: &PromPlannerContext, - right_ctx: &PromPlannerContext, + &self, + left_schema: &DFSchemaRef, + right_schema: &DFSchemaRef, + left_context: &PromPlannerContext, + right_context: &PromPlannerContext, only_join_time_index: bool, modifier: &Option, - ) -> (BTreeSet, BTreeSet) { + ) -> Result<(BTreeSet, BTreeSet, bool)> { + let has_tsid = |schema: &DFSchemaRef| { + schema + .fields() + .iter() + .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME) + }; let use_tsid_join = !only_join_time_index - && modifier.as_ref().is_none_or(|modifier| { - modifier.matching.is_none() - && matches!(modifier.card, VectorMatchCardinality::OneToOne) - }) - && Self::plan_has_tsid_column(left) - && Self::plan_has_tsid_column(right); + && self.binary_modifier_preserves_tsid_join_key(left_context, right_context, modifier) + && left_context.use_tsid + && right_context.use_tsid + && has_tsid(left_schema) + && has_tsid(right_schema); let (mut left_tag_columns, mut right_tag_columns) = if use_tsid_join { ( @@ -3999,25 +4029,22 @@ impl PromPlanner { BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]), ) } else { - let left_tag_columns = if only_join_time_index { - BTreeSet::new() + if only_join_time_index { + (BTreeSet::new(), BTreeSet::new()) } else { - left_ctx - .tag_columns - .iter() - .cloned() - .collect::>() - }; - let right_tag_columns = if only_join_time_index { - BTreeSet::new() - } else { - right_ctx - .tag_columns - .iter() - .cloned() - .collect::>() - }; - (left_tag_columns, right_tag_columns) + ( + left_context + .tag_columns + .iter() + .cloned() + .collect::>(), + right_context + .tag_columns + .iter() + .cloned() + .collect::>(), + ) + } }; if !use_tsid_join @@ -4039,7 +4066,56 @@ impl PromPlanner { } } - (left_tag_columns, right_tag_columns) + let force_empty_join = + !use_tsid_join && !only_join_time_index && left_tag_columns != right_tag_columns; + if force_empty_join { + let common_tag_columns = left_tag_columns + .intersection(&right_tag_columns) + .cloned() + .collect::>(); + left_tag_columns = common_tag_columns.clone(); + right_tag_columns = common_tag_columns; + } + + Ok((left_tag_columns, right_tag_columns, force_empty_join)) + } + + fn binary_modifier_preserves_tsid_join_key( + &self, + left_context: &PromPlannerContext, + right_context: &PromPlannerContext, + modifier: &Option, + ) -> bool { + let Some(modifier) = modifier else { + return true; + }; + + if !matches!(modifier.card, VectorMatchCardinality::OneToOne) { + return false; + } + + match &modifier.matching { + None => true, + Some(LabelModifier::Exclude(ignoring)) => ignoring.labels.iter().all(|label| { + !left_context.tag_columns.contains(label) + && !right_context.tag_columns.contains(label) + }), + Some(LabelModifier::Include(on)) => { + let on_labels = on.labels.iter().cloned().collect::>(); + let left_labels = left_context + .tag_columns + .iter() + .cloned() + .collect::>(); + let right_labels = right_context + .tag_columns + .iter() + .cloned() + .collect::>(); + + on_labels == left_labels && on_labels == right_labels + } + } } /// Build a inner join on time index column and tag columns to concat two logical plans. @@ -4055,15 +4131,18 @@ impl PromPlanner { right_time_index_column: Option, only_join_time_index: bool, modifier: &Option, + left_context: &PromPlannerContext, + right_context: &PromPlannerContext, ) -> Result { - let (mut left_tag_columns, mut right_tag_columns) = Self::binary_join_key_columns( - &left, - &right, - &self.ctx, - &self.ctx, - only_join_time_index, - modifier, - ); + let (mut left_tag_columns, mut right_tag_columns, force_empty_join) = self + .binary_join_key_columns( + left.schema(), + right.schema(), + left_context, + right_context, + only_join_time_index, + modifier, + )?; // push time index column if it exists if let (Some(left_time_index_column), Some(right_time_index_column)) = @@ -4096,7 +4175,7 @@ impl PromPlanner { .map(Column::from_name) .collect::>(), ), - None, + force_empty_join.then_some(lit(false)), NullEquality::NullEqualsNull, ) .context(DataFusionPlanningSnafu)? @@ -4859,14 +4938,29 @@ mod test { async fn build_test_table_provider_with_tsid_fields( table_specs: &[((String, String), usize)], num_tag: usize, + ) -> DfTableSourceProvider { + let table_specs = table_specs + .iter() + .map(|(table_name_tuple, num_field)| (table_name_tuple.clone(), num_tag, *num_field)) + .collect::>(); + build_test_table_provider_with_tsid_tag_fields(&table_specs).await + } + + async fn build_test_table_provider_with_tsid_tag_fields( + table_specs: &[((String, String), usize, usize)], ) -> DfTableSourceProvider { let catalog_list = MemoryCatalogManager::with_default_setup(); let physical_table_name = "phy"; let physical_table_id = 999u32; + let physical_num_tag = table_specs + .iter() + .map(|(_, num_tag, _)| *num_tag) + .max() + .unwrap_or(0); let physical_num_field = table_specs .iter() - .map(|(_, num_field)| *num_field) + .map(|(_, _, num_field)| *num_field) .max() .unwrap_or(0); @@ -4884,7 +4978,7 @@ mod test { false, ), ]; - for i in 0..num_tag { + for i in 0..physical_num_tag { columns.push(ColumnSchema::new( format!("tag_{i}"), ConcreteDataType::string_datatype(), @@ -4908,11 +5002,13 @@ mod test { } let schema = Arc::new(Schema::new(columns)); - let primary_key_indices = (0..(2 + num_tag)).collect::>(); + let primary_key_indices = (0..(2 + physical_num_tag)).collect::>(); let table_meta = TableMetaBuilder::empty() .schema(schema) .primary_key_indices(primary_key_indices) - .value_indices((2 + num_tag..2 + num_tag + 1 + physical_num_field).collect()) + .value_indices( + (2 + physical_num_tag..2 + physical_num_tag + 1 + physical_num_field).collect(), + ) .engine(METRIC_ENGINE_NAME.to_string()) .next_column_id(1024) .build() @@ -4939,9 +5035,10 @@ mod test { } // Register metric engine logical tables without `__tsid`, referencing the physical table. - for (idx, ((schema_name, table_name), num_field)) in table_specs.iter().enumerate() { + for (idx, ((schema_name, table_name), num_tag, num_field)) in table_specs.iter().enumerate() + { let mut columns = vec![]; - for i in 0..num_tag { + for i in 0..*num_tag { columns.push(ColumnSchema::new( format!("tag_{i}"), ConcreteDataType::string_datatype(), @@ -4973,8 +5070,8 @@ mod test { let table_id = 1024u32 + idx as u32; let table_meta = TableMetaBuilder::empty() .schema(schema) - .primary_key_indices((0..num_tag).collect()) - .value_indices((num_tag + 1..num_tag + 1 + *num_field).collect()) + .primary_key_indices((0..*num_tag).collect()) + .value_indices((*num_tag + 1..*num_tag + 1 + *num_field).collect()) .engine(METRIC_ENGINE_NAME.to_string()) .options(options) .next_column_id(1024) @@ -5443,6 +5540,71 @@ mod test { ); } + #[tokio::test] + async fn timestamp_binary_join_falls_back_when_tsid_is_projected_out() { + for query in [ + "timestamp(some_metric) / some_metric", + "some_metric / timestamp(some_metric)", + ] { + let eval_stmt = build_eval_stmt(query); + + let table_provider = build_test_table_provider_with_tsid( + &[(DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string())], + 1, + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!(!plan_str.contains("__tsid ="), "{query}: {plan_str}"); + assert!( + plan_str.contains("lhs.tag_0 = rhs.tag_0"), + "{query}: {plan_str}" + ); + assert!( + !plan + .schema() + .fields() + .iter() + .any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME), + "{query}: {plan_str}" + ); + } + } + + #[tokio::test] + async fn timestamp_binary_join_rejects_default_matching_on_mismatched_labels() { + let eval_stmt = build_eval_stmt("timestamp(left_host_job) / right_by_job"); + + let table_provider = build_test_table_provider_with_tsid_tag_fields(&[ + ( + (DEFAULT_SCHEMA_NAME.to_string(), "left_host_job".to_string()), + 2, + 1, + ), + ( + (DEFAULT_SCHEMA_NAME.to_string(), "right_by_job".to_string()), + 1, + 1, + ), + ]) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + let plan_str = plan.display_indent_schema().to_string(); + + assert!( + plan_str.contains("Boolean(false)") || plan_str.contains("false"), + "{plan_str}" + ); + } + #[tokio::test] async fn tsid_is_preserved_for_nested_default_binary_joins() { let eval_stmt = build_eval_stmt("(some_metric - some_alt_metric) / some_third_metric"); @@ -5866,6 +6028,51 @@ mod test { assert_eq!(value_columns, 1, "{field_names:?}"); } + #[tokio::test] + async fn comparison_binary_join_uses_shorter_field_side() { + let eval_stmt = build_eval_stmt("two_field_metric > one_field_metric"); + + let table_provider = build_test_table_provider_with_tsid_fields( + &[ + ( + ( + DEFAULT_SCHEMA_NAME.to_string(), + "two_field_metric".to_string(), + ), + 2, + ), + ( + ( + DEFAULT_SCHEMA_NAME.to_string(), + "one_field_metric".to_string(), + ), + 1, + ), + ], + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let field_names = plan + .schema() + .fields() + .iter() + .map(|field| field.name().clone()) + .collect::>(); + assert!( + field_names.iter().any(|name| name == "field_0"), + "{field_names:?}" + ); + assert!( + !field_names.iter().any(|name| name == "field_1"), + "{field_names:?}" + ); + } + #[tokio::test] async fn label_matching_modifier_disables_tsid_binary_join() { let eval_stmt = build_eval_stmt("some_metric / ignoring(tag_0) some_alt_metric"); @@ -5895,6 +6102,161 @@ mod test { ); } + #[tokio::test] + async fn ignoring_absent_label_keeps_tsid_binary_join() { + let eval_stmt = build_eval_stmt("some_metric / ignoring(missing) some_alt_metric"); + + let table_provider = build_test_table_provider_with_tsid( + &[ + (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()), + ( + DEFAULT_SCHEMA_NAME.to_string(), + "some_alt_metric".to_string(), + ), + ], + 2, + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!( + plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"), + "{plan_str}" + ); + assert!(!plan_str.contains("tag_0 ="), "{plan_str}"); + assert!(!plan_str.contains("tag_1 ="), "{plan_str}"); + } + + #[tokio::test] + async fn range_function_keeps_tsid_for_absent_ignoring_binary_join() { + let eval_stmt = + build_eval_stmt("rate(some_metric[5m]) / ignoring(missing) some_alt_metric"); + + let table_provider = build_test_table_provider_with_tsid( + &[ + (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()), + ( + DEFAULT_SCHEMA_NAME.to_string(), + "some_alt_metric".to_string(), + ), + ], + 2, + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!( + plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"), + "{plan_str}" + ); + assert!(!plan_str.contains("tag_0 ="), "{plan_str}"); + assert!(!plan_str.contains("tag_1 ="), "{plan_str}"); + } + + #[tokio::test] + async fn on_full_label_set_keeps_tsid_binary_join() { + let eval_stmt = build_eval_stmt("some_metric / on(tag_0, tag_1) some_alt_metric"); + + let table_provider = build_test_table_provider_with_tsid( + &[ + (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()), + ( + DEFAULT_SCHEMA_NAME.to_string(), + "some_alt_metric".to_string(), + ), + ], + 2, + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!( + plan_str.contains("some_metric.__tsid = some_alt_metric.__tsid"), + "{plan_str}" + ); + assert!(!plan_str.contains("tag_0 ="), "{plan_str}"); + assert!(!plan_str.contains("tag_1 ="), "{plan_str}"); + } + + #[tokio::test] + async fn on_partial_label_set_disables_tsid_binary_join() { + let eval_stmt = build_eval_stmt("some_metric / on(tag_0) some_alt_metric"); + + let table_provider = build_test_table_provider_with_tsid( + &[ + (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()), + ( + DEFAULT_SCHEMA_NAME.to_string(), + "some_alt_metric".to_string(), + ), + ], + 2, + 1, + ) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!(!plan_str.contains("__tsid ="), "{plan_str}"); + assert!( + plan_str.contains("some_metric.tag_0 = some_alt_metric.tag_0"), + "{plan_str}" + ); + assert!(!plan_str.contains("tag_1 ="), "{plan_str}"); + } + + #[tokio::test] + async fn on_label_set_must_cover_both_sides_to_use_tsid_binary_join() { + let eval_stmt = build_eval_stmt("some_metric / on(tag_0) some_alt_metric"); + + let table_provider = build_test_table_provider_with_tsid_tag_fields(&[ + ( + (DEFAULT_SCHEMA_NAME.to_string(), "some_metric".to_string()), + 2, + 1, + ), + ( + ( + DEFAULT_SCHEMA_NAME.to_string(), + "some_alt_metric".to_string(), + ), + 1, + 1, + ), + ]) + .await; + let plan = + PromPlanner::stmt_to_plan(table_provider, &eval_stmt, &build_query_engine_state()) + .await + .unwrap(); + + let plan_str = plan.display_indent_schema().to_string(); + assert!(!plan_str.contains("__tsid ="), "{plan_str}"); + assert!( + plan_str.contains("some_metric.tag_0 = some_alt_metric.tag_0"), + "{plan_str}" + ); + assert!(!plan_str.contains("tag_1 ="), "{plan_str}"); + } + #[tokio::test] async fn comparison_binary_join_uses_tsid_and_keeps_it_in_filtered_result() { let eval_stmt = build_eval_stmt("some_metric > some_alt_metric"); diff --git a/tests/cases/distributed/flow-tql/tsid_on_phy.result b/tests/cases/distributed/flow-tql/tsid_on_phy.result index 6ae3e77b0c..d008a24e19 100644 --- a/tests/cases/distributed/flow-tql/tsid_on_phy.result +++ b/tests/cases/distributed/flow-tql/tsid_on_phy.result @@ -100,31 +100,31 @@ TQL EXPLAIN ( ) ); -+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| plan_type | plan | -+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| logical_plan | HistogramFold: le=le, field=sum(prom_avg_over_time(ts_range,v)), quantile=0.5 | -| | MergeScan [is_placeholder=false, remote_input=[ | -| | Sort: test_tsid.le ASC NULLS LAST, test_tsid.tag4 ASC NULLS LAST, test_tsid.tag5 ASC NULLS LAST, test_tsid.ts ASC NULLS LAST | -| | Aggregate: groupBy=[[test_tsid.le, test_tsid.tag4, test_tsid.tag5, test_tsid.ts]], aggr=[[sum(prom_avg_over_time(ts_range,v))]] | -| | Filter: prom_avg_over_time(ts_range,v) IS NOT NULL | -| | Projection: test_tsid.ts, prom_avg_over_time(ts_range, v) AS prom_avg_over_time(ts_range,v), test_tsid.le, test_tsid.tag1, test_tsid.tag2, test_tsid.tag4, test_tsid.tag5, test_tsid.tag6, test_tsid.tag7, test_tsid.tag8 | -| | PromRangeManipulate: req range=[1769139000000..1769139900000], interval=[60000], eval range=[1800000], time index=[ts], values=["v"] | -| | PromSeriesNormalize: offset=[0], time index=[ts], filter NaN: [true] | -| | PromSeriesDivide: tags=["__tsid"] | -| | Sort: test_tsid.__tsid ASC NULLS FIRST, test_tsid.ts ASC NULLS FIRST | -| | Filter: test_tsid.ts >= TimestampMillisecond(1769137200001, None) AND test_tsid.ts <= TimestampMillisecond(1769139900000, None) | -| | Projection: test_tsid.v, test_tsid.le, test_tsid.tag1, test_tsid.tag2, test_tsid.tag4, test_tsid.tag5, test_tsid.tag6, test_tsid.tag7, test_tsid.tag8, test_tsid.__tsid, test_tsid.ts | -| | SubqueryAlias: test_tsid | -| | Filter: phy.__table_id=UInt32(REDACTED) | -| | TableScan: phy projection=[ts, v, tag1, tag2, le, tag4, tag5, tag6, tag7, tag8, __table_id, __tsid] | -| | ]] | -| physical_plan | HistogramFoldExec: le=@0, field=@4, quantile=0.5 | -| | SortExec: expr=[tag4@1 ASC NULLS LAST, tag5@2 ASC NULLS LAST, ts@3 ASC NULLS LAST, CAST(le@0 AS Float64) ASC NULLS LAST], preserve_partitioning=[true] | ++---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| logical_plan | HistogramFold: le=le, field=sum(prom_avg_over_time(ts_range,v)), quantile=0.5 | +| | MergeScan [is_placeholder=false, remote_input=[ | +| | Sort: test_tsid.le ASC NULLS LAST, test_tsid.tag4 ASC NULLS LAST, test_tsid.tag5 ASC NULLS LAST, test_tsid.ts ASC NULLS LAST | +| | Aggregate: groupBy=[[test_tsid.le, test_tsid.tag4, test_tsid.tag5, test_tsid.ts]], aggr=[[sum(prom_avg_over_time(ts_range,v))]] | +| | Filter: prom_avg_over_time(ts_range,v) IS NOT NULL | +| | Projection: test_tsid.ts, prom_avg_over_time(ts_range, v) AS prom_avg_over_time(ts_range,v), test_tsid.le, test_tsid.tag1, test_tsid.tag2, test_tsid.tag4, test_tsid.tag5, test_tsid.tag6, test_tsid.tag7, test_tsid.tag8, test_tsid.__tsid | +| | PromRangeManipulate: req range=[1769139000000..1769139900000], interval=[60000], eval range=[1800000], time index=[ts], values=["v"] | +| | PromSeriesNormalize: offset=[0], time index=[ts], filter NaN: [true] | +| | PromSeriesDivide: tags=["__tsid"] | +| | Sort: test_tsid.__tsid ASC NULLS FIRST, test_tsid.ts ASC NULLS FIRST | +| | Filter: test_tsid.ts >= TimestampMillisecond(1769137200001, None) AND test_tsid.ts <= TimestampMillisecond(1769139900000, None) | +| | Projection: test_tsid.v, test_tsid.le, test_tsid.tag1, test_tsid.tag2, test_tsid.tag4, test_tsid.tag5, test_tsid.tag6, test_tsid.tag7, test_tsid.tag8, test_tsid.__tsid, test_tsid.ts | +| | SubqueryAlias: test_tsid | +| | Filter: phy.__table_id=UInt32(REDACTED) | +| | TableScan: phy projection=[ts, v, tag1, tag2, le, tag4, tag5, tag6, tag7, tag8, __table_id, __tsid] | +| | ]] | +| physical_plan | HistogramFoldExec: le=@0, field=@4, quantile=0.5 | +| | SortExec: expr=[tag4@1 ASC NULLS LAST, tag5@2 ASC NULLS LAST, ts@3 ASC NULLS LAST, CAST(le@0 AS Float64) ASC NULLS LAST], preserve_partitioning=[true] | | | RepartitionExec: REDACTED | | MergeScanExec: REDACTED -| | | -+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | | ++---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ CREATE FLOW IF NOT EXISTS test_tsid SINK TO 'test_tsid_output' diff --git a/tests/cases/standalone/common/promql/label.result b/tests/cases/standalone/common/promql/label.result index 1bdc37b5aa..f83bbd38e6 100644 --- a/tests/cases/standalone/common/promql/label.result +++ b/tests/cases/standalone/common/promql/label.result @@ -202,38 +202,14 @@ TQL EVAL(0, 15, '5s') label_replace(vector(1), "host", "host1", "", ""); -- SQLNESS SORT_RESULT 3 1 TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} * label_replace(vector(1), "host", "host1", "", ""); -+-------+---------------------+----------------------------+ -| host | time | test.val * .greptime_value | -+-------+---------------------+----------------------------+ -| host1 | 1970-01-01T00:00:00 | 1.0 | -| host1 | 1970-01-01T00:00:05 | 1.0 | -| host1 | 1970-01-01T00:00:05 | 3.0 | -| host1 | 1970-01-01T00:00:10 | 1.0 | -| host1 | 1970-01-01T00:00:10 | 3.0 | -| host1 | 1970-01-01T00:00:10 | 5.0 | -| host1 | 1970-01-01T00:00:15 | 1.0 | -| host1 | 1970-01-01T00:00:15 | 3.0 | -| host1 | 1970-01-01T00:00:15 | 5.0 | -| host1 | 1970-01-01T00:00:15 | 7.0 | -+-------+---------------------+----------------------------+ +++ +++ -- SQLNESS SORT_RESULT 3 1 TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} + label_replace(vector(1), "host", "host1", "", ""); -+-------+---------------------+----------------------------+ -| host | time | test.val + .greptime_value | -+-------+---------------------+----------------------------+ -| host1 | 1970-01-01T00:00:00 | 2.0 | -| host1 | 1970-01-01T00:00:05 | 2.0 | -| host1 | 1970-01-01T00:00:05 | 4.0 | -| host1 | 1970-01-01T00:00:10 | 2.0 | -| host1 | 1970-01-01T00:00:10 | 4.0 | -| host1 | 1970-01-01T00:00:10 | 6.0 | -| host1 | 1970-01-01T00:00:15 | 2.0 | -| host1 | 1970-01-01T00:00:15 | 4.0 | -| host1 | 1970-01-01T00:00:15 | 6.0 | -| host1 | 1970-01-01T00:00:15 | 8.0 | -+-------+---------------------+----------------------------+ +++ +++ -- Empty regex with existing source label -- SQLNESS SORT_RESULT 3 1 @@ -303,7 +279,8 @@ TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} * label_replace(vector(1), -- SQLNESS SORT_RESULT 3 1 TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} * label_replace(vector(1), "addr", "host1", "instance", ""); -Error: 3000(PlanQuery), Internal error during building DataFusion plan: No field named addr. Valid fields are test.ts, test.host, test.idc, test.val. +++ +++ TQL EVAL label_replace(demo_num_cpus, "~invalid", "", "src", "(.*)"); diff --git a/tests/cases/standalone/common/promql/tsid_binary_join_regression.result b/tests/cases/standalone/common/promql/tsid_binary_join_regression.result index 722360e6b1..8dbbd4131b 100644 --- a/tests/cases/standalone/common/promql/tsid_binary_join_regression.result +++ b/tests/cases/standalone/common/promql/tsid_binary_join_regression.result @@ -38,6 +38,20 @@ WITH( Affected Rows: 0 +CREATE TABLE tsid_binary_join_right_by_job ( + job STRING NULL, + ts TIMESTAMP(3) NOT NULL, + greptime_value DOUBLE NULL, + TIME INDEX (ts), + PRIMARY KEY(job), +) +ENGINE = metric +WITH( + on_physical_table = 'tsid_binary_join_physical' +); + +Affected Rows: 0 + CREATE TABLE tsid_binary_join_third ( host STRING NULL, job STRING NULL, @@ -69,6 +83,14 @@ INSERT INTO tsid_binary_join_right (host, job, ts, greptime_value) VALUES Affected Rows: 4 +INSERT INTO tsid_binary_join_right_by_job (job, ts, greptime_value) VALUES + ('job1', 0, 3), + ('job2', 0, 6), + ('job1', 5000, 5), + ('job2', 5000, 7); + +Affected Rows: 4 + INSERT INTO tsid_binary_join_third (host, job, ts, greptime_value) VALUES ('host1', 'job1', 0, 2), ('host2', 'job2', 0, 3), @@ -244,6 +266,45 @@ TQL ANALYZE (0, 5, '5s') tsid_binary_join_left / ignoring(host) tsid_binary_join |_|_| Total rows: 4_| +-+-+-+ +-- `on(job)` must stay label-based when only the left side has extra row-key labels. +-- SQLNESS REPLACE (metrics.*) REDACTED +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE Hash\(\[job@1,\sts@2\],.* Hash([job@1, ts@2],REDACTED +-- SQLNESS REPLACE Hash\(\[job@1,\sts@3\],.* Hash([job@1, ts@3],REDACTED +-- SQLNESS REPLACE input_partitions=\d+ input_partitions=REDACTED +-- SQLNESS REPLACE "partition_count":\{(.*?)\} "partition_count":REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +TQL ANALYZE (0, 5, '5s') tsid_binary_join_left / on(job) tsid_binary_join_right_by_job; + ++-+-+-+ +| stage | node | plan_| ++-+-+-+ +| 0_| 0_|_ProjectionExec: expr=[job@2 as job, ts@4 as ts, __tsid@3 as __tsid, greptime_value@0 / greptime_value@1 as tsid_binary_join_left.greptime_value / tsid_binary_join_right_by_job.greptime_value] REDACTED +|_|_|_HashJoinExec: mode=Partitioned, join_type=Inner, on=[(job@1, job@1), (ts@2, ts@3)], projection=[greptime_value@0, greptime_value@3, job@4, __tsid@5, ts@6], NullsEqual: true REDACTED +|_|_|_RepartitionExec: partitioning=Hash([job@1, ts@2],REDACTED +|_|_|_ProjectionExec: expr=[greptime_value@0 as greptime_value, job@2 as job, ts@4 as ts] REDACTED +|_|_|_MergeScanExec: REDACTED +|_|_|_RepartitionExec: partitioning=Hash([job@1, ts@3],REDACTED +|_|_|_MergeScanExec: REDACTED +|_|_|_| +| 1_| 0_|_PromInstantManipulateExec: range=[0..5000], lookback=[300000], interval=[5000], time index=[ts] REDACTED +|_|_|_PromSeriesDivideExec: tags=["__tsid"] REDACTED +|_|_|_ProjectionExec: expr=[greptime_value@1 as greptime_value, host@3 as host, job@4 as job, __tsid@2 as __tsid, ts@0 as ts] REDACTED +|_|_|_CooperativeExec REDACTED +|_|_|_SeriesScan: region=REDACTED, "partition_count":REDACTED, "distribution":"PerSeries" REDACTED +|_|_|_| +| 1_| 0_|_PromInstantManipulateExec: range=[0..5000], lookback=[300000], interval=[5000], time index=[ts] REDACTED +|_|_|_PromSeriesDivideExec: tags=["__tsid"] REDACTED +|_|_|_ProjectionExec: expr=[greptime_value@1 as greptime_value, job@3 as job, __tsid@2 as __tsid, ts@0 as ts] REDACTED +|_|_|_CooperativeExec REDACTED +|_|_|_SeriesScan: region=REDACTED, "partition_count":REDACTED, "distribution":"PerSeries" REDACTED +|_|_|_| +|_|_| Total rows: 4_| ++-+-+-+ + -- Comparison filters can join on `__tsid`, but the filtered result must still behave like -- a regular derived vector downstream. -- SQLNESS REPLACE (metrics.*) REDACTED @@ -390,7 +451,8 @@ TQL ANALYZE (0, 5, '5s') ((tsid_binary_join_left > bool tsid_binary_join_right) | stage | node | plan_| +-+-+-+ | 0_| 0_|_ProjectionExec: expr=[host@2 as host, job@3 as job, ts@5 as ts, __tsid@4 as __tsid, tsid_binary_join_right.tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value + tsid_binary_join_left.greptime_value@0 / greptime_value@1 as lhs.tsid_binary_join_right.tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value + tsid_binary_join_left.greptime_value / rhs.greptime_value] REDACTED -|_|_|_HashJoinExec: mode=Partitioned, join_type=Inner, on=[(__tsid@1, __tsid@3), (ts@0, ts@4)], projection=[tsid_binary_join_right.tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value + tsid_binary_join_left.greptime_value@2, greptime_value@3, host@4, job@5, __tsid@6, ts@7], NullsEqual: true REDACTED +|_|_|_HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(__tsid@1, __tsid@3), (ts@0, ts@4)], projection=[tsid_binary_join_right.tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value + tsid_binary_join_left.greptime_value@2, greptime_value@3, host@4, job@5, __tsid@6, ts@7], NullsEqual: true REDACTED +|_|_|_CoalescePartitionsExec REDACTED |_|_|_ProjectionExec: expr=[ts@3 as ts, __tsid@2 as __tsid, tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value@0 + greptime_value@1 as tsid_binary_join_right.tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value + tsid_binary_join_left.greptime_value] REDACTED |_|_|_HashJoinExec: mode=Partitioned, join_type=Inner, on=[(__tsid@1, __tsid@1), (ts@0, ts@2)], projection=[tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value@2, greptime_value@3, __tsid@4, ts@5], NullsEqual: true REDACTED |_|_|_ProjectionExec: expr=[ts@3 as ts, __tsid@2 as __tsid, CAST(greptime_value@1 < greptime_value@0 AS Float64) as tsid_binary_join_left.greptime_value > tsid_binary_join_right.greptime_value] REDACTED @@ -404,7 +466,7 @@ TQL ANALYZE (0, 5, '5s') ((tsid_binary_join_left > bool tsid_binary_join_right) |_|_|_RepartitionExec: partitioning=Hash([REDACTED |_|_|_ProjectionExec: expr=[greptime_value@0 as greptime_value, __tsid@3 as __tsid, ts@4 as ts] REDACTED |_|_|_MergeScanExec: REDACTED -|_|_|_RepartitionExec: partitioning=Hash([REDACTED +|_|_|_CooperativeExec REDACTED |_|_|_MergeScanExec: REDACTED |_|_|_| | 1_| 0_|_PromInstantManipulateExec: range=[0..5000], lookback=[300000], interval=[5000], time index=[ts] REDACTED @@ -501,8 +563,8 @@ TQL ANALYZE (0, 5, '5s') (tsid_binary_join_left / ignoring(host) group_left tsid | stage | node | plan_| +-+-+-+ | 0_| 0_|_ProjectionExec: expr=[host@2 as host, job@3 as job, ts@5 as ts, __tsid@4 as __tsid, tsid_binary_join_left.greptime_value / tsid_binary_join_right.greptime_value@0 / greptime_value@1 as tsid_binary_join_right.tsid_binary_join_left.greptime_value / tsid_binary_join_right.greptime_value / tsid_binary_join_left.greptime_value] REDACTED -|_|_|_HashJoinExec: mode=Partitioned, join_type=Inner, on=[(__tsid@1, __tsid@3), (ts@0, ts@4)], projection=[tsid_binary_join_left.greptime_value / tsid_binary_join_right.greptime_value@2, greptime_value@3, host@4, job@5, __tsid@6, ts@7], NullsEqual: true REDACTED -|_|_|_RepartitionExec: partitioning=Hash([REDACTED +|_|_|_HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(__tsid@1, __tsid@3), (ts@0, ts@4)], projection=[tsid_binary_join_left.greptime_value / tsid_binary_join_right.greptime_value@2, greptime_value@3, host@4, job@5, __tsid@6, ts@7], NullsEqual: true REDACTED +|_|_|_CoalescePartitionsExec REDACTED |_|_|_ProjectionExec: expr=[ts@3 as ts, __tsid@2 as __tsid, greptime_value@0 / greptime_value@1 as tsid_binary_join_left.greptime_value / tsid_binary_join_right.greptime_value] REDACTED |_|_|_HashJoinExec: mode=Partitioned, join_type=Inner, on=[(job@1, job@1), (ts@2, ts@3)], projection=[greptime_value@0, greptime_value@3, __tsid@5, ts@6], NullsEqual: true REDACTED |_|_|_RepartitionExec: partitioning=Hash([REDACTED @@ -511,7 +573,7 @@ TQL ANALYZE (0, 5, '5s') (tsid_binary_join_left / ignoring(host) group_left tsid |_|_|_RepartitionExec: partitioning=Hash([REDACTED |_|_|_ProjectionExec: expr=[greptime_value@0 as greptime_value, job@2 as job, __tsid@3 as __tsid, ts@4 as ts] REDACTED |_|_|_MergeScanExec: REDACTED -|_|_|_RepartitionExec: partitioning=Hash([REDACTED +|_|_|_CooperativeExec REDACTED |_|_|_MergeScanExec: REDACTED |_|_|_| | 1_| 0_|_PromInstantManipulateExec: range=[0..5000], lookback=[300000], interval=[5000], time index=[ts] REDACTED @@ -547,6 +609,18 @@ TQL EVAL (0, 5, '5s') tsid_binary_join_left / tsid_binary_join_right; | host2 | job2 | 1970-01-01T00:00:05 | 3.0 | +-------+------+---------------------+------------------------------------------------------------------------------+ +-- SQLNESS SORT_RESULT 3 1 +TQL EVAL (0, 5, '5s') tsid_binary_join_left / on(job) tsid_binary_join_right_by_job; + ++------+---------------------+-------------------------------------------------------------------------------------+ +| job | ts | tsid_binary_join_left.greptime_value / tsid_binary_join_right_by_job.greptime_value | ++------+---------------------+-------------------------------------------------------------------------------------+ +| job1 | 1970-01-01T00:00:00 | 4.0 | +| job1 | 1970-01-01T00:00:05 | 3.0 | +| job2 | 1970-01-01T00:00:00 | 3.0 | +| job2 | 1970-01-01T00:00:05 | 3.0 | ++------+---------------------+-------------------------------------------------------------------------------------+ + -- SQLNESS SORT_RESULT 3 1 TQL EVAL (0, 5, '5s') (tsid_binary_join_left + tsid_binary_join_right) / tsid_binary_join_left; @@ -621,6 +695,10 @@ DROP TABLE tsid_binary_join_third; Affected Rows: 0 +DROP TABLE tsid_binary_join_right_by_job; + +Affected Rows: 0 + DROP TABLE tsid_binary_join_right; Affected Rows: 0 diff --git a/tests/cases/standalone/common/promql/tsid_binary_join_regression.sql b/tests/cases/standalone/common/promql/tsid_binary_join_regression.sql index 74ef2a2529..726651f8e4 100644 --- a/tests/cases/standalone/common/promql/tsid_binary_join_regression.sql +++ b/tests/cases/standalone/common/promql/tsid_binary_join_regression.sql @@ -33,6 +33,18 @@ WITH( on_physical_table = 'tsid_binary_join_physical' ); +CREATE TABLE tsid_binary_join_right_by_job ( + job STRING NULL, + ts TIMESTAMP(3) NOT NULL, + greptime_value DOUBLE NULL, + TIME INDEX (ts), + PRIMARY KEY(job), +) +ENGINE = metric +WITH( + on_physical_table = 'tsid_binary_join_physical' +); + CREATE TABLE tsid_binary_join_third ( host STRING NULL, job STRING NULL, @@ -58,6 +70,12 @@ INSERT INTO tsid_binary_join_right (host, job, ts, greptime_value) VALUES ('host1', 'job1', 5000, 5), ('host2', 'job2', 5000, 7); +INSERT INTO tsid_binary_join_right_by_job (job, ts, greptime_value) VALUES + ('job1', 0, 3), + ('job2', 0, 6), + ('job1', 5000, 5), + ('job2', 5000, 7); + INSERT INTO tsid_binary_join_third (host, job, ts, greptime_value) VALUES ('host1', 'job1', 0, 2), ('host2', 'job2', 0, 3), @@ -117,6 +135,19 @@ TQL ANALYZE (0, 5, '5s') ((tsid_binary_join_left + tsid_binary_join_right) * (ts -- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED TQL ANALYZE (0, 5, '5s') tsid_binary_join_left / ignoring(host) tsid_binary_join_right; +-- `on(job)` must stay label-based when only the left side has extra row-key labels. +-- SQLNESS REPLACE (metrics.*) REDACTED +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE Hash\(\[job@1,\sts@2\],.* Hash([job@1, ts@2],REDACTED +-- SQLNESS REPLACE Hash\(\[job@1,\sts@3\],.* Hash([job@1, ts@3],REDACTED +-- SQLNESS REPLACE input_partitions=\d+ input_partitions=REDACTED +-- SQLNESS REPLACE "partition_count":\{(.*?)\} "partition_count":REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +TQL ANALYZE (0, 5, '5s') tsid_binary_join_left / on(job) tsid_binary_join_right_by_job; + -- Comparison filters can join on `__tsid`, but the filtered result must still behave like -- a regular derived vector downstream. -- SQLNESS REPLACE (metrics.*) REDACTED @@ -198,6 +229,9 @@ TQL ANALYZE (0, 5, '5s') (tsid_binary_join_left / ignoring(host) group_left tsid -- SQLNESS SORT_RESULT 3 1 TQL EVAL (0, 5, '5s') tsid_binary_join_left / tsid_binary_join_right; +-- SQLNESS SORT_RESULT 3 1 +TQL EVAL (0, 5, '5s') tsid_binary_join_left / on(job) tsid_binary_join_right_by_job; + -- SQLNESS SORT_RESULT 3 1 TQL EVAL (0, 5, '5s') (tsid_binary_join_left + tsid_binary_join_right) / tsid_binary_join_left; @@ -219,6 +253,7 @@ TQL EVAL (0, 5, '5s') (tsid_binary_join_left or tsid_binary_join_right) / tsid_b TQL EVAL (0, 5, '5s') rate(tsid_binary_join_left[5s]) / tsid_binary_join_left; DROP TABLE tsid_binary_join_third; +DROP TABLE tsid_binary_join_right_by_job; DROP TABLE tsid_binary_join_right; DROP TABLE tsid_binary_join_left; DROP TABLE tsid_binary_join_physical;