Why Dropping a Column does not Reclaim Disk Space? (or better, why is it so fast?)
Simple answer: because PostgreSQL knows how to do its job at best!Let’s create a dummy table to test this behavior against:
testdb=> CREATE TABLE foo( i int );
CREATE TABLE
testdb=> INSERT INTO foo
SELECT generate_series( 1, 10000000 );
INSERT 0 10000000
testdb=> SELECT pg_size_pretty( pg_relation_size( 'foo' ) );
pg_size_pretty
----------------
346 MB
(1 row)
testdb=> \timing
Timing is on.
testdb=> ALTER TABLE foo
ADD COLUMN t text
DEFAULT md5( random()::text );
ALTER TABLE
Time: 30702,872 ms (00:30,703)
31 secs
the table has grown with random data on every row to the extent of 651 MB
(almost the double of the original size):
testdb=> SELECT pg_size_pretty( pg_relation_size( 'foo' ) );
pg_size_pretty
----------------
651 MB
(1 row)
pg_attribute
catalog on all those attributes that are user-defined (i.e., attnum
is a positive value) and inspect the attisdropped
value that indicates if the column belongs or not to the table:
testdb=> SELECT attnum, attname, attisdropped
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE c.relname = 'foo'
AND c.relkind = 'r'
AND a.attnum > 0;
attnum | attname | attisdropped
--------|---------|--------------
1 | i | f
2 | t | f
(2 rows)
foo.i
and foo.t
are valid in the table, that means they have not been dropped.
It is now time to drop the columns and see the results:
testdb=> ALTER TABLE foo DROP COLUMN t;
ALTER TABLE
Time: 20,237 ms
We waited almost
31
seconds to add the new data and no one (20
milliseconds) to drop it away?
The documentation helps understanding it:
The DROP COLUMN form does not physically remove the column, but simply makes it invisible to SQL operations. Subsequent insert and update operations in the table will store a null value for the column. Thus, dropping a column is quick but it will not immediately reduce the on-disk size of your table, as the space occupied by the dropped column is not reclaimed. The space will be reclaimed over time as existing rows are updated.
DROP COLUMN
invalidates the column so that is has disappeared logically but not physically. Let’s inspect the table and its attributes again:
testdb=> SELECT pg_size_pretty( pg_relation_size( 'foo' ) );
pg_size_pretty
----------------
651 MB
(1 row)
testdb=> SELECT attnum, attname, attisdropped
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE c.relname = 'foo'
AND c.relkind = 'r'
AND a.attnum > 0;
attnum | attname | attisdropped
--------|------------------------------|--------------
1 | i | f
2 | ........pg.dropped.2........ | t
(2 rows)
t
attribute has been renamed as ........pg.dropped.2........
and is now marked as dropped from the table (attisdropped = t
).
Does that mean that it is possible from SQL to query the dropped column? No, this is not a recycle bin like mechanism:
testdb=> SELECT i, "........pg.dropped.2........" FROM foo limit 10;
ERROR: column "........pg.dropped.2........" does not exist
LINE 1: SELECT i, "........pg.dropped.2........" FROM foo limit 10;
pg_attribute
to allow the system to mangle that column even if the data type itself disappears.
Last, let’s fire a full table rewrite, for example with a
VACUUM
:
testdb=> VACUUM FULL foo;
VACUUM
Time: 20231,232 ms (00:20,231)
testdb=> SELECT pg_size_pretty( pg_relation_size( 'foo' ) ); pg_size_pretty
----------------
346 MB
(1 row)
Time: 1,519 ms
testdb=> SELECT attnum, attname, attisdropped
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE c.relname = 'foo'
AND c.relkind = 'r'
AND a.attnum > 0;
attnum | attname | attisdropped
--------|------------------------------|--------------
1 | i | f
2 | ........pg.dropped.2........ | t
(2 rows)
VACUUM
something good must be happened, and in fact the table space was reduced to the right (or better, the original) amount of space.
But why the dropped column is still mentioned in
pg_attribute
?
In this particular case it would have been dropped quite easily also from
pg_attribute
, but imagine a more complex tble where you drop a column in the middle of the attribute list: PostgreSQL would also have to rewrite all the attribute ordering with a quite expensive amount of work.
However, this approach has a potential drawback: being dropped attributes mentioned in
pg_attribute
as normal ones, they do count as table attributes and therefore could lower the number of real active attributes you can have in the table.