Wie viel Platz braucht NULL?

Beim letzten Beratungseinsatz beim Kunden, kam dieser freudestrahlend auf mich zu mit der Bemerkung: Er habe meinen Rat befolgt und sämtiche Primary Key Spalten von BIGINT (8 byte) auf INT (4 byte) geändert und das habe viel gebracht! Seine MySQL 8.4er Datenbank sei jetzt um 750 Gbyte kleiner geworden (von 5.5 Tbyte). Schön!

Und ja, ich weiss, dass wiederspricht den Empfehlungen einiger meiner PostgreSQL Kollegen (hier und hier). In der MySQL-Welt wird eher auf solche Dinge Wert gelegt (Quelle):

Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables

Zudem funktioniert InnoDB ein klein wening anders (Index Clusterd Table und Primary Key in allen Secondary Keys) als PostgreSQL (Heap Table, Indices mit Row Pointer (ctid)).

Aber das ist eigentlich nicht das Thema. Sofort im Anschluss kam er nämlich mit der Frage, ob das AusNULLen von Spalten vom Typ DOUBLE (8 byte, in PostgreSQL-Sprech DOUBLE PRECISION) auch Platzeinsparungen brächte oder ob er die Spalten lieber gleich droppen solle. Meine erste reflexartige Antwort bei DOUBLE war: NULL bringt was, gefolgt von OPTIMIZE TABLE (in PostgreSQL-Sprech VACUUM FULL). Der zweite Gedanke war aber, DOUBLE ist ein Datentyp fixer länger, gilt das mit NULL da auch oder nur bei Datentypen mit variabler Länge? Vorsicht ist die Mutter der Porzelankiste! Liebe zuerst mal das Manual konsultieren…

Und dort steht (Quelle):

Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.

und (Quelle):

The variable-length part of the record header contains a bit vector for indicating NULL columns. … Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.

Versuch mit MariaDB/MySQL

Versuchsaufbau

Irgendwie ist mir das etwas zu komplizert beschrieben. Eine kleine Skizze würde vielleicht helfen? Also machen wir einen Versuch:

SQL> -- DROP TABLE IF EXISTS tracking;

SQL> CREATE TABLE tracking (
  id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE
);

SQL> INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
SQL> INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows

Die Tabelle wird sowohl bei MariaDB als auch bei MySQL bei 16 M Rows ca. 1.8 Gbyte gross. Da dieses Informationen im INFORMATION_SCHEMA nur sehr unpräzise angegeben werden schauen wir auf dem Filesystem nach:

MariaDB 11.8:

SQL> system ls -l tracking.ibd
-rw-rw---- 1 mysql mysql       1206 Feb  7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb  7 10:32 tracking.ibd

MySQL 8.4:

SQL> system ls -l tracking.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb  7 10:33 tracking.ibd

Defragementieren der Tabelle

Dann “defragmentieren” wir die Tabelle mit dem OPTIMIZE TABLE Befehl:

SQL> OPTIMIZE TABLE tracking;
+---------------+----------+----------+-------------------------------------------------------------------+
| Table         | Op       | Msg_type | Msg_text                                                          |
+---------------+----------+----------+-------------------------------------------------------------------+
| test.tracking | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
| test.tracking | optimize | status   | OK                                                                |
+---------------+----------+----------+-------------------------------------------------------------------+

Achtung: Die Tabelle wir einmal umkopiert! Es braucht also kurzzeitig die doppelte Menge an Diskplatz! Dies können wird schön beobachten während der OPTIMIZE TABLE Befehls läuft:

MariaDB:

$ watch -d -n 1 'ls -l trac* \#*'
-rw-rw---- 1 mysql mysql       1206 Feb  7 10:39 '#sql-alter-d57-8c.frm'
-rw-rw---- 1 mysql mysql  968884224 Feb  7 10:39 '#sql-alter-d57-8c.ibd'
-rw-rw---- 1 mysql mysql       1206 Feb  7 10:28  tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb  7 10:32  tracking.ibd

MySQL:

$ watch -d -n 1 'ls -l trac* \#*'
-rw-r----- 1 mysql mysql  369098752 Feb  7 10:40 #sql-ib1594-4164062678.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb  7 10:33 tracking.ibd

Das Resultat ist dann aber erstaunlich! Bei MariaDB ist die Tabelle in etwas gleich gross geblieben:

