TL;DR: Top 5 changes
- Asynchronous I/O subsystem: Broad speedups for seq scans, bitmap scans, and VACUUM — no query changes needed.
- Btree skip scans: Reuse multi-column indexes even when filtering by a non-leading column.
- Right semi join plans: New plan type to speed up “exists-at-least-one” patterns.
- Parallel GIN index builds: Faster builds for large text/jsonb arrays.
- Virtual generated columns by default: Compute-on-read, simpler writes, optional
STOREDfor heavy cases.
Quick wins
- Turn on async I/O thoughtfully: Start with defaults; if your storage is fast but fragmented, raise
io_combine_limitand validate withEXPLAIN (ANALYZE, BUFFERS). - Lean on skip scans: Before adding a new index on a trailing column, check if your existing multi-column btree already helps.
- Parallelize GIN builds: Rebuild huge text/jsonb indexes faster during maintenance windows.
- Prefer virtual generated columns: Keep writes light; switch to
STOREDonly if read cost dominates. - Observe more, guess less: Use
pg_stat_io, richer EXPLAIN details, and updated logs to guide tuning.
Overview
If PostgreSQL 17 felt fast, 18 feels like it found another gear. You get smarter plans, less waiting on disk, and more insight into what your database is actually doing — without rewriting queries.
Highlights include a brand-new asynchronous I/O subsystem, btree skip scans, right semi join plans, parallel GIN builds, OAuth auth, generated columns in logical replication, virtual generated columns by default, richer EXPLAIN/ANALYZE, and a wave of psql and tooling upgrades. The full list is long (and fun to read) — see the official notes. [1]PostgreSQL 18 Release Notes
Migration to Version 18
A dump/restore via pg_dumpall, pg_upgrade, or logical replication is required when migrating from previous major versions. Review compatibility changes carefully before upgrading.
- Default
initdbnow enables data checksums (disable with--no-data-checksums; clusters must match forpg_upgrade). - Time zone abbreviation resolution prioritizes the current session before
timezone_abbreviations. - MD5 password authentication is deprecated;
CREATE ROLE/ALTER ROLEwarn (configurable viamd5_password_warnings). VACUUMandANALYZEnow process inheritance children by default; useONLYfor former behavior.COPY FROMno longer treats\.as EOF in CSV;psqlstill does for STDIN, and\.must be alone on its line.- Unlogged partitioned tables are disallowed.
- AFTER triggers execute as the role active when events were queued.
- Removal of non-functional rule privileges in GRANT/REVOKE.
- Memory stats catalog adjustments (e.g.,
pg_backend_memory_contexts). - Full text search now respects the cluster’s default collation provider; for non-libc defaults, reindex FTS/
pg_trgmafterpg_upgrade.
Quick migration checklist
- Back up:
pg_dumpall > dump.sql(or per-dbpg_dump), and/or ensure your backup tool has a fresh snapshot. - Initialize new cluster (checksums are enabled by default):
initdb -D /path/to/pg18- Stop old cluster and run
pg_upgrade(consider-kfor links and-jfor parallelism):
pg_upgrade \ -d /path/to/old/datadir \ -D /path/to/pg18 \ -b /path/to/old/bin \ -B /path/to/pg18/bin \ -j 4 -k- Post-upgrade analyze (and optionally reindex FTS/trgm if your cluster uses non-libc collations):
./analyze_new_cluster.sh-- For non-libc default providers after pg_upgrade:REINDEX (CONCURRENTLY) INDEX your_fts_or_trgm_idx; -- repeat as appropriate- Review auth (MD5 is deprecated), time zone abbreviation behavior, and inheritance
ONLYchanges for VACUUM/ANALYZE. - Restart apps and monitor with the new
pg_stat_io, logs, and EXPLAIN details.
Docs: [1]pg_upgrade, [1]initdb, [1]psql
Highlights by Area
Optimizer
- Eliminates certain unnecessary self-joins (toggle with
enable_self_join_elimination). - Smarter transformations:
IN (VALUES ...)→= ANY(...); OR-to-array conversions for faster index processing. - Faster
INTERSECT,EXCEPT, window aggregates, and view column aliasing. - Reorders
SELECT DISTINCTkeys to avoid sorts (toggleenable_distinct_reordering). - Drops GROUP BY columns functionally dependent on unique keys.
- Pushes some HAVING on GROUPING SETS to WHERE.
- Better row estimates for
generate_series()with numeric/timestamp. - Introduces right semi join plans and incremental sort for merge joins.
- Improved planning for many partitions; expanded partitionwise joins; refined partition cost estimates.
- Better plan caching for SQL-language functions; clearer handling of disabled optimizer features.
Indexes
- Btree skip scans let multi-column btree indexes serve queries filtering by later columns.
- Non-btree unique indexes can be partition keys and used in materialized views (must support equality).
- Parallel GIN index creation.
- Sorted value builds speed up range-type GiST/btree index creation.
Btree skip scans: when they help (examples)
Skip scans allow PostgreSQL to use a multi-column btree index even if the query filters only on a non-leading column. This can reduce the need for additional indexes and speed up existing workloads, depending on data distribution.
Example setup:
CREATE TABLE orders ( id bigserial PRIMARY KEY, customer_id bigint NOT NULL, order_date date NOT NULL, total numeric(12,2) NOT NULL);
-- Common index built for typical lookups per customer and timeCREATE INDEX orders_customer_date_idx ON orders (customer_id, order_date);Queries that can now benefit without adding a separate (order_date) index:
-- Equality on trailing column can leverage skip scans on (customer_id, order_date)EXPLAIN (COSTS OFF)SELECT count(*)FROM ordersWHERE order_date = DATE '2025-01-01';
-- Range on trailing column may also benefitEXPLAIN (COSTS OFF)SELECT *FROM ordersWHERE order_date BETWEEN DATE '2025-01-01' AND DATE '2025-01-31'ORDER BY order_dateLIMIT 50;Practical guidance:
- Best when the number of distinct values of the leading column(s) is moderate; extremely high cardinality can erode the benefit.
- Skip scans reduce the need for redundant indexes, but not always. If most queries filter by
order_datealone, a leading(order_date, ...)index can still outperform. - Validate with
EXPLAIN (ANALYZE, BUFFERS)to compare against a sequential scan on your data.
Why it matters
Fewer indexes, faster plans. Skip scans let you lean on existing multi-column btrees for more query shapes, which means less write overhead (fewer indexes to maintain) and simpler schema design.
Pitfalls
Skip scans aren’t a replacement for proper index design. Here’s when they break down and what to do instead:
High cardinality gotcha:
- Problem: If
customer_idhas millions of distinct values, skip scans become inefficient. PostgreSQL must examine many index entries to find matchingorder_datevalues. - Solution: Use
EXPLAIN (ANALYZE, BUFFERS)to compare. If skip scan shows high “rows examined” vs “rows returned”, consider a dedicated(order_date, ...)index.
Heavy workload gotcha:
- Problem: If 80% of your queries filter by
order_datealone, skip scans add unnecessary overhead compared to a purpose-built index. - Solution: Monitor query patterns. When trailing-column queries dominate, create
(order_date, customer_id)or(order_date)indexes.
Performance decision framework:
-- Test your actual query patternsEXPLAIN (ANALYZE, BUFFERS)SELECT count(*) FROM orders WHERE order_date = '2025-01-01';
-- Look for these warning signs in skip scan plans:-- • "Rows Removed by Filter" >> "returned rows"-- • High buffer reads relative to result size-- • Execution time consistently slower than seq scanWhen to stick with skip scans: Occasional queries (< 20% of workload), moderate leading column cardinality (< 100K distinct values), acceptable performance for your SLA.
When to add dedicated indexes: Frequent queries, high-cardinality leading columns, performance-critical workloads, or when EXPLAIN shows poor efficiency ratios.
Real-world example
An analytics app stores orders with (customer_id, order_date) index. Product wants a monthly report by order_date only. Instead of adding a new (order_date) index, skip scans let the existing index serve those queries acceptably well. You keep writes cheap (no extra index maintenance) and production stays simpler. If reports become daily and heavy, re-evaluate a dedicated (order_date, ...) index.
Details: [1]PostgreSQL 18 Release Notes — Indexes
General Performance
- New asynchronous I/O subsystem: better sequential scans, bitmap heap scans, vacuum, etc. Tunables:
io_method,io_combine_limit,io_max_combine_limit. New view:pg_aios. - Faster, lower-memory hash joins and GROUP BY; improvements also benefit
EXCEPTand subplan lookups. - Normal vacuum can eagerly freeze some all-visible pages (tunable via
vacuum_max_eager_freeze_failure_rate). vacuum_truncateserver variable controls truncation behavior.- Defaults for
effective_io_concurrencyandmaintenance_io_concurrencyraised to 16.
Asynchronous I/O subsystem: how to benefit (examples)
PostgreSQL 18 backends can queue multiple reads, improving throughput for sequential scans, bitmap heap scans, and maintenance (e.g., VACUUM). It’s transparent to applications and configurable via server variables.
Typical tuning (choose OS‑appropriate values; see docs):
-- postgresql.conf or via ALTER SYSTEM (requires reload/restart depending on setting)ALTER SYSTEM SET io_method = 'posix'; -- pick the method suitable for your platformALTER SYSTEM SET io_combine_limit = 32; -- number of adjacent reads to combineALTER SYSTEM SET io_max_combine_limit = 128; -- hard upper bound for combiningSELECT pg_reload_conf();Example workload that benefits (large range scan on time-series):
CREATE TABLE events ( id bigserial PRIMARY KEY, ts timestamptz NOT NULL, user_id bigint NOT NULL, payload jsonb);
CREATE INDEX events_ts_idx ON events (ts);
-- Range scan over recent dataEXPLAIN (ANALYZE, BUFFERS)SELECT count(*)FROM eventsWHERE ts >= now() - interval '7 days';Observability:
-- See asynchronous I/O file handles/queuesSELECT * FROM pg_aios LIMIT 20;
-- High-level I/O bytes and timingsSELECT backend_type, context, read_bytes, write_bytes, extend_bytesFROM pg_stat_ioORDER BY read_bytes DESCLIMIT 10;Tips:
- Start with defaults; adjust combine limits if you observe fragmented I/O patterns.
- Use
EXPLAIN (ANALYZE, BUFFERS)to validate improvements (look at read times and buffer hits/misses). - Maintenance tasks like
VACUUM (VERBOSE)also benefit by issuing concurrent reads more efficiently.
Docs: [1]PostgreSQL 18 Release Notes — Asynchronous I/O, [1]Monitoring Statistics, [1]Runtime Configuration — Resources
Why it matters
It’s a free performance upgrade for read-heavy workloads and maintenance: more throughput with the same hardware and no app changes.
Pitfalls
- Overly aggressive combine limits can backfire on highly random I/O; measure and dial in.
- Always test on staging with representative data and load before rolling changes to production.
Real-world example
A multi-tenant SaaS runs periodic range scans and nightly VACUUM on large tables. Enabling async I/O with a modest io_combine_limit reduces wall time for the scans and VACUUM cycles without code changes. Observability via pg_stat_io confirms more bytes read per second and fewer waits; EXPLAIN shows lower I/O timings while plans remain stable.
Monitoring
- Finer-grained
log_connectionsand stage duration reporting;%Llog prefix for client IP. log_lock_failuresfor NOWAIT lock failures.pg_stat_all_tablesadds total times for vacuum/analyze and auto variants.- Delay time reporting across logs,
pg_stat_progress_vacuum/analyze, and VERBOSE output. - ANALYZE VERBOSE: WAL/CPU/avg read stats; autovacuum/VACUUM report full WAL buffer count.
- Per-backend I/O and WAL statistics with reset functions.
pg_stat_ioreports bytes read/written/extend; adds WAL activity rows;track_wal_io_timingrefocused here.- Checkpointer stats additions and SLRU writes coverage.
- Query id jumbling improvements.
- Memory context
typeandpathcolumns.
Privileges
- New
pg_get_acl()andhas_largeobject_privilege(); default privileges for large objects viaALTER DEFAULT PRIVILEGES. - Predefined role
pg_signal_autovacuum_workerto signal autovacuum workers.
Server Configuration
- OAuth authentication method support (pg_hba, libpq options,
oauth_validator_libraries). - TLS updates:
ssl_tls13_ciphers;ssl_groupsdefault includes X25519;ssl_ecdh_curverenamed tossl_groups(compat preserved). - 256-bit cancel request keys (requires wire protocol 3.2).
autovacuum_worker_slotscaps background workers enabling runtime increases ofautovacuum_max_workers.- Fixed dead tuple threshold for autovacuum via
autovacuum_vacuum_max_threshold. max_files_per_processnow limits only backend-opened files.num_os_semaphoresexported;extension_control_pathconfigurable.
Streaming Replication & Recovery
- Auto-invalidate inactive replication slots with
idle_replication_slot_timeout. max_active_replication_originsdecouples origin limit from slot count.
Logical Replication
- Generated columns can be replicated;
publish_generated_columnscontrols behavior when no column list is specified. - Default
CREATE SUBSCRIPTION ... STREAMINGis nowparallel. ALTER SUBSCRIPTIONcan change slot 2PC behavior; conflict logging andpg_stat_subscription_statscolumns added.
Utility & SQL
- Virtual generated columns (on read) now supported and default;
STOREDremains available. RETURNINGcan referenceold/newacross DML including MERGE.CREATE FOREIGN TABLE ... LIKElocal tables; LIKE and text-position functions support nondeterministic collations.- Built-in collation provider
PG_UNICODE_FAST(codepoint order, natural-language case mapping). VACUUM/ANALYZE ONLYfor partitioned tables; functions to modify/clear relation/attribute stats.file_copy_methodcontrols DB/TB file copy vs clone behavior.
Constraints
- Non-overlapping PRIMARY KEY/UNIQUE via
WITHOUT OVERLAPSon last column. - CHECK and FK constraints can be
NOT ENFORCED; newpg_constraint.conenforced. - Deterministic or identical nondeterministic collations required for PK/FK relationships (affects dump/upgrade correctness).
NOT NULLstored inpg_constraint(named constraints, adds NOT NULL to foreign tables and inheritance control).ALTER TABLEsupports[NO] INHERITfor NOT NULL; NOT VALID FKs on partitioned tables; constraint droppingONLYon partitioned tables.
COPY
REJECT_LIMITwithON_ERROR='ignore';LOG_VERBOSITY=silentfor ignored rows.COPY TOfrom populated materialized views; disallowCOPY FREEZEon foreign tables.
EXPLAIN
- EXPLAIN ANALYZE includes BUFFERS automatically; WAL buffer count in EXPLAIN (WAL).
- Fractional row counts; index lookups per index scan; memory/disk usage for Material/WindowAgg/CTEs; detailed window args.
- Parallel Bitmap Heap Scan cache stats; disabled nodes flagged in ANALYZE output.
Data Types
- Unicode 16; improved full case mapping and
casefold(); conditional/title case. jsonbnulls cast to scalar NULL;json{b}_strip_nullssupports removing null array elements.- Array helpers:
array_sort(),array_reverse();reverse(bytea); casts between integers andbytea. - Estonian stemming; improved XML error codes.
Functions
casefold()for sophisticated case-insensitive matching.- MIN/MAX on arrays and composite types.
EXTRACT(WEEK); improved negativeEXTRACT(QUARTER).- Roman numerals in
to_number()via RN. - UUID v7
uuidv7()(plus explicit aliasuuidv4());crc32()/crc32c();gamma()/lgamma(). - Named args for PL/pgSQL cursor
=>and for regexp-related functions.
psql
- Parse/bind/close named prepared statements:
\parse,\bind_named,\close_prepared. - Pipeline query commands and prompt/vars for pipeline status.
- Prompt includes connection service name; expanded mode for list commands via
xsuffix. - Richer
\conninfo; function leakproof indicator flags; partitioned access method details;default_versionin\dx. WATCH_INTERVALvariable.
Server applications & tools
initdbdefaults to checksums (disable with--no-data-checksums);--no-sync-data-filesadded.vacuumdb --missing-stats-only(superuser; with--analyze-only/--analyze-in-stages).pg_combinebackup -k/--link;pg_verifybackupverifies tar backups.pg_rewinduses dbname in--write-recovery-confwhen provided;pg_resetwal --char-signedness.
pg_upgrade
- Preserves optimizer statistics by default (except extended stats);
--no-statisticsdisables. - Parallelize database checks via
--jobs; directory--swapmode;--set-char-signednessoption for CPU char signedness.
Extensions & ecosystem
- New extensions:
pg_logicalinspect,pg_overexplain;postgres_fdw_get_connections()adds columns includingused_in_xact,closed,user_name,remote_backend_pid. - SCRAM passthrough for
postgres_fdwanddblink(avoid storing creds; new libpq paramsscram_client_key,scram_server_key). file_fdwaddson_error,log_verbosity, andreject_limitwhenON_ERROR='ignore'.passwordcheckgainsmin_password_length.pgbenchreports failed/retried/skipped txn counts per script.isnadds server varweak.btree_gistandamcheckimprovements;pg_buffercacheeviction helpers.- Extensions can add EXPLAIN options and use cumulative stats API.
pg_stat_statements & pgcrypto
- Track
CREATE TABLE ASandDECLAREqueries; parameterizeSETto reduce bloat; parallel activity columns; WAL buffers full. pgcrypto: addssha256crypt,sha512crypt, CFB mode;fips_mode();builtin_crypto_enabledto disable non-FIPS crypto.
Practical recipes you can use today
Virtual generated columns (lighter writes)
Use virtual generated columns to compute values on read rather than on write. Great for keeping insert/update paths lean.
-- New tablesCREATE TABLE users ( id bigserial PRIMARY KEY, email text NOT NULL, email_lower text GENERATED ALWAYS AS (lower(email)) VIRTUAL);
-- Existing tablesALTER TABLE users ADD COLUMN email_lower text GENERATED ALWAYS AS (lower(email)) VIRTUAL;
-- Switch to STORED if reads dominate and you want fixed read costALTER TABLE users ALTER COLUMN email_lower SET STORAGE PLAIN; -- and redefine as STORED if neededWhy it matters: fewer write-time CPU cycles; better for bulk loads. Pitfall: heavy read hotspots may prefer STORED.
Parallel GIN index builds (faster maintenance)
Large text/jsonb indexes can build faster with parallel workers.
-- Allow the server to use more workers for index buildsSET max_parallel_maintenance_workers = 4; -- session-level; set in postgresql.conf for global
-- Non-concurrent builds can use parallelismCREATE INDEX docs_content_gin ON docs USING GIN (to_tsvector('english', content));Why it matters: shorter maintenance windows. Pitfall: parallelism generally applies to non-CONCURRENTLY builds; plan accordingly.
Docs: [1]GIN Indexes, [1]max_parallel_maintenance_workers
COPY FROM with REJECT_LIMIT (safer ingest)
Load imperfect CSVs without aborting the entire batch.
-- Stage into a temp/staging table firstCREATE TEMP TABLE stage_users ( id bigint, email text, created_at timestamptz);
COPY stage_users FROM '/path/to/users.csv' WITH ( FORMAT csv, HEADER true, ON_ERROR 'ignore', REJECT_LIMIT 100, LOG_VERBOSITY 'silent' );
-- Inspect bad rows (e.g., by difference from source row count) before merging into targetINSERT INTO users (id, email)SELECT id, email FROM stage_usersWHERE email ~ '^[^@]+@[^@]+$';Why it matters: better DX for data onboarding. Pitfall: ensure you log/track rejects elsewhere if silent is used.
Docs: [1]COPY
Logical replication of generated columns
Replicate generated columns without recomputing on the subscriber.
-- PublisherCREATE PUBLICATION app_pub FOR TABLE public.orders;ALTER PUBLICATION app_pub SET (publish_generated_columns = true);
-- SubscriberCREATE SUBSCRIPTION app_sub CONNECTION 'host=... dbname=... user=... password=...' PUBLICATION app_pub WITH (streaming = parallel);Why it matters: simpler heterogenous replication; less custom logic on subscribers. Pitfall: confirm column lists/options per table.
Docs: [1]Logical Replication
EXISTS patterns and right semi join plans
Exists-at-least-one checks are everywhere; 18 can plan them more efficiently.
-- Classic EXISTS patternSELECT o.*FROM orders oWHERE EXISTS ( SELECT 1 FROM order_items i WHERE i.order_id = o.id AND i.sku = 'ABC');
-- Helpful indexesCREATE INDEX ON order_items (order_id, sku);Why it matters: faster existence checks on large tables. Pitfall: ensure join key indexes exist on the inner side.
Details: [1]PostgreSQL 18 Release Notes — Optimizer
VACUUM/ANALYZE ONLY for partitioned tables
Operate on the parent without touching children when needed.
VACUUM (VERBOSE, ONLY) sales_by_month; -- parent onlyANALYZE (ONLY) sales_by_month; -- parent onlyWhy it matters: targeted maintenance; avoids surprise work on children. Pitfall: remember autovacuum still manages children.
RETURNING old/new for auditing and debugging
Use the new old/new aliases to capture before/after values in a single statement.
-- Update and capture both versionsUPDATE accounts AS aSET balance = a.balance - 100WHERE a.id = 42RETURNING old.balance AS balance_before, new.balance AS balance_after;
-- Delete and capture the old rowDELETE FROM sessions sWHERE s.last_seen < now() - interval '30 days'RETURNING old.id, old.user_id, old.last_seen;Why it matters: less boilerplate for audit logs; easier debugging. Pitfall: validate that client ORM/driver surfaces both aliases.
Docs: [1]UPDATE, [1]DELETE, [1]Generated Columns
Top 5 gotchas
- MD5 auth is deprecated: Move to SCRAM; watch for deprecation warnings in role changes.
- VACUUM/ANALYZE inheritance behavior changed: They now include children by default; use
ONLYfor the old behavior. - CSV
\\.handling:COPY FROMno longer treats\\.as EOF;psqlstill does for STDIN, and it must be on its own line. - FTS/trgm + non-libc collations: After
pg_upgrade, reindex affected indexes to avoid surprises. - Skip scans ≠ magic: Great for occasional trailing-column queries, but if >20% of your workload hits those columns or you have high-cardinality leading columns (>100K distinct values), you still need purpose-built indexes. Use
EXPLAIN (ANALYZE, BUFFERS)to validate performance vs dedicated indexes.
Acknowledgments
Thanks to the PostgreSQL community of contributors, reviewers, and testers for another strong release. See the full contributor list in the official notes. [1]