IntermediateReferenceDatabases

PostgreSQL 19 - new SQL and COPY surface

The SQL surface PostgreSQL 19 adds - INSERT ON CONFLICT DO SELECT, window IGNORE NULLS, COPY TO JSON and partitioned tables, GRANTED BY, CHECK constraint enforcement toggles, oid8, jsonpath methods and the new encodings. Performance and I/O changes have their own page.

Applies to
PostgreSQL19PostgreSQL 19 was still in beta at the last review (Beta 4, 24 September 2026 is the latest pre-release) - syntax and defaults are from the beta documentation and can still shift before GA. Comparisons are against PostgreSQL 18.
emergingUpdated Reviewed

PostgreSQL 19 - new SQL surface and performance changes

Not everything in a major release is a headline. This page collects the SQL additions that will show up in application code once 19 ships - new clauses, new COPY behaviour, and one planner change (NOT IN) that is invisible until you read the plan. The I/O side of the release has its own page in asynchronous I/O; the smaller executor speedups are summarised on the overview. Everything below is from the beta documentation.

INSERT … ON CONFLICT DO SELECT

ON CONFLICT used to have two outcomes: DO NOTHING (skip silently) or DO UPDATE (write the conflict away). 19 adds DO SELECT: return the conflicting row, optionally locking it, without modifying it:

INSERT INTO accounts (id, balance) VALUES (42, 100)
ON CONFLICT (id) DO SELECT FOR UPDATE
RETURNING *;

If the insert succeeds you get the new row; if it conflicts you get the existing row under a FOR UPDATE lock (NO KEY UPDATE, SHARE and KEY SHARE are also accepted), and a WHERE condition can filter which conflicts are returned at all. This is the “upsert-or-read” round trip collapsed into one statement - an idempotent “get or create”. Privileges: DO SELECT needs SELECT on the table; adding FOR UPDATE/SHARE needs UPDATE on at least one column too.

Window functions: IGNORE NULLS

IGNORE NULLS and RESPECT NULLS are now supported on lead(), lag(), first_value(), last_value() and nth_value():

