PostgreSQL 18: The 6 Features Worth Upgrading For

# postgres# database# sql# backend
PostgreSQL 18: The 6 Features Worth Upgrading ForAmaresh Pelleti

PostgreSQL 18 adds async I/O, virtual generated columns, and uuidv7(). Here's what's actually worth upgrading for, what might break, and why you shouldn't deploy 18.0 as-is.

Originally published on DevToolHub.

PostgreSQL 18 is out, and it's a bigger release than the version bump suggests. The headline feature is a new asynchronous I/O subsystem that can cut read latency by up to 3x on the right workload. But the release also ships virtual generated columns as the new default, a uuidv7() function for sortable primary keys, and skip scan support for indexes that used to sit unused half the time.

Here's what's actually worth upgrading for, what changed under the hood, and what to check before you run pg_upgrade on production. The full technical detail lives in the official PostgreSQL 18 release notes — this covers the parts that change how you design and operate a database day to day.

PostgreSQL 18's Async I/O: Up to 3x Faster Reads

The AIO (asynchronous I/O) subsystem is the biggest architectural change in this release. Instead of issuing one disk read and waiting for it before starting the next, PostgreSQL 18 can queue multiple read requests at once. Sequential scans, bitmap heap scans, and vacuum operations all benefit directly.

Turn it on with a single setting:

SHOW io_method;
Enter fullscreen mode Exit fullscreen mode

Two more settings control the behavior: io_combine_limit and io_max_combine_limit cap how many reads get batched together. If you're on a system without native fadvise() support, effective_io_concurrency and maintenance_io_concurrency now accept values above zero too, which wasn't possible before.

Want to see it working? Query the new pg_aios view — it shows the file handles currently in flight for async reads. If it's empty on a read-heavy workload, io_method probably isn't set the way you think it is.

[IMAGE: articles/images/2026-07-24-postgresql-18-new-features-diagram.png | alt: "sequential reads compared to the new asynchronous I/O subsystem"]

Storage-bound workloads see the biggest jump. If your bottleneck is CPU, not disk, don't expect PostgreSQL 18 to feel dramatically different — profile your actual queries with pg_stat_statements before assuming AIO will fix a slow endpoint.

Virtual Generated Columns Are Now the Default

Generated columns compute a value from other columns automatically. Before PostgreSQL 18, that computation always happened at write time, and the result got stored on disk. Now, virtual is the default:

CREATE TABLE orders (
  id INT,
  price NUMERIC,
  quantity INT,
  total NUMERIC GENERATED ALWAYS AS (price * quantity) VIRTUAL
);
Enter fullscreen mode Exit fullscreen mode

Virtual columns compute the value at read time instead of write time — nothing gets stored. That saves disk space and write overhead, but it costs a bit of CPU on every read. If you need the old write-time behavior for a hot read path, say so explicitly:

CREATE TABLE orders (
  id INT,
  price NUMERIC,
  quantity INT,
  total NUMERIC GENERATED ALWAYS AS (price * quantity) STORED
);
Enter fullscreen mode Exit fullscreen mode

Existing STORED columns from earlier versions keep working exactly as before — this only changes the default for new tables.

uuidv7(): Sortable UUIDs for Primary Keys

Random UUIDs (uuidv4()) have always been bad for index locality — every insert lands in a random spot in the B-tree, which fragments the index over time. PostgreSQL 18 ships uuidv7(), which encodes a timestamp into the leading bits:

SELECT uuidv7();

ALTER TABLE events ADD COLUMN id uuid DEFAULT uuidv7() PRIMARY KEY;
Enter fullscreen mode Exit fullscreen mode

Because the timestamp sits at the front of the value, new rows insert close together in index order, the same way an auto-incrementing integer would. You get the collision-safety of a UUID without the write-amplification problem that's plagued uuidv4() primary keys for years. If you're designing a new schema, this is the default worth reaching for.

Skip Scan: Multicolumn Indexes That Actually Get Used

Multicolumn B-tree indexes have a well-known limitation: PostgreSQL could only use them efficiently if your query filtered on the leading column. An index on (tenant_id, status, created_at) was mostly useless for a query that filtered on status alone.

Skip scan changes that. PostgreSQL 18 can now use a multicolumn index even when the leading column has no restriction, by internally probing each distinct value of that column:

CREATE INDEX idx_orders ON orders(tenant_id, status, created_at);

-- Now benefits from the index above, even without a tenant_id filter
SELECT * FROM orders WHERE status = 'pending' AND created_at > now() - interval '1 day';
Enter fullscreen mode Exit fullscreen mode

It's not free — skip scan works best when the leading column has a small number of distinct values. If tenant_id has millions of distinct values, the planner will likely still choose a sequential scan. Run EXPLAIN ANALYZE before and after upgrading to confirm the planner actually picks it up for your specific queries.

OAuth Authentication Support

PostgreSQL 18 adds native OAuth token authentication, configured in pg_hba.conf like any other auth method:

host    database    user    address    oauth
Enter fullscreen mode Exit fullscreen mode

You'll need to build with --with-libcurl and load a token validation library via oauth_validator_libraries. This matters if your org is trying to get off long-lived database passwords and onto short-lived tokens tied to an identity provider — previously that meant a third-party proxy in front of Postgres. Now it's built in.

