An experiment says more than a thousand theoretical considerations:
/* helper function to create large byteas */
CREATE OR REPLACE FUNCTION noise(size integer) RETURNS bytea
LANGUAGE plpgsql AS
$$DECLARE
b bytea := '';
i integer;
BEGIN
FOR i IN 0..size/16 LOOP
b := b || ('\x' || md5(random()::text))::bytea;
END LOOP;
RETURN b;
END;$$;
CREATE TABLE toastme (
id integer PRIMARY KEY,
a bytea,
b bytea
);
INSERT INTO toastme VALUES (
1,
noise(2048),
noise(2048)
);
Let's check the physical location of the new row in the main table:
SELECT ctid
FROM toastme
WHERE id = 1;
ctid
-------
(0,1)
(1 row)
Now let's have a look at the toast table, along with the physical location of the rows:
SELECT ctid
FROM toastme
WHERE id = 1;
ctid
-------
(0,2)
SELECT reltoastrelid::regclass AS toast_table
FROM pg_class
WHERE relname = 'toastme' \gset
SELECT ctid,
chunk_id,
chunk_seq,
substring(chunk_data FOR 10)
FROM :toast_table;
ctid | chunk_id | chunk_seq | substring
-------+----------+-----------+------------------------
(0,1) | 57174 | 0 | \x0f9eae178bcda3dbf745
(0,2) | 57174 | 1 | \xecbe99e83b8416d4f31c
(0,3) | 57175 | 0 | \x4b3b9425742c299c7d20
(0,4) | 57175 | 1 | \x43d1b1782ad3e0c1bf4c
(4 rows)
There are two chunk_id
s for the two table columns, each of which consists of two “slices” of TOAST.
Let's update one of the toasted columns:
UPDATE toastme
SET a = noise(2048)
WHERE id = 1;
Now let's check again:
SELECT ctid,
chunk_id,
chunk_seq,
substring(chunk_data FOR 10)
FROM :toast_table;
ctid | chunk_id | chunk_seq | substring
-------+----------+-----------+------------------------
(0,3) | 57175 | 0 | \x4b3b9425742c299c7d20
(0,4) | 57175 | 1 | \x43d1b1782ad3e0c1bf4c
(0,5) | 57176 | 0 | \xc780f3b8e2a5e4976f4a
(0,6) | 57176 | 1 | \x5702616863dfbc54829b
(4 rows)
What do we see:
When there is an update of the table row — even if only a toasted value is modified — a new row version is written.
Only the toasted values that are modified are changed, the unmodified toasted values stay the same.