Date histogram aggregation #
This multi-bucket aggregation is similar to the normal histogram, but it can only be used with date fields. Dates are represented internally as epoch milliseconds, so the plain histogram can technically run on them too — but calendar intervals are not a fixed length (a month is 28–31 days), which is exactly what this aggregation handles.
How it works #
Date fields store epoch milliseconds at ingest, and the aggregation
builds its grid on those numbers. calendar_interval and
fixed_interval select between two grid types (exactly one must be
set):
- Calendar intervals (
minute,hour,day,week,month,quarter,year, or1m/1h/1d/1w/1M/1q/1y— single quantities only). Months, quarters, years and weeks floor to civil boundaries: bucket keys are real month starts and Monday weeks, so February is as long as it is. Use these for anything a human reads. - Fixed intervals (
ms/s/m/h/dunits with multiples, like"30m"or"7d"). A fixed number of milliseconds, anchored at the epoch — the grid never bends for the calendar.
Each bucket reports key (the boundary as epoch milliseconds) and
key_as_string, rendered in UTC as strict_date_optional_time
(2024-01-01T00:00:00.000Z). The format and time_zone parameters
are accepted for compatibility; this engine currently always renders
the UTC ISO form and computes boundaries in UTC.
min_doc_count (default 0) drops thin buckets. Unlike the numeric
histogram, only buckets that actually received documents are emitted —
there is no gap-filling between the data minimum and maximum. Two
parameters widen the picture: offset (a duration string like "+1d"
or a millisecond number) shifts every boundary — the keys report the
shifted boundaries — and hard_bounds ({min, max} as epoch
millis) both clips the window and force-materializes every bucket
inside it, calendar units stepping real months, up to a 100,000-bucket
guard. date_nanos-magnitude values (≥ 10¹⁷) are scaled down to
milliseconds so both mappings land on one grid.
Examples #
Sales per year — DATE_TRUNC(date, 'year') is the same grouping key on
the SQL side:
POST /sales/_search
{
"aggs": {
"sales_over_time": {
"date_histogram": {
"field": "date",
"calendar_interval": "year"
}
}
}
}
Response:
{
...
"aggregations": {
"sales_over_time": {
"buckets": [
{ "key_as_string": "2024-01-01T00:00:00.000Z", "key": 1704067200000, "doc_count": 92 },
{ "key_as_string": "2025-01-01T00:00:00.000Z", "key": 1735689600000, "doc_count": 28 }
]
}
}
}
Parameters for date_histogram
#
field
(Required, string) Field you wish to aggregate (must be a date / epoch_millis field).min_doc_count
(Optional, integer) Minimum number of documents in a bucket to include it. Defaults to0.format
(Optional, string) Date format for the bucket key string. Accepted for compatibility; this engine always renders thestrict_date_optional_timeUTC form.time_zone
(Optional, string) Time zone offset, e.g.+08:00or-05:00. Accepted for compatibility; bucketing and keys are currently always UTC.offset
(Optional, string | number) Shift every bucket boundary by this duration ("+1d","-2h") or millisecond count; keys report the shifted boundaries.hard_bounds
(Optional, object){min, max}epoch-millis bounds: clip the window and force-materialize every bucket inside it, even at zero documents.aggs
(Optional, object) Nested sub-aggregations, computed per bucket.
One of calendar_interval or fixed_interval must be set.
calendar_interval
#
(Optional, string) Calendar-aware intervals are configured with the
calendar_interval parameter. You can specify calendar intervals using the unit
name, such as month, or as a single unit quantity, such as 1M. For example,
day and 1d are equivalent. Multiple quantities, such as 2d, are not
supported.
The accepted calendar intervals are:
minute,1m
All minutes begin at 00 seconds. One minute is the interval between 00 seconds of the first minute and 00 seconds of the following minute in the specified time zone, compensating for any intervening leap seconds, so that the number of minutes and seconds past the hour is the same at the start and end.hour,1h
All hours begin at 00 minutes and 00 seconds. One hour (1h) is the interval between 00:00 minutes of the first hour and 00:00 minutes of the following hour in the specified time zone, compensating for any intervening leap seconds, so that the number of minutes and seconds past the hour is the same at the start and end.day,1d
All days begin at the earliest possible time, which is usually 00:00:00 (midnight). One day (1d) is the interval between the start of the day and the start of the following day in the specified time zone, compensating for any intervening time changes.week,1w
One week is the interval between the start day_of_week:hour:minute:second and the same day of the week and time of the following week in the specified time zone.month,1M
One month is the interval between the start day of the month and time of day and the same day of the month and time of the following month in the specified time zone, so that the day of the month and time of day are the same at the start and end.quarter,1q
One quarter is the interval between the start day of the month and time of day and the same day of the month and time of day three months later, so that the day of the month and time of day are the same at the start and end.year,1y
One year is the interval between the start day of the month and time of day and the same day of the month and time of day the following year in the specified time zone, so that the date and time are the same at the start and end.
fixed_interval
#
Fixed intervals are configured with the fixed_interval parameter.
In contrast to calendar-aware intervals, fixed intervals are a fixed number of
SI units and never deviate, regardless of where they fall on the calendar. One
second is always composed of 1000ms. This allows fixed intervals to be
specified in any multiple of the supported units.
However, it means fixed intervals cannot express other units such as months, since the duration of a month is not a fixed quantity. Attempting to specify a calendar interval like month or quarter will throw an exception.
The accepted units for fixed intervals are:
- milliseconds (
ms)
A single millisecond. This is a very, very small interval. - seconds (
s)
Defined as 1000 milliseconds each. - minutes (
m)
Defined as 60 seconds each (60,000 milliseconds). All minutes begin at 00 seconds. - hours (
h)
Defined as 60 minutes each (3,600,000 milliseconds). All hours begin at 00 minutes and 00 seconds. - days (
d)
Defined as 24 hours (86,400,000 milliseconds). All days begin at the earliest possible time, which is usually 00:00:00 (midnight).
Range fields #
date_histogram accepts date_range fields: each stored range counts
in every calendar bucket it overlaps. offset shifts the bucket grid
(keys report the shifted boundaries), and hard_bounds clips the window
while force-materializing every in-calendar-window bucket — open-ended
stored ranges extend to that window.
Use cases #
- Log timelines. The teaching
logsdataset carries 150 events across one June week; acalendar_interval: "day"histogram returns one bucket per day (24 events each, 6 on the last), exactly the series a monitoring chart plots. - Period comparisons. Bucket sales by
monthorquarterand nest asum— revenue per period falls out of one query. Over the 120 teaching sales, ayearhistogram splits them 92 (2024) / 28 (2025). - Business-day windows.
calendar_interval: "week"floors to Mondays; combined withoffsetyou can slide week boundaries to Sunday starts without changing the data. - Fixed-window SLA views.
fixed_interval: "1h"gives wall-clock-aligned, never-drifting windows for rate charts, while calendar intervals keep human-readable month edges. - Bounded reports.
hard_boundspins a chart to the reporting window (empty in-window buckets included) regardless of when the first and last matching documents happened to arrive.
Related #
histogram— the same fixed-grid idea over plain numeric fields.auto_date_histogram— pick the bucket count, let the engine choose day/week/month/year.variable_width_histogram— adaptive bucket edges over numeric data.date_range— a handful of named periods instead of a regular grid.composite— paginate adate_histogramdimension with anaftercursor.