September 25, 2025·6 min read·engineeringpostgresqlperformancestorage

PostgreSQL 18 overlaps storage reads and expands useful index scans

PostgreSQL 18 adds asynchronous reads for selected operations and B-tree skip scans. Both changes depend on what work the database can anticipate, but they affect different parts of query execution.

PostgreSQL knows more about its next page request than the operating system does. A scan has an execution context and an expected sequence of database reads, while operating-system readahead infers demand from file access. PostgreSQL 18 uses that database knowledge to issue multiple reads concurrently through a new asynchronous I/O subsystem.

The September 25 release announcement identifies sequential scans, bitmap heap scans and vacuum as supported operations. It reports gains of up to three times in particular benchmark scenarios. That number describes the project's reported results, not a general multiplier for every query or an observation from my own systems.

I default to PostgreSQL for relational work and value its explicit execution plans. The useful story in this release is that storage scheduling changes underneath familiar operations. B-tree skip scans address a different limitation by reducing which parts of an index need reading in the first place.

Concurrent reads and parallel query are separate mechanisms

The parallel-query documentation describes plans that use multiple CPUs to answer a query faster. Asynchronous I/O addresses overlap between storage requests. These mechanisms can contribute to the same workload without being interchangeable descriptions of what the server does.

A scan that waits for each read before submitting its next request cannot take advantage of every opportunity to overlap device work. Issuing several requests allows the storage system to work on them concurrently. This can help when time is spent waiting for reads, while leaving other costs in the operation unchanged.

That distinction prevents an overly broad interpretation of the release. The announcement does not say that every query plan becomes parallel or that every storage operation uses the new subsystem. It names a set of supported scan and maintenance paths. An improvement within those paths can matter to a serial query as well as to a workload using parallel execution.

The I/O method is an explicit server choice

The resource-configuration reference defines io_method. Its default is worker, which uses worker processes for asynchronous I/O. io_uring uses the corresponding operating-system interface and requires a build with liburing support. sync executes eligible I/O synchronously. Selecting the method requires a server start.

The same reference defines effective_io_concurrency as the expected number of simultaneous storage operations and distinguishes it from maintenance concurrency. Higher values can help higher-latency storage or devices with substantial I/O capacity. Excessively high values can increase latency for other queries, so this setting represents a resource assumption about the shared storage system.

These are distinct choices. The method describes how PostgreSQL submits eligible work, while concurrency settings bound how much work it attempts to overlap. Seeing asynchronous I/O enabled does not identify a single fixed queue depth or establish that a particular device can absorb every concurrent workload efficiently.

The settings can be inspected without changing a running system. These commands report the current server values relevant to that distinction:

SHOW io_method;
SHOW effective_io_concurrency;
SHOW maintenance_io_concurrency;

The result describes configuration, not a benchmark. A performance explanation still needs to connect the query's actual work to the resource that limits it. This release provides more direct control over storage scheduling within supported operations, which is a narrower and more useful claim than saying that database I/O has become universally faster.

Skip scans avoid reading some index regions

The multicolumn-index documentation explains B-tree skip scans with an index on (x, y) and a condition on y alone. PostgreSQL may generate repeated searches for the distinct leading-column values, combining each generated equality with the supplied condition on the later column.

This can skip index pages that cannot contain relevant tuples. It is attractive when the leading column has few distinct values. With many distinct leading values, repeatedly searching may offer little advantage, and the planner may choose a sequential table scan instead.

The SQL does not need to enumerate those leading values. In an illustrative schema, a composite index can therefore be relevant to a query that omits its first column:

CREATE TABLE measurements (site integer, sensor integer, reading numeric);
CREATE INDEX measurements_site_sensor ON measurements (site, sensor);
EXPLAIN SELECT reading FROM measurements WHERE sensor = 42;

This example shows an eligible shape, not a promised plan for an empty table. The planner's decision depends on its estimates and the available alternatives. Skip scan broadens useful navigation within an existing multicolumn index, while retaining the significance of column distribution and index order. A small set of site values makes repeated searches a different proposition from a site identifier that is almost unique in every row. The index definition can remain identical in those cases while the number of searches needed to cover the leading values changes substantially.

Plans need to separate estimates from observations

The EXPLAIN guide distinguishes planner estimates from execution measurements collected with ANALYZE. The latter actually runs the statement. Buffer information helps describe which pages execution accessed, and execution plans show the operators through which the work passed.

That evidence is especially useful when two improvements can change the same query's duration. Skip scan can change the portion of an index visited. Asynchronous I/O can change the overlap of reads in supported scan paths. Elapsed time alone does not identify which mechanism contributed.

I find the distinction between avoiding a read and overlapping a read useful here. They solve different resource problems. A smaller amount of work and a better schedule for remaining work can both reduce duration, but they imply different explanations for another workload that behaves differently.

Virtual generated columns move computation to reads

The generated-column reference describes the release's new default of virtual generated columns. A stored generated column computes its value on write and occupies storage. A virtual generated column computes on read and does not store a separate column value.

This choice places the expression's cost at a different point in the workload. PostgreSQL permits explicit VIRTUAL or STORED declarations, so schema intent can remain visible despite the default change. Generation expressions must use immutable functions and cannot contain subqueries or refer beyond the current row.

Virtual columns also have restrictions beyond stored columns. Their expressions cannot use user-defined types or functions, including indirect use through operators and casts. The feature therefore supplies a defined form of read-time derivation, with constraints that affect which existing expressions can use it.

UUIDv7 gives an identifier an ordering component

The UUID-function reference adds uuidv7(), which generates time-ordered UUIDs using a millisecond timestamp, sub-millisecond information and random data. PostgreSQL also provides uuid_extract_timestamp() for suitable UUID versions and uuid_extract_version() for determining a UUID's version.

The ordering property is useful when considering identifiers stored in indexes, but it is also information in the identifier. A UUID that encodes time should not be treated as an opaque random value with no temporal meaning. The extraction function makes that distinction observable through the database interface.

For an application choosing identifiers, the release removes the need to supply a separate generator merely to obtain this form. It does not establish a transaction's commit order from an identifier alone. Generation time and the database operation's eventual outcome are different facts.

OAuth support includes a validation integration point

The OAuth authentication reference describes PostgreSQL as a resource server accepting bearer tokens. It does not provide the authorization server that issues those tokens. Server configuration identifies an issuer and scopes, and a validation library determines how the token is checked.

That division matters for adoption. Enabling a protocol mechanism is only part of integrating an existing authorization service. The database still needs token validation and a mapping from the token identity to the requested PostgreSQL role. The documentation permits a mapping configuration and specifies exact matching when no map is provided.

PostgreSQL's extension interface is one reason I continue to favor it. This arrangement leaves provider-specific validation in an extension while the server supplies the authentication integration. The resulting deployment still has explicit responsibilities for issuer trust and identity mapping.

The PostgreSQL 18 release notes document the broader migration and compatibility changes. For the storage changes, the immediate practical distinction is between plans that can avoid unnecessary index reads and supported operations that can overlap necessary reads. The active I/O method and the chosen execution plan provide concrete places to inspect those two decisions.