-rw-rw---- 1 mysql mysql       1206 Feb  7 10:39 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb  7 10:39 tracking.ibd

Bei MySQL hingegen ist die Tabelle nach dem “defragmentieren” sogar angewachsen und zwar um ca. 14%:

-rw-r----- 1 mysql mysql 2197815296 Feb  7 10:41 tracking.ibd

Wenn wir den OPTIMIZE TABLE Befehl nochmal ausführen bleibt die Grösse sowohl bei MariaDB als auch MySQL konstant:

MariaDB:

-rw-rw---- 1 mysql mysql       1206 Feb  7 10:46 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb  7 10:48 tracking.ibd

MySQL:

-rw-r----- 1 mysql mysql 2197815296 Feb  7 10:48 tracking.ibd

Versuch 1: AusNULLen

Jetzt NULLen wir die Werte aus:

SQL> UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
  , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;

Nach diesem Schritt sind die Grössen der Dateien sogar noch etwas gewachsen:

MariaDB (+1.3%):

-rw-rw---- 1 mysql mysql       1206 Feb  7 10:49 tracking.frm
-rw-rw---- 1 mysql mysql 1937768448 Feb  7 11:04 tracking.ibd

MySQL (+0.2%):

-rw-r----- 1 mysql mysql 2202009600 Feb  7 11:04 tracking.ibd

Anschliessend defragemntieren wir die Tabelle erneut mit dem OPTIMIZE TABLE Befehl. Die Tabellen schrumpfen wie erwartet.

MariaDB (auf 23%):

-rw-rw---- 1 mysql mysql      1206 Feb  7 11:09 tracking.frm
-rw-rw---- 1 mysql mysql 448790528 Feb  7 11:10 tracking.ibd

MySQL (auf 24%):

-rw-r----- 1 mysql mysql 520093696 Feb  7 11:10 tracking.ibd

Ein erneutes OPTIMIZE TABLE bringt anschliessend KEINE Veränderung mehr in der Dateigrösse…

Versuch 2: Löschen der Spalten

Jetzt probieren wir das Ganze nochmal mit dem DROP COLUMN Befehl. Die Ausganglage ist wieder die selbe wie oben beschrieben:

MariaDB:

-rw-rw---- 1 mysql mysql       1206 Feb  7 11:15 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb  7 11:18 tracking.ibd

MySQL:

-rw-r----- 1 mysql mysql 1929379840 Feb  7 11:19 tracking.ibd

Nach dem OPTIMIZE TABLE Befehl sehen die Werte ähnlich aus wie im ersten Versuch:

MariaDB:

-rw-rw---- 1 mysql mysql       1206 Feb  7 11:20 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb  7 11:21 tracking.ibd

MySQL:

-rw-r----- 1 mysql mysql 2197815296 Feb  7 11:21 tracking.ibd

Ein erneutes OPTIMIZE TABLE bring ebenfalls keine weiteren Veränderungen mehr, wie oben:

MariaDB:

-rw-rw---- 1 mysql mysql       1206 Feb  7 11:22 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb  7 11:23 tracking.ibd

MySQL:

-rw-r----- 1 mysql mysql 2197815296 Feb  7 11:24 tracking.ibd

Und jetzt der eigentliche zweite Versuch mit Löschen der Spalten:

SQL> ALTER TABLE tracking
  DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;

Als erstes stellen wir fest, dass der Befehl INSTANTANEOUS ist, als keinerlei Änderung an den Daten vornimmt sondern nur Änderung an den Metadaten. Das ist auf der einen Seite gut, da der Einfluss auf die Applikation dadurch minimal gehalten wird.. Auf der anderen Seite bedeutet das ja auch: Keine Platzersparnis.

Also rücken wir dem Ganzen wieder mit dem OPTIMIZE TABLE Befehl zu Leibe:

MariaDB (auf 93%):

-rw-rw---- 1 mysql mysql       925 Feb  7 11:28 tracking.frm
-rw-rw---- 1 mysql mysql 415236096 Feb  7 11:29 tracking.ibd

MySQL (auf 92%):

-rw-r----- 1 mysql mysql 478150656 Feb  7 11:28 tracking.ibd

Fazit

Sowohl das ausNULLen der Spalten als auch das Löschen der Spalten bringen signifikante Platzersparnis. Wobei das Löschen der Spalten nochmal ca. 7% mehr bringt als das ausNULLen. Wenn es applikatorisch möglich ist, sollte man daher nicht mehr benötigte Spalen löschen, wenn nicht möglich, diese zumindest ausNULLen.

Versuch mit PostgreSQL

Und jetzt schauen wir uns das Ganze noch bei PostgreSQL 19devel an.

Versuchsaufbau

Der Versuchsaufbau erfolgt analog zu MariaDB/MySQL:

postgres=# -- DROP TABLE IF EXISTS tracking;

postgres=# CREATE TABLE tracking (
  id SERIAL PRIMARY KEY
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION
);

postgres=# \timing

postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
  SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
  SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows

Zuerst wollen wir mal wissen, wie gross die Tabelle eigentlich geworden ist. PostgreSQL scheint diese Informationen sehr genau zu kennen:

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
     , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
     , pg_indexes_size('tracking') AS idx_siz
     , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
     , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
     , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
     , pg_total_relation_size('tracking') AS tot_rel_siz
     , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
  tab_siz   | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB       | 376856576 | 359 MB        |      2340274176 | 2232 MB               |  2340798464 | 2232 MB

Dann wollen wir wissen, wo im Filesystem diese Dateien zu finden sind:

postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();
 db_oid
--------
      5

postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode
  FROM pg_class WHERE relname = 'tracking';
 table_oid | relname  | relnamespace | relfilenode
-----------+----------+--------------+-------------
     40965 | tracking |         2200 |       40965

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid
  FROM pg_index i
  JOIN pg_class c ON i.indrelid = c.oid
 WHERE c.relname = 'tracking';
  index_name   | index_oid
---------------+-----------
 tracking_pkey |     40970

postgres=# SELECT pg_relation_filepath('tracking');
 pg_relation_filepath
----------------------
 base/5/40965

Tabellen- und Indexgrösse im Filesystem:

$ ls -ltr 40965* 40970*
-rw------- 1 mysql mysql      40960 Feb  7 18:33 40965_vm
-rw------- 1 mysql mysql     499712 Feb  7 18:33 40965_fsm
-rw------- 1 mysql mysql  889675776 Feb  7 18:34 40965.1
-rw------- 1 mysql mysql 1073741824 Feb  7 18:34 40965
-rw------- 1 mysql mysql  376856576 Feb  7 18:35 40970
  • *_fsm bedeutet “free space map”
  • *_vm bedeutet “visibility map”
  • *.1 bedeutet 2. Segment des Objekts (Tabelle oder Index)

PostgreSQL scheint per default mit Segmenten von 1 Gbyte Grösse zu arbeiten und im Unterschied zu MariaDB/MySQL (INFORMATION_SCHEMA) sehr genau zu wissen, wie gross seine Dateien sind. Und die Diskrepanz von oben (zwischen tot_rel_siz und tab_and_idx_siz) lässt sich durch die fsm und die vm-Dateien erklären.

Das PostgreSQL Equivalent zum MariaDB/MySQL OPTIMIZE TABLE ist der Befehl VACUUM FULL:

postgres=# VACUUM FULL tracking;

$ ls -ltr
-rw------- 1 mysql mysql      40960 Feb  7 18:39 40965_vm
-rw------- 1 mysql mysql     499712 Feb  7 18:39 40965_fsm
-rw------- 1 mysql mysql 1073741824 Feb  7 18:39 40965
-rw------- 1 mysql mysql  889675776 Feb  7 18:39 40965.1
-rw------- 1 mysql mysql 1073741824 Feb  7 18:39 40972
-rw------- 1 mysql mysql  889675776 Feb  7 18:39 40972.1
-rw------- 1 mysql mysql          0 Feb  7 18:39 40975

...

