Query histogram fields in ES|QL
Histogram fields store pre-aggregated value distributions rather than individual data points. They are useful anywhere you have high-volume numeric data and need percentile, average, or count queries without the storage cost of keeping every raw value. The most common source is observability pipelines: an OpenTelemetry agent can record thousands of HTTP request durations per minute and ship them as a single exponential histogram per collection interval.
This page explains the histogram field types available in ES|QL, how to query them, and how to create value-distribution histograms from them.
When you need to understand the distribution of values across a large population, storing every individual observation is often impractical. A histogram compresses those observations into a compact summary that still supports percentile queries, averages, and counts. This makes histograms useful anywhere you have high-volume numeric data and care about the shape of the distribution, not just a single aggregate.
For example, knowing that your average HTTP response time is 3ms tells you little about outliers. A histogram preserves enough detail to answer questions like "what is the 99th percentile?" without storing every request.
Histograms are widely used in metrics pipelines. OpenTelemetry and Prometheus both define exponential histogram formats that dynamically adapt bucket boundaries to the data, giving a guaranteed upper bound on relative error for every percentile. Classic Prometheus-style histograms use fixed buckets instead, which requires knowing the value distribution up front.
For a deeper explanation of how exponential bucketing works, refer to the OpenTelemetry exponential histograms introduction.
ES|QL recognizes three histogram field types:
exponential_histogram- The recommended type for new data. Uses an exponential bucketing scheme that dynamically adapts to your data and provides a guaranteed upper bound on relative error for every percentile. This is the native format used by OpenTelemetry exponential histograms and maps directly to Prometheus native histograms. Most aggregation functions work natively with this type.
tdigest- Stores a T-Digest data structure. Provides good accuracy for extreme percentiles (like p99) but loses accuracy for mid-range percentiles like the median. Before Elasticsearch 9.4, OpenTelemetry histograms were converted to T-Digest for storage. Use this type when your data is already stored as T-Digest values.
histogram(legacy)- The original histogram field type.
This type can represent either T-Digest or HDR histogram data, but ES|QL only supports
T-Digest. Cast
histogramfields with::tdigestto query them, or with::exponential_histogramto convert them for use alongside newer data. Refer to Cast between histogram types.
To check which type a metric uses, run
METRICS_INFO against the
data stream.
For new data, use exponential_histogram. It provides a guaranteed upper bound on relative
error for every percentile and eliminates the lossy conversion step that T-Digest requires.
Percentile computation on exponential histograms is also more efficient at query time.
Use tdigest only when your data is already stored in that format. T-Digest provides good
accuracy at the tails of the distribution (p99, p99.9) but lower accuracy for mid-range
percentiles like the median. It also only supports delta temporality (not cumulative).
Apply regular aggregation functions
directly to histogram fields. Aggregations act as if you were running them on the raw
observations that produced the histogram. For example, COUNT(responseTime) returns the
total number of HTTP requests whose response times were recorded, not the number of
histogram documents. The following functions support histogram inputs:
| Function | What it returns for histogram fields |
|---|---|
COUNT |
Total number of values recorded across all histograms |
SUM |
Sum of all recorded values |
AVG |
Average of all recorded values (computed as SUM / COUNT) |
MIN |
Minimum recorded value |
MAX |
Maximum recorded value |
MEDIAN |
Estimated median (50th percentile) |
PERCENTILE |
Estimated value at a given percentile |
FIRST / EARLIEST |
First histogram value by sort order / timestamp
|
LAST / LATEST |
Last histogram value by sort order / timestamp
|
For example, to calculate the count, average, and 99th-percentile duration of garbage collection events per action:
FROM metrics-*
| STATS count = COUNT(jvm.gc.duration),
avg = AVG(jvm.gc.duration),
p99 = PERCENTILE(jvm.gc.duration, 99)
BY jvm.gc.action
Because PERCENTILE works on the histogram directly, you can query any percentile at
runtime without having to pre-define bucket boundaries at index time. This is a key advantage
of the exponential_histogram type over classic fixed-bucket approaches.
TS is the recommended source command
for time series data. Before your aggregation runs, TS performs an implicit per-series
merge: all histogram documents within each time bucket and series are combined into a single histogram,
and the metric's temporality (delta or cumulative) is respected during the merge. The outer
aggregation then operates on these merged per-series histograms.
You do not need a time series aggregation function like
RATE or
AVG_OVER_TIME
to query histogram fields. Use the regular aggregation functions from the table above, combined
with TBUCKET for time-based grouping:
TS metrics-*
| WHERE TRANGE(1 hour)
| STATS count = COUNT(jvm.gc.duration),
avg = AVG(jvm.gc.duration),
p99 = PERCENTILE(jvm.gc.duration, 99)
BY jvm.gc.action, TBUCKET(5 minutes)
The time series aggregation functions
(*_OVER_TIME variants) also accept histogram inputs for windowed aggregation within a time
series. You can use these when you need finer control over the aggregation window, for example
FIRST_OVER_TIME
or LAST_OVER_TIME
to select the histogram from a specific point in the
time series. For most use cases, the regular aggregation functions are sufficient.
FROM also supports histogram
aggregations. Unlike TS, FROM does not perform an implicit per-series merge or handle
metric temporality.
If your data is stored in a time series data stream, use TS. Use FROM for non-TSDS data
such as legacy indices or non-metrics use cases.
FROM metrics-*
| STATS count = COUNT(responseTime),
avg = AVG(responseTime),
p99 = PERCENTILE(responseTime, 99)
BY instance
To break down the values recorded in a histogram field into fixed-width buckets, combine
BUCKET
with COUNT.
When applied to a histogram field, BUCKET returns double_range buckets instead of single
values. A histogram that spans several buckets contributes a row to each of them. Pass the
bucket as the second argument to COUNT to count the histogram values that fall into each
bucket:
FROM exp_histo_sample
| WHERE instance == "instance-0"
| STATS count = COUNT(responseTime, bucket) BY bucket = BUCKET(responseTime, 1)
| SORT RANGE_MIN(bucket)
| count:long | bucket:double_range |
|---|---|
| 8723 | 0.0..1.0 |
| 112 | 1.0..2.0 |
| 1 | 2.0..3.0 |
| 2 | 3.0..4.0 |
| 2 | 5.0..6.0 |
| 1 | 6.0..7.0 |
Use RANGE_MIN
or RANGE_MAX
to extract the start or end of each double_range bucket for sorting or further computation.
Histograms record approximate value distributions, so the counts per bucket are estimates.
The same pattern works for tdigest fields. Cast the field if needed:
FROM histogram_timeseries_index
| WHERE instance == "instance-0"
| STATS count = COUNT(responseTime::tdigest, bucket) BY bucket = BUCKET(responseTime::tdigest, 1)
| SORT RANGE_MIN(bucket)
| count:long | bucket:double_range |
|---|---|
| 8733 | 0.0..1.0 |
| 100 | 1.0..2.0 |
| 4 | 2.0..3.0 |
| 2 | 3.0..4.0 |
| 0 | 4.0..5.0 |
| 0 | 5.0..6.0 |
| 2 | 6.0..7.0 |
A T-Digest does not track which ranges between its centroids are empty. BUCKET returns every
bucket between the smallest and the largest centroid, so some buckets may show a count of 0.
Exponential histograms skip empty buckets.
Use the casting operator (::) to convert between histogram types inline:
field::exponential_histogramconverts to an exponential histogram. This is the recommended default becauseexponential_histogramis the native type used for newly ingested metrics.field::tdigestconverts to a T-Digest. Use this when you know the data was originally stored as T-Digest centroids.
FROM metrics-*
| STATS avg = AVG(response_time::exponential_histogram) BY instance
You can also use the explicit conversion functions
TO_EXPONENTIAL_HISTOGRAM
and TO_TDIGEST
in an EVAL step. Both functions accept all three histogram types as input and return the
target type (identity conversion is a no-op).
Before Elasticsearch 9.4, OpenTelemetry histograms were converted to T-Digest for storage in the
histogram field type. After upgrading, newer indices use exponential_histogram while
older indices still contain histogram data.
Thanks to union types,
you can query across both by adding a ::exponential_histogram cast:
FROM metrics-*
| STATS avg = AVG(jvm.gc.duration::exponential_histogram) BY jvm.gc.action
When this query encounters histogram fields, it converts them to exponential histograms.
When it encounters exponential_histogram fields, the cast has no effect. If you are building
queries or dashboards that may run on pre-9.4 data, adding ::exponential_histogram casts is
recommended.
Use METRICS_INFO to inspect
which field types are in use across backing indices.
- Sorting on histogram fields is not allowed. Use
SORTon aggregated results (likeRANGE_MIN(bucket)) instead of on the histogram field itself. RATEdoes not accept histogram fields. Use other aggregation functions on the histogram directly.VALUESdoes not accept histogram types.- Multivalue functions like
MV_FIRST,MV_LAST, andMV_COUNTreject histogram fields. - Counts and percentiles derived from histogram fields are estimates because the underlying data structures store distributions, not exact values.
- OTel histograms: Working with ES|QL histogram metrics: A walkthrough of querying OpenTelemetry exponential histograms in ES|QL, including percentile analysis of JVM garbage collection metrics.
- Work with histogram metrics:
Histogram-specific guidance in the
TScommand reference. - Ingest OpenTelemetry data via OTLP: How to send OpenTelemetry exponential histograms to Elasticsearch.
- Exponential histogram field type:
Mapping reference for the
exponential_histogramfield type. - Downsampling time series data: How histogram fields are preserved during downsampling.
- Create a histogram from regular data:
Use
BUCKETon plain numeric or date fields to group rows into buckets. BUCKETfunction reference: Full syntax and examples for theBUCKETgrouping function.