Most Postgres releases are welcome but quiet. PostgreSQL 18 is different for application developers: it changes how the database reads from disk, gives you a better UUID for primary keys, and changes a default that can surprise you during migrations.

This guide covers the three changes developers notice first, with examples and the upgrade gotchas.

Asynchronous I/O: faster reads without code changes

Until now, PostgreSQL mostly read data synchronously: request a block, wait, request the next one. PostgreSQL 18 adds an asynchronous I/O (AIO) subsystem that lets the database issue several read requests at once instead of waiting for each in turn.

The official release announcement highlights sequential scans, bitmap heap scans and vacuum as the main beneficiaries, and reports up to 3x faster reads from storage in some scenarios. Your gains depend on your storage and workload; cloud disks with high latency tend to benefit most.

The method is controlled by the io_method setting:

sql
SHOW io_method;
  • worker is the default and uses background I/O worker processes; it works on all platforms.
  • io_uring uses the Linux io_uring interface and needs a build with that support.
  • sync keeps the old synchronous behaviour, useful for comparison.

Writes still go through the usual synchronous path to keep durability guarantees. You do not need to change any queries; benchmark your slowest reports before and after upgrading to see the difference.

uuidv7(): better UUID primary keys

Random UUIDs (version 4) are popular as public IDs, but they make poor primary keys in a B-tree index. Every insert lands at a random place in the index, which causes page splits, a bigger index and more cache misses.

UUID version 7 starts with a timestamp, so new values are roughly in order. Inserts go to the end of the index, like a sequence, while IDs stay globally unique and hard to guess. PostgreSQL 18 includes a built-in function:

sql
CREATE TABLE orders (
  id         uuid PRIMARY KEY DEFAULT uuidv7(),
  customer   text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO orders (customer) VALUES ('ada') RETURNING id;

Two things to know:

  • The ID reveals roughly when the row was created. That is fine for orders, but avoid it where creation time is sensitive.
  • Existing uuid columns keep working; you can switch the default for new rows without touching old data.

Virtual generated columns are now the default

Generated columns compute their value from other columns. Before PostgreSQL 18 they had to be STORED: computed on write and saved on disk. Version 18 adds virtual generated columns, computed when you read them and taking no storage.

sql
CREATE TABLE products (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  price_cents integer NOT NULL,
  vat_rate    numeric(4,3) NOT NULL DEFAULT 0.200,
  price_gross numeric GENERATED ALWAYS AS (price_cents * (1 + vat_rate) / 100)
);

Here is the gotcha: when you omit the keyword, PostgreSQL 18 creates a virtual column. The same CREATE TABLE on version 17 failed or required STORED; migration tools and ORMs that generate DDL without the keyword now produce virtual columns.

That matters because virtual generated columns cannot be indexed. If you need to filter or sort on the value with an index, say so explicitly:

sql
ALTER TABLE products
  ADD COLUMN search_name text
  GENERATED ALWAYS AS (lower(name)) STORED;

CREATE INDEX ON products (search_name);

A good rule: use virtual for cheap, display-only values; use stored for values you index or that are expensive to compute. Stored generated columns can also now be replicated with logical replication.

Other changes worth knowing

  • OLD and NEW in RETURNING. UPDATE ... RETURNING can return both the old and new values of a row, which makes audit logs and change detection simpler.
  • Faster major upgrades. pg_upgrade can keep planner statistics, so the database performs well right after an upgrade instead of waiting for ANALYZE to finish.
  • OAuth authentication support for connecting with tokens from an identity provider.
  • Skip scan for multicolumn B-tree indexes, so queries that do not filter on the first index column can still use the index in more cases.

Check the official release notes for the full list and for any changes that affect your extensions.

An upgrade checklist

  1. Read the release notes' compatibility section and test your migrations against version 18 in CI.
  2. Search your schema and migration files for GENERATED ALWAYS AS without STORED, and decide which behaviour you want.
  3. Confirm your extensions and managed provider support version 18.
  4. Benchmark representative read-heavy queries with the default io_method.
  5. Plan the upgrade with pg_upgrade or your provider's tooling, and keep a tested rollback path.

Key takeaways

  • PostgreSQL 18's asynchronous I/O speeds up scans and vacuum without query changes.
  • Use uuidv7() for UUID primary keys that index well; remember it encodes creation time.
  • Generated columns are virtual by default in 18; add STORED when you need an index.
  • RETURNING can now return OLD and NEW values.
  • Test migrations and benchmark reads before upgrading production.

Sources: PostgreSQL 18 released, PostgreSQL 18 release notes, Crunchy Data: Get Excited About Postgres 18.