pgColumnar 1.0-alpha4 released

pgColumnar 1.0-alpha4 release notes

2026-09-17

pgColumnar 1.0-alpha4 release notes

Release date: 2026-09-17 Previous release: 1.0-alpha3 (2026-09-02)

pgColumnar is a columnar table access method for PostgreSQL. This is the fourth alpha. Its theme is layout and skipping. A table can now be laid out on the Hilbert curve, which keeps neighbouring keys closer together than Z-order does. A star-schema join now skips fact-table groups and rejects non-matching rows, and it does so without being asked. The on-disk native format, PGCN v1, is unchanged. Existing tables are read and written as before.

This release requires one upgrade command. See "Upgrading" at the end. The upgrade is the smallest of any release so far: it adds two functions and changes nothing else.

Highlights

  • Hilbert clustering. pgcolumnar.cluster_hilbert and pgcolumnar.recluster_hilbert lay a table out on the Hilbert curve. The curve has no jumps at a bit boundary, so a range filter reads fewer chunk groups. Measured on 200,000 rows over two columns, Hilbert read 1.24x to 2.04x fewer groups than Z-order.
  • Star-schema joins skip and reject by default. A serial inner Hash Join now uses the build-side keys to skip fact-table chunk groups and to reject non-matching rows. pgcolumnar.enable_join_runtime_filter is on.
  • An index-driven read of a wide table does less I/O. Adjacent column reads in one row group are coalesced into a single read.
  • A parallel index build now uses its workers. Every participant claims distinct row groups. Before this release one backend read the whole table while the launched workers sat idle.
  • The planner stops preferring a fetching index scan that does far more work. A correlated range over tens of thousands of rows now takes the columnar scan.

Hilbert clustering

pgcolumnar.cluster_hilbert(table, VARIADIC columns) rewrites a table in Hilbert order. It holds AccessExclusiveLock, as CLUSTER and VACUUM FULL do. pgcolumnar.recluster_hilbert(table, VARIADIC columns) does the same work online under ShareUpdateExclusiveLock, so reads and writes continue.

The curve is sticky. Plain pgcolumnar.recluster maintains a Hilbert table rather than converting it back. Naming the Hilbert verb is how a Z-ordered table is switched to the curve.

Choose the curve by the predicate. Z-order is fine for point lookups. Hilbert wins on range filters over several columns, and the gap narrows as the query box grows. Measure your own corpus when the two look close. See docs/best-practices.md for the guidance and pgcolumnar.sort_status for how much of a table is currently in order.

There are two verbs rather than a parameter on the existing two because PostgreSQL cannot extend cluster(regclass, VARIADIC name[]) in either direction. A defaulted parameter cannot precede a VARIADIC one.

Join acceleration

A serial inner Hash Join over a columnar fact table now builds a filter from the join keys it has already hashed. The filter does two things. A key range skips whole fact-table chunk groups. A Bloom filter rejects rows that cannot match.

Three measured cases decided the default:

fact table

result

clustered on the join key

19 of 20 chunk groups removed, 1 read

scattered

0 groups removed, Bloom rejects over 15,000 of 19,800 non-matches

build side too large

the Bloom disables itself

The third case is why this is on for everyone. A filter that helps nothing turns itself off. Group skip still needs the fact table clustered on the join key. Set pgcolumnar.enable_join_runtime_filter to off to compare.

An ungrouped vectorized aggregate also keeps running over a unique-key inner Hash Join. A unique dimension acts as a filter of the fact table, so the fold does not have to stop. Duplicate-key dimensions and LEFT joins stay on the core plan.

Reads and the planner

  • Coalesced column reads. An index-driven fetch of several columns in one row group issues one read for adjacent chunks instead of one per column.
  • Parallel index build. The table access method's parallel scan claims row groups per participant from the shared counter, the way the custom scan already did. A parallel CREATE INDEX now spreads across its workers.
  • Parallel scan cost. The planner no longer divides a parallel scan's I/O by the worker count. PostgreSQL divides CPU across workers and leaves the disk work whole, and the columnar cost now matches.
  • Clustered index fetch cost. The fetch penalty now charges a per-row term, capped at half a chunk group. Without it a correlated range of 50,000 rows stayed on a fetching index scan while doing far more work than a scan.

Correctness fixes

  • A truncated column chunk is refused rather than read. A chunk whose recorded length exceeded 4 GB was cast to 32 bits on the index-fetch path. A fetch could therefore read the wrong bytes and report them as data. Both cast sites now raise XX001.
  • A coalesced fetch cannot read past its buffer. The validity bitmap copy is now bounded by the chunk length before it runs.
  • Projections survive DDL. A rewrite re-records its projections. ALTER TABLE ... RENAME COLUMN carries the new name into the projection. ALTER TABLE ... DROP COLUMN is refused when a projection depends on the column. An in-place TRUNCATE clears each projection's storage as well as the base.
  • The block codec frees its buffer. Both paths abandoned it.
  • Object storage refuses URL userinfo on s3:// and gs://, as http(s):// has since #706.

Known issues

  • Setting a codec can make a table larger, on high-entropy text (#1074). The writer keeps FSST only when it beats the alternative by fsst_min_gain_percent, and it measures both sides after the block codec has run. Storing the FSST codes uncompressed is never compared. Measured on 200,000 rows of random hex text, zstd wrote 1.777% more than compression = none. lz4 was unaffected. If a table stores long high-entropy text and size matters, measure both settings.
  • The block codec compresses a region it then discards (#1075). On incompressible data this costs about 25% more write CPU. It does not affect what is stored or read.

Upgrading

Install this build, then run the following in every database that has the extension:

ALTER EXTENSION pgcolumnar UPDATE;

This is required. The upgrade creates pgcolumnar.cluster_hilbert and pgcolumnar.recluster_hilbert. It changes nothing else. No table data is converted, no existing function is replaced, no catalog column is added, and no SQL you write changes.

See docs/installation.md for the commands, including how to list the databases that need the update.

Scope and limitations

  • This is an alpha. Interfaces may change before 1.0.
  • Hilbert clustering orders whole row groups on rewrite. A table that is written to after the rewrite drifts out of order until the next one.
  • The join runtime filter applies to a serial inner Hash Join on a direct columnar scan. It does not wrap LEFT, SEMI, ANTI, CROSS, parallel, or projection scans.
  • On PGXN this release is 1.0.0-alpha.4, while CREATE EXTENSION reports 1.0-alpha4. PGXN requires a semantic version, which needs three integer components. The extension's own version has two. The two names refer to the same release.

The complete, itemized list of changes is in CHANGELOG.md.