# Dataset structured filters
Datasets support two kinds of filtering:
1. **SQL** — put a `WHERE` clause in your query. This is the most flexible and
runs inside DuckDB.
2. **Structured filters** — a typed filter tree sent alongside the query. The
service compiles it into a parameterized SQL predicate, so values are bound,
never interpolated.
The Hub's **Filters** panel builds structured filters for you. The same AST is
available in `QueryDatasetRequest.filter` (and `ExportDatasetRequest.filter`),
where values are bound as SQL parameters. Project-wide queries filter with SQL
`WHERE` clauses instead.
## UI filter builder
The Query tab starts with no filters. Use **Add filter**, choose a column and an
operator, enter a value, then press **Run**.
- **Match all / any** combines the conditions with AND or OR.
- Operators are limited to the selected column kind (see below).
- `in` and `not in` accept comma-separated values.
- Booleans use a true/false selector; timestamps use a date-time input.
- **is empty** / **is not empty** are `IS NULL` / `IS NOT NULL` and need no
value.
- Filters are not applied live; they take effect when the query runs.
## Operators
|
Operator
|
Enum
|
Values
|
Compiled predicate
|
|
Is
|
FILTER_OP_EQ
|
1
|
"column" = ?
|
|
Is not
|
FILTER_OP_NE
|
1
|
"column" <> ?
|
>
|
FILTER_OP_GT
|
1
|
"column" > ?
|
>=
|
FILTER_OP_GTE
|
1
|
"column" >= ?
|
<
|
FILTER_OP_LT
|
1
|
"column" < ?
|
<=
|
FILTER_OP_LTE
|
1
|
"column" <= ?
|
|
Contains
|
FILTER_OP_CONTAINS
|
1
|
strpos("column", ?) > 0
|
|
Starts with
|
FILTER_OP_STARTS_WITH
|
1
|
starts_with("column", ?)
|
|
Ends with
|
FILTER_OP_ENDS_WITH
|
1
|
ends_with("column", ?)
|
|
In
|
FILTER_OP_IN
|
1+
|
"column" IN (?, …)
|
|
Not in
|
FILTER_OP_NOT_IN
|
1+
|
"column" NOT IN (?, …)
|
|
Is empty
|
FILTER_OP_IS_NULL
|
0
|
"column" IS NULL
|
|
Is not empty
|
FILTER_OP_IS_NOT_NULL
|
0
|
"column" IS NOT NULL
|
Text operators are **case-sensitive** and literal: `contains` uses `strpos`, so
`%` and `_` have no wildcard meaning. Use SQL `ILIKE` if you need
case-insensitive matching.
## Groups and negation
```text
FilterNode
├── group { any: bool, nodes: FilterNode[] } AND when any = false
├── condition { column, op, values: DatasetValue[] }
└── not { node: FilterNode }
```
- An empty AND group compiles to `TRUE`; an empty OR group compiles to `FALSE`.
- `not` wraps any node: `NOT ()`.
- Column names must be valid identifiers. They are quoted, and all values are
parameterized.
## Where filters run
This distinction matters when your SQL aggregates or renames columns.
### Single-dataset queries and exports
`QueryDataset` and `ExportDataset` apply the structured filter **outside** your
`SELECT`:
```sql
SELECT * FROM () AS __bh_dataset_query WHERE
```
So the filter sees your query's **output columns**:
- With the default `SELECT * FROM dataset`, every schema column plus
`__ingested_at` and `__deleted` is available.
- If you project `SELECT host, count(*) AS n ...`, only `host` and `n` exist.
Referencing `severity` fails; filter on the output name or move the predicate
into the SQL `WHERE`.
### Project-wide queries
`QueryDatasets` does not use the structured filter builder. Filter inside the
SQL with a `WHERE` clause:
```sql
SELECT s.host, w.status_code
FROM subdomains AS s
JOIN "web-services" AS w ON s.host = w.host
WHERE s.resolved AND w.status_code BETWEEN 200 AND 299
```
The per-relation `filters` field on `QueryDatasetsRequest` is reserved and is
not supported by the current execution path.
### Views
`ExecuteDatasetView` has no filter or time-range fields. Put predicates in the
view SQL.
## Time ranges and history
|
Field
|
Applies to
|
Notes
|
ingestedAtFrom
|
__ingested_at >= value
|
Same “outside your SELECT” rule as filters.
|
ingestedAtTo
|
__ingested_at <= value
|
Must not be before
ingestedAtFrom
.
|
includeHistory
|
Switches the relation to raw observations
|
Filters and time bounds apply to history too.
|
The default `SELECT * FROM dataset` includes `__ingested_at`. If you project a
subset and set a time range, include `__ingested_at` in the projection, or
filter inside SQL.
The Hub's date-time inputs use the browser's local time zone. API clients send
RFC 3339 timestamps.
## API examples
### AND group
```json
{
"filter": {
"group": {
"any": false,
"nodes": [
{
"condition": {
"column": "status_code",
"op": "FILTER_OP_GTE",
"values": [{ "integerValue": "200" }]
}
},
{
"condition": {
"column": "status_code",
"op": "FILTER_OP_LT",
"values": [{ "integerValue": "300" }]
}
},
{
"condition": {
"column": "ip",
"op": "FILTER_OP_IS_NOT_NULL",
"values": []
}
}
]
}
}
}
```
### OR group with a negation
```json
{
"filter": {
"group": {
"any": true,
"nodes": [
{
"condition": {
"column": "severity",
"op": "FILTER_OP_IN",
"values": [{ "textValue": "high" }, { "textValue": "critical" }]
}
},
{
"not": {
"node": {
"condition": {
"column": "program",
"op": "FILTER_OP_EQ",
"values": [{ "textValue": "internal" }]
}
}
}
}
]
}
}
}
```
`integerValue` is an int64 and therefore a JSON string. Timestamps are RFC 3339
strings inside `timestampValue`.
## Value types
|
Column kind
|
Value field
|
Example
|
|
Text
|
textValue
|
{"textValue":"api.example.com"}
|
|
Integer
|
integerValue
(string)
|
{"integerValue":"200"}
|
|
Number
|
numberValue
|
{"numberValue":9.8}
|
|
Boolean
|
booleanValue
|
{"booleanValue":true}
|
|
Timestamp
|
timestampValue
|
{"timestampValue":"2026-01-01T00:00:00Z"}
|
Values must match the column kind. The UI builds the correct value type from
the schema; API clients should do the same. A mismatch surfaces as a DuckDB
binder or conversion error.
SQL `NULL` comparisons never match, so `is = null` is not a useful filter — use
**is empty** (`IS NULL`).
## Errors
|
Error
|
Cause
|
Invalid dataset filter
|
Unsupported operator, wrong number of values, or an empty value (except null checks).
|
DuckDB could not prepare the query
|
The column does not exist in the filtered scope, or a value type does not match.
|
Invalid filter for dataset ''
|
A project-wide
RelationFilter
failed to compile against that relation.
|
## Related
- [SQL reference](/datasets/sql) — write the query the filter wraps.
- [Overview](/datasets/overview#current-state-and-history) — current state vs
history.
- [API and CLI](/datasets/api-cli) — request shapes.