Amaresh PelletiPostgreSQL 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.
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;
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.
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
);
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
);
Existing STORED columns from earlier versions keep working exactly as before — this only changes the default for new tables.
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;
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.
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';
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.
PostgreSQL 18 adds native OAuth token authentication, configured in pg_hba.conf like any other auth method:
host database user address oauth
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.
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
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.
A few defaults changed in ways that can bite you mid-migration:
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.ONLY to keep it: VACUUM ONLY parent_table.CREATE ROLE and ALTER ROLE now emit a warning when you set one.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.
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.
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.
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:
uuidv7() gives you sortable, collision-safe UUIDs without the index fragmentation uuidv4() causespg_upgrade now preserves planner statistics by default, removing the post-upgrade performance dippg_stat_statements; always pull the current minorTest 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.