Hands-on OpenSearch PPL examples covering search, stats, eval, span, join, timechart, and more - ready for logs, metrics, and traces.

OpenSearch PPL Examples: 30+ Copy-Paste Queries for Logs, Metrics, and Traces

OpenSearch PPL (Piped Processing Language) is a pipe-chained query syntax for OpenSearch, modelled after Splunk SPL and Unix pipes. A PPL query starts with source=<index> and chains commands with |, like source=logs | where status=500 | stats count() by host | sort -count. PPL ships with the SQL/PPL plugin in every OpenSearch distribution and runs through Dashboards' Query Workbench or the POST _plugins/_ppl REST endpoint.

This guide is a copy-paste catalog of the 30+ PPL queries you will actually use in production - filtering, aggregation, time-bucketing, regex extraction, joins, subsearches, and the new commands shipped in OpenSearch 3.3-3.5. Every example assumes the Apache Calcite engine is enabled (the default since OpenSearch 3.3, October 2025).

What Is OpenSearch PPL and Why Use It?

PPL is a sequential, left-to-right query language designed for semi-structured data: logs, metrics, and traces. A pipeline reads as: read source, then apply each command in order, then return rows. It maps naturally onto how engineers reason about logs, especially anyone arriving from Splunk SPL or Unix |-style tools. PPL ships in the SQL plugin bundled with the standard OpenSearch distribution, and AWS OpenSearch Service has it enabled by default.

PPL is one of three first-class query interfaces in OpenSearch, alongside the JSON-based DSL and SQL. Each has a different sweet spot:

Dimension PPL DSL SQL
Syntax style Pipe-chained, Splunk-like JSON, nested SQL standard
Best for Ad-hoc investigation, observability Programmatic search, scoring control Reporting, joins on structured data
Aggregations stats, top, rare, timechart aggregations DSL GROUP BY
Joins (3.3+) Yes (join, lookup, subquery) Limited Yes
Score control Limited Full BM25 / function_score None
Learning curve Lowest for SPL/Splunk users Steepest Familiar to analysts

PPL is the right tool when you are iterating on a question - reading rows, filtering, grouping, then reading again to spot what matters. DSL still wins when you need fine-grained scoring control or programmatic search. SQL is the natural fit for reports and BI tools. None of these is exclusive: you can mix them in the same cluster.

Prerequisites

The SQL plugin ships with the standard OpenSearch distribution, so most clusters have PPL out of the box. On the minimal distribution, install with bin/opensearch-plugin install opensearch-sql-plugin. On Amazon OpenSearch Service, PPL is enabled by default; AWS also exposes PPL in CloudWatch Logs, with a subset of commands available there.

Authorization requires the cluster:admin/opensearch/ppl permission. Subsearches and a handful of other advanced commands additionally need plugins.calcite.enabled set to true - the default since 3.3, but worth verifying on upgraded clusters.

PPL Syntax Fundamentals

A PPL query is one or more commands separated by |. The first command always defines the data source:

source=<index> | <command_1> | <command_2> | ... | <command_n>
  

Two facts that bite newcomers: order matters (filter before aggregating to cut data volume), and PPL drops fields you have not referenced after stats collapses rows. If you need a column in the final output, project it explicitly with fields or include it in the by clause.

You run queries from Dashboards' Query Workbench (Plugins menu, switch to the PPL tab) or via the REST API:

POST _plugins/_ppl
  {
    "query": "source=logs | head 10"
  }
  

Add ?format=jdbc for typed schema metadata, ?format=csv to pull results as CSV. To inspect the execution plan, post to _plugins/_ppl/_explain and you will get the underlying Calcite tree - useful for tuning joins and filter placement.

Basic PPL Query Examples

These are the everyday building blocks: filtering rows, picking columns, computing fields, sorting, deduplicating.

Filtering and selecting fields

source=accounts
  | where age > 18 AND state = 'CA'
  | fields firstname, lastname, age
  

Comparison operators (=, !=, <, >, <=, >=, IN, LIKE) and Boolean logic (AND, OR, NOT) work the way you expect. IN takes a literal list; pair it with a subsearch for set-membership against another index (covered later).

Sorting and limiting

source=web_logs
  | sort -timestamp
  | head 20
  

A - prefix sorts descending; default is ascending. head N is the PPL equivalent of LIMIT N and is the standard way to cap result size during interactive use.

Deduplication

source=events
  | dedup hostname
  | fields hostname, status
  

Useful when you want one row per host. dedup accepts modifiers like keepempty=true (preserve null values as a distinct group) and consecutive=true (only collapse adjacent duplicates - handy in time-ordered streams).

Computing new fields with eval

source=orders
  | eval total = price * quantity, discount_pct = (list_price - price) / list_price * 100
  | fields order_id, total, discount_pct
  

eval is the workhorse for derived columns: arithmetic, string functions, type casts, and a conditional if(condition, then, else). Multiple assignments in one eval are evaluated left-to-right, so later expressions can reference earlier ones.

Renaming fields

source=metrics
  | rename cpu_user_pct as cpu, mem_used_bytes as mem
  

Trivial but useful when joining indices that use different naming conventions or when you want shorter labels in dashboards.

Aggregation and Stats Examples

stats is where PPL earns its keep. The pattern is the same in every example: aggregate functions on the left, by clause on the right.

Counts, averages, sums

source=web_logs
  | stats count() as total_requests by status_code, host
  

count() with no argument counts rows; count(field) counts non-null values; dc(field) (distinct count) counts unique values. Group by multiple dimensions by listing them in by.

Percentile aggregations

source=traces
  | stats percentile(duration_ms, 50) as p50,
          percentile(duration_ms, 95) as p95,
          percentile(duration_ms, 99) as p99
          by service_name
  

percentile(field, n) takes a percentile from 1 to 100. For a single-call multi-percentile result, use percentiles(duration_ms, 50, 95, 99) - it returns an array, which is convenient for compact dashboards.

Conditional counts

source=auth_logs
  | stats count_if(action='failed_login') as failures,
          count_if(action='success') as successes
          by user
  

count_if lets you compute multiple conditional aggregates in one pass without subqueries. Pair it with eval afterward to compute ratios (failure rate, conversion rate) without re-scanning the data.

Top and rare

source=errors | top 5 error_type
  source=access_logs | rare user_agent
  

top N field and rare field are shortcuts for the most and least common values - the kind of query you write twenty times a day during incident response.

Time-bucketed aggregations with span()

source=metrics
  | stats avg(cpu_usage) as avg_cpu by span(@timestamp, 1m), host
  

span(field, interval) accepts intervals like 1s, 1m, 5m, 1h, 1d. This is the canonical pattern for time-series queries, but in 3.3+ the dedicated timechart command (below) is usually clearer.

timechart (added in 3.3)

source=metrics
  | timechart span=5m avg(cpu_usage) by host
  

timechart is a dashboard-first time-series command. Its key advantage over stats ... by span(...) is that it auto-fills missing buckets - so a host that went silent shows up as null rather than disappearing from the chart. That is what you want in nearly every observability dashboard.

eventstats and streamstats

source=requests
  | eventstats avg(duration_ms) as avg_dur by service
  | where duration_ms > avg_dur * 2
  

eventstats computes the same aggregations as stats but appends them as new fields on every original row, preserving row count. The query above flags every request more than 2x slower than the per-service average.

streamstats, added in OpenSearch 3.5, performs cumulative statistical calculations as events are processed - running counts, rolling sums, and similar sliding-window aggregates without the row-collapse of stats.

Extracting Structured Data from Logs

Most production logs are semi-structured: a timestamp, a level, a hostname, and a free-text message. PPL has four extraction commands with different trade-offs.

parse - regex with named capture groups

source=raw_logs
  | parse log_line "(?<ip>\\S+) - - \\[(?<ts>[^\\]]+)\\] \"(?<method>\\S+) (?<path>\\S+)"
  | stats count() by ip, method
  

parse is the simplest option when you have a stable line format. Named capture groups become field names you can use downstream.

rex - regex extraction (added in 3.3)

source=raw_logs
  | rex field=message "user=(?<user>\\w+) ip=(?<ip>\\S+)"
  

rex targets a specific field rather than the full row. It is the right call when extracting a few fields from a structured message column.

grok - pattern-library extraction

source=apache_logs | grok message "%{COMMONAPACHELOG}"
  

grok plugs in the same pattern library Logstash uses, so common log formats (Apache, Nginx, syslog) work out of the box without writing regex by hand.

spath - JSON path extraction (added in 3.3)

source=app_logs
  | spath input=raw_message path=request.headers.user-agent output=ua
  

If your raw_message is JSON-encoded, spath pulls a nested field by path without an explicit regex. Combine with parse upstream when the JSON is wrapped in a wider envelope.

Advanced PPL Examples (OpenSearch 3.3+)

The 3.3 release substantially expanded PPL's reach with 9 new commands and 15 new functions. The headline additions are joins and subsearches.

Joining datasets with join

source=web_logs
  | join type=inner left=client_ip right=ip [
      source=ip_geodata | fields ip, country, city
    ]
  | stats count() as requests by country
  

type= accepts inner, left, right, and cross. The right side can be an index reference or a full subsearch (as above). Filter both sides before joining - large unfiltered joins blow up memory budgets.

Appending results from multiple queries

source=logs-2026-04-*
  | stats count() as errors by service
  | append [
      source=logs-2026-03-*
      | stats count() as errors by service
    ]
  

append is the row-wise concatenation - useful for week-over-week comparisons or merging data from different index patterns. appendcol glues columns side-by-side; appendpipe re-runs the same pipeline with a different filter, which is the most common pattern for "compare to total" reports.

Subsearches

source=alerts
  | where service IN [
      source=critical_services | fields service_name
    ]
  

A subsearch in the IN clause turns the right-hand side into a dynamic list - here, "alerts for any service in the critical_services index". This requires plugins.calcite.enabled=true, which is the default on 3.3 and later.

Lookup tables for enrichment

source=requests
  | lookup ip_geodata ip OUTPUT country, city
  

lookup does fast in-memory enrichment from a small reference index. It is far cheaper than a join for static data (geo databases, asset inventories, employee directories) and avoids reshaping the row.

Multivalue Field Operations (OpenSearch 3.5)

OpenSearch 3.5 (February 2026) shipped six commands and functions specifically for multivalue field manipulation. These matter for any data model with array fields: tags, headers, tracing spans, OpenTelemetry attributes.

mvcombine

source=log_events
  | mvcombine values=tag delim=","
  | stats count() by tag
  

mvcombine consolidates multivalue fields - useful for collapsing arrays when you want one tag-string per row rather than per-element exploding.

mvzip, mvfind, mvmap

source=traces
  | eval pairs = mvzip(span_names, span_durations, "=")
  | where mvfind(span_names, "checkout") >= 0
  | eval squared = mvmap(span_durations, value * value)
  

mvzip stitches two parallel arrays into one. mvfind returns the index of the first match in an array (useful inside where filters). mvmap applies an expression to every element and returns a transformed array.

addtotals

source=daily_revenue
  | stats sum(amount) as total by region, span(@timestamp, 1d)
  | addtotals row=true col=true
  

addtotals adds a row that totals every column and a column that totals every row - the kind of thing analysts expect from a pivot table. New in 3.5.

Real-World Use Cases

The examples below combine the building blocks above into the queries you would actually keep around in a runbook.

Troubleshooting application latency

source=traces
  | where service_name='checkout' AND duration_ms > 1000
  | stats percentile(duration_ms, 99) as p99,
          count() as slow_requests
          by endpoint
  | sort -p99
  | head 10
  

Top 10 slowest endpoints in the checkout service, by p99 latency, filtered to traces over a second. Standard incident-response query.

Detecting anomalous logins

source=auth_logs
  | where action='failed_login'
  | stats count() as failures by src_ip, span(@timestamp, 5m)
  | where failures > 50
  | sort -failures
  

Source IPs with more than 50 failed logins in any 5-minute window - the kind of signal you want feeding into an alert rule.

Error spike investigation

source=app_logs
  | where level='ERROR'
  | timechart span=1m count() by service
  

A per-service error count by minute, with auto-filled gaps. Drop into Dashboards or Grafana and you have a tactical incident view in seconds.

Capacity planning

source=node_metrics
  | timechart span=1h avg(cpu_pct) as cpu, avg(mem_pct) as mem by host
  

Hourly resource trends across hosts, suitable for the kind of "are we close to capacity" review that should happen weekly, not when the page goes off.

Using PPL with Grafana

Grafana shipped first-class PPL support in its OpenSearch data source in November 2025. Pick PPL as the query type in the panel editor; the same syntax above runs unchanged. Visualizations that benefit most are time-series panels (timechart), tables (stats with multi-dimension by), and stat panels (single-row stats).

PPL on AWS CloudWatch Logs

AWS exposes PPL in CloudWatch Logs Insights. The supported subset is smaller than OpenSearch's, and joins/subsearches are not available, but for cross-account log analysis the syntax portability is real. Build the query in OpenSearch, then port it to CloudWatch with minor edits.

Performance Tips and Best Practices

A few rules cover most of what you need to know to keep PPL queries fast.

The Calcite engine is doing real work

Apache Calcite became the default PPL engine in OpenSearch 3.3 (October 14, 2025). It does predicate pushdown, projection pruning, and join reordering - which is to say, you can write queries in the natural order and the optimizer will rearrange them. _plugins/_ppl/_explain shows the rewritten plan; check it when a query is slower than you expect.

Filter early, project narrow

Even with a smart optimizer, putting where immediately after source= and fields right after that gives Calcite less work and often hits the index in fewer round-trips. The cost is a few seconds of writing; the benefit is a query that scales.

Index pattern hygiene

Narrow source= to specific indices when you can. Wildcards like source=logs-* work but expand to every matching shard. ISM-rolled indices with date suffixes (logs-2026-05-*) let you write naturally time-bounded queries without a where on @timestamp.

source=cluster_two:my_index | head 10
  

Cross-cluster PPL works when the remote cluster is registered. Authorization, latency, and version compatibility are the three things to verify; mismatched OpenSearch versions across clusters can produce surprising errors at the planner stage.

Common pitfalls

Forgetting the SQL plugin on the minimal distribution is the most common newcomer issue - PPL queries return a 404. stats ... by span(...) is technically equivalent to timechart but does not auto-fill missing buckets, so use timechart for dashboards. Joins without filtering both sides first will run, but they will run slowly. And dedup operates on rows; for multivalue field deduplication, you want mvcombine or dedup_splitvalues.

PPL Command Reference Cheat Sheet

A grouped reference for the commands you will reach for most often:

  • Data retrieval: source=, where, fields, head, dedup
  • Transformation: eval, rename, parse, rex, grok, spath, convert, fillnull, flatten
  • Aggregation and analysis: stats, eventstats, streamstats, top, rare, sort, chart, timechart, bin, addtotals
  • Multi-source (3.3+): join, append, appendcol, appendpipe, subquery, lookup, multisearch
  • Multivalue (3.5+): mvcombine, mvzip, mvfind, mvmap
  • Pattern matching and ML: regex, patterns, trendline, kmeans, ad, ml

Full reference at docs.opensearch.org/latest/sql-and-ppl/ppl/commands/syntax/.

Frequently Asked Questions

Is PPL faster than SQL or DSL in OpenSearch?

With the Apache Calcite engine in 3.3+, PPL performance is competitive with SQL for analytical workloads. For pure search-relevance scoring, DSL still has the most expressive power. For ad-hoc investigation and observability, PPL is generally the fastest path from question to answer.

Does PPL work on AWS OpenSearch Service?

Yes - PPL is enabled by default on Amazon OpenSearch Service for clusters running OpenSearch 1.0+. AWS CloudWatch Logs Insights also supports a subset of PPL commands.

Can I write data with PPL?

PPL is read-only. Use the Index API or DSL bulk operations for writes.

What is the difference between stats and eventstats?

stats collapses rows into aggregated buckets - one row per group. eventstats computes the same aggregations but appends them as new fields on every original row, preserving the row count. Use eventstats when you need to compare each row to a per-group statistic (e.g., flag rows above the per-service average).

How do I save and reuse a PPL query?

Save it as a Dashboards search, schedule via Anomaly Detection, or wrap it in an alert monitor. There is no native "saved PPL" object distinct from saved searches.

Key Takeaways

  • PPL is OpenSearch's pipe-chained query language, ideal for log analytics, observability, and ad-hoc investigation.
  • Apache Calcite became the default engine in OpenSearch 3.3 (October 2025) and powers predicate pushdown, projection pruning, and join reordering.
  • Joins, subsearches, timechart, rex, and spath arrived in 3.3; multivalue commands (mvcombine, mvzip, mvfind, mvmap, addtotals, streamstats) shipped in 3.5 (February 2026).
  • Filter early, project narrow, and prefer timechart over stats ... by span(...) for dashboards.
  • AWS OpenSearch Service has PPL on by default; CloudWatch Logs supports a subset; Grafana added native PPL support in November 2025.

If you are operating an OpenSearch cluster at scale and want help tuning PPL workloads, designing log analytics pipelines, or migrating from Splunk, our OpenSearch consulting team works on these problems daily.