Skip to content

4 SQL Statements

SQL syntax, data types, functions, optimizer behavior, and worked examples.
Beta 1Source pages: PDF 45-88Original PDF on GitHub

PDF page 45

This section explains new features related to SQL statements.

3.2.1 ALTER DOMAIN statement

The lock level required to execute the ALTER DOMAIN VALIDATE CONSTRAINT statement has been changed from SHARE to SHARE UPDATE EXCLUSIVE. [16a0039]

3.2.2 ALTER FOREIGN DATA WRAPPER statement

The CONNECTION clause, which specifies the function used to establish a new connection, has been added. [8185bb5]

Syntax 4 - ALTER FOREIGN DATA WAPPER CONNECTION statement

ALTER FOREIGN DATA WAPPER fdw_name CONNECTION function_name
ALTER FOREIGN DATA WAPPER fdw_name NO CONNECTION

3.2.3 ALTER PUBLICATION statement

It is now possible to specify the SET ALL TABLES clause and the SET ALL SEQUENCES clause in an ALTER PUBLICATION statement. This is only possible for publications that specify the ALL TABLES/SEQUENCES clause or for empty publications. [5984ea8, fd36606, 493f8c6, 5509055]

Syntax 5 - ALTER PUBLICATION statement

ALTER PUBLICATION publication_name SET ALL TABLES [EXCEPT (TABLE expression)]
ALTER PUBLICATION publication_name SET ALL SEQUENCES

PDF page 46

Example 40 Executing the ALTER PUBLICATION statement
postgres=# CREATE PUBLICATION pub1 FOR ALL TABLES ;
CREATE PUBLICATION
postgres=# ALTER PUBLICATION pub1 SET ALL TABLES EXCEPT (TABLE data2) ;
ALTER PUBLICATION
postgres=# CREATE PUBLICATION pub2 ;
CREATE PUBLICATION
postgres=# ALTER PUBLICATION pub2 SET ALL SEQUENCES ;
ALTER PUBLICATION

3.2.4 ALTER SUBSCRIPTION statement

The ALTER SUBSCRIPTION REFRESH SEQUENCES statement, which updates the sequence information included in the subscription, has been added. [f0b3573]

Syntax 6 - ALTER SUBSCRIPTION REFRESH SEQUENCES statement

ALTER SUBSCRIPTION subscription_name REFRESH SEQUENCES
Example 41 Specifying the REFRESH SEQUENCES clause
postgres=# ALTER SUBSCRIPTION sub1 REFRESH SEQUENCES ;
ALTER SUBSCRIPTION

3.2.5 ALTER TABLE statement

The following enhancements have been added to the ALTER TABLE statement.

Changing the Expression for Virtual Generated Columns

It is now possible to change the expression for virtual generated columns with a CHECK constraint using the SET EXPRESSION clause. [f80bedd]

Syntax 7 - SET EXPRESSION clause

ALTER TABLE table_name ALTER COLUMN column_name SET EXPRESSION AS expression

PDF page 47

Example 42 Altering Virtual Columns with CHECK Constraints
postgres=> CREATE TABLE data1(col1 INT, col2 INT GENERATED ALWAYS AS
(col1 * 100) CHECK (col2 < 10000)) ;
CREATE TABLE
postgres=> ALTER TABLE data1 ALTER COLUMN col2 SET EXPRESSION AS
(col1 * 200) ;
ALTER TABLE
Setting the CHECK constraint to ENFORCED

It is now possible to change the setting of the ENFORCED clause for CHECK constraints. [342051d]

Syntax 8 - ENFORCED clause

ALTER TABLE table_name ALTER CONSTRAINT constraint_name [NOT] ENFORCED
Example 43 Setting the ENFORCED clause for CHECK constraints
postgres=> CREATE TABLE data1(col1 INT PRIMARY KEY, col2 INT) ;
CREATE TABLE
postgres=> ALTER TABLE data1 ADD CONSTRAINT data1_col2_check1
CHECK(col2 < 1000) ;
ALTER TABLE
postgres=> INSERT INTO data1 SELECT g1, g1 FROM generate_series(1, 999) g1 ;
INSERT 0 999
postgres=> ALTER TABLE data1 ALTER CONSTRAINT data1_col2_check1 NOT ENFORCED ;
ALTER TABLE
postgres=> INSERT INTO data1 VALUES (1000, 1000) ;
INSERT 0 1
postgres=> ALTER TABLE data1 ALTER CONSTRAINT data1_col2_check1 ENFORCED ;
ERROR:  check constraint "data1_col2_check1" of relation "data1" is violated by
some row
postgres=> DELETE FROM data1 WHERE col2=1000 ;
DELETE 1
postgres=> ALTER TABLE data1 ALTER CONSTRAINT data1_col2_check1 ENFORCED ;
ALTER TABLE

PDF page 48

Option autovacuum_parallel_workers

The autovacuum_parallel_workers option has been added for tables. This option can also be specified in the CREATE TABLE statement. It specifies the number of parallel VACUUM worker processes. The default value for this option is -1, and it inherits the value from the autovacuum_max_parallel_workers parameter. If this option is set to 0, parallel VACUUM is disabled. [1ff3180]

Example 44 Configuring the number of parallel workers for automatic VACUUM
postgres=> ALTER TABLE data1 SET (autovacuum_parallel_workers = 4) ;
ALTER TABLE
Optimizing ALTER TABLE Statements

When using the ALTER TABLE ADD COLUMN statement, the addition of a column with a CHECK constraint or a NOT NULL constraint on a domain can now be checked for the need to rewrite the table. If no rewrite is necessary, the table will not be rewritten. [a0b6ef2]

3.2.6 CHECKPOINT statement

The following options have been added to the CHECKPOINT statement.

MODE option

The MODE option determines how checkpoints are executed. The MODE FAST option is the default behavior and executes checkpoints as quickly as possible. If the MODE SPREAD option is specified, checkpoints are executed with reduced load based on relevant parameters. [2f698d7]

Syntax 9 - CHECKPOINT statement

CHECKPOINT [(MODE {FAST|SPREAD})]
Example 45 MODE option
postgres=# CHECKPOINT (MODE FAST) ;
CHECKPOINT
postgres=# CHECKPOINT (MODE SPREAD) ;
CHECKPOINT
Depending on the options specified, the output log will change as follows.

PDF page 49

OptionLog messageNote
FASTLOG: checkpoint starting: fast force wait
SPREADLOG: checkpoint starting: force wait
Table 21 Output logs
FLUSH_UNLOGGED option

Enabling the FLUSH_UNLOGGED option causes the database to also flush dirty buffers that have not been logged at the checkpoint. [8d33fba, a4f1265]

Syntax 10 - FLUSH_UNLOGGED option

CHECKPOINT [(FLUSH_UNLOGGED [boolean])]
Example 46 FLUSH_UNLOGGED option
postgres=# CHECKPOINT (FLUSH_UNLOGGED) ;
CHECKPOINT
Depending on the options specified, the output log will change as follows.
OptionLog message
No specificationLOG: checkpoint starting: fast force wait
FLUSH_UNLOGGEDLOG: checkpoint starting: fast force wait flush-unlogged
Table 22 Output logs

3.2.7 COPY statement

The following new features have been implemented to the COPY statement.

ON_ERROR option setting

It is now possible to specify the “set_null” for the on_error option in the COPY FROM statement. This option sets the column value to NULL if a data type conversion error occurs. [2a525cc]

PDF page 50

Example 47 ON_ERROR option
postgres=# \d data1
               Table "public.data1"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 col1   | integer |           |          |
 col2   | integer |           |          |
postgres=# COPY data1 FROM stdin WITH (format 'csv', on_error 'set_null') ;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> 100,100
>> 200,bad_data
>> 300,300
>> \.
NOTICE:  in 1 row, columns were set to null due to data type incompatibility
COPY 3
postgres=# SELECT * FROM data1 ;
 col1 | col2
------+------
  100 |  100
  200 | null
  300 |  300
(3 rows)
Partition Table Support

The COPY TO statement can now be executed on partition tables. In previous versions, it was necessary to execute the COPY (SELECT …) TO statement instead of using the partition table name. [4bea91f]

PDF page 51

Example 48 Specifying the partition table
postgres=# \d part1
                Partitioned table "public.part1"
 Column |         Type          | Collation | Nullable | Default
--------+-----------------------+-----------+----------+---------
 col    | integer               |           | not null |
 col2   | character varying(10) |           |          |
Partition key: RANGE (c1)
Indexes:
    "part1_pkey" PRIMARY KEY, btree (col1)
Number of partitions: 2 (Use \d+ to list them.)
postgres=# COPY part1 TO '/tmp/part1.csv' CSV ;
COPY 2000000
Number of header rows

It is now possible to specify the number of header rows in the data. [bc2f348]

Example 49 Specifying the number of header rows
postgres=# COPY data1 FROM stdin WITH (FORMAT CSV, HEADER 2) ;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> HEADER1
>> HEADER2
>> 100,data1
>> 200,data2
>> \.
COPY 2
JSON format output

It is now possible to specify “json” as the FORMAT option for the COPY TO statement. The additional option “force_array” can also be specified. [7dadd38, 4c0390a]

PDF page 52

Example 50 Output to JSON format
postgres=# COPY data1 TO stdout WITH (FORMAT json) ;
{"col1":1,"col2":"data1"}
{"col1":2,"col2":"data2"}
{"col1":3,"col2":"data3"}
{"col1":4,"col2":"data4"}
{"col1":5,"col2":"data5"}
postgres=# COPY data1 TO stdout WITH (FORMAT json, FORCE_ARRAY) ;
[
 {"col1":1,"col2":"data1"}
,{"col1":2,"col2":"data2"}
,{"col1":3,"col2":"data3"}
,{"col1":4,"col2":"data4"}
,{"col1":5,"col2":"data5"}
]

3.2.8 CREATE PUBLICATION statement

It is now possible to specify the FOR ALL SEQUENCES clause, which applies to all sequences. This clause requires a user with the SUPERUSER privilege to execute the CREATE PUBLICATION statement. To indicate whether this clause has been specified, the puballsequences column has been added to the pg_publication catalog. [96b3784]

Syntax 11 - FOR ALL SEQUENCES clause

CREATE PUBLICATION publication_name FOR ALL SEQUENCES

PDF page 53

Example 51 Specifying the FOR ALL SEQUENCES clause
postgres=# CREATE PUBLICATION pub1 FOR ALL SEQUENCES ;
CREATE PUBLICATION
postgres=# \dRp
                                                 List of publications
 Name |  Owner   | All tables | All sequences | Inserts | Updates | Deletes…
------+----------+------------+---------------+---------+---------+--------…
 pub1 | postgres | f          | t             | t       | t       | t      …
(1 row)
No tables can be added to a publication that specifies the ALL SEQUENCES clause.
Example 52 Add a table to the publication
postgres=# ALTER PUBLICATION pub1 ADD TABLE data1 ;
ERROR:  publication "pub1" is defined as FOR ALL SEQUENCES
DETAIL:  Tables or sequences cannot be added to or dropped from FOR ALL SEQUENCES
publications.

3.2.9 CREATE SCHEMA statement

It is now possible to specify objects other than tables, views, indexes, sequences, triggers, and GRANT in the CREATE SCHEMA statement. [a9c350d, 404db8f]

Example 53 The CREATE SCHEMA Statement and Creating Objects
postgres=# CREATE SCHEMA schema1
             CREATE TABLE data1(col1 INT PRIMARY KEY, col2 VARCHAR(10))
  CREATE COLLATION col1 (LOCALE="C")
            CREATE DOMAIN d1 AS INT ;
CREATE SCHEMA
The following CREATE clauses can be specified:

PDF page 54

ObjectPostgreSQL 18PostgreSQL 19Note
AGGREGATENoYes
COLLATIONNoYes
DOMAINNoYes
FUNCTIONNoYes
INDEXYesYes
OPERATORNoYes
PROCEDURENoYes
SEQUENCEYesYes
TABLEYesYes
TEXT SEARCH CONFIGURATIONNoYes
TEXT SEARCH DICTIONARYNoYes
TEXT SEARCH PARSERNoYes
TEXT SEARCH TEMPLATE TRIGGERNoYes
YesYes
TYPENoYes
VIEWYesYes
GRANTYesYes
Table 23 Types of objects that can be created at the same time

3.2.10 CREATE STATISTICS statement

It is now possible to create extended statistics by specifying a virtual column. [f7f4052]

Example 54 Creating Extended Statistics for Virtual Generated Columns
 postgres=> CREATE TABLE data1(col1 INT, col2 INT GENERATED ALWAYS AS
(col1 + 100) VIRTUAL) ;
CREATE TABLE
postgres=> CREATE STATISTICS data1_stat1 ON (col2) FROM data1 ;
CREATE STATISTICS

PDF page 55

3.2.11 DELETE / UPDATE statement

The FOR PORTION OF clause can now be specified in UPDATE and DELETE statements. When a range is specified in the FOR PORTION OF clause, a new tuple matching that range is created, and the updates specified in the SET clause are applied to it. The original tuple is split into tuples that fall outside the range specified in the FOR PORTION OF clause. [8e72d91]

Syntax 12 - FOR PORTION OF clause

UPDATE table_name FOR PORTION OF column FROM start TO end SET … WHERE …
DELETE FROM table_name FOR PORTION OF column FROM start TO end WHERE …

Since the behavior can be confusing, a sample will be used to explain it. Initially, the range1 table contains two tuples: one in the col1 column (of type DATERANGE) representing the range “April 1, 2026 - May 1, 2026” and another representing the range “May 1, 2026 - June 1, 2026”.

Example 55 Tuples before the update
postgres=> SELECT * FROM range1 ORDER BY col1 ;
          col1           | col2
-------------------------+------
 [2026-04-01,2026-05-01) |  100
 [2026-05-01,2026-06-01) |  200
(2 rows)
If the FOR PORTION OF clause is set to "2026/04/10-2026/04/20", the tuples to be updated are
selected (tuples where col2=100). These tuples are split into the following three tuples.
• 2026/04/01-2026/04/10 (The first half of the original tuple range)
• 2026/04/10-2026/04/20 (Tuple to be updated; updated using a SET clause)
• 2026/04/20-2026/05/01 (The second half of the original tuple range)

PDF page 56

Example 56 Tuples after update
postgres=> UPDATE range1 FOR PORTION OF col1 FROM '2026-04-10' TO '2026-04-20'
SET col2=300 ;
UPDATE 1
postgres=> SELECT * FROM range1 ORDER BY col1 ;
          col1           | col2
-------------------------+------
 [2026-04-01,2026-04-10) |  100
 [2026-04-10,2026-04-20) |  300
 [2026-04-20,2026-05-01) |  100
 [2026-05-01,2026-06-01) |  200
(4 rows)

3.2.12 EXPLAIN statement

The following features have been added to the EXPLAIN statement.

Output of I/O Statistics

The IO option has been added to the EXPLAIN statement to output I/O statistics. The IO option must be specified together with the ANALYZE option. [681daed, 3b1117d, e157fe6]

Example 57 Executing an EXPLAIN (IO) statement
postgres=> EXPLAIN (ANALYZE, IO, COSTS OFF) SELECT COUNT(*) FROM data1 ;
                                  QUERY PLAN
------------------------------------------------------------------------------
 Aggregate (actual time=425.023..425.024 rows=1.00 loops=1)
   Buffers: shared hit=1099 read=52956
   ->  Seq Scan on data1 (actual time=0.416..235.808 rows=10000000.00 loops=1)
         Prefetch: avg=70.84 max=91 capacity=94
         I/O: count=3314 waits=7 size=15.98 in-progress=4.54
         Buffers: shared hit=1099 read=52956
 Planning Time: 0.059 ms
 Execution Time: 425.056 ms
(8 rows)

PDF page 57

Output of Memoize Estimation Information

Estimation information (Estimates:) is now output when Memoize node is included in the execution plan. [4bc62b8]

Example 58 Outputting Memoize Estimate Information
postgres=> EXPLAIN SELECT * FROM data1 AS d1 INNER JOIN data1 AS d2
ON d1.col1 = d2.col2::numeric AND d1.col2::numeric = d2.col1 ;
                                         QUERY PLAN
