UPDATE(7)

πŸ“¦ version 17.11, in opensuse-leap 16.0

πŸ¦‹ UPDATE πŸ¦‹

Okay bestie, this is the PostgreSQL 17.11 documentation page for UPDATE(7), and it’s literally the glow-up guide for changing rows in a table. No cap, it’s giving β€œmake it new, keep the old stuff you didn’t touch” energy. πŸ’…


🌸 NAME 🌸

  • UPDATE β€” update rows of a table. That’s it. That’s the whole vibe. ✨

✨ SYNOPSIS ✨

[ WITH [ RECURSIVE ] with_query [, ...] ]
UPDATE [ ONLY ] table_name [ * ] [ [ AS ] alias ]
    SET { column_name = { expression | DEFAULT } |
          ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) |
          ( column_name [, ...] ) = ( sub-SELECT )
        } [, ...]
    [ FROM from_item [, ...] ]
    [ WHERE condition | WHERE CURRENT OF cursor_name ]
    [ RETURNING { * | output_expression [ [ AS ] output_name ] } [, ...] ]

πŸ’– DESCRIPTION πŸ’–

UPDATE changes the values of the specified columns in all rows that satisfy the condition. Only the columns to be modified need to be mentioned in the SET clause; columns not explicitly modified retain their previous values. Periodt. 🫢

There are two ways to modify a table using information contained in other tables in the database: using sub-selects, or specifying additional tables in the FROM clause. Which technique is more appropriate depends on the specific circumstances. It’s giving β€œtwo paths, same destination, pick your fighter.” πŸ‘‘

The optional RETURNING clause causes UPDATE to compute and return value(s) based on each row actually updated. Any expression using the table’s columns, and/or columns of other tables mentioned in FROM, can be computed. The new (post-update) values of the table’s columns are used. The syntax of the RETURNING list is identical to that of the output list of SELECT. So yes, you can get receipts. 🧾✨

You must have the UPDATE privilege on the table, or at least on the column(s) that are listed to be updated. You must also have the SELECT privilege on any column whose values are read in the expressions or condition. Permissions check: slay only if you’re allowed. πŸ”


πŸŽ€ PARAMETERS πŸŽ€

  • with_query β€” the WITH clause allows you to specify one or more subqueries that can be referenced by name in the UPDATE query. See Section 7.8 and SELECT(7) for details. It’s giving β€œnamed subquery besties.” 🧚
  • table_name β€” the name (optionally schema-qualified) of the table to update. If ONLY is specified before the table name, matching rows are updated in the named table only. If ONLY is not specified, matching rows are also updated in any tables inheriting from the named table. Optionally, * can be specified after the table name to explicitly indicate that descendant tables are included. Know your table tree, bestie. 🌳
  • alias β€” a substitute name for the target table. When an alias is provided, it completely hides the actual name of the table. For example, given UPDATE foo AS f, the remainder of the UPDATE statement must refer to this table as f not foo. No deadnaming the table. πŸ’…
  • column_name β€” the name of a column in the table named by table_name. The column name can be qualified with a subfield name or array subscript, if needed. Do not include the table’s name in the specification of a target column β€” for example, UPDATE table_name SET table_name.col = 1 is invalid. That one is lowkey a no. πŸ™ˆ
  • expression β€” an expression to assign to the column. The expression can use the old values of this and other columns in the table. Old values are still in the chat. πŸ’¬
  • DEFAULT β€” set the column to its default value (which will be NULL if no specific default expression has been assigned to it). An identity column will be set to a new value generated by the associated sequence. For a generated column, specifying this is permitted but merely specifies the normal behavior of computing the column from its generation expression. Default era. ✨
  • sub-SELECT β€” a SELECT sub-query that produces as many output columns as are listed in the parenthesized column list preceding it. The sub-query must yield no more than one row when executed. If it yields one row, its column values are assigned to the target columns; if it yields no rows, NULL values are assigned to the target columns. The sub-query can refer to old values of the current row of the table being updated. One row max, bestie. 🚦
  • from_item β€” a table expression allowing columns from other tables to appear in the WHERE condition and update expressions. This uses the same syntax as the FROM clause of a SELECT statement; for example, an alias for the table name can be specified. Do not repeat the target table as a from_item unless you intend a self-join (in which case it must appear with an alias in the from_item). Self-join is a choice, not a default. πŸͺž
  • condition β€” an expression that returns a value of type boolean. Only rows for which this expression returns true will be updated. Boolean says yes or no, and UPDATE listens. βœ…
  • cursor_name β€” the name of the cursor to use in a WHERE CURRENT OF condition. The row to be updated is the one most recently fetched from this cursor. The cursor must be a non-grouping query on the UPDATE’s target table. Note that WHERE CURRENT OF cannot be specified together with a Boolean condition. See DECLARE(7) for more information about using cursors with WHERE CURRENT OF. Cursor said β€œthis one,” so it’s this one. πŸ–±οΈ
  • output_expression β€” an expression to be computed and returned by the UPDATE command after each row is updated. The expression can use any column names of the table named by table_name or table(s) listed in FROM. Write * to return all columns. Gimme the whole fit. 🌈
  • output_name β€” a name to use for a returned column. Name it and claim it. 🏷️

