Skip to content

Hypertables

Krister-Johansson edited this page Jun 17, 2026 · 1 revision

Hypertables

Annotate a model with @timescale.hypertable and the generator converts the table into a TimescaleDB hypertable in a reset-safe migration.

/// @timescale.hypertable(column: "time", chunkInterval: "1 day")
model SensorReading {
  time        DateTime
  deviceId    Int
  temperature Float

  @@id([deviceId, time])
  @@index([deviceId, time])
}
  • column (required) — the time partitioning column (a DateTime field).
  • chunkInterval (optional, default "7 days") — the chunk size, an interval.

Insert with the normal Prisma API; query with normal Prisma or the typed timeBucket helper. Add retention / compression policies on the same model.

To change the chunk interval of a live hypertable later, use $timescale.setChunkInterval — re-generating the schema can't change it (create_hypertable is a no-op once the table exists).

Space partitioning

Add partitionColumn + partitions for a hash space dimension (add_dimension(..., by_hash(column, partitions))) on top of the time dimension:

/// @timescale.hypertable(column: "time", chunkInterval: "1 day", partitionColumn: "deviceId", partitions: 4)
model SensorReading {
  time     DateTime
  deviceId Int

  @@id([deviceId, time]) // a partitioning column must be part of the table's PK / unique key
}

partitionColumn takes a Prisma field name (mapped to its @map column); partitions is a positive integer. It's transparent to timeBucket queries and survives prisma migrate reset. The generator rejects a partitionColumn that isn't in every PK / unique constraint (TimescaleDB requires all partitioning columns in unique indexes).

Chunk skipping

For a compressed hypertable, chunk skipping records per-chunk min/max range stats for a secondary (non-time) column, so the planner can exclude whole chunks that can't match a filter on that column — a big win for WHERE deviceId = … / WHERE eventId BETWEEN … on data that correlates with time. Add chunkSkipping (one Prisma field name, or several comma-separated):

/// @timescale.hypertable(column: "time", chunkInterval: "1 day", chunkSkipping: "eventId")
/// @timescale.compression(after: "7 days", segmentBy: "deviceId")
model SensorReading {
  time        DateTime
  deviceId    Int
  eventId     BigInt
  temperature Float

  @@id([deviceId, time])
}

The generator emits a reset-safe block that sets the timescaledb.enable_chunk_skipping GUC (SET LOCAL) and calls enable_chunk_skipping(...) inside one self-contained DO block:

DO $$ BEGIN
  SET LOCAL timescaledb.enable_chunk_skipping = on;
  PERFORM enable_chunk_skipping('"SensorReading"', 'eventId', if_not_exists => TRUE);
END $$;

You can also toggle it at runtime (the path to use with the manual config); the column is a Prisma field name mapped to its @map column:

await prisma.$timescale.enableChunkSkipping("SensorReading", "eventId");
await prisma.$timescale.disableChunkSkipping("SensorReading", "eventId");

Important

Three things to know — chunk skipping is otherwise a silent no-op:

  • Turn the GUC on at query time too. The planner only skips chunks when timescaledb.enable_chunk_skipping is on for the querying session. Set it as a persistent default (run once, e.g. in a manual migration): ALTER DATABASE your_db SET timescaledb.enable_chunk_skipping = on;, or per connection via …?options=-c%20timescaledb.enable_chunk_skipping%3Don. This package doesn't emit the ALTER DATABASE automatically — it's database-wide and may need elevated privileges (e.g. on managed/Tiger Cloud).
  • Range stats only exist on compressed chunks. Pair it with compression; uncompressed chunks are always scanned.
  • Not a partitioning or segmentBy column. segmentBy already gives per-segment min/max, and enabling skipping there returns wrong (empty) results — the generator rejects it, along with the time column and the hash space-partition column.

See also: $timescale chunk management (inspect / compress / drop / resize chunks).

Clone this wiki locally