How to find the best order of columns to save on storage ("Column Tetris")
Does the column order matter in Postgres, storage-wise?
The answer is yes. Let's consider an example (as usual, my advice is NOT to use int4 PKs, but here it is done for educational purposes):
create table t(
id int4 primary key,
created_at timestamptz,
is_public boolean,
modified_at timestamptz,
verified boolean,
published_at timestamptz,
score int2
);
insert into t
select
i,
clock_timestamp(),
true,
clock_timestamp(),
true,
clock_timestamp(),
0
from generate_series(1, 1000000) as i;
vacuum analyze t;
Checking the size:
nik=# \dt+ t
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+-------+-------------
public | t | table | postgres | 81 MB |
(1 row)
Now, let's use the report p1
from postgres_dba (assuming it's installed):
:dba
Type your choice and press <Enter>:
p1
Table | Table Size | Comment | Wasted * | Suggested Columns Reorder
-------+------------+---------+-----------------+-----------------------------
t | 81 MB | | ~23 MB (28.40%) | is_public, score, verified +
| | | | id, created_at, modified_at+
| | | | published_at
(1 row)
-- the report claims we can save ~28%
of disk space just changing the column order. Note that this is an estimate.
Let's check the optimized order:
drop table t;
create table t(
is_public boolean,
verified boolean,
score int2,
id int4 primary key,
created_at timestamptz,
modified_at timestamptz,
published_at timestamptz
);
insert into t
select
true,
true,
0::int2,
i::int4,
clock_timestamp(),
clock_timestamp(),
clock_timestamp()
from generate_series(1, 1000000) as i;
vacuum analyze t;
Checking the size:
nik=# \dt+ t
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+-------+-------------
public | t | table | postgres | 57 MB |
(1 row)
– we saved ~30%
, very close to what was expected (57 / 81 ~= 0.7037).
postgres_dba's report p1 doesn't show potential savings anymore:
Type your choice and press <Enter>:
p1
Table | Table Size | Comment | Wasted * | Suggested Columns Reorder
-------+------------+---------+----------+---------------------------
t | 57 MB | | |
(1 row)
Why these changes reduced our table size
In Postgres, the storage system may add alignment padding to column values. If a data type's natural alignment requirement is more than the size of the value, Postgres may pad the value with zeros up to the alignment boundary. For instance, when a column has a value with a size less than 8 bytes followed by a value requiring 8-byte alignment, Postgres pads the first value to align on an 8-byte boundary. This helps ensure that the values in memory are aligned with CPU word boundaries for the particular hardware architecture, which can lead to performance improvements.
As an example, a row (int4
, timestamptz
) occupies 16 bytes:
- 4 bytes for
int4
- 4 zeroes to align to 8-bytes
- 8 bytes for
timestamptz
Some people prefer an inverted approach: first we start with 16- and 8-byte columns, then proceed to smaller ones. In
any case, it makes sense to put columns having VARLENA
types (text
, varchar
, json
, jsonb
, array) in the end.
Articles to read on this topic: