---
title: "TimescaleDB 2.28: Faster Queries, Lighter Operations, and Better Schema Evolution"
published: 2026-07-29T08:30:13.000-04:00
updated: 2026-07-29T08:30:13.000-04:00
excerpt: "TimescaleDB 2.28 adds schema evolution for continuous aggregates, lighter refreshes, faster compressed queries, and ends PostgreSQL 15 support."
tags: Announcements & Releases, TimescaleDB
authors: Nicole Ghalwash
---

> **TimescaleDB is now Tiger Data.**

Time-series analytics at scale creates operational friction. When you're running continuous aggregates, columnar storage, and complex analytical patterns, each new metric, query pattern, and configuration tuning attempt adds complexity. Refreshes block each other, configuration changes require rebuilds, and new aggregates mean recomputing entire rollups.

Over recent [TimescaleDB releases](https://github.com/timescale/timescaledb/releases), we've prioritized minimizing these hurdles by leveraging bloom filters to bypass redundant processing during high-volume operations on columnar storage. We also expanded vectorized execution across more query patterns and simplified continuous aggregate workflows by combining refresh and compression.

Now with [TimescaleDB 2.28](https://github.com/timescaledb/releases/tag/2.28.0), we're making common analytical queries faster without code changes, making continuous aggregate operations less disruptive and more flexible, and eliminating friction when evolving your schema and configuration. The result is faster queries, lighter operations, and the ability to evolve your analytics alongside your application as it scales.

## TL;DR

  
**Lighter, more flexible continuous aggregates:**

-   `ADD COLUMN` on CAggs: Add new aggregations in place without rebuilding
-   Fine-grained locking: Refreshes no longer serialize unrelated operations
-   Incremental refresh batching: Break large refreshes into batches instead of one heavy operation
-   `ANALYZE` and `VACUUM`: Maintenance commands now work directly on continuous aggregates

**Faster queries on compressed data:**

-   Metadata-driven first() and last() queries: Answer from batch metadata without decompression
-   Vectorized `CASE` expressions: Conditional logic stays on the fast path
-   Batch Sorted Merge for more queries: Lighter-weight sorting on compressed data

**Better operational flexibility:**

-   Sparse index retrofitting: Update configuration on existing chunks without recompression
-   Compression settings clarity: Warnings prevent misconfiguration
-   GUC for bulk loads: Skip invalidation tracking during migrations
-   Nullable `ORDER BY` safety: Fallback to correct compression path

**Also:** PostgreSQL 15 support ends with 2.28. Plan your upgrade to PG16, PG17, or PG18.

## Schema evolution: ADD COLUMN on CAggs

Schema evolution lets you add new aggregated columns without dropping and rebuilding. Before 2.28, adding a metric meant recreating the entire CAgg, recomputing all historical data (hours on large datasets), and breaking downstream consumers. Now you can `ADD COLUMN` with `GENERATED ALWAYS AS` and backfill incrementally.

```sql
-- Add a new metric to an existing CAgg
ALTER MATERIALIZED VIEW conditions_summary_hourly
  ADD COLUMN max_temp double precision
  GENERATED ALWAYS AS (
    max(temperature)
  ) STORED;

-- Backfill historical data
CALL refresh_continuous_aggregate(
  'conditions_summary_hourly',
  NULL,
  NULL,
  force => true
);
```

## How we made continuous aggregates operational at scale

Real-time dashboards on operational data used to force a choice: refresh frequently and block bulk loads, or refresh less often and accept stale data. Every continuous aggregate refresh acquired a table-wide lock on its materialized hypertable, blocking concurrent operations. In 2.28, we switched to row-level locking on the continuous aggregate catalog entry. Only one refresh processes a CAgg's invalidation log at a time, but the materialized table is free for concurrent operations. Refreshes and bulk loads now run in parallel.

We also made continuous aggregates more flexible. `ADD COLUMN` on CAggs enables you to evolve your analytical schema in production by adding new aggregations as you learn what data you need, without downtime or rebuilds. Your dashboard design no longer locks you into upfront predictions. 

![TimescaleDB 2.28 - Continuous Aggregates](https://storage.ghost.io/c/6b/cb/6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e/content/images/2026/07/diagram.svg)

```sql
-- Before 2.28: refresh acquired table-wide lock
-- After 2.28: refresh acquires only catalog row lock
CALL refresh_continuous_aggregate(
  'metrics_hourly',
  '2026-01-01',
  '2026-02-01'
);
-- Other refreshes, bulk loads, and DDL can proceed
```

2.28 also adds two capabilities that make continuous aggregates evolve with your application:

1.  **Incremental refresh batching** processes large time windows in smaller batches instead of single long-running transactions. Before 2.28, `refresh_continuous_aggregate()` materialized the entire window atomically, holding resources and blocking vacuums. Large windows could take hours and fail mid-way. Now you can batch them.

```sql
-- Refresh a 30-day window in smaller batches
CALL refresh_continuous_aggregate(
  'metrics_hourly',
  now() - INTERVAL '30 days',
  now(),
  options => '{
    "buckets_per_batch": 10,
    "max_batches_per_execution": 20
  }'::jsonb
);
```

2.  **Schema evolution** lets you add new aggregated columns without dropping and rebuilding. Before 2.28, adding a metric meant recreating the entire CAgg, recomputing all historical data (hours on large datasets), and breaking downstream consumers. Now you can `ADD COLUMN` with `GENERATED ALWAYS AS` and backfill incrementally.

```sql
-- Add a new metric to an existing CAgg
ALTER MATERIALIZED VIEW conditions_summary_hourly
  ADD COLUMN max_temp double precision
  GENERATED ALWAYS AS (
    max(temperature)
  ) STORED;

-- Backfill historical data
CALL refresh_continuous_aggregate(
  'conditions_summary_hourly',
  NULL,
  NULL,
  force => true
);
```

`ANALYZE` and `VACUUM` now work on continuous aggregates directly. Maintenance commands automatically maintain accurate planner statistics across the materialized hypertable and all its chunks, ensuring query plans stay optimal.

## Faster queries on compressed data

One of the most common queries in time-series workloads is "give me the latest value per series." Dashboards and monitoring systems run it constantly. It's a simple pattern: find the first or last value in a time range. But on compressed data, answering that query used to require decompressing entire batches just to find values that are already in the metadata. Time-sorted batches store first and last values as metadata. But before 2.28, the database decompressed anyway to answer those queries.

In 2.28, TimescaleDB extracts first(value, time) and last(value, time) aggregates directly from batch sparse indexes. No decompression. For "latest reading" queries that consume significant resources at scale, that means meaningful speedup with zero query changes. Here's what that looks like:

![TimescaleDB 2.28 - Metadata Optimization](https://storage.ghost.io/c/6b/cb/6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e/content/images/2026/07/diagram-1.svg)

```sql
-- Define firstlast sparse index
ALTER TABLE metrics SET (
  tsdb.index = 'firstlast(temperature)'
);

-- Note: Use rebuild_sparse_index() to retrofit existing chunks
-- New chunks now answer this query from metadata alone
SELECT
  device_id,
  last(temperature, time)
FROM metrics
WHERE time > now() - INTERVAL '7 days'
GROUP BY device_id;
```

Beyond first/last, we also extended vectorized execution to cover `CASE` expressions. Before 2.28, conditional logic in aggregations forced columnar queries to fall back to row-by-row processing, making identical queries perform very differently depending on whether a `CASE` expression was present.

```sql
-- This query now stays fully vectorized
SELECT
  time_bucket('1 hour', ts) AS bucket,
  SUM(
    CASE WHEN status_code >= 500 THEN 1 ELSE 0 END
  ) AS error_count,
  AVG(
    CASE WHEN latency_ms > 1000 THEN latency_ms END
  ) AS slow_avg_latency
FROM requests
WHERE ts > now() - INTERVAL '7 days'
GROUP BY bucket;
```

Pivot-style queries, error rate calculations, and conditional metrics all stay on the fast vectorized path. For workloads that use conditional aggregations on compressed history, that eliminates the surprise performance cliffs.

Finally, Batch Sorted Merge now applies to more query patterns. `ORDER BY` queries on unordered compressed chunks without segmentation no longer fall back to expensive external sorts. The planner now replaces those sorts with lightweight metadata merges over pre-sorted batches.

## Better operational flexibility and efficiency

Tuning time-series workloads means iterating on compression strategy. But historically, every configuration change (adding a sparse index, adjusting `segmentby`, optimizing `orderby`) only applied to future compressions. Existing chunks kept their old settings, creating inconsistent performance and confusion about why newly tuned queries behaved differently.

2.28 eliminates that friction. `rebuild_sparse_index` lets you retrofit sparse index configuration to existing chunks without recompressing. Sparse indexes are metadata. Updating them shouldn't require rewriting data. Below is the actual code snippet you can leverage:

```sql
-- Change sparse index settings on the hypertable
ALTER TABLE metrics SET (
  tsdb.index = 'minmax(value)'
);

-- Retrofit existing chunks without recompression
SELECT _timescaledb_functions.rebuild_sparse_index(
  '_timescaledb_internal._hyper_1_42_chunk'
);
```

**Compression settings changes now emit warnings** clarifying that new settings apply only to future compressions. This closes the gap: users assume `ALTER TABLE` sets configuration across the entire dataset, but it only affects new chunks.

For correctness, **nullable ORDER BY columns now safely fall back** to decompress-compress during recompression, preventing silent incorrect results when min/max metadata doesn't account for `NULL`s.

**Gapfill row count estimation** improved so `time_bucket_gapfill` queries get more accurate planner statistics and better query plans.

For bulk migrations, a new GUC **timescaledb.skip\_cagg\_invalidation** suppresses continuous aggregate invalidation tracking during bulk loads. Migration tools no longer generate useless invalidation entries that trigger expensive refresh storms.

## PostgreSQL 15: Final Release and Migration Path

PostgreSQL 15 support ends with TimescaleDB 2.28. Going forward, only PostgreSQL 16, 17, and 18 are supported.

If you're on PG15, plan your upgrade to PG17 or PG18 now. We'll begin upgrading instances in production the week of September 15th, so you should plan your migration path over the next two months to avoid downtime. Postgres upgrades are typically non-disruptive (seconds to minutes of downtime using logical replication or physical backup/restore), and newer Postgres versions bring improvements in query parallelism, vector search optimization, and compression.

## Upgrade to 2.28 today

For workloads running continuous aggregates at scale, columnar queries with conditional logic, or iterating on compression configuration, 2.28 removes operational friction: refreshes don't serialize, queries stay vectorized, and tuning doesn't require rewrites.

2.28 is available now. To learn more, [check out the full release notes](https://github.com/timescale/timescaledb/releases/tag/2.28.0) for a complete list of improvements, or [_try Tiger Cloud for free_](https://console.cloud.tigerdata.com/signup) and experience TimescaleDB 2.28 on your largest hypertables. We welcome your feedback on [GitHub](https://github.com/timescale/timescaledb). Please note that for Tiger Cloud customers, all improvements are live immediately. For self-hosted deployments, download the [latest release](https://github.com/timescale/timescaledb/releases/tag/2.28.0) and follow the [upgrade guide](https://www.tigerdata.com/docs/reference/timescaledb/install/).