-----------------------------------------------------------------------------
 Nested Loop  (cost=0.15..10.49 rows=1 width=36)
   ->  Seq Scan on data1 d1  (cost=0.00..1.40 rows=40 width=18)
   ->  Memoize  (cost=0.15..0.41 rows=1 width=18)
         Cache Key: d1.col1, (d1.col2)::numeric
         Cache Mode: logical
         Estimates: capacity=20 distinct keys=20 lookups=40 hit percent=50.00%
         ->  Index Only Scan using idx1_data1 on data1 d2  (cost=0.14..0.40…
               Index Cond: (col1 = (d1.col2)::numeric)
               Filter: (d1.col1 = (col2)::numeric)
(9 rows)
Output of WAL FPI Byte Count

The WAL data now includes FPI bytes. [5ab0b6a]

PDF page 58

Example 59 Output of FPI byte information
postgres=> EXPLAIN (ANALYZE, WAL) UPDATE data1 SET col2='update'
WHERE col1 < 100 ;
                                   QUERY PLAN
------------------------------------------------------------------------------
 Update on data1  (cost=0.00..1.50 rows=0 width=0) (actual time=0.225..0.225…
   Buffers: shared hit=121 read=1 dirtied=3
   WAL: records=80 fpi=2 bytes=9790 fpi bytes=3656
   ->  Seq Scan on data1  (cost=0.00..1.50 rows=40 width=38) (actual time=0.…
         Filter: (col1 < '100'::numeric)
         Buffers: shared hit=1
 Planning Time: 0.281 ms
 Execution Time: 0.363 ms
(8 rows)
Subquery Name

The subquery blocks in the executed execution plan now display unique names. Previously, only numbers were displayed. [8c49a48]

PDF page 59

Example 60 PostgreSQL 19 Output
postgres=> EXPLAIN (COSTS OFF) SELECT COUNT(*) FROM data1 WHERE col1 =
(SELECT MAX(col1) FROM data1) ;
                                    QUERY PLAN
------------------------------------------------------------------------------
 Aggregate
   InitPlan expr_1
     ->  Result
           Replaces: MinMaxAggregate
           InitPlan minmax_1
             ->  Limit
                   ->  Index Only Scan Backward using data1_pkey on data1
data1_1
   ->  Index Only Scan using data1_pkey on data1
         Index Cond: (col1 = (InitPlan expr_1).col1)
(9 rows)
Example 61 PostgreSQL 18 Output
postgres=>  EXPLAIN (COSTS OFF) SELECT COUNT(*) FROM data1 WHERE col1 =
(SELECT MAX(col1) FROM data1) ;
                                    QUERY PLAN
------------------------------------------------------------------------------
 Aggregate
   InitPlan 2
     ->  Result
           InitPlan 1
             ->  Limit
                   ->  Index Only Scan Backward using data1_pkey on data1
data1_1
   ->  Index Only Scan using data1_pkey on data1
         Index Cond: (col1 = (InitPlan 2).col1)
(8 rows)

PDF page 60

3.2.13 GRANT statement

It is now possible to specify a grantee other than the current user in the GRANTED BY clause of GRANT and REVOKE statements. In previous versions, this resulted in the error “ERROR: grantor must be current user.”. [dd1398f]

Example 62 Specifying the GRANTED BY clause
postgres=> CREATE TABLE data1(col1 INT PRIMARY KEY, col2 VARCHAR(10)) ;
CREATE TABLE
postgres=> GRANT SELECT ON data1 TO demo2 WITH GRANT OPTION ;
GRANT
postgres=> \connect postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=# GRANT SELECT ON data1 TO demo3 GRANTED BY demo2 ;
GRANT
postgres=# \dp+
                             Access privileges
 Schema | Name  | Type  | Access privileges  | Column privileges | Policies
--------+-------+-------+--------------------+-------------------+----------
 public | data1 | table | demo=arwdDxtm/demo+|                   |
        |       |       | demo2=r*/demo     +|                   |
        |       |       | demo3=r/demo2      |                   |
(1 row)

3.2.14 INSERT statement

It is now possible to specify the DO SELECT clause in an INSERT ON CONFLICT statement. [8832709]

Syntax 13 - ON CONFLICT DO SELECT

INSERT INTO table_name VALUES (…) ON CONFLICT (column_name)
DO SELECT [FOR {UPDATE}|{SHARE}] [RETURNING …]

PDF page 61

Example 63 INSERT ON CONFLICT DO SELECT statement
postgres=> INSERT INTO data1 VALUES (100, 'coflict100') ON CONFLICT (col1)
DO SELECT FOR UPDATE RETURNING * ;
col1 |    col2
------+------------
  100 | coflict100
(1 row)
INSERT 0 1

3.2.15 REPACK statement

The REPACK statement has been added to PostgreSQL 19. The REPACK statement is the SQL statement that executes both the VACUUM FULL and CLUSTER statements in the single command. When the REPACK statement is executed on its own, it targets all tables and materialized views for which the current user has MAINTAIN privileges. It is also possible to specify specific tables, but UNLOGGED tables and partitioned tables cannot be specified. The USING INDEX clause sorts the data based on the information in the specified index. The progress of the REPACK statement can be monitored via the pg_stat_progress_repack view. [ac58465, 28d534e, e76d8c7, 5dbb63f]

Syntax 14 - REPACK statement

REPACK [(options)] [object] [USING INDEX [index_name]]

The following options can be specified in a REPACK statement.

OptionDescription
ANALYZEExecute an ANALYZE statement after the REPACK operation is complete
CONCURRENTLYMinimize locks and allow access to other SQL statements
VERBOSEOutput a detailed execution log
Table 24 Options for the REPACK statement

PDF page 62

Example 64 Executing the REPACK statement
postgres=> REPACK (ANALYZE, VERBOSE) data1 ;
INFO:  repacking "public.data1" in physical order
INFO:  "public.data1": found 1666667 removable, 3333333 nonremovable row
versions in 27028 pages
DETAIL:  0 dead row versions cannot be removed yet.
CPU: user: 0.58 s, system: 0.20 s, elapsed: 0.94 s.
INFO:  analyzing "public.data1"
INFO:  "data1": scanned 18019 of 18019 pages, containing 3333333 live rows and
0 dead rows; 30000 rows in sample, 3333333 estimated total rows
INFO:  finished analyzing table "postgres.public.data1"
avg read rate: 1582.075 MB/s, avg write rate: 0.088 MB/s
buffer usage: 23 hits, 18023 reads, 1 dirtied
WAL usage: 3 records, 1 full page images, 2165 bytes, 1560 full page image
bytes, 0 buffers full
system usage: CPU: user: 0.06 s, system: 0.01 s, elapsed: 0.08 s
REPACK
If the CONCURRENTY option is specified in the REPACK statement, the SHARE UPDATE
EXCLUSIVE lock is used on the table. Update transactions during the REPACK statement execution
generate update changes via the logical decoding feature. These update changes are ultimately applied
to the new table. During the execution of a REPACK statement, a temporary logical replication slot
named “repack_{ID}” is automatically generated internally. The number of replication slots reserved
for REPACK is determined by the max_repack_replication_slots parameter.

3.2.16 SELECT statement

The following features have been added to SELECT statements.

GROUP BY ALL clause

The ALL clause can now be specified in the GROUP BY clause. This executes the GROUP BY operation using all output columns. [ef38a4d]

PDF page 63

Example 65 Specifying the GROUP BY ALL clause
postgres=> SELECT col1, col2 FROM data1 GROUP BY ALL ORDER BY 1 ;
 col1 | col2
------+-------
    1 | data1
    2 | data2
    3 | data3
    4 | data4
(4 rows)
GROUPING SETS and Subqueries

Subqueries now support references to GROUPING SETS columns. The following syntax would have resulted in an error in previous versions. [415100a]

Example 66 GROUPING SETS and Subqueries
postgres=> SELECT (SELECT col1),
 (SELECT col2),
 (SELECT GROUPING(col1, col2)),
 (SELECT SUM(t1.col3)),
 (SELECT max(t2.col3))
FROM
gstest1 t1 FULL JOIN gstest2 t2 USING (col1, col2)
GROUP BY GROUPING SETS ((col1, col2), ()) ;
 col1 | col2 | grouping | sum | max
------+------+----------+-----+-----
      |      |        3 | 271 |   2
    1 |    1 |        0 | 147 |   2
    1 |    3 |        0 |  14 |
    3 |    4 |        0 |  17 |
    3 |    3 |        0 |  16 |
    1 |    2 |        0 |  25 |   2
    2 |    2 |        0 |     |   2
    4 |    1 |        0 |  37 |
    2 |    3 |        0 |  15 |
(9 rows)

PDF page 64

3.2.17 SET statement

It is now possible to specify a NULL value in SET statements and ALTER SYSTEM SET statements. [ff4597a]

Example 67 Specifying NULL values in the SET statement
postgres=> SHOW search_path ;
   search_path
-----------------
 "$user", public
(1 row)
postgres=> SET search_path TO NULL ;
SET
postgres=> SHOW search_path ;
 search_path
-------------
(1 row)

3.2.18 VACUUM statement

The following information has been added to the output of the VACUUM VERBOSE statement.

Memory Information

Memory information beginning with “memory usage:” is output. [736f754e]

PDF page 65

Example 68 Memory Information
postgres=> VACUUM (VERBOSE) data1 ;
INFO:  vacuuming "postgres.public.data1"
…
WAL usage: 185293 records, 92311 full page images, 613464473 bytes, 576198816
full page image bytes, 1416
memory usage: dead item storage 2.58 MB accumulated across 1 reset (limit 64.00
MB each)
system usage: CPU: user: 0.33 s, system: 0.95 s, elapsed: 32.05 s
VACUUM
Parallel Worker Information

Information about the parallel workers used by the VACUUM statement is output. [adcdbe9]

Example 69 Parallel Worker Information
postgres=> VACUUM (PARALLEL 2, VERBOSE) data1 ;
INFO:  vacuuming "postgres.public.data1"
INFO:  launched 1 parallel vacuum worker for index vacuuming (planned: 2)
INFO:  finished vacuuming "postgres.public.data1": index scans: 1
…
item identifiers removed
parallel workers: index vacuum: 1 planned, 1 launched in total
index "data1_pkey": pages: 27421 in total, 0 newly deleted, 0 currently deleted,
0 reusable
…
system usage: CPU: user: 0.11 s, system: 1.35 s, elapsed: 5.66 s
VACUUM

3.2.19 WAIT FOR LSN statement

The WAIT FOR LSN statement, a new SQL syntax, has been added to PostgreSQL 19. This SQL statement is mainly executed on standby instances in a streaming replication environment. When the WAIT FOR LSN statement is executed, it waits until the specified LSN is applied. [447aae1, 3b4e53a, 49a181b]

PDF page 66

Syntax 15 - WAIT FOR LSN statement

WAIT FOR LSN 'lsn' [WITH (option [, …])]

The following options are available.

MODE ‘mode’

Specify the mode for how long to wait. The following values can be specified. “primary_flush” is specified on the primary server; all others are specified on the standby server.

Setting valueDescriptionNote
primary_flushstandby_replaystandby_writestandby_flushPrimary server flush Apply WAL to the standby server Write WAL to the standby server Flush WAL on the standby serverOn the primary server
Table 25 MODE Option Settings
TIMEOUT ’timeout'

Specify the maximum wait time. If no value is specified or if zero (0) is specified, this statement will wait indefinitely.

NO_THROW

Even if a timeout occurs, no error is thrown.

PDF page 67

Example 70 Executing the WAIT FOR LSN statement
postgres=> WAIT FOR LSN '0/0301EF70' ;
 status
---------
 success
(1 row)
postgres=> WAIT FOR LSN '0/0301FF00' WITH (TIMEOUT '10s') ;
ERROR:  timed out while waiting for target LSN 0/0301FF00 to be replayed;
current standby_replay LSN 0/0301EF70
postgres=> WAIT FOR LSN '0/0301FF00' WITH (TIMEOUT '10s', NO_THROW) ;
 status
---------
 timeout
(1 row)
The WAIT FOR LSN statement returns the following value.
Return valueDescriptionNote
successThe specified LSN has been reached
timeoutThe wait time has exceeded the timeout
not in recoveryThe executed instance is not in recovery state
Table 26 Return value of the WAIT FOR LSN statement

3.2.20 Functions

The following functions have been added or enhanced.

Window Functions

It is now possible to specify the IGNORE NULLS clause (ignore NULL values) and the RESPECT NULLS clause (include NULL values) for the window functions LEAD, LAG, FIRST_VALUE, LAST_VALUE, and NTH_VALUE. The default is RESPECT NULLS. [25a30bb]

PDF page 68

Example 71 Specifying the IGNORE NULLS clause and the RESPECT NULLS clause
postgres=> SELECT LEAD(col1) IGNORE NULLS OVER (), LAG(col1) RESPECT NULLS OVER
() FROM data1 ;
 lead  |  lag
-------+-------
     2 |
     3 |     1
     4 |     2
     5 |     3
     6 |     4
…
Function encode and decode

It is now possible to specify base64url and base32hex as encoding formats. [e1d9171, 497c117]

Example 72 Base64 URL encoding and Base32 hex encoding
postgres=> SELECT encode('\x69b73eff', 'base64url'),
decode('abc-_w', 'base64url') ;
 encode |   decode
--------+------------
 abc-_w | \x69b73eff
(1 row)
postgres=> SELECT encode('\x1122334455', 'base32hex'),
decode('24H36H2L', 'base32hex') ;
  encode  |    decode
----------+--------------
 24H36H2L | \x1122334455
(1 row)
Function error_on_null

The error_on_null function has been added; it returns an error if the input value is NULL, and returns the input value if it is not NULL. [2b75c38]

PDF page 69

Syntax 16 - Function error_on_null

anyelement error_on_null(anyelement)
Example 73 Execution of the error_on_null function
postgres=> SELECT error_on_null(NULL::INT) ;
ERROR:  null value not allowed
Function random

Variations of the random function that specify maximum and minimum values now support the date, timestamp, and timestamp with time zone types. [faf071b]

Syntax 17 - Function random variations

date random(min date, max date)
timestamp random(min timestamp, max timestamp)
timestamptz random(min timestamptz, max timestamptz)
Example 74 Variations of the random function
postgres=> SELECT random('2024-09-09'::date, '2025-09-09'::date),
random('2024-09-09'::timestamptz, '2025-09-09'::timestamptz) ;
   random   |            random
------------+-------------------------------
 2025-02-04 | 2025-02-16 07:24:48.518771+09
(1 row)
Function range_minus_multi and multirange_minus_multi

The range_minus_multi and multirange_minus_multi functions have been added to remove ranges from a range and return multiple ranges, respectively. [5eed8ce]

Syntax 18 - Function range_minus_multi and multirange_minus_multi

SETOF anyrange range_minus_multi(anyrange, anyrange)
SETOF anymultirange multirange_minus_multi(anymultirange, anymultirange)

PDF page 70

Example 75 The range_minus_multi function and the multirange_minus_multi function
postgres=> SELECT range_minus_multi(numrange(1.0, 3.0), numrange(1.5, 2.0)) ;
 range_minus_multi
-------------------
 [1.0,1.5)
 [2.0,3.0)
(2 rows)
postgres=> SELECT multirange_minus_multi(nummultirange(numrange(1,4)),
 nummultirange(numrange(2,3))) ;
 multirange_minus_multi
------------------------
 {[1,2),[3,4)}
(1 row)
Function tid_block and tid_offset

The tid_block and tid_offset functions have been added to retrieve the block number and row offset from the CTID. [df6949c]

Syntax 19 - Function tid_block and tid_offset

bigint tid_block(tid)
integer tid_offset(tid)
Example 76 Executing the tid_block and tid_offset functions
postgres=> SELECT ctid, tid_block(ctid), tid_offset(ctid), col1 FROM data1 ;
 ctid  | tid_block | tid_offset | col1
-------+-----------+------------+------
 (0,1) |         0 |          1 |  100
 (0,2) |         0 |          2 |  200
(2 rows)

PDF page 71

Function width_bucket

It is now possible to specify NaN as the first parameter, operand, of the width_bucket function. The following SQL statement would have caused an error in previous versions. [7374b3a]

Example 77 Executing the width_bucket function
postgres=> SELECT width_bucket('NaN', 3.0, 4.0, 888) ;
 width_bucket
--------------
          889
(1 row)
DDL Export

Functions have been added to output DDL for databases, tablespaces, and roles. [4881981, 76e514e, b99fd9f, a4f774c]

Syntax 20 - Export DDL functions

SETOF text pg_get_database_ddl(regdatabase, VARIADIC text DEFAULT NULL::text)
SETOF text pg_get_tablespace_ddl(name, VARIADIC text DEFAULT NULL::text)
SETOF text pg_get_role_ddl(regrole, VARIADIC text DEFAULT NULL::text)

The following settings can be specified in the options.

OptionData typeDescriptionNote
prettybooleanFormatted output
ownerbooleanInclude ownerCannot be specified with pg_get_role_ddl
tablespacemembershipsbooleanInclude tablespacepg_get_database_ddl only
booleanInclude memberpg_get_role_ddl only
Table 27 Available options

PDF page 72

Example 78 Retrieving DDL for a database
postgres=> SELECT pg_get_database_ddl('postgres', 'pretty', 'true') ;
            pg_get_database_ddl
--------------------------------------------
 CREATE DATABASE postgres                  +
     WITH TEMPLATE = template0             +
     ENCODING = 'UTF8'                     +
     LOCALE_PROVIDER = libc                +
     LOCALE = 'C';
 ALTER DATABASE postgres OWNER TO postgres;
(2 rows)
Function pg_clear_extended_stats

The pg_clear_extended_stats function, which clears a table’s extended statistics, has been added. [d756fa1]

Syntax 21 - Function pg_clear_extended_stats

void pg_clear_extended_stats(schemaname text, relname text,
statistics_schemaname text, statistics_name text, inherited boolean)
Example 79 Executing of the pg_clear_extended_stats
postgres=> CREATE STATISTICS data1_stat1 ON col1,col2 FROM data1 ;
CREATE STATISTICS
postgres=> SELECT pg_clear_extended_stats('public', 'data1', 'public',
'data1_stat1', true) ;
-[ RECORD 1 ]-----------+-
pg_clear_extended_stats |
Function pg_get_multixact_stats

The pg_get_multixact_stats function, which retrieves the status of multi-transactions, has been added. This function returns the following values. [97b1017]

PDF page 73

Column nameData typeDescription
num_mxidsbigintMulti-transaction IDs in use
num_membersbigintSize of multi-transaction ID members in use
members_sizebigintStorage size used by members
oldest_multixactxidOldest multi-transaction ID
Table 28 Return value of the pg_get_multixact_stats function
Example 80 Execution of the pg_get_multixact_stats function
postgres=# SELECT * FROM pg_get_multixact_stats() ;
 num_mxids | num_members | members_size | oldest_multixact
-----------+-------------+--------------+------------------
         1 |           2 |            0 |                1
(1 row)
Function pg_get_sequence_data

The page_lsn column, which indicates the LSN, has been added to the output results. [b93172c]

Example 81 Executing of the pg_get_sequence_data function
postgres=> SELECT * FROM pg_get_sequence_data('seq01') ;
 last_value | is_called |  page_lsn
------------+-----------+------------
          4 | t         | 3/0701C0A0
(1 row)
Function pg_get_publication_tables

The pg_get_publication_tables function now includes a variant that allows table information to be specified. [fd7a25a]

Syntax 22 - Function pg_get_publication_tables with table id

SETOF record pg_get_publication_tables(pubnames text[], target_relid oid)

PDF page 74

Example 82 Executing of the pg_get_publication_tables function
postgres=> SELECT * FROM
pg_get_publication_tables('{pub1}', 'data1'::regclass::oid) ;
 pubid | relid | attrs | qual
-------+-------+-------+------
 16405 | 16399 | 1 2   |
(1 row)
Function pg_replication_origin_session_setup

The pid parameter has been added to specify the worker process ID using origin. The default value is 0, which indicates the first process ID. [5b14870] [5b14870]

Example 83 Definition of the pg_replication_origin_session_setup function
postgres=> \dfS pg_replication_origin_session_setup
List of functions
-[ RECORD 1 ]-------+--------------------------------------
Schema              | pg_catalog
Name                | pg_replication_origin_session_setup
Result data type    | void
Argument data types | node_name text, pid integer DEFAULT 0
Type                | func
Function pg_restore_extended_stats

The pg_restore_extended_stats function, which restores a table’s extended statistics, has been added. [0e80f3f, 302879b, ba97bf9]

Syntax 23 - Function pg_restore_extended_stats

boolean pg_restore_extended_stats(VARIADIC kwargs "any")

PDF page 75

Example 84 Executing of the pg_restore_extended_stats function
postgres=> CREATE STATISTICS stat1_data1 ON col1,col2 FROM data1 ;
CREATE STATISTICS
postgres=> SELECT pg_restore_extended_stats(
  'schemaname', 'public',
  'relname', 'data1',
  'statistics_schemaname', 'public',
  'statistics_name', 'stat1_data1',
  'inherited', false,
  'n_distinct', '[{"attributes" : [1,2], "ndistinct" : 4}]'::pg_ndistinct) ;
-[ RECORD 1 ]-------------+--
pg_restore_extended_stats | t
Function pg_stat_get_backend_wal

The pg_stat_get_backend_wal function now includes wal_fpi_bytes (of type numeric), which indicates the number of FPI bytes, in its return value. [f9a09aa]

Function pg_sync_replication_slots

The pg_sync_replication_slots function now includes a feature that retries until synchronization is complete. [0d2d4a0]

3.2.21 JSON

The following enhancements regarding JSON data have been implemented.

Added jsonpath methods

The following jsonpath methods have been added. [bd4f879]

PDF page 76

Method nameDescriptionNote
ltrim / rtrim / btrim lower upper initcap replace split_partRemove leading and trailing characters Convert to lowercase Convert to uppercase Capitalize the first letter and convert the rest to lowercase Convert the specified string Return elements split by the specified string
Table 29 Added string methods
Example 85 Executing the newly added jsonpath string method
postgres=> SELECT jsonb_path_query('"xTRIMx"'::jsonb, '$.btrim("x")') ;
-[ RECORD 1 ]----+-------
jsonb_path_query | "TRIM"
postgres=> SELECT jsonb_path_query('"LOWER"'::jsonb, '$.lower()') ;
-[ RECORD 1 ]----+--------
jsonb_path_query | "lower"
postgres=> SELECT jsonb_path_query('"upper"'::jsonb, '$.upper()') ;
-[ RECORD 1 ]----+--------
jsonb_path_query | "UPPER"
postgres=> SELECT jsonb_path_query('"iNITCaP"'::jsonb, '$.initcap()') ;
-[ RECORD 1 ]----+----------
jsonb_path_query | "Initcap"
postgres=> SELECT jsonb_path_query('"REpLACE"'::jsonb, '$.replace("p","P")') ;
-[ RECORD 1 ]----+----------
jsonb_path_query | "REPLACE"
postgres=> SELECT jsonb_path_query('"test@data"'::jsonb,
'$.split_part("@",2)') ;
-[ RECORD 1 ]----+-------
jsonb_path_query | "data"

PDF page 77

IS JSON predicate

It is now possible to specify the IS JSON predicate for the domain. In previous versions, the IS JSON predicate was only available for the text, json, jsonb, and bytea data types. [3b4c2b9]

Example 86 IS JSON Predicate for the Domain
postgres=> CREATE DOMAIN jd1 AS jsonb CHECK ((VALUE ->'val')::text <> '3') ;
CREATE DOMAIN
postgres=> SELECT '{"val": 1}'::jd1 IS JSON ;
 ?column?
----------
 t
(1 row)
postgres=> SELECT '{"val": 3}'::jd1 IS JSON ;
ERROR:  value for domain jd1 violates check constraint "jd1_check"

3.2.22 Data Types

The following extensions have been implemented for data types in PostgreSQL 19.

Data type regdatabase

The regdatabase data type, which represents a database OID, has been added. To retrieve the OID of the current database, it can be written as follows. [bd09f02]

Example 87 Querying by regdatabase data type
postgres=> SELECT * FROM pg_shdepend WHERE
dbid = current_database()::regdatabase ;
 dbid | classid | objid | objsubid | refclassid | refobjid | deptype
------+---------+-------+----------+------------+----------+---------
    5 |    2615 |  2200 |        0 |       1260 |    16384 | a
(1 row)
Data type oid8

The oid8 data type, which represents 8-byte OIDs, has been added. [b139bd3]

PDF page 78

Example 88 Data type oid8
postgres=> SELECT 9999999999::oid8 ;
    oid8
------------
 9999999999
(1 row)
Converting between the UUID and BYTEA types

The uuid and bytea types can now be converted between each other. [ba21f5b]

Example 89 Data type conversion between UUID and BYTEA
postgres=> SELECT '\x019a2f859ced7225b99d9c55044a2563'::bytea::uuid ;
                 uuid
--------------------------------------
 019a2f85-9ced-7225-b99d-9c55044a2563
(1 row)
postgres=> SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid::bytea ;
               bytea
------------------------------------
 \x5b35380a714349129b55f322699c6770
(1 row)
GiST Indexes of Type inet and cidr

The operator classes for GiST indexes of type inet and cidr have been changed to inet_ops and cidr_ops, respectively. As a result of this change, the btree_gist extension is no longer required. The following CREATE INDEX statements will result in an error in PostgreSQL 18 and earlier. The pg_upgrade command will fail if the older database contains the gist_inet_ops or gist_cidr_ops operator classes. [b352d3d]

PDF page 79

Example 90 GiST indexes for inet and cidr types
postgres=> CREATE TABLE data1(col1 INET, col2 CIDR) ;
CREATE TABLE
postgres=> CREATE INDEX idx1_data1 ON data1 USING gist (col1) ;
CREATE INDEX
postgres=> CREATE INDEX idx2_data1 ON data1 USING gist (col2) ;
CREATE INDEX

The analysis now uses the database’s default collation. Previous versions used the database’s CTYPE setting. [e113f9c]

3.2.23 PL/pgSQL

The following features have been added to the PL/pgSQL module.

Optimizing Simple SELECT INTO Statements

The execution of the SELECT INTO statement that uses a simple scalar expression to assign to a single variable is no longer treated as an SQL statement execution; instead, it is now converted into an assignment operation. Due to this update, the target SQL statement will no longer trigger hooks in the pg_stat_statements module. [ce8d5fe]

Reserved Word Removal

EXECUTE and STRICT have been removed from the reserved word list. These words can now be used as variable names and for other purposes. [999f172]

Example 91 Using reserved keywords as variable names
postgres=> DO $$
DECLARE
   EXECUTE INT;
BEGIN
   EXECUTE := 100;
END;
$$;
DO
postgres=>

PDF page 80

3.2.24 PL/Python

Events trigger support has been added. [53eff47]

3.2.25 Optimizer

The faster execution plan is now selected.

Eager Aggregation

Eager aggregation optimization has been implemented to reduce the number of input tuples in join operations by performing partial aggregation before the join. This optimization is controlled by the enable_eager_aggregate parameter. The default value is “on”, meaning it is enabled. The group size required to consider eager aggregation is specified by the min_eager_agg_group_size parameter. The default value for this parameter is 8. [8e11859]

Example 92 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.col1, avg(t2.col3)
FROM eager1 AS t1 JOIN eager2 AS t2 ON t1.col2 = t2.col2
GROUP BY t1.col1 ORDER BY t1.col1 ;
                      QUERY PLAN
-------------------------------------------------------
 GroupAggregate
   Output: t1.col1, avg(t2.col3)
   Group Key: t1.col1
   ->  Sort
         Output: t1.col1, t2.col3
         Sort Key: t1.col1
         ->  Hash Join
               Output: t1.col1, t2.col3
               Hash Cond: (t2.col2 = t1.col2)
               ->  Seq Scan on public.eager2 t2
                     Output: t2.col1, t2.col2, t2.col3
               ->  Hash
                     Output: t1.col1, t1.col2
                     ->  Seq Scan on public.eager1 t1
                           Output: t1.col1, t1.col2
(15 rows)

PDF page 81

Example 93 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN (VERBOSE, COSTS OFF) SELECT t1.col1, avg(t2.col3)
FROM eager1 AS t1 JOIN eager2 AS t2 ON t1.col2 = t2.col2
GROUP BY t1.col1 ORDER BY t1.col1 ;
                            QUERY PLAN
-------------------------------------------------------------------
 Finalize GroupAggregate
   Output: t1.col1, avg(t2.col3)
   Group Key: t1.col1
   ->  Sort
         Output: t1.col1, (PARTIAL avg(t2.col3))
         Sort Key: t1.col1
         ->  Hash Join
               Output: t1.col1, (PARTIAL avg(t2.col3))
               Hash Cond: (t1.col2 = t2.col2)
               ->  Seq Scan on public.eager1 t1
                     Output: t1.col1, t1.col2, t1.col3
               ->  Hash
                     Output: t2.col2, (PARTIAL avg(t2.col3))
                     ->  Partial HashAggregate
                           Output: t2.col2, PARTIAL avg(t2.col3)
                           Group Key: t2.col2
                           ->  Seq Scan on public.eager2 t2
                                 Output: t2.col1, t2.col2, t2.col3
(18 rows)
Using Memoize with ANTI JOIN

The memoization feature is now available for SEMI JOIN and ANTI JOIN. [0da29e4]

PDF page 82

Example 94 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN (COSTS OFF, BUFFERS OFF)
SELECT COUNT(*) FROM data1 d1 LEFT JOIN
LATERAL (SELECT DISTINCT ON (c1) c1, c2, d1.c1 AS x FROM data1 d2) d2
ON d1.c1 + 1 = d2.c1 WHERE d2.c1 IS NULL ;
                      QUERY PLAN
------------------------------------------------------
 Aggregate
   ->  Nested Loop Anti Join
         ->  Seq Scan on data1 d1
         ->  Subquery Scan on d2
               Filter: ((d1.c1 + 1) = d2.c1)
               ->  Unique
                     ->  Sort
                           Sort Key: d2_1.c1
                           ->  Seq Scan on data1 d2_1
(9 rows)

PDF page 83

Example 95 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN (COSTS OFF, BUFFERS OFF)
SELECT COUNT(*) FROM data1 d1 LEFT JOIN
LATERAL (SELECT DISTINCT ON (c1) c1, c2, d1.c1 AS x FROM data1 d2) d2
ON d1.c1 + 1 = d2.c1 WHERE d2.c1 IS NULL ;
                         QUERY PLAN
------------------------------------------------------------
 Aggregate
   ->  Nested Loop Anti Join
         ->  Seq Scan on data1 d1
         ->  Memoize
               Cache Key: (d1.c1 + 1), d1.c1
               Cache Mode: binary
               ->  Subquery Scan on d2
                     Filter: ((d1.c1 + 1) = d2.c1)
                     ->  Unique
                           ->  Sort
                                 Sort Key: d2_1.c1
                                 ->  Seq Scan on data1 d2_1
(12 rows)
Rewriting the COUNT function

If possible, the COUNT(any) clause is rewritten as a COUNT(*) clause. This rewrite is limited to cases where a non-NULL column is specified and there is no ORDER BY or DISTINCT clause. This change reduces overhead and improves performance. [42473b3]

PDF page 84

Example 96 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN (VERBOSE) SELECT COUNT(col1) FROM data1 ;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Aggregate  (cost=17906.00..17906.01 rows=1 width=8)
   Output: count(col1)
   ->  Seq Scan on public.data1  (cost=0.00..15406.00 rows=1000000 width=4)
         Output: col1, col2
(4 rows)
Example 97 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN (VERBOSE) SELECT COUNT(col1) FROM data1 ;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Aggregate  (cost=17906.00..17906.01 rows=1 width=8)
   Output: count(*)
   ->  Seq Scan on public.data1  (cost=0.00..15406.00 rows=1000000 width=0)
         Output: col1, col2
(4 rows)
Parallelization of TID Range Scans

It is now possible to execute TID Range Scans using parallel queries. [0ca3b16]

Example 98 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE ctid > '(10,0)' AND ctid < '(30,1)';
                           QUERY PLAN
-----------------------------------------------------------------
 Tid Range Scan on data1  (cost=0.01..61.00 rows=3700 width=10)
   TID Cond: ((ctid > '(10,0)'::tid) AND (ctid < '(30,1)'::tid))
(2 rows)

PDF page 85

Example 99 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE ctid > '(10,0)' AND ctid < '(30,1)';
                                  QUERY PLAN
------------------------------------------------------------------------------
 Gather  (cost=0.01..33.25 rows=3700 width=10)
   Workers Planned: 4
   ->  Parallel Tid Range Scan on data1  (cost=0.01..33.25 rows=925 width=10)
         TID Cond: ((ctid > '(10,0)'::tid) AND (ctid < '(30,1)'::tid))
(4 rows)
Partial Hash Index

HASH indexes created as partial indexes can now be used in execution plans when a WHERE clause is specified that matches their conditions. This feature has been backported to PostgreSQL 14 and later. [5528e8d]

Example 100 PostgreSQL 18 Execution Plan
postgres=> CREATE INDEX idx1_data1 ON data1 USING hash (col2) WHERE col2=1000 ;
CREATE INDEX
postgres=> EXPLAIN SELECT * FROM data1 WHERE col2=2000 ;
                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on data1  (cost=0.00..17906.00 rows=1 width=14)
   Filter: (col2 = 2000)
(2 rows)
Example 101 PostgreSQL 19 Execution Plan
postgres=> CREATE INDEX idx1_data1 ON data1 USING hash (col2) WHERE col2=1000 ;
CREATE INDEX
postgres=> EXPLAIN SELECT * FROM data1 WHERE col2=1000 ;
                               QUERY PLAN
-------------------------------------------------------------------------
 Index Scan using idx1_data1 on data1  (cost=0.00..8.02 rows=1 width=14)
   Index Cond: (col2 = 1000)
(2 rows)

PDF page 86

Optimization of the IS DISTINCT FROM clause

The IS DISTINCT FROM clause for columns with a NOT NULL constraint is now converted to the “<>” operator. [0a37961]

Example 102 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE col1 IS DISTINCT FROM col1 ;
                          QUERY PLAN
---------------------------------------------------------------
 Seq Scan on data1  (cost=0.00..17906.00 rows=995000 width=10)
   Filter: (col1 IS DISTINCT FROM col1)
(2 rows)
Example 103 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE col1 IS DISTINCT FROM col1 ;
                          QUERY PLAN
---------------------------------------------------------------
 Seq Scan on data1  (cost=0.00..17906.00 rows=995000 width=10)
   Filter: (col1 <> col1)
(2 rows)
Anti-Join Transformation of the NOT IN Clause

If it is determined to be safe, the NOT IN clause uses an Anti-Join in the execution plan. [383eb21]

PDF page 87

Example 104 PostgreSQL 18 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE col1 NOT IN (SELECT col1 FROM
 data1 WHERE col1 < 10000) ;
                                QUERY PLAN
--------------------------------------------------------------------------
 Seq Scan on data1  (cost=194.99..364.97 rows=5000 width=8)
   Filter: (NOT (ANY (col1 = (hashed SubPlan 1).col1)))
   SubPlan 1
     ->  Seq Scan on data1 data1_1  (cost=0.00..169.99 rows=9999 width=4)
           Filter: (col1 < 10000)
(5 rows)
Example 105 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN SELECT * FROM data1 WHERE col1 NOT IN (SELECT col1 FROM
data1 WHERE col1 < 10000) ;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Hash Anti Join  (cost=29.98..48.71 rows=1 width=8)
   Hash Cond: (data1.col1 = data1_1.col1)
   ->  Seq Scan on data1  (cost=0.00..14.99 rows=999 width=8)
   ->  Hash  (cost=17.49..17.49 rows=999 width=4)
         ->  Seq Scan on data1 data1_1  (cost=0.00..17.49 rows=999 width=4)
               Filter: (col1 < 10000)
(6 rows)
Details of the Result Node

Information about the source of the result has been added to the Result node. As a result, information from the “Replace:” node will now be included in the execution results of the EXPLAIN statement. [f2bae51]

PDF page 88

Example 106 PostgreSQL 19 Execution Plan
postgres=> EXPLAIN (COSTS OFF) SELECT * FROM data1 WHERE
col1=(SELECT MAX(col1) FROM data1) ;
                                    QUERY PLAN
----------------------------------------------------------------------------…
 Index Scan using data1_pkey on data1
   Index Cond: (col1 = (InitPlan expr_1).col1)
   InitPlan expr_1
     ->  Result
           Replaces: MinMaxAggregate
           InitPlan minmax_1
             ->  Limit
                   ->  Index Only Scan Backward using data1_pkey on data1 …
(8 rows)