What a filter matches
Every filter is evaluated against one span at a time. The data shape then determines which rows the query returns.
In the UI, the Row type selector on the Logs page chooses the shape for you. Traces selects the
traces shape and Spans selects the spans shape.
The same filter returns different rows on different shapes. For example, this query returns only errored spans:
shape => 'traces' returns every span from each trace that contains an error, so most returned spans have a null error:
spans shape, or keep the trace selection and narrow the returned spans with FILTER_SPANS().
On the
traces shape, LIMIT caps the number of traces, not the number of spans returned.On the
summary shape, a WHERE condition is still evaluated one span at a time, so it selects traces where at least one span matches rather than comparing against the trace’s aggregate. To filter on a value aggregated across the whole trace, use HAVING, for example HAVING avg(scores.Factuality) > 0.8. See Data shapes.Filters match one span at a time
Braintrust indexes fields per span. A condition only sees the fields on the span it is evaluating, and fields are not inherited from a trace’s root span down to its child spans. This matters because applications typically log different fields on different spans:- Request context such as
metadata.user_idormetadata.tenant_idis usually logged on the root span. - Token and cost metrics are recorded on the child LLM spans.
- Scores are written on scorer spans.
- Log the field on every span you intend to filter on.
- Match the conditions across different spans with separate
ANY_SPAN()calls. - Pair fields at the trace level by aggregating with
GROUP BY root_span_id. See Aggregate span data across a trace.
Match conditions across spans
On thetraces and summary shapes, a plain WHERE clause matches a trace only when one span satisfies every condition. That is what you want when the conditions describe a single span. For example, a failed LLM call is one span that is both an LLM span and errored:
ANY_SPAN() covers the other case, where the conditions describe different spans in the same trace. Wrap the conditions for each span in their own call. Each call is matched independently, so each can be satisfied by a different span. For example, tags are typically set on the root span while scores are written on a scorer span, so no single span carries both:
WHERE clause match one span:
By default,
ANY_SPAN() matches against all spans in a trace. To restrict matching to only root spans, add is_root to the condition: ANY_SPAN(is_root AND error IS NOT NULL).NOT ANY_SPAN(), see Single span filters.
Return only the matching spans
On thetraces shape, a WHERE condition selects traces and returns all of their spans. To keep that trace selection but return only the spans meeting a condition, wrap the condition in FILTER_SPANS().
FILTER_SPANS() works on the traces and summary shapes. On the spans shape it does nothing, because that shape already returns only matching spans. See Matching spans filters.
Choose the right operator
Several operators look interchangeable but match differently. These are the distinctions that most often produce empty or unexpected results.INCLUDES and CONTAINS are not valid in SQL queries. Use IN for exact array membership. See Syntax styles if you maintain older queries that use them.
Where you filter
Each product surface applies these rules with its own default scope.Common pitfalls
These are common causes of unexpected results. The same symptom can also have other causes.Next steps
- Look up clause syntax in Query structure.
- Find operators and functions in Functions and operators.
- Make queries faster with SQL best practices.
- Apply filters in the UI from Filter and search logs.