-rw------- 1 mysql mysql      49152 Feb  7 18:39 2704
-rw------- 1 mysql mysql      32768 Feb  7 18:39 2703
-rw------- 1 mysql mysql      32768 Feb  7 18:39 2696
-rw------- 1 mysql mysql      65536 Feb  7 18:39 2674
-rw------- 1 mysql mysql      81920 Feb  7 18:39 2673
-rw------- 1 mysql mysql      98304 Feb  7 18:39 2659
-rw------- 1 mysql mysql     139264 Feb  7 18:39 2658
-rw------- 1 mysql mysql      24576 Feb  7 18:39 2619_fsm
-rw------- 1 mysql mysql     163840 Feb  7 18:39 2619
-rw------- 1 mysql mysql     106496 Feb  7 18:39 2608
-rw------- 1 mysql mysql     491520 Feb  7 18:39 1249
-rw------- 1 mysql mysql     122880 Feb  7 18:39 1247
-rw------- 1 mysql mysql      32768 Feb  7 18:39 2662
-rw------- 1 mysql mysql     114688 Feb  7 18:39 1259
-rw------- 1 mysql mysql      16384 Feb  7 18:39 3455
-rw------- 1 mysql mysql      49152 Feb  7 18:39 2663
-rw------- 1 mysql mysql 1073741824 Feb  7 18:39 40972
-rw------- 1 mysql mysql  889675776 Feb  7 18:39 40972.1
-rw------- 1 mysql mysql  376864768 Feb  7 18:39 40975
-rw------- 1 mysql mysql          0 Feb  7 18:39 40965
-rw------- 1 mysql mysql          0 Feb  7 18:39 40970

Das erste, was man feststellt, ist, dass PostgreSQL ganz viele Dateien anlangt und die “free space map” Datei ist verschwunden. Die Tabellen-Segmente sind dafür, im Unterschied zu MariaDB/MySQL gleich gross geblieben. Zudem sieht man, dass die alte Tabelle “verschwunden” (40965, 40970) und eine neue entstanden (40972 und 40975) ist. Der VACUUM FULL Befehl bei PostgreSQL erstellt also ebenfalls, wie bei MariaDB/MySQL eine Kopie der Daten.

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
     , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
     , pg_indexes_size('tracking') AS idx_siz
     , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
     , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
     , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
     , pg_total_relation_size('tracking') AS tot_rel_siz
     , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
  tab_siz   | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB       | 376864768 | 359 MB        |      2340282368 | 2232 MB               |  2340282368 | 2232 MB

Um zu verstehen, welche Dateien/Objekte denn sonst noch so alles angelangt wurden, hilft folgendes Query:

postgres=# SELECT c.oid, c.relname, ns.nspname
FROM pg_class AS c
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
  oid  |              relname              |  nspname
-------+-----------------------------------+------------
 40965 | tracking                          | public
 40970 | tracking_pkey                     | public
  2619 | pg_statistic                      | pg_catalog
  1247 | pg_type                           | pg_catalog
  2703 | pg_type_oid_index                 | pg_catalog
  2704 | pg_type_typname_nsp_index         | pg_catalog
  2658 | pg_attribute_relid_attnam_index   | pg_catalog
  2659 | pg_attribute_relid_attnum_index   | pg_catalog
  2662 | pg_class_oid_index                | pg_catalog
  2663 | pg_class_relname_nsp_index        | pg_catalog
  3455 | pg_class_tblspc_relfilenode_index | pg_catalog
  2696 | pg_statistic_relid_att_inh_index  | pg_catalog
  2673 | pg_depend_depender_index          | pg_catalog
  2674 | pg_depend_reference_index         | pg_catalog
  1249 | pg_attribute                      | pg_catalog
  1259 | pg_class                          | pg_catalog
  2608 | pg_depend                         | pg_catalog

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname
  FROM pg_index i
  JOIN pg_class c ON i.indrelid = c.oid
  JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
            index_name             | index_oid |  nspname
-----------------------------------+-----------+------------
 pg_type_typname_nsp_index         |      2704 | pg_catalog
 pg_attribute_relid_attnam_index   |      2658 | pg_catalog
 tracking_pkey                     |     40970 | public
 pg_class_relname_nsp_index        |      2663 | pg_catalog
 pg_class_tblspc_relfilenode_index |      3455 | pg_catalog
 pg_type_oid_index                 |      2703 | pg_catalog
 pg_attribute_relid_attnum_index   |      2659 | pg_catalog
 pg_statistic_relid_att_inh_index  |      2696 | pg_catalog
 pg_depend_depender_index          |      2673 | pg_catalog
 pg_depend_reference_index         |      2674 | pg_catalog
 pg_class_oid_index                |      2662 | pg_catalog

Versuch 1: AusNULLen

