From a18ab759813fdfe30241231da41fb8cb8a97be67 Mon Sep 17 00:00:00 2001
From: Bernice Southey <bernice.southey@gmail.com>
Date: Wed, 24 Dec 2025 15:13:00 +0000
Subject: [PATCH] rework update and delete self-join examples

---
 doc/src/sgml/ddl.sgml        |  2 +-
 doc/src/sgml/ref/delete.sgml | 33 ++++++++++++++++++-----
 doc/src/sgml/ref/update.sgml | 51 +++++++++++++++---------------------
 3 files changed, 48 insertions(+), 38 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index cea28c00f8a..9070aaa5a7c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1558,7 +1558,7 @@ CREATE TABLE circles (
       locate the row version very quickly, a row's
       <structfield>ctid</structfield> will change if it is
       updated or moved by <command>VACUUM FULL</command>.  Therefore
-      <structfield>ctid</structfield> is useless as a long-term row
+      <structfield>ctid</structfield> should not be used as a row
       identifier.  A primary key should be used to identify logical rows.
      </para>
     </listitem>
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 5b52f77e28f..6a512b6bbaa 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -306,23 +306,42 @@ DELETE FROM tasks WHERE CURRENT OF c_tasks;
 </programlisting>
   </para>
 
-  <para>
-   While there is no <literal>LIMIT</literal> clause
-   for <command>DELETE</command>, it is possible to get a similar effect
-   using the same method described in <link linkend="update-limit">the
-   documentation of <command>UPDATE</command></link>:
+  <para id="delete-limit">
+   Deletes and updates affecting many rows can have negative effects on system
+   performance, such as table bloat, increased replica lag, and increased
+   lock contention.  In such situations it can make sense to perform the
+   operation in smaller batches, possibly with a <command>VACUUM</command>
+   operation on the table between batches.  While there is no <literal>LIMIT
+   </literal> clause for <command>DELETE</command>, it is possible to get a
+   similar effect through the use of a <link linkend="queries-with">Common
+   Table Expression</link> and a self-join.  A related example for <literal>
+   ORDER BY</literal> is described in <link linkend="update-order-by">the
+   documentation of <command>UPDATE</command></link>.  With the standard
+   <productname>PostgreSQL</productname> table access method, a self-join on
+   the system column <link linkend="ddl-system-columns-ctid">ctid</link> is
+   very efficient:
 <programlisting>
 WITH delete_batch AS (
   SELECT l.ctid FROM user_logs AS l
     WHERE l.status = 'archived'
-    ORDER BY l.creation_date
-    FOR UPDATE
+    FOR UPDATE SKIP LOCKED
     LIMIT 10000
 )
 DELETE FROM user_logs AS dl
   USING delete_batch AS del
   WHERE dl.ctid = del.ctid;
 </programlisting>
+   This command will need to be repeated until no rows remain to be deleted.
+   Use of <literal>FOR UPDATE</literal> with <literal>SKIP LOCKED</literal>
+   prevents deadlocks from occurring if another command has locked the same
+   rows in a different order. However an independent check for remaining rows
+   without <literal>SKIP LOCKED</literal> will be needed to ensure that no
+   matching rows were overlooked.  <literal>ORDER BY</literal> can be added to
+   prioritize which rows will be deleted.  <structfield>ctid</structfield> is 
+   safe here because <literal>FOR UPDATE</literal> with <literal>SKIP LOCKED in
+   </literal> <link linkend="xact-read-committed">Read Committed</link> mode
+   guarantees the rows in the <literal>DELETE</literal> are the same rows
+   returned by the <literal>SELECT</literal>. 
   </para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 40cca063946..24a814bc633 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -477,41 +477,32 @@ UPDATE films SET kind = 'Dramatic' WHERE CURRENT OF c_films;
 </programlisting>
   </para>
 
-  <para id="update-limit">
-   Updates affecting many rows can have negative effects on system
-   performance, such as table bloat, increased replica lag, and increased
-   lock contention.  In such situations it can make sense to perform the
-   operation in smaller batches, possibly with a <command>VACUUM</command>
-   operation on the table between batches.  While there is
-   no <literal>LIMIT</literal> clause for <command>UPDATE</command>, it is
-   possible to get a similar effect through the use of
+  <para id="update-order-by">
+   Updating or deleting multiple rows in the same table at the same time often
+   causes deadlocks. This can be solved by locking the rows in a consistent
+   order. While there is no <literal>ORDER BY</literal> clause for <literal>
+   UPDATE</literal>, it is possible to get a similar effect through the use of
    a <link linkend="queries-with">Common Table Expression</link> and a
-   self-join.  With the standard <productname>PostgreSQL</productname>
-   table access method, a self-join on the system
-   column <link linkend="ddl-system-columns-ctid">ctid</link> is very
-   efficient:
+   self-join.  A related example for <literal>LIMIT</literal> is described in
+   <link linkend="delete-limit"> the documentation of <command>DELETE</command>
+   </link>:
 <programlisting>
-WITH exceeded_max_retries AS (
-  SELECT w.ctid FROM work_item AS w
-    WHERE w.status = 'active' AND w.num_retries &gt; 10
-    ORDER BY w.retry_timestamp
+WITH lock_jobs AS (
+  SELECT id FROM jobs
+    JOIN complete_jobs USING (id)
+    ORDER BY id
     FOR UPDATE
-    LIMIT 5000
 )
-UPDATE work_item SET status = 'failed'
-  FROM exceeded_max_retries AS emr
-  WHERE work_item.ctid = emr.ctid;
+UPDATE jobs j SET status = l.status 
+  FROM lock_jobs AS l
+  WHERE j.id = l.id;
 </programlisting>
-   This command will need to be repeated until no rows remain to be updated.
-   Use of an <literal>ORDER BY</literal> clause allows the command to
-   prioritize which rows will be updated; it can also prevent deadlock
-   with other update operations if they use the same ordering.
-   If lock contention is a concern, then <literal>SKIP LOCKED</literal>
-   can be added to the <acronym>CTE</acronym> to prevent multiple commands
-   from updating the same row.  However, then a
-   final <command>UPDATE</command> without <literal>SKIP LOCKED</literal>
-   or <literal>LIMIT</literal> will be needed to ensure that no matching
-   rows were overlooked.
+   Use of <literal>FOR UPDATE</literal> prevents lock contention with other
+   update operations if they use the same ordering. In <link linkend=
+   "xact-read-committed">Read Committed</link> mode it's very important to use
+   an immutable column or (columns) for the self-join to guarantee the rows in
+   the <literal>UPDATE</literal> are the same rows returned by the <literal>
+   SELECT</literal>.
   </para>
  </refsect1>
 
-- 
2.43.0

