Parallel index scans for temporal joins

YSQL supports native PostgreSQL parallel queries (PQ) for a common temporal join pattern used in analytics. This example counts rows in a time window on entity_validity and joins them to entity_payload by primary key. The planner can scan the range-sharded tt_to index in parallel and look up that primary key with a batched nested loop.

For more information about when the planner chooses that plan, see Enable a parallel index scan for a temporal join.

Before you begin

This example assumes:

  • YugabyteDB v2025.2.3 or later, because the parallel scan is of the range-sharded index idx_entity_validity_tt_to_asc_vkey (yb_enable_parallel_scan_range_sharded)
  • YSQL cost-based optimizer enabled

Enable the required settings

To enable this behavior at the database level, set the following parameters:

SET yb_enable_cbo = on;
SET yb_enable_parallel_scan_colocated = on;
SET yb_enable_parallel_scan_range_sharded = on;
SET yb_enable_parallel_scan_hash_sharded = on;

For this example, set the session-level degree of parallelism (DOP) as follows:

SET max_parallel_workers_per_gather = 6;

All other parallel-cost settings can remain at their defaults, including:

  • parallel_tuple_cost
  • parallel_setup_cost

With default costs, the planner still assumes parallel execution has meaningful overhead, so it typically chooses a parallel plan only when the workload is large enough to justify it.

Example schema

The following anonymized schema demonstrates a temporal join pattern where PQ can activate.

CREATE SCHEMA pq_anon_parallel_demo;
SET search_path TO pq_anon_parallel_demo;

CREATE TABLE entity_payload (
  version_ref BIGINT NOT NULL,
  entity_type_id INT NOT NULL,
  payload JSONB NOT NULL,
  PRIMARY KEY ((version_ref) HASH)
);

CREATE TABLE entity_validity (
  version_ref BIGINT NOT NULL,
  entity_ref TEXT NOT NULL,
  tt_from TIMESTAMPTZ NOT NULL,
  tt_to TIMESTAMPTZ NOT NULL,
  vt_from TIMESTAMPTZ NOT NULL,
  vt_to TIMESTAMPTZ NOT NULL,
  aux_metric INT NOT NULL,
  PRIMARY KEY ((version_ref) HASH)
);

CREATE INDEX idx_entity_validity_tt_to_asc_vkey
ON entity_validity (tt_to ASC, version_ref ASC);

Example query

The following query counts rows in a temporal validity window and joins to the payload table by version_ref:

SELECT count(*)
FROM entity_validity v
JOIN entity_payload p ON p.version_ref = v.version_ref
WHERE v.tt_to > timestamptz '2025-11-17 06:06:09.391+00'
  AND v.tt_to <= timestamptz '2025-11-17 06:06:09.391+00' + interval '180 day'
  AND v.vt_to > timestamptz '2025-11-17 06:06:09.391+00'
  AND v.tt_from <= timestamptz '2025-11-17 06:06:09.391+00'
  AND v.vt_from <= timestamptz '2025-11-17 06:06:09.391+00'
  AND ((p.payload->>'hasNonFlatPosition')::boolean = true);

Verify that Parallel Query is being used

Run the following:

EXPLAIN (ANALYZE, DIST, COSTS OFF)
SELECT count(*)
FROM entity_validity v
JOIN entity_payload p ON p.version_ref = v.version_ref
WHERE v.tt_to > timestamptz '2025-11-17 06:06:09.391+00'
  AND v.tt_to <= timestamptz '2025-11-17 06:06:09.391+00' + interval '180 day'
  AND v.vt_to > timestamptz '2025-11-17 06:06:09.391+00'
  AND v.tt_from <= timestamptz '2025-11-17 06:06:09.391+00'
  AND v.vt_from <= timestamptz '2025-11-17 06:06:09.391+00'
  AND ((p.payload->>'hasNonFlatPosition')::boolean = true);

Look for a plan shape similar to the following:

Finalize Aggregate
  ->  Gather
        Workers Planned: 6
        Workers Launched: 6
        ->  Partial Aggregate
              ->  YB Batched Nested Loop Join
                    ->  Parallel Index Scan using idx_entity_validity_tt_to_asc_vkey on entity_validity v
                    ->  Index Scan using entity_payload_pkey on entity_payload p

The key indicator is:

Parallel Index Scan using idx_entity_validity_tt_to_asc_vkey

If you see this note, the temporal side of the join is using a parallel index scan. For when the planner chooses that plan, see When the planner chooses this plan.