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:
SHOW io_method;workeris the default and uses background I/O worker processes; it works on all platforms.io_uringuses the Linux io_uring interface and needs a build with that support.synckeeps 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:
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
uuidcolumns 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.
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:
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 ... RETURNINGcan return both the old and new values of a row, which makes audit logs and change detection simpler. - Faster major upgrades.
pg_upgradecan keep planner statistics, so the database performs well right after an upgrade instead of waiting forANALYZEto 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
- Read the release notes' compatibility section and test your migrations against version 18 in CI.
- Search your schema and migration files for
GENERATED ALWAYS ASwithoutSTORED, and decide which behaviour you want. - Confirm your extensions and managed provider support version 18.
- Benchmark representative read-heavy queries with the default
io_method. - Plan the upgrade with
pg_upgradeor 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
STOREDwhen you need an index. RETURNINGcan 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.

TanStack Start 1.0: Build a Full-Stack React App with Server Functions5 min read
TanStack Query v5 Optimistic Updates: Two Patterns and When to Use Each4 min read
Vite 8 and Rolldown: What Changes and How to Upgrade Safely4 min read