Dann NULLen wir die Spalten bei PostgreSQL ebenfalls aus. Die Sicht aufs Datei-System sparen wir uns ab hier, da PostgreSQL die Dateigrössen ja genau zu kennen scheint, wie wir oben gesehen haben:

postgres=# UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
  , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
     , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
     , pg_indexes_size('tracking') AS idx_siz
     , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
     , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
     , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
     , pg_total_relation_size('tracking') AS tot_rel_siz
     , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
  tab_siz   | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 2695716864 | 2571 MB       | 753696768 | 719 MB        |      3449413632 | 3290 MB               |  3450101760 | 3290 MB

Hier sehen wir, dass die Tabellen-Segmente massiv anwachsen (+37%), was in der PostgreSQL Terminologie als “bloat” bezeichnet wird. Die MVCC Implementierung von PostgreSQL speichert sowohl die alte auch nie neue Version der Row “in-place” also direkt in der Tabelle im Gegensatz zu MariaDB/MySQL welche die alte Version im UNDO Space und die neue Row “in-place” speichert. Auch die Index-Datei vergrössert sich signifikant (+100%). Dazu müssen wir noch weiter forschen, warum das so ist. Zudem wird wieder eine “free space map” erstellt (Differenz zwischen tot_rel_siz und tab_and_idx_siz).

Ein anschliessendes VACUUM FULL verkleinert die Tabelle (auf 28%) und den Index (auf 50%) wieder im Bezug auf die vorherige Grösse:

postgres=# VACUUM FULL tracking;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
     , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
     , pg_indexes_size('tracking') AS idx_siz
     , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
     , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
     , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
     , pg_total_relation_size('tracking') AS tot_rel_siz
     , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
  tab_siz  | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB        | 376864768 | 359 MB        |      1119780864 | 1068 MB               |  1119780864 | 1068 MB

und auch in Bezug zur ursprünglichen Grösse wird die Tabelle (auf 38%) und der Index (auf 100%) wieder kleiner. Warum der Index gleich gross geblieben ist und nur die Tabelle geschrumpft ist, muss noch recherchiert werden…

Versuch 2: Löschen der Spalten

Anschliessend werden die Spalten noch mit DROP COLUMN gelöscht.

postgres=# ALTER TABLE tracking
  DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;

Da die Rückmeldung sofort kam, kann davon ausgegangen werden, dass diese Operation ebenfalls instant erfolgt. Leider habe ich auf die Schnelle nichts in der PostgreSQL Dokumentation dazu gefunden.

An der Grösse hat sich nichts signifikant geändert, was bei einer Instant-Operation ja eigentlich auch zu erwarten ist. Dass sich jedoch nach dem VACUUM FULL Befehl die Grösse nicht mehr geändert hat, hat doch ein wenig erstaunt:

  tab_siz  | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB        | 376864768 | 359 MB        |      1119780864 | 1068 MB               |  1120010240 | 1068 MB

postgres=# VACUUM FULL tracking;

  tab_siz  | tab_siz_prtty |  idx_siz  | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB        | 376864768 | 359 MB        |      1119780864 | 1068 MB               |  1119780864 | 1068 MB

Bemerkungen

Locking bei PostgreSQL funktioniert wie folgt:

  • VACUUM Nebenläufige DML Befehle sind möglich ähnlich wie beim MariaDB/MySQL OPTIMIZE TABLE Befehl. Das Resultat ist aber nicht ganz das Selbe.
  • VACUUM FULL verursacht einen ACCESS EXCLUSIVE Lock. Ähnlich wie bei MariaDB/MySQL 5.5 und älter beim OPTIMIZE TABLE Befehl. DML und SELECT Befehle sind NICHT zulässig.

Quellen

Zusatzversuche

  1. Statt 0.0 wurde NULL in die Spalten d0 - d9 abgefüllt. Die Tabelle blieb klein (tot_rel_siz_prtty = 1068 MB). Es lohnt sich also auch bei PostgreSQL NULL statt Dummy-Werte zu speichern.
  2. Die Spalten d0 - d9 wurden mit DOUBLE PRECISION NOT NULL angelegt und die Werte 0.0 abgefüllt. Keinen Effekt: Die Tabelle blieb gross (tot_rel_siz_prtty = 2232 MB).