SELECT ts,
       last_value(price) IGNORE NULLS
         OVER (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS last_known
FROM quotes;

IGNORE NULLS answers “the most recent non-null value”, which previously took a subquery or a FILTER workaround. Default remains RESPECT NULLS.

COPY grows up

Four changes to COPY in 19:

  • JSON output: COPY ... TO (FORMAT JSON) emits one JSON object per row; FORCE_ARRAY wraps the whole result in a single JSON array. JSON is output-only (COPY TO). Caveat documented: a SQL NULL and a JSON column containing the literal null are indistinguishable in the output.
  • Bad input to NULL: COPY FROM ... (ON_ERROR SET_NULL) stores NULL for invalid input values instead of failing or skipping - distinct from ON_ERROR ignore, which skips the row.
  • Multi-line headers: HEADER on COPY FROM accepts a non-negative integer, discarding that many lines; HEADER MATCH requires the first line to name the table’s columns exactly, in order. Previously only a single header line could be skipped.
  • Partitioned tables: COPY TO can now dump a partitioned table directly instead of needing COPY (SELECT ...). The release notes note this also speeds up logical replication table sync. One restriction to know: COPY FROM ... WHERE can no longer reference system columns (xmin, ctid) - their values were never well-defined there.
COPY (SELECT * FROM events WHERE ts >= '2026-01-01') TO '/tmp/events.json' (FORMAT JSON, FORCE_ARRAY);
COPY measurements TO '/tmp/measurements.csv' (HEADER);           -- partitioned table, directly
COPY staging FROM '/tmp/dirty.csv' (FORMAT CSV, HEADER 2, ON_ERROR SET_NULL);

Smaller SQL and type additions

Addition Detail
GRANTED BY GRANT/REVOKE can specify the effective role performing the grant, for administrators acting on behalf of another role
CHECK constraint ENFORCED toggle ALTER TABLE ... ALTER CONSTRAINT ... [NOT] ENFORCED now works for CHECK constraints, not just foreign keys - a NOT ENFORCED constraint is documented but not checked
oid8 New 64-bit unsigned integer type
jsonpath string methods ltrim(), rtrim(), btrim(), lower(), upper(), initcap(), replace(), split_part() - immutable like their text counterparts
random(min, max) for temporal types date, timestamp and timestamptz variants join the numeric ones
base64url and base32hex encodings New encode()/decode() formats; base32hex retains ordering, unlike base32
error_on_null(x) Returns x, or raises an error on NULL
bytea/uuid casts Direct casts between the two types
regdatabase Cast between database names and oids
tid_block(), tid_offset() Extract block number and offset from a tid
range_minus_multi() / multirange_minus_multi() Range subtraction that returns the gaps as a set of ranges

The planner change hidden in your WHERE clause

NOT IN can now be planned as an ANTI JOIN when the planner can prove NULLs cannot occur - more LEFT JOINs convert to anti joins too, and Memoize applies to anti joins with unique inner sides. The caveat deserves emphasis: the transform fires only when NULLs are not present, which is exactly the case where NOT IN already returns surprising results. NOT EXISTS remains the robust formulation; the transform removes the performance argument against it rather than the correctness one.

The rest of 19’s performance work - faster foreign-key checks, radix sort, SIMD COPY FROM parsing, targeted NOTIFY wakeups, scans setting pages all-visible - is summarised in the overview’s performance section, and the I/O worker autoscaling plus EXPLAIN (ANALYZE, IO) is on the asynchronous I/O page.

What changed, and in which release

Behaviour PG 18 and earlier PG 19
Upsert that also returns the row DO UPDATE with a no-op SET, or a second query ON CONFLICT DO SELECT [FOR UPDATE]
Window “last non-null” Subquery or FILTER workarounds IGNORE NULLS
COPY TO output formats text, csv, binary plus json with FORCE_ARRAY
Bad COPY input rows Fail, or skip with ON_ERROR ignore ON_ERROR SET_NULL writes NULLs
COPY FROM header lines At most one Integer count or MATCH
COPY TO on partitioned tables Required COPY (SELECT ...) Direct
ALTER CONSTRAINT ... [NOT] ENFORCED FK only Also CHECK
NOT IN planning Subplan/antijoin limits Converted to ANTI JOIN when NULLs impossible

Where it breaks

  • ON CONFLICT DO SELECT returning fewer rows than expected - a row locked but filtered out by the WHERE condition is not returned; check RETURNING output rather than assuming row count.
  • JSON COPY ambiguity - 'null'::json and SQL NULL produce identical output; do not ingest the JSON back expecting to distinguish them.
  • ON_ERROR SET_NULL masking data problems - invalid input becomes NULL silently; pair it with REJECT_LIMIT or a post-load audit on nullable columns.
  • Beta caveat - all syntax above is from the 19 beta documentation.

What this is not for

  • Replacing NOT EXISTS defensively - the anti-join transform only fires when the planner can prove no NULLs; keep writing NOT EXISTS where correctness matters regardless.
  • JSON export as a stable interchange format - COPY ... FORMAT JSON loses the null-versus-JSON-null distinction; use a SELECT to JSON function composition if you need explicit null markers.

PostgreSQL release support

5 supported
ReleaseLatestReleasedEnd of lifeStatus
1818.6Sept 2025Nov 2030Supported
1717.11Sept 2024Nov 2029Supported
1616.15Sept 2023Nov 2028Supported
1515.19Oct 2022Nov 2027Supported
1414.24Sept 2021Nov 2026Supported
1313.23Sept 2020Nov 2025End of life
1212.22Oct 2019Nov 2024End of life
1111.22Oct 2018Nov 2023End of life

Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.

What to watch

Specific things that would change what this page concludes.

  • Whether the beta-only libpq protocol grease parameters affect anything after GA - they stop being sent at release but the parameter reservations remain

Where PostgreSQL sits

How this component relates to the rest of the stack. Items without a link are not covered here yet.

Depends on

Must be present for this to run

Deployed with

Commonly run alongside

Alternatives

Solves a similar problem differently