
By Tiger Data Team
Updated at Jun 10, 2026
If you run aggregate queries over months of time-series data on standard PostgreSQL, you already know what happens when the dataset gets large. PostgreSQL’s native function, date_trunc, has limitations; for example, you can only aggregate in the units of "hour, day, week, or month". Even grouping in 15 minutes is quite poorly readable. A query that finishes in under a second at 10 million rows can take tens of seconds at 500 million rows. The query isn't wrong. It's doing exactly what PostgreSQL asks: scanning the entire table, recalculating every bucket, from the first timestamp to the last. A continuous aggregate on the same dataset can reduce that to single-digit seconds. A hierarchical continuous aggregate can drop it to sub-second response times. (Exact figures vary by schema, hardware, and query pattern.)
There's no native PostgreSQL mechanism to change this. REFRESH MATERIALIZED VIEW performs a full-table recompute every time it runs. A cron job calling it every hour doesn't make it faster; it just makes it run more often. This results in increased operational overhead: setting up the functionality, testing it, monitoring it, and maintaining it. For growing datasets, this doesn't scale.
Continuous aggregates are TimescaleDB's answer. Let’s explore them below.
A continuous aggregate is a TimescaleDB materialized view that refreshes incrementally over a hypertable. When new data arrives, only the affected time buckets are recalculated. Historical data that hasn't changed is never touched. The result is a pre-computed aggregate store that stays current without full-table rescans. For context on how this compares to standard views, see our PostgreSQL views guide.
Three properties define how they work:
Automatic background refresh via add_continuous_aggregate_policy(). No cron jobs, no manual REFRESH calls.
Real-time aggregation as an opt-in: pre-computed data combined with the most recent raw data at query time, so results stay current.
time_bucket() as the required grouping dimension. This is what makes continuous aggregates time-series-aware rather than a general-purpose incremental view. See the documentation for more details on time_bucket.
Continuous aggregates are a feature of TimescaleDB, the open-source PostgreSQL extension maintained by Tiger Data, and are available in both the self-hosted and managed versions. Tiger Cloud, Tiger Data's fully managed service, handles the underlying infrastructure automatically, so there's no server configuration, extension upgrades, or background worker management to worry about.
Here's the minimal syntax to create one. This example buckets raw sensor readings into hourly averages, per device:
CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', recorded_at) AS bucket,
device_id,
AVG(temperature) AS avg_temp
FROM sensor_readings
GROUP BY bucket, device_id
WITH NO DATA;WITH NO DATA creates the view immediately without waiting for an initial backfill. The refresh policy picks it up from there:
SELECT add_continuous_aggregate_policy('sensor_hourly',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);Once the policy is in place, TimescaleDB refreshes the aggregate automatically on the defined schedule — no manual REFRESH calls needed.
For the full DDL reference, see the TimescaleDB continuous aggregates documentation.
Developers who know PostgreSQL materialized views will immediately ask why a standard CREATE MATERIALIZED VIEW plus a cron job doesn't work. The answer is in how refresh works.
Feature | PostgreSQL Materialized View | TimescaleDB Continuous Aggregate |
Refresh mechanism | Full table recompute | Incremental (only changed time buckets) |
Triggering refresh | Manual ( | Automatic via |
Query freshness | Stale until manually refreshed | Real-time available (pre-computed + raw data combined) |
Storage | Stores full result set | Stores partial aggregation state; supports columnstore compression |
Time-series awareness | None |
|
Maintenance overhead | High (full recompute on any schema change or data update) | Low (background worker refreshes only invalidated ranges) |
Requires hypertable | No | Yes |
Columnstore | N/A | A CAgg can be on columnstore, saving significant storage space and return results even faster with the Hypercore engine. |
Historical data rarely changes. Only recent data does. A full-table refresh on a two-year sensor dataset to incorporate five minutes of new readings wastes significant compute. Continuous aggregates track a materialization watermark, a timestamp representing the upper bound of what has been pre-computed, and only recalculate the time buckets where the underlying data has changed.
One trade-off: continuous aggregates require TimescaleDB. They're not a vanilla PostgreSQL feature. If you're not using TimescaleDB, a scheduled REFRESH MATERIALIZED VIEW is still your best option. For a deeper look at how continuous aggregates compare to materialized views and when to use each approach, see our guide on the topic.
TimescaleDB tracks a materialization watermark per continuous aggregate: a timestamp representing the upper bound of what has been pre-computed. Data older than the watermark is served from the pre-computed store. Data newer than the watermark can be computed from the raw hypertable chunks at query time when real-time aggregation is enabled.
You configure when and how often the background job refreshes via add_continuous_aggregate_policy():
SELECT add_continuous_aggregate_policy('sensor_hourly',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);start_offset: how far back in time to look for data to refresh
end_offset: a buffer before the current time, to avoid refreshing chunks that may still be receiving writes
schedule_interval: how frequently the background job runs
The policy runs automatically. You do not trigger it manually.
Real-time aggregation behavior is the most common point of confusion. Tiger Docs is the source of truth here:
In TimescaleDB v2.13 and later, real-time aggregates are disabled by default. Queries return only pre-computed data up to the materialization watermark. Data newer than the watermark is not visible in the aggregate until the next refresh.
To enable real-time aggregation, set materialized_only = false. With real-time on, queries combine pre-computed data for older time ranges with on-the-fly computation for data newer than the watermark. The result is always current.
Use materialized_only = true (the v2.13+ default) when you need deterministic, time-bounded results. Use materialized_only = false when dashboard freshness matters more than strict consistency.
For the full implementation walkthrough, see create a continuous aggregate in the docs.
As data ages, per-second granularity becomes unnecessary and expensive to store. A monitoring system needs 1-second metrics for the last hour, 1-minute rollups for the last week, and hourly averages for the last year. Continuous aggregates handle this pattern natively.
Hierarchical continuous aggregates are the mechanism: a continuous aggregate built on top of another continuous aggregate rather than directly on the raw hypertable. A 15-minute CAGG feeds a 4-hour CAGG, which feeds a daily CAGG. Each tier refreshes from the tier below, not from the full raw dataset. The performance difference is real: queries that scan raw data at hundreds of millions of rows can go from tens of seconds on the raw table to single-digit seconds on a single-tier CAGG, and to sub-second on a hierarchical CAGG at the appropriate granularity. Exact numbers vary by schema and hardware.
Tiger Data has published examples showing continuous aggregate tables with columnstore compression achieving 80%+ storage reduction compared to the raw hypertable. Verify the specific figures for your schema in the docs.
Pair this with data retention policies and you get a full data lifecycle: keep raw high-frequency data for 30 days, hourly rollups for one year, daily aggregates indefinitely. For more on how retention policies fit this pattern, see our guide on data retention policies.
Hierarchical downsampling is built into TimescaleDB, not a separate feature or API. The Tiger Data docs on hierarchical continuous aggregates have worked examples.
Continuous aggregates are a strong fit for these workloads:
Real-time monitoring and observability dashboards. Grafana panels and similar tools run aggregate queries over rolling time windows, often on refresh intervals measured in seconds. Pre-computing the aggregates keeps dashboards fast as datasets grow into the hundreds of millions of rows. This is one of the strongest arguments for using PostgreSQL as a real-time analytics database: the same database that ingests your data serves fast, pre-computed aggregate results without a separate OLAP layer. The FlightAware engineering team found that continuous aggregates dropped one of their query times from 6.4 seconds to 30 milliseconds.
Financial data: OHLC candlesticks from tick data. Generating open/high/low/close values per time interval is a canonical continuous aggregate use case. Continuous aggregates handle the aggregation continuously; queries read the pre-computed result.
IoT and sensor rollups. Per-device or per-site averages, min, and max at configurable intervals from high-frequency raw data. This covers industrial telemetry, energy monitoring, and logistics tracking.
Long-term trend analysis. Pre-computing daily or weekly aggregates over two or more years of data for historical analytics workloads. Without continuous aggregates, these queries scan the full raw table on every execution.
Dashboard query load reduction. If expensive GROUP BY queries are hitting your raw hypertable on every dashboard refresh, continuous aggregates move that work to a background job. Query the pre-computed result instead.
On the competitive landscape: continuous aggregates are a TimescaleDB-specific feature in the managed PostgreSQL ecosystem. InfluxDB 3.0 does not have native continuous aggregates. Earlier InfluxDB versions (1.x) had "Continuous Queries," which were deprecated in InfluxDB 2.x and are not present in InfluxDB 3.0. QuestDB does not support automatic incremental rollups or continuously-refreshed materialized views; it offers query-time downsampling via SAMPLE BY, but no built-in mechanism for pre-computing and auto-refreshing aggregate results. CrateDB has no equivalent feature.
For a broader comparison, see the best time-series databases compared.
Window functions. Window functions inside a continuous aggregate view definition are experimental as of TimescaleDB v2.20.0 (disabled by default; enable with timescaledb.enable_cagg_window_functions). In earlier versions, window functions inside a CAGG definition are not supported. The workaround: define the continuous aggregate using standard aggregates, then apply window functions at query time against the CAGG result.
time_bucket() is required. The primary grouping dimension must be a time_bucket() call on the time column. You cannot use an arbitrary non-time GROUP BY as the primary bucket. Additional grouping dimensions (device_id, symbol, etc.) are supported as secondary dimensions alongside time_bucket().
Join support. As of TimescaleDB v2.10+, inner joins between a hypertable and a non-hypertable (a regular PostgreSQL table) are supported inside a continuous aggregate definition. As of v2.16+, left joins and lateral joins are also supported. Joins between two hypertables remain unsupported. If your continuous aggregate query needs to join two time-series tables, this is a current hard limit.
Hypertable requirement. The source table must be a TimescaleDB hypertable. Continuous aggregates cannot be created over standard PostgreSQL tables.
Most time-series workloads (monitoring, IoT, financial data) aren't affected by the window function or join limitations. For the authoritative, version-specific list, see the TimescaleDB continuous aggregates documentation.
For related context on PostgreSQL aggregation best practices and when standard aggregation is sufficient, that guide covers the decision points in more depth.
Tiger Cloud includes a built-in console with guided UX for creating and managing continuous aggregates - no SQL required to get started. Refresh policies, compression, and retention are all configurable from the UI, and the console provides built-in visualizations so you can inspect aggregate output directly.
For teams with existing observability stacks, Tiger Cloud ships with metrics exporters that let you pipe database and policy performance data into Prometheus, Grafana, or whatever tooling you already use.
To get hands-on, follow the create a continuous aggregate guide in the docs. To try Tiger Cloud, start a free trial. No credit card required.
A continuous aggregate is a TimescaleDB feature that creates an incrementally-refreshed materialized view over aggregate queries on a hypertable. Unlike standard PostgreSQL materialized views, which require a full-table recompute on each refresh, continuous aggregates only reprocess time buckets where the underlying data has changed.
The primary difference is the refresh model. A PostgreSQL materialized view refreshes by recomputing the entire result set. A continuous aggregate tracks a watermark and only recalculates time buckets that have changed. For large time-series datasets, this makes continuous aggregates significantly faster to refresh and cheaper to maintain.
By default in TimescaleDB v2.13 and later, continuous aggregates are delayed: they return only pre-computed data up to the materialization watermark. In earlier versions, real-time aggregation was enabled by default. You can enable real-time aggregation by setting materialized_only = false. With real-time on, queries combine pre-computed data for older time ranges with on-the-fly computation for data newer than the watermark.
InfluxDB 3.0 does not have native continuous aggregates. Earlier versions of InfluxDB (1.x) had Continuous Queries, which were deprecated in InfluxDB 2.x and are absent from InfluxDB 3.0. As of 2026, TimescaleDB is the primary open-source time-series database that supports incremental, automatically-refreshed aggregate views natively in PostgreSQL.
QuestDB does not support automatic incremental rollups or continuously-refreshed materialized views. It offers query-time downsampling via SAMPLE BY, but no built-in mechanism for pre-computing and auto-refreshing aggregate results.
The main limitations are: (1) window functions inside a continuous aggregate definition are experimental since v2.20.0, disabled by default; apply window functions at query time instead; (2) the primary grouping dimension must use time_bucket(); (3) as of v2.10+, inner joins are supported; as of v2.16+, left and lateral joins are also supported, but joins between two hypertables remain unsupported. These constraints affect a minority of time-series workloads.
A hierarchical continuous aggregate is a continuous aggregate built on top of another continuous aggregate rather than directly on the raw hypertable. This enables multi-tier rollup pipelines: a 15-minute rollup feeds a 4-hour rollup, which feeds a daily rollup. Each tier refreshes from the tier below, not from the full raw dataset.
You configure a refresh policy with add_continuous_aggregate_policy(). The key parameters are start_offset (how far back to look for data), end_offset (a buffer before current time), and schedule_interval (how often the background job runs). The policy runs automatically.
Yes. Continuous aggregates store data in their own internal tables, which can be compressed using TimescaleDB's columnstore compression. The compression does not affect query behavior; the continuous aggregate remains queryable as normal.
TimescaleDB's invalidation log tracks inserts, updates, and deletes to the raw hypertable. When data is inserted into an already-materialized time range, the affected time buckets are marked as invalid and recalculated on the next refresh. You can also trigger a manual refresh with CALL refresh_continuous_aggregate() for a specific time range.
Yes. Continuous aggregates are a TimescaleDB extension feature and require a hypertable as the source. They are available in the open-source TimescaleDB extension and fully managed in Tiger Cloud. Standard PostgreSQL's closest analog is REFRESH MATERIALIZED VIEW, which performs a full-table recompute with no incremental behavior.
materialized_only = true (the default in TimescaleDB v2.13+) returns only pre-computed data; data newer than the watermark is not visible until the next refresh. materialized_only = false enables real-time aggregation: queries combine pre-computed data with on-the-fly computation for the most recent data. Use false when you need fresh results; use true when you need deterministic, time-bounded results.