Skip to content

Further query perf updates for oximeter field lookups.#10110

Open
jmcarp wants to merge 1 commit intomainfrom
jmcarp/oximeter-field-union
Open

Further query perf updates for oximeter field lookups.#10110
jmcarp wants to merge 1 commit intomainfrom
jmcarp/oximeter-field-union

Conversation

@jmcarp
Copy link
Contributor

@jmcarp jmcarp commented Mar 20, 2026

Joins are expensive in clickhouse. In #9262, we dropped the number of joins in the field lookup query from the number of fields to the number of relevant field tables. However, this still leaves us with up to six field tables to join. In this patch, we do away with joins for the field lookup query entirely. Instead, we select the relevant rows from each field table, then combine them with UNION ALL, and use anyIf and GROUP BY to pivot from long format to wide. This speeds up the field query with indentical results, with greater speedups for timeseries that use more field tables. A timeseries whose labels are all strings will see no difference in performance; a series that has string, uuid, u8, u16, u32, and i16 labels (like the switch_port_control_data_link metrics) will return results much faster.

Joins are expensive in clickhouse. In #9262, we dropped the number of joins in
the field lookup query from the number of fields to the number of relevant
field tables. However, this still leaves us with up to six field tables to
join. In this patch, we do away with joins for the field lookup query entirely.
Instead, we select the relevant rows from each field table, then combine them
with UNION ALL, and use anyIf and GROUP BY to pivot from long format to wide.
This speeds up the field query with indentical results, with greater speedups
for timeseries that use more field tables. A timeseries whose labels are all
strings will see no difference in performance; a series that has string, uuid,
u8, u16, u32, and i16 labels (like the switch_port_control_data_link metrics)
will return results much faster.
@jmcarp jmcarp requested a review from bnaecker March 20, 2026 14:25
Copy link
Collaborator

@bnaecker bnaecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever! I'd like to see some performance numbers if possible. Could we look at the returned query summary from some representative OxQL queries, and see what the performance impact is? If possible, looking at memory consumption would be nice too, but we don't have an easy way to access that per-query. You would have to take the query ID and go back to the system.query_log table, I think.

Comment on lines +961 to +962
.into_iter()
.collect();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually need to be a Vec<_>? It looks like we iterate over it below, which could happen just as well with the set itself.

} else {
query.push_str(" WHERE ");
}
query.push_str(" HAVING ");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it helpful to have a test checking this new syntax element? I always find it hard to build a SQL query programmatically without seeing the final output.

@@ -0,0 +1,44 @@
SELECT
assumeNotNull(anyIf(fields_i32, field_name = 'foo')) AS foo,
assumeNotNull(anyIf(fields_u32, field_name = 'index')) AS INDEX,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is INDEX capitalized here?

let union_parts: Vec<String> = field_types
.iter()
.map(|&this_type| {
let value_cols: Vec<String> = field_types
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested-loop strikes me as inefficient, given that we know that all but one of the elements is going to be NULL AS <some_table_name>. I wonder if we can:

  • build an array of all the possible NULL AS fields_{type} entries we'll need
  • on L995, call enumerate().iter() instead of just .iter()
  • inside the .map() on L996, clone the array and overwrite the one element at that index with the string like field_value AS fields_{type} instead

I'm not positive that will be noticeably better, given the small sizes, but it's definitely asymptotically better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants