I got a large (>100M rows) Postgres table with structure {integer, integer, integer, timestamp without time zone}. I expected the size of a row to be 3*integer + 1*timestamp = 3*4 + 1*8 = 20 bytes.
In reality the row size is pg_relation_size(tbl) / count(*) = 52 bytes. Why?
(No deletes are done against the table: pg_relation_size(tbl, 'fsm') ~= 0)
Calculation of row size is more complex than that.
Storage is typically partitioned in data pages of 8 kB. There is a small fixed overhead per page, possible remainders not big enough to fit another tuple, and more importantly dead rows or a percentage initially reserved with the
FILLFACTORsetting.And there is more overhead per row (tuple): an item identifier of 4 bytes at the start of the page, the
HeapTupleHeaderof 23 bytes and alignment padding. The start of the tuple header as well as the start of tuple data are aligned at a multiple ofMAXALIGN, which is 8 bytes on a typical 64-bit machine. Some data types require alignment to the next multiple of 2, 4 or 8 bytes.Quoting the manual on the system table
pg_type:Read about the basics in the manual.
Your example
This results in 4 bytes of padding after your 3
integercolumns, because thetimestampcolumn requiresdoublealignment and needs to start at the next multiple of 8 bytes.So, one row occupies:
Plus item identifier per tuple in the page header (as pointed out by @A.H. in the comment):
So we arrive at the observed 52 bytes.
The calculation
pg_relation_size(tbl) / count(*)is a pessimistic estimation.pg_relation_size(tbl)includes bloat (dead rows) and space reserved byfillfactor, as well as overhead per data page and per table. (And we didn’t even mention compression for longvarlenadata in TOAST tables, since it doesn’t apply here.)You can install the additional module pgstattuple and call
SELECT * FROM pgstattuple('tbl_name');for more information on table and tuple size.Related: