Databases

PostgreSQL 18: Async I/O, UUIDv7, and What’s New

PostgreSQL 18 new features including async I/O and UUIDv7

PostgreSQL 18 landed on May 11 with the kind of changelog that makes database engineers stop and read it twice. The headline is async I/O — a redesigned I/O subsystem that delivers 2–3x faster sequential scans without touching a single query. But underneath that are four other changes that will shift how you write SQL day-to-day: native UUIDv7 generation, RETURNING with OLD and NEW access, virtual generated columns as the new default, and built-in OAuth 2.0. This is not a maintenance release.

Async I/O: The Configuration That Changes Everything

PostgreSQL 18 introduces io_method, a new parameter in postgresql.conf that controls how read operations hit disk. There are three settings:

  • sync — PostgreSQL 17 behavior. Blocking reads with posix_fadvise readahead. Safe, familiar, slower.
  • worker — The new default. Dedicated background I/O worker processes handle reads; query backends submit requests and move on.
  • io_uring — Linux kernel 5.1+ only. A shared ring buffer between PostgreSQL and the kernel, minimal syscall overhead, best cold-cache throughput.

The benchmarks are hard to ignore. Both worker and io_uring consistently deliver 2–3x faster reads on cold cache across sequential scans and bitmap heap scans. On AWS i4i.4xlarge with NVMe storage, teams are reporting 35–40% throughput improvements. VACUUM also benefits — critical for write-heavy deployments. pganalyze has a thorough breakdown of the I/O mechanics if you want to understand what’s happening under the hood.

The practical path: start with worker. It’s cross-platform and requires no special compilation flags. If you’re on bare-metal Linux with a recent kernel and PostgreSQL compiled with --with-liburing, test io_uring — but confirm your managed cloud service supports it first. RDS and Cloud SQL will likely default to worker initially.

# postgresql.conf
io_method = 'worker'              # Safe default, all platforms
io_method = 'io_uring'            # Linux + liburing required
effective_io_concurrency = 16     # Was 1 in PostgreSQL 17

One important caveat: async I/O in PostgreSQL 18 currently covers reads — sequential scans, bitmap scans, VACUUM. Write paths remain synchronous. That’s still a massive win for read-heavy analytics and reporting workloads, which is where most performance complaints live anyway.

UUIDv7: The End of the uuid-ossp Era

If you’ve ever watched a UUIDv4-keyed table’s index bloat under heavy insert load, you already know the problem. UUIDv4 is fully random — every new row lands in a different part of the B-tree, triggering constant page splits. At a million rows, that’s 5,000–10,000+ splits. With sequential or timestamp-ordered keys, it’s 10–20. That gap compounds fast.

PostgreSQL 18 adds a native uuidv7() function (RFC 9562 compliant) that embeds a 48-bit Unix epoch timestamp in the most significant bits. New UUIDs sort chronologically, inserts land sequentially, and page splits drop to integer-level rates. No extension required.

CREATE TABLE events (
  id          uuid DEFAULT uuidv7() PRIMARY KEY,
  payload     jsonb,
  created_at  timestamptz DEFAULT now()
);

INSERT INTO events (payload) VALUES ('{"type": "click"}');

If exposing a sortable timestamp in your primary key is a privacy concern — and it often is — pass an offset interval to mask the exact creation time:

id uuid DEFAULT uuidv7(INTERVAL '1000 years') PRIMARY KEY

The uuid-ossp extension isn’t going anywhere, but new projects have no reason to reach for it. Xata’s deep-dive on PostgreSQL 18 features covers the UUIDv7 implementation details, including the sub-millisecond precision bits, for the curious.

RETURNING Gets a Memory

This is the feature that will quietly save the most developer time. PostgreSQL’s RETURNING clause has always let you pull values out of a DML operation — but only the post-operation state. Getting the “before” values for an UPDATE, or the row you just DELETEd, required a CTE with FOR UPDATE or a trigger. Both options are annoying. Now you use OLD and NEW:

-- Price update audit in one round trip
UPDATE products
  SET price = price * 1.10
  WHERE category = 'electronics'
RETURNING
  name,
  old.price  AS was,
  new.price  AS now_is,
  round((new.price - old.price) / old.price * 100, 2) AS pct_change;

In an INSERT, OLD is NULL. In a DELETE, NEW is NULL. In an INSERT … ON CONFLICT DO UPDATE, OLD holds the conflicting row’s values before the update — exactly what you need for upsert audit trails. Crunchy Data’s walkthrough covers the full set of edge cases including custom aliases and MERGE behavior.

Virtual Generated Columns Are Now the Default

Since PostgreSQL 12, generated columns have been STORED only — computed on write, physically stored on disk. PostgreSQL 18 changes the default to VIRTUAL: computed on read, zero disk footprint, no write overhead. For most derived display fields, that’s the right tradeoff.

The limitations matter though: virtual columns can’t be indexed, can’t be replicated via logical replication, and are limited to built-in functions and types. If you filter frequently by a generated expression, make it STORED or add a regular expression index.

-- Virtual (new default) — no storage, computed on SELECT
CREATE TABLE orders (
  subtotal  numeric,
  tax_rate  numeric,
  total     numeric GENERATED ALWAYS AS (subtotal * (1 + tax_rate))
);

-- Stored — when you need indexing or logical replication
CREATE TABLE orders (
  subtotal  numeric,
  tax_rate  numeric,
  total     numeric GENERATED ALWAYS AS (subtotal * (1 + tax_rate)) STORED
);

OAuth 2.0: Enterprise Auth Without the Middleware

Teams connecting PostgreSQL to enterprise SSO have historically needed a proxy layer — PgBouncer configured with an OAuth shim, or a custom authentication script. PostgreSQL 18 eliminates that. Native OAuth 2.0 support lives directly in pg_hba.conf, using the SASL OAUTHBEARER mechanism.

The flow: a client connects, the server issues a SASL challenge with a well-known URI and required scopes, the client completes an OAuth device authorization flow, then reconnects with a bearer token. A pluggable validator module checks the token against your IdP — Google, Microsoft Entra, Keycloak, Auth0, or Okta — and maps the authenticated identity to a database role. For organizations managing identity centrally, onboarding a new developer no longer requires creating a database user.

Before You Upgrade

The most underrated PostgreSQL 18 improvement might be in pg_upgrade itself: it now preserves optimizer statistics by default. Previously, upgrading a major version meant running ANALYZE across every table before the planner would produce sane query plans. Now n_distinct values, null fractions, most common values, and histograms all carry over. The planner stays sharp immediately after the upgrade. Bytebase’s developer-focused PostgreSQL 18 guide covers the full upgrade checklist including known extension incompatibilities.

A few things to handle manually: configuration files (postgresql.conf, pg_hba.conf) are not migrated automatically — copy them before upgrading and reconcile your settings. Drop incompatible extensions (age, hll, pg_diskann) before pg_upgrade runs. Data checksums are now on by default for new clusters, though existing clusters upgrading from PG17 are unaffected.

PostgreSQL 18 is available now. The async I/O gains alone justify the upgrade planning for any read-heavy workload — and the developer-ergonomics improvements are the kind you don’t notice until you try to go back.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:Databases