πŸ‘‘ OUTPUTS πŸ‘‘

On successful completion, an UPDATE command returns a command tag of the form:

UPDATE count

The count is the number of rows updated, including matched rows whose values did not change. Note that the number may be less than the number of rows that matched the condition when updates were suppressed by a BEFORE UPDATE trigger. If count is 0, no rows were updated by the query (this is not considered an error). Zero is valid, bestie. πŸ’€

If the UPDATE command contains a RETURNING clause, the result will be similar to that of a SELECT statement containing the columns and values defined in the RETURNING list, computed over the row(s) updated by the command. Receipts, but make them SQL. 🧾πŸ”₯


🌸 NOTES 🌸

When a FROM clause is present, what essentially happens is that the target table is joined to the tables mentioned in the from_item list, and each output row of the join represents an update operation for the target table. When using FROM you should ensure that the join produces at most one output row for each row to be modified. In other words, a target row shouldn’t join to more than one row from the other table(s). If it does, then only one of the join rows will be used to update the target row, but which one will be used is not readily predictable. That’s not a cute mystery, that’s a β€œplease don’t do this” mystery. 😭

Because of this indeterminacy, referencing other tables only within sub-selects is safer, though often harder to read and slower than using a join. Safety first, readability second, speed third. Pick your tradeoff, babe. πŸ¦‹

In the case of a partitioned table, updating a row might cause it to no longer satisfy the partition constraint of the containing partition. In that case, if there is some other partition in the partition tree for which this row satisfies its partition constraint, then the row is moved to that partition. If there is no such partition, an error will occur. Behind the scenes, the row movement is actually a DELETE and INSERT operation. Plot twist: the move is a delete-and-insert combo. πŸ’…

There is a possibility that a concurrent UPDATE or DELETE on the row being moved will get a serialization failure error. Suppose session 1 is performing an UPDATE on a partition key, and meanwhile a concurrent session 2 for which this row is visible performs an UPDATE or DELETE operation on this row. In such case, session 2’s UPDATE or DELETE will detect the row movement and raise a serialization failure error (which always returns with an SQLSTATE code '40001'). Applications may wish to retry the transaction if this occurs. In the usual case where the table is not partitioned, or where there is no row movement, session 2 would have identified the newly updated row and carried out the UPDATE/DELETE on this new row version. Concurrent chaos, honestly. 🚨

Note that while rows can be moved from local partitions to a foreign-table partition (provided the foreign data wrapper supports tuple routing), they cannot be moved from a foreign-table partition to another partition. One-way trip, iykyk. 🧳

An attempt of moving a row from one partition to another will fail if a foreign key is found to directly reference an ancestor of the source partition that is not the same as the ancestor that’s mentioned in the UPDATE query. Foreign keys said β€œnot on my watch.” πŸ”—

✨ EXAMPLES ✨

Okay bestie, real talk β€” this is the part where it all clicks. Let's go πŸŽ€

🌸 Swapping out a word

So we wanna change the word Drama to Dramatic in the kind column of the table films:

UPDATE films SET kind = 'Dramatic' WHERE kind = 'Drama';

Periodt. One line, whole column fixed. πŸ’…


🌈 Tweaking temps and resetting a default

Adjusting the temperature entries and resetting prcp back to its default value, all in one row of the table weather:

UPDATE weather SET temp_lo = temp_lo+1, temp_hi = temp_lo+15, prcp = DEFAULT
  WHERE city = 'San Francisco' AND date = '2003-07-03';

Same operation, but this time we get the updated entries back too:

UPDATE weather SET temp_lo = temp_lo+1, temp_hi = temp_lo+15, prcp = DEFAULT
  WHERE city = 'San Francisco' AND date = '2003-07-03'
  RETURNING temp_lo, temp_hi, prcp;

And if you're feeling the alternative column-list syntax vibe, same update, different outfit:

UPDATE weather SET (temp_lo, temp_hi, prcp) = (temp_lo+1, temp_lo+15, DEFAULT)
  WHERE city = 'San Francisco' AND date = '2003-07-03';

πŸ’– The FROM clause glow-up

Incrementing the sales count of whichever salesperson manages the account for Acme Corporation, using the FROM clause syntax:

UPDATE employees SET sales_count = sales_count + 1 FROM accounts
  WHERE accounts.name = 'Acme Corporation'
  AND employees.id = accounts.sales_person;

Same thing, but with a sub-select tucked inside the WHERE clause:

UPDATE employees SET sales_count = sales_count + 1 WHERE id =
  (SELECT sales_person FROM accounts WHERE name = 'Acme Corporation');

πŸ¦‹ Sub-select vs. join β€” the plot twist

Updating contact names in an accounts table so they match the currently assigned salespeople:

UPDATE accounts SET (contact_first_name, contact_last_name) =
    (SELECT first_name, last_name FROM employees
     WHERE employees.id = accounts.sales_person);

A similar result could be accomplished with a join β€” like, literally similar, not identical, and this is the important part:

UPDATE accounts SET contact_first_name = first_name,
                    contact_last_name = last_name
  FROM employees WHERE employees.id = accounts.sales_person;

However (🚨 read this twice), the second query may give unexpected results if employees.id is not a unique key, whereas the first query is guaranteed to raise an error if there are multiple id matches. Also, if there's no match for a particular accounts.sales_person entry, the first query will set the corresponding name fields to NULL, whereas the second query will not update that row at all. Different behaviour, different vibe β€” know which one you're picking. πŸ’€


🧚 Summary-table stats refresh

Updating the statistics in a summary table so they match the current data:

UPDATE summary s SET (sum_x, sum_y, avg_x, avg_y) =
    (SELECT sum(x), sum(y), avg(x), avg(y) FROM data d
     WHERE d.group_id = s.group_id);

πŸ“ The insert-or-update savepoint situation

Attempting to insert a new stock item along with the quantity of stock β€” and if the item already exists, updating the stock count of the existing item instead. To do this without failing the entire transaction, use savepoints:

BEGIN;
-- other operations
SAVEPOINT sp1;
INSERT INTO wines VALUES('Chateau Lafite 2003', '24');
-- Assume the above fails because of a unique key violation,
-- so now we issue these commands:
ROLLBACK TO sp1;
UPDATE wines SET stock = stock + 24 WHERE winename = 'Chateau Lafite 2003';
-- continue with other operations, and eventually
COMMIT;

Iconic. Transaction saved, vibe intact. πŸ’œ


🌟 Cursor-targeted update

Changing the kind column of the table films in the row the cursor c_films is currently sitting on:

UPDATE films SET kind = 'Dramatic' WHERE CURRENT OF c_films;

πŸ‘‘ Batching the big ones (this part matters, ngl)

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 VACUUM operation on the table between batches.

While there is no LIMIT clause for UPDATE, it is possible to get a similar effect through the use of a Common Table Expression and a self-join. With the standard PostgreSQL table access method, a self-join on the system column ctid is very efficient:

WITH exceeded_max_retries AS (
  SELECT w.ctid FROM work_item AS w
    WHERE w.status = 'active' AND w.num_retries > 10
    ORDER BY w.retry_timestamp
    FOR UPDATE
    LIMIT 5000
)
UPDATE work_item SET status = 'failed'
  FROM exceeded_max_retries AS emr
  WHERE work_item.ctid = emr.ctid;

This command will need to be repeated until no rows remain to be updated. (This use of ctid is only safe because the query is repeatedly run, avoiding the problem of changed ctids.) πŸ”₯

Use of an ORDER BY 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 SKIP LOCKED can be added to the CTE to prevent multiple commands from updating the same row. However, then a final UPDATE without SKIP LOCKED or LIMIT will be needed to ensure that no matching rows were overlooked. Don't skip that last step, bestie, I'm begging πŸ™ˆ


πŸ’… COMPATIBILITY πŸ’…

This command conforms to the SQL standard β€” except that the FROM and RETURNING clauses are PostgreSQL extensions, as is the ability to use WITH with UPDATE. So like, standard-adjacent. Standard with ✨flair✨.

Some other database systems offer a FROM option in which the target table is supposed to be listed again within FROM. That is not how PostgreSQL interprets FROM. Be careful when porting applications that use this extension β€” this is the kind of thing that bites you at 2am, fr fr. 😭

According to the standard, the source value for a parenthesized sub-list of target column names can be any row-valued expression yielding the correct number of columns. PostgreSQL only allows the source value to be a row constructor or a sub-SELECT. An individual column's updated value can be specified as DEFAULT in the row-constructor case, but not inside a sub-SELECT. No cap.


That's the whole page, bestie!! πŸ’– Go forth and UPDATE responsibly β€” you've got this, and honestly? You're gonna slay it. 🫢

This little masterpiece came to you from PostgreSQL 17.11, dated 2026, and it lives at UPDATE(7). Mwah. πŸŽ€πŸ’œ

πŸ“¦ where this page comes from