Upgrading to PostgreSQL 18 Without Losing Planner Statistics

The upgrade pain point that's kept people on old major versions isn't the schema — it's the hours-long window where a freshly upgraded cluster runs on empty planner statistics until ANALYZE catches up. PostgreSQL 18's pg_upgrade now preserves those statistics by default:

pg_upgrade -d /path/to/old_data -D /path/to/new_data
Enter fullscreen mode Exit fullscreen mode

Extended statistics (the kind built with CREATE STATISTICS) aren't carried over — you'll still need to rebuild those manually. But regular column statistics survive the jump, so query plans stay sane immediately after cutover instead of degrading until autovacuum's analyze catches up. If you'd rather start clean, --no-statistics disables the behavior.

Breaking Changes to Check Before You Upgrade

A few defaults changed in ways that can bite you mid-migration:

  • Data checksums are now on by default in initdb. pg_upgrade requires matching checksum settings between the old and new cluster, so mismatched checksum config is the first thing to check if the upgrade fails immediately.
  • VACUUM and ANALYZE now process partitioned table children by default. If you were relying on the old skip-children behavior, add ONLY to keep it: VACUUM ONLY parent_table.
  • MD5 password authentication is deprecated — not removed yet, but CREATE ROLE and ALTER ROLE now emit a warning when you set one.
  • Full-text search now follows the cluster's default collation provider instead of always using libc. If your cluster runs a non-libc provider, reindex your full-text and pg_trgm indexes after upgrading.

None of these block an upgrade on their own, but any one of them can produce a confusing error if you're not expecting it — pgpedia's PostgreSQL 18 page tracks the full list if you want to check something not covered here first. If you hit something not covered here, our PostgreSQL troubleshooting guide walks through the most common post-upgrade failures.

Patch to the Latest Minor Version Before You Deploy

Don't install 18.0 straight off the release notes. PostgreSQL has shipped 18.1 through 18.6 since the initial release, fixing 46 CVEs along the way — several of them CVSS 8.8 remote-code-execution-class bugs. That includes CVE-2026-14676, a heap buffer overflow in pg_stat_statements itself, the exact extension this article points you to for profiling queries before you chase an AIO-related performance win. Pull the current minor (18.6 as of this writing) before you touch production, and set a reminder to keep pulling new minors — none of this is a one-time patch.

Should You Upgrade to PostgreSQL 18 Now

If you're running a workload where disk I/O is the bottleneck, the async I/O subsystem alone justifies testing PostgreSQL 18 in staging. Combined with statistics-preserving upgrades, the operational risk of the jump itself is lower than past major version upgrades. PostgreSQL 19 is in beta (Beta 3 as of this writing) and not recommended for production, so there's no reason to wait on 18 if you're evaluating a new deployment now.

If your database runs on Kubernetes, check how CloudNativePG handles major version upgrades before scheduling this — the sequencing matters more in an operator-managed cluster than a standalone install. And if you're running partitioned tables at scale, revisit your partitioning strategy against the new VACUUM defaults before you cut over, since the children-by-default change affects partition maintenance directly.

Frequently Asked Questions

Q: Do I need to change my application code to benefit from async I/O in PostgreSQL 18?
A: No. AIO works at the storage engine level. You get the benefit automatically once io_method is configured, with no query or schema changes required.

Q: Will my existing STORED generated columns break after upgrading to PostgreSQL 18?
A: No. Existing STORED columns keep working exactly as before. The new VIRTUAL default only applies to columns you create after upgrading, unless you explicitly write STORED.

Q: Is uuidv7() a drop-in replacement for uuidv4()?
A: For new tables, yes — swap the default and you get sortable inserts. For existing tables with uuidv4() primary keys already in production, migrating is a separate project since existing values won't retroactively sort.

Q: How long does upgrading to PostgreSQL 18 actually take with statistics preservation?
A: It depends on database size, but the statistics-preservation feature removes the multi-hour "cold cache, bad plans" window that used to follow a pg_upgrade. The physical upgrade time itself is unchanged — you're saving the recovery period after it, not the upgrade itself.

Q: Is it safe to run PostgreSQL 18.0, or should I be on a specific minor version?
A: Don't run 18.0 in production. PostgreSQL has shipped 18.1 through 18.6 since the initial release, fixing 46 CVEs, several of them CVSS 8.8 remote-code-execution bugs — including one in pg_stat_statements itself. Always deploy the current minor release and keep pulling new ones as they ship.

Quick Summary:

  • PostgreSQL 18's async I/O subsystem can cut read latency up to 3x on storage-bound workloads, with no query changes required
  • Virtual generated columns are now the default — computed at read time instead of write time, use STORED explicitly if you need the old behavior
  • uuidv7() gives you sortable, collision-safe UUIDs without the index fragmentation uuidv4() causes
  • Skip scan makes multicolumn indexes usable even when queries don't filter on the leading column
  • pg_upgrade now preserves planner statistics by default, removing the post-upgrade performance dip
  • Don't deploy 18.0 as-is — 18.1 through 18.6 fixed 46 CVEs, including a critical RCE in pg_stat_statements; always pull the current minor

Test PostgreSQL 18 against your actual query patterns in staging before committing to a production upgrade — EXPLAIN ANALYZE your slowest queries first and last, since the AIO and skip scan gains vary a lot by workload shape.