Skip to content

PostgreSQL: Edit and delete rows of a partitioned table - #1326

Merged
vrana merged 1 commit into
vrana:mainfrom
davidkrmela:pgsql-partitioned-rows
Aug 31, 2026
Merged

PostgreSQL: Edit and delete rows of a partitioned table#1326
vrana merged 1 commit into
vrana:mainfrom
davidkrmela:pgsql-partitioned-rows

Conversation

@davidkrmela

Copy link
Copy Markdown
Contributor

Editing or deleting a single row of a partitioned table affects one row in every partition.

limit1() identifies the row by its ctid, which is unique only within a single relation. A partitioned table is not one relation, so the condition is evaluated in each partition and matches whatever row sits at the same (block, offset) there. The ctid is used only when the row has no unique key, which is also the case where the damage goes unnoticed, because the other affected rows are unrelated to the edited one.

I hit this on a table with 64 partitions loaded without primary keys (they are created after the import for speed): deleting one row in Adminer reported 64 deleted rows, and the 63 unrelated posts were gone. It took a while to suspect the tool rather than the import.

Reproduction

CREATE TABLE parts (id int, val text) PARTITION BY LIST (id);
CREATE TABLE parts_1 PARTITION OF parts FOR VALUES IN (1);
CREATE TABLE parts_2 PARTITION OF parts FOR VALUES IN (2);
INSERT INTO parts VALUES (1, 'one'), (2, 'two');

Both rows are the first one in their partition, so both have the ctid (0,1). Open parts in Adminer, edit the row 1, one and save: the other row becomes uno as well. Deleting one row reports two affected rows.

Without the UI:

SELECT count(*) FROM parts WHERE ctid = (SELECT ctid FROM parts WHERE id = 1 LIMIT 1);
-- 2, one row per partition

SELECT count(*) FROM parts WHERE (tableoid, ctid) = (SELECT tableoid, ctid FROM parts WHERE id = 1 LIMIT 1);
-- 1

The change

(tableoid, ctid) restricts the condition to the partition holding the row.

  • tableoid is a system column of every table, so on an ordinary table the condition still matches the same single row.
  • Tables using the older INHERITS mechanism have the same ambiguity and are fixed too.
  • Views keep taking the is_view() branch.
  • Row-wise comparison against a subquery is long-standing PostgreSQL syntax; partitioned tables exist since PostgreSQL 10.

The test creates the table above, edits one row, checks that the row in the other partition kept its value, deletes one row and expects one affected row. It fails on the current code with 2 items have been affected.

Verified manually on PostgreSQL 18.6 for both UPDATE and DELETE, on partitioned and ordinary tables.

limit1() identifies the row by its ctid, which is unique only within a
single relation. A partitioned table is not one relation, so the
condition is evaluated in every partition and matches whatever row sits
at the same (block, offset) in each of them. Editing or deleting one row
of a table with 64 partitions can affect 64 rows.

The ctid is used only when the row has no unique key, which is also the
case where the damage goes unnoticed - the affected rows are unrelated to
the edited one.

Matching (tableoid, ctid) restricts the condition to the partition
holding the row. tableoid is a system column of every table, so the
statement keeps its behaviour on ordinary tables, and tables using
INHERITS are fixed as well.
@vrana

vrana commented Aug 31, 2026

Copy link
Copy Markdown
Owner

I am really sorry about this. Thanks for the excellent report and the fix.

@vrana
vrana merged commit ccc6ece into vrana:main Aug 31, 2026
1 check passed
@davidkrmela

Copy link
Copy Markdown
Contributor Author

Thank you for merging it, and for Adminer itself. It has helped me over the years, nice for something to go the other way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants