Advanced Query Patterns¶
This guide collects practical query patterns for the more expressive parts of AdxLite. It is aimed at users who already know the basics of where, project, and summarize and want to compose richer pipelines.
If you need operator syntax details, see Operators reference. If you need function signatures, see Functions reference. If you are new to AdxLite, read Quickstart first.
What “advanced” means in AdxLite¶
In this project, advanced queries usually involve one or more of the following:
- multiple pipeline stages chained together
- computed columns with scalar functions
parsefor structured extraction from text- datetime bucketing or relative-time filtering
- regex matching and extraction
- JSON parsing and extraction
- grouped aggregation
- conditional expressions
letbindings for named sub-expressionsunionfor combining tables with compatible schemasjoinfor correlating tables by key columns.appendfor materializing query results locally
Pattern: build pipelines incrementally¶
A good KQL habit is to build a query in readable stages.
Example:
Events
| where city == "London"
| extend user_upper = toupper(user), bucket = bin(ts, 1h)
| summarize total=count(), max_value=max(value) by bucket, user_upper
| sort by bucket asc, user_upper asc
Why this pattern works well:
- each stage has one job
- debugging is easier because you can stop after any stage and inspect results
- it mirrors the nested-subquery execution model described in Architecture
Pattern: chain filtering before expensive transformations¶
Even though AdxLite is local, you still benefit from reducing the row set early.
Prefer:
Logs
| where Message matches regex "error|warn"
| parse Message with "user=" user " action=" action
| project user, action
Over:
The first form lets SQLite do more work before the pandas-only parse stage begins.
The parse operator¶
parse extracts columns from a source expression using a pattern composed of literals, wildcard skips, and capture names.
Basic syntax¶
Pattern parts¶
| Pattern part | Meaning |
|---|---|
| string literal | Match this exact text |
* |
Skip any matching text without capturing it |
| bare identifier | Capture the matching text into a new column |
Example: simple capture¶
If Message is user=ada action=login, the result columns become user = "ada" and action = "login".
Example: skip uninterested text¶
Use * when you know a message contains text between fields but you do not want to capture it.
Example: parse a payload tail¶
Logs
| parse Message with "user=" user " action=" action " payload=" payload
| project user, action, payload
This is common when you want to extract a JSON payload and then query inside it with extractjson().
Notes on parse behavior¶
parsecurrently runs in pandas, not in SQLite SQL- captures are added as string columns
- the generated regex uses non-greedy matching when more captures follow and greedy matching near the end of the pattern
- the entire parse regex is anchored from start to end of the source string
DateTime query patterns¶
Datetime support is a major strength of AdxLite's local analytics model.
Pattern: filter using ago()¶
Use this to find recent rows relative to the current UTC time.
Pattern: use explicit datetime literals¶
This is useful for repeatable tests and deterministic reporting windows.
Pattern: bucket timestamps with bin()¶
Events
| extend hour_bucket = bin(ts, 1h)
| summarize total=count() by hour_bucket
| sort by hour_bucket asc
bin() rounds timestamps down to the nearest bucket boundary.
Pattern: compare timestamps with datetime_diff()¶
Events
| extend hours_from_now = datetime_diff("hour", now(), ts)
| project user, ts, hours_from_now
The function returns an integer count in the requested unit.
Pattern: add durations with datetime_add()¶
AdxLite uses a simplified signature: datetime_add(timespan, value).
Regex matching and extraction¶
Regex support appears in two places:
matches regexoperator for predicatesextract()function for capture extraction
Pattern: partial-match filtering¶
Important note: AdxLite uses partial-match semantics. The pattern can match anywhere in the string.
Pattern: pull a capture group with extract()¶
This returns the selected regex group as a string or null if the pattern does not match.
JSON parsing and extraction¶
AdxLite represents JSON-oriented values as text, so JSON workflows typically combine parse, parse_json, dynamic, and extractjson.
Pattern: keep JSON payload as text but extract a field¶
Logs
| parse Message with "payload=" payload
| extend count = extractjson("$.count", payload)
| project payload, count
extractjson() returns string values for scalar JSON leaves. Convert them if you need numeric behavior.
Pattern: convert extracted JSON field to an integer¶
Logs
| parse Message with "payload=" payload
| extend count = toint(extractjson("$.count", payload))
| where count >= 10
Pattern: normalize JSON text first¶
Logs
| extend payload_json = parse_json(payload)
| extend first_value = extractjson("$.items[0]", payload_json)
| project payload_json, first_value
dynamic(payload) is an alias for parse_json(payload) in AdxLite's current implementation.
Aggregation with grouping¶
Grouped aggregation is one of the main reasons to use KQL pipelines.
Pattern: count and max by category¶
Pattern: distinct count by group¶
Pattern: conditional aggregation¶
These functions let you calculate metrics for subsets of each group without pre-filtering the entire table.
Pattern: aggregate after bucketing time¶
Events
| extend bucket = bin(ts, 1h)
| summarize total=count(), avg_value=avg(value) by bucket
| sort by bucket asc
This is a classic local time-series rollup pattern.
Conditional expressions¶
Use iif() or iff() to branch inside projections or computed columns.
Pattern: classify rows¶
Pattern: fill nulls with coalesce()¶
Pattern: null and emptiness checks¶
These helpers are especially useful when you are cleaning semi-structured or user-entered data.
String transformation patterns¶
Pattern: build a composite key¶
Pattern: trim and normalize text¶
Pattern: slice strings with 0-based indexing¶
AdxLite uses 0-based substring indexing, matching the implementation in the translator and pandas fallback layer.
.append command usage¶
The .append command materializes a query result into an existing table.
Basic pattern¶
Typical workflow¶
- create an empty destination table with the desired schema
- run a query that returns matching columns in matching order
- append those rows into the destination table
Example¶
archive = pd.DataFrame(
{
"user": pd.Series(dtype="string"),
"city": pd.Series(dtype="string"),
"value": pd.Series(dtype="int64"),
"ok": pd.Series(dtype="bool"),
"ts": pd.Series(dtype="datetime64[ns]"),
}
)
with AdxLiteClient(":memory:") as client:
client.ingest("Events", events)
client.ingest("Archive", archive)
client.query('.append Archive <| Events | where ok == true')
print(client.query('Archive | count'))
Notes¶
.appendreturns an empty DataFrame- append validation still applies, so the query output schema must match the destination schema exactly
.appendis local-only; it does not move data to or from external systems
Recommended query-writing habits¶
- filter early when possible
- keep parse late in the pipeline unless its output is required sooner
- use explicit conversion for JSON scalars
- prefer readable multi-line pipelines over overly compressed single-line queries
- inspect intermediate projections during debugging
Putting it all together¶
A realistic AdxLite query may combine several advanced patterns:
Logs
| where Message matches regex "login|logout"
| parse Message with "user=" user " action=" action " payload=" payload
| extend count = toint(extractjson("$.count", payload))
| extend bucket = bin(ts, 1h)
| summarize total=count(), max_count=max(count) by bucket, action
| sort by bucket asc, action asc
This example shows the intended style of the engine:
- SQLite handles the early filter and aggregate-friendly operations
- pandas handles
parse - JSON helpers and conversions make semi-structured payloads usable
- the final result is a normal pandas DataFrame
Pattern: scalar let for reusable thresholds¶
Use let to define a scalar constant and reference it across the pipeline.
Pattern: tabular let for intermediate results¶
Define a named sub-query with let and reference it as a table later.
Pattern: combine tables with union¶
Use source-form union to query across multiple tables:
Use withsource to add a column that identifies which table each row came from:
Pipe-form union appends another table inside a pipeline:
Pattern: correlate tables with join¶
Inner join matches rows from two tables on a key:
Left outer join keeps all rows from the left, filling NaN for unmatched right columns:
Left anti join finds rows in the left table with no match in the right:
Qualified key columns¶
When the join keys have different names in each table, use $left and $right:
Join with sub-pipeline¶
The right side of a join can be a full pipeline:
Pattern: combine let with join¶
Use let to define the right-side table, then join against it:
let ActiveUsers = Users | where active == true;
Events
| join kind=inner (ActiveUsers) on user
| project user, city, email