mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-07 02:30:42 +01:00
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Materialized existence checks now render as an explicit comparison
instead of a bare bool column. Results are unchanged; only skip-index
usage improves.
```sql
-- before
WHERE `attribute_string_gen_ai$$request$$model_exists`
OR `attribute_string_gen_ai$$provider$$name` = 'anthropic'
-- after
WHERE `attribute_string_gen_ai$$request$$model_exists` = true
OR `attribute_string_gen_ai$$provider$$name` = 'anthropic'
```
<details>
<summary>EXPLAIN indexes = 1 (trace-matching phase, 123M
spans)</summary>
Before: bare `col_exists`
```
Name: idx_gen_ai_span_exists
Granules: 15193/15193
Name: <Combined skip indexes>
Granules: 15193/15193
```
After: `col_exists = true`
```
Name: idx_gen_ai_span_exists
Granules: 15193/15193
Name: <Combined skip indexes>
Granules: 488/15193
```
</details>
----
- ClickHouse can use a different skip index for each side of an OR and
union the results, but it can't when one side is a bare bool column.
Comparing with `= true` fixes that.
- This shape comes from the AI explorer trace list with a span filter: a
trace qualifies when it has a gen_ai span *and* a span matching the
filter (possibly different spans), so the WHERE is `(gen_ai gate) OR
<filter>` followed by a HAVING.
- Needs the gen_ai materialized columns and `idx_gen_ai_span_exists`
from SigNoz/signoz-otel-collector#929; without them there's no index to
combine.
<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Part of https://github.com/SigNoz/nerve-pod/issues/282
<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
- Benchmarked the AI trace list filtered on `gen_ai.provider.name`
against a 123M-span table (direct I/O, caches off): from ~30M spans in
the window, latency drops 16–17% and CPU 35–38%, with ~25x fewer rows
read (123M spans: 510 → 427 ms, 1.5 → 0.9 sCPU). The saved time and CPU
keep growing with span count, so larger windows save more.
- Single-condition filters (`gen_ai.request.model EXISTS` in dashboard
panels, the AND-ed gate in AI aggregations) already pruned with the bare
form; no change there.
112 lines
4.3 KiB
Go
112 lines
4.3 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
|
"github.com/SigNoz/signoz/pkg/clickhousesql"
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
)
|
|
|
|
// ExistsExpression renders the existence predicate for a key resolved to the given
|
|
// columns (negated when exists is false). Comparisons are against constants rendered
|
|
// as literals, so the expression carries no bind args and can guard column expressions
|
|
// directly. Signal-specific presence checks (body JSON paths, label maps) are handled
|
|
// by the field mappers before falling through to this.
|
|
func ExistsExpression(columns []*schema.Column, key *telemetrytypes.TelemetryFieldKey, tsStart, tsEnd uint64, fieldExpression string, exists bool) (string, error) {
|
|
newColumns, evolutionsEntries, err := qbtypes.SelectEvolutionsForColumns(columns, key.Evolutions, tsStart, tsEnd)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(newColumns) == 0 {
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "no valid evolution found for field %s in the given time range", key.Name)
|
|
}
|
|
|
|
comparison := func(operator string, value string) string {
|
|
return fmt.Sprintf("%s %s %s", fieldExpression, operator, value)
|
|
}
|
|
|
|
if len(newColumns) > 1 {
|
|
if exists {
|
|
return fieldExpression + " IS NOT NULL", nil
|
|
}
|
|
return fieldExpression + " IS NULL", nil
|
|
}
|
|
|
|
column := newColumns[0]
|
|
switch column.Type.GetType() {
|
|
case schema.ColumnTypeEnumJSON:
|
|
// the ::String cast in the value expression folds NULL to '', so the
|
|
// presence check must address the raw JSON path
|
|
columnName := column.Name
|
|
if len(evolutionsEntries) > 0 && evolutionsEntries[0] != nil {
|
|
columnName = evolutionsEntries[0].ColumnName
|
|
}
|
|
rawPath := fmt.Sprintf("%s.%s", columnName, clickhousesql.Identifier(key.Name))
|
|
if exists {
|
|
return rawPath + " IS NOT NULL", nil
|
|
}
|
|
return rawPath + " IS NULL", nil
|
|
case schema.ColumnTypeEnumString,
|
|
schema.ColumnTypeEnumFixedString:
|
|
if exists {
|
|
return comparison("<>", "''"), nil
|
|
}
|
|
return comparison("=", "''"), nil
|
|
case schema.ColumnTypeEnumDateTime64:
|
|
zero := fmt.Sprintf("toDateTime64(0, %d)", column.Type.(schema.DateTime64ColumnType).Precision)
|
|
if exists {
|
|
return comparison("<>", zero), nil
|
|
}
|
|
return comparison("=", zero), nil
|
|
case schema.ColumnTypeEnumLowCardinality:
|
|
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
|
case schema.ColumnTypeEnumString:
|
|
if exists {
|
|
return comparison("<>", "''"), nil
|
|
}
|
|
return comparison("=", "''"), nil
|
|
default:
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "exists operator is not supported for low cardinality column type %s", elementType)
|
|
}
|
|
case schema.ColumnTypeEnumUInt64,
|
|
schema.ColumnTypeEnumUInt32,
|
|
schema.ColumnTypeEnumUInt8,
|
|
schema.ColumnTypeEnumInt8,
|
|
schema.ColumnTypeEnumInt16,
|
|
schema.ColumnTypeEnumBool:
|
|
if exists {
|
|
return comparison("<>", "0"), nil
|
|
}
|
|
return comparison("=", "0"), nil
|
|
case schema.ColumnTypeEnumArray:
|
|
if exists {
|
|
return fmt.Sprintf("notEmpty(%s)", fieldExpression), nil
|
|
}
|
|
return fmt.Sprintf("empty(%s)", fieldExpression), nil
|
|
case schema.ColumnTypeEnumMap:
|
|
keyType := column.Type.(schema.MapColumnType).KeyType
|
|
if _, ok := keyType.(schema.LowCardinalityColumnType); !ok {
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "key type %s is not supported for map column type %s", keyType, column.Type)
|
|
}
|
|
|
|
switch valueType := column.Type.(schema.MapColumnType).ValueType; valueType.GetType() {
|
|
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumBool, schema.ColumnTypeEnumFloat64:
|
|
if key.Materialized {
|
|
return telemetrytypes.FieldKeyToMaterializedExistsCondition(key, exists), nil
|
|
}
|
|
leftOperand := fmt.Sprintf("mapContains(%s, %s)", column.Name, clickhousesql.StringLiteral(key.Name))
|
|
if exists {
|
|
return leftOperand, nil
|
|
}
|
|
return "NOT " + leftOperand, nil
|
|
default:
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "exists operator is not supported for map column type %s", valueType)
|
|
}
|
|
default:
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "exists operator is not supported for column type %s", column.Type)
|
|
}
|
|
}
|