Estimer la taille totale d’une table¶
Stockage de données sous PostgreSQL¶
PostgreSQL stocke les données des tables dans ce qu’il appelle en interne la heap (« tas »). Il y a d’autres petits fichiers séparés pour la maintenance de la table (free space map, visibility map), négligés ici.
Les index sont stockés séparément.
Structure d’un enregistrement¶
Un enregistrement est toujours stocké dans le heap.
Chaque enregistrement a un entête incompressible.
Cet entête stocke des informations de transaction
ou de chaînage de versions d’enregistrements.
Une partie variable (t_bits
) indique quels champs ont
pour valeur NULL
(s’il y en a dans la ligne), pour éviter
de les stocker sur disque.
Pour des raisons d’alignement, l’entête fera donc au moins 24 octets (une estimation suffisante la plupart du temps pour les calculs), mais 32 octets s’il y a 9 champs ou plus, 40 à partir de 73 champs, etc.
Chaque enregistrement fait donc au moins 24 octets + la taille de l’enregistrement.
Référence : Disposition des données dans une page
Structure d’un bloc¶
Par défaut, la taille de bloc (ou page) est de 8 ko. Il faut recompiler PostgreSQL pour le changer, ce qui est très rarement fait et ouvre de possibles incompatibilités avec nombre d’outils. On gardera donc 8 ko pour la suite.
Dans un bloc, les données sont à la fin :
L’entête (PageHeaderData) fait 24 octets : adresse du journal auquel ce bloc a été modifié pour la dernière fois, numéro de version, somme de contrôle, adresse du début de l’espace libre, etc… (détails)
Après lui, se trouve un tableau de pointeurs, chacun de 4 octets, vers les enregistrements (Item) qui sont à la fin du bloc.
La ligne de ctid (0,2) est donc le 2ᵉ item en partant de la fin du premier bloc de la table.
Place perdue¶
Le « surcoût total » par ligne de données est donc d’au moins 28 octets (24 d’entête, au moins + 4 de pointeur), plus l’entête partagé entre tous les enregistrements d’un bloc (24 octets, soit 0,3 % d’un bloc plein).
Référence : PostgreSQL - Architecture et notions avancées (Guillaume Lelarge), chapitre 3.2
Fillfactor¶
De plus, chaque table ou index peut se voir affecté un fillfactor,
c’est-à-dire un pourcentage maximum de remplissage du bloc lors
des INSERT
(cette limite pouvant être dépassée lors d’un UPDATE
).
Intérêt d’un fillfactor inférieur à 100%
Les UPDATE
ont plus de chance de se réaliser dans le même bloc,
améliorant l’effet de cache.
Les mises à jour HOT sont favorisées.
Par défaut le fillfactor d’une table est de 100 %, pour une compacité maximale. On le modifie ainsi :
TOAST¶
Une ligne doit tenir dans un unique bloc de 8 ko. C’est parfois insuffisant. Pour contourner cette limitation, sur certaines lignes, certaines colonnes sont « toastées », c’est-à-dire stockées compressées dans le champ, ou stockées à l’extérieur de la table, dans une table d’extension.
Seuls les types à longueur variable subissent ce traitement :
chaînes (varchar
,json
, hstore
…), binaires (bytea
, jsonb
…),
composites (record
)… Les champs numeric
peuvent être compressés
dans la heap.
Ne sont pas toastés les champs fixes, soit
int
et float
et leurs variantes, char
, les divers champs
date et heure, uuid
, bool
, les champs techniques…
Dans le doute, voir le champ typstorage
de la table des types
pg_type.
Avantages du TOAST
Le système est totalement automatique et transparent pour l’utilisateur.
Les données de ces colonnes ne sont lues que si elles sont
réellement nécessaires, le reste du temps, elles ne sont pas consultées
(c’est une raison supplémentaire d’éviter SELECT *
).
Lors d’un UPDATE
, les colonnes toastées et non modifiées
peuvent être partagées entre les différentes versions de l’enregistrement :
la nouvelle version pointe simplement vers la même entrée TOAST.
Les valeurs toastées sont parfois compressées, dans la heap ou la table d’extension.
Le coût supplémentaire en place est proportionnellement très faible.
Un champ est candidat à TOAST si l’enregistrement dépasse le quart de la taille du bloc (2 ko). Les plus gros champs seront toastés jusqu’à ce que l’enregistrement redescende à 2 ko ou moins.
PostgreSQL décide du mode de stockage champ par champ et ligne par ligne.
Taille de chaque type¶
Types de taille fixe¶
Ce sont des champs très courants : numériques, date/heure…
Ils n’ont pas d’entête variable (varlena
dans la nomenclature PostgreSQL).
Ils ont des contraintes d’alignement.
Type | Taille (octets) | Alignement (octets) |
---|---|---|
bool | 1 | 1 |
char | 1 | 1 |
int8 | 8 | 8 |
int2 | 2 | 2 |
int4 | 4 | 4 |
float4 | 4 | 4 |
float8 | 8 | 8 |
date | 4 | 4 |
time | 8 | 8 |
timestamp | 8 | 8 |
timestamptz | 8 | 8 |
interval | 16 | 8 |
Types de taille variable¶
Ils ont un entête de 4 octets incompressibles indiquant leur taille réelle (jusqu’à 1 Go en théorie), dont 2 bits indiquant s’ils sont inline ou toasté, compressé ou non-compressé (cela peut varier pour chaque enregistrement).
Leur taille est suffisamment importante pour que les contraintes d’alignement soient sans importance.
Types texte¶
Ne pas confondre caractère et octet
La taille en caractères d’une chaîne est différente de sa taille en octets à cause de l’encodage UTF-8 généralement utilisé (1 à 4 octets selon le caractère).
La taille occupée est :
- si la chaîne est plus courte que 126 octets : 1 octet + la longueur de la chaîne
- si elle est fait 126 octets ou plus : 4 octets + la longueur de la chaîne
- pour les plus grandes chaînes : le mécanisme TOAST compresse ou remplace par un pointeur.
Ces trois types sont identiques du point de vue du stockage :
-
char(N)
(oubpchar
) a toujours la taille N caractères (si non précisé : 1 caractère) et au besoin est complété par des espaces à la fin. -
varchar(N)
(oucharacter varying
) est de taille variable, limitée par N (au plus 10 485 760). -
text
(identique àvarchar
sans limite de taille) a une taille arbitraire, éventuellement plusieurs mégaoctets.
Référence : Character types
Types numériques à précision arbitraire¶
numeric
/numeric (p,s)
(identique à decimal
) peut
avoir une précision allant jusqu’à 147 455 chiffres
(131 072 avant la virgule, 16 383 après), hors zéros en début ou fin
(pour du numeric
sans contrainte).
La place est de 2 octets pour chaque groupe de 4 chiffres significatifs, plus 3 à 8 octets d’entête. Au-delà d’environ 2000 octets, le mécanisme TOAST compresse les données.
Référence : Arbitrary Precision Numbers
Types binaires¶
-
bytea
suit la même règle quetext
: taille de la chaîne + 1 ou 4 octets, éventuellement avec TOAST. -
bit strings (
bit (N)
etbit varying(N)
) : nombre de bits dans le bit string, aligné à l’octet (tout octet commencé est dû), plus 5 à 8 octets d’entête selon la taille de la chaîne, éventuellement avec TOAST.
Référence : Binary Data Types, Bit String Types
Types composés¶
Pour les tableaux et types composites, la taille est grosso modo la taille des éléments les composant.
Autres types¶
Toutes les informations peuvent être retrouvées dans pg_type :
où :typname
est le nom du typetyplen
est sa taille en octets (ou -1 pour un type de longueur variable)typalign
est la contrainte d’alignement du type :c
pour 1 octet,s
pour 2 octets,i
pour 4 octets,d
pour 8 octets (qui peut différer entre machines 32 et 64 bits).
Alignement¶
L’alignement des types (ou padding) a surtout un sens pour les types fixes. Il indique sur quelle adresse une donnée peut être stockée. Par exemple :
- un type
char(1)
(1 octet) peut être stocké à toute adresse (en octet) - un type
smallint
(ouint2
) ne peut être stocké qu’à chaque adresse multiple de 2 octets.
L’ordre de déclaration des champs a un impact :
CREATE TABLE demopadd0 ( j int8, i int2, a char (1), b char(1) ) ;
CREATE TABLE demopadd2 ( a char (1), j int8, b char(1), i int2 ) ;
INSERT INTO demopadd0 SELECT 255,16, 'A','B' FROM generate_series (1,100000) ;
INSERT INTO demopadd2 SELECT 'A', 255, 'B', 16 FROM generate_series (1,100000) ;
ANALYZE demopadd0,demopadd2;
SELECT relname, pg_size_pretty(relpages*8192::bigint) FROM pg_class
WHERE relname LIKE 'demopadd%' ORDER BY 1;
Dans le deuxième cas, l’alignement des deux entiers est gêné par les char
.
Il ne faut pas exagérer ce problème d’alignement lors de la conception
Il est rare d’utiliser les types de petite taille (char, int2, bool). Le plus gros des données est aligné à 4 octets ou plus.
Si on veut éviter de sous-estimer l’espace utilisé par ces types on peut leur attribuer 4 octets en tenant compte de l’alignement.
C’est à garder à l’esprit cependant pour les plus grandes tables.
Noter qu’une modification implique la reconstruction complète de la table.
Règle de calcul pour une table¶
Pour estimer rapidement la taille d’une table :
Taille estimée d’une table
sachant que nous avons vu que :
-
la taille des champs fixes peut être surévaluée à au moins 4 octet par champ (alignement), les champs plus grands étant indiqués plus haut
-
la taille des chaînes est de 1 ou 4 octets + longueur moyenne de la chaîne
-
la taille des
numeric
serait de 8 + taille mantisse/2 -
le 1,003 tient compte de l’entête de chaque bloc
-
le fillfactor est ici exprimé en fraction
Ceci ne peut être qu’une approximation pour une table fraîchement chargée ou reconstruite.
Les blocs de 8192 octets ne seront jamais remplis exactement, et encore moins le tout dernier.
Les modifications des lignes génèrent des lignes mortes, et l’autovacuum attendra toujours un peu avant de les netoyer (par défaut, le seuil est de 20 % de lignes mortes).
La taille des chaînes variables est délicate à estimer.
Les valeurs NULL ne prennent pas de place (un bit est positionné dans l’entête).
L’entête de ligne peut faire beaucoup plus de 24 octets s’il y a trop de colonnes.
Les lignes de plus de 2 ko (par défaut) seront toastées. La compression est difficilement estimable, menant à une surestimation. Une valeur déplacée dans la table TOAST associée sera découpée en autant de morceaux de 2 ko, avec le coût pour gérer ces lignes (entêtes, index).
Estimation de taille d’un index B-tree¶
Nous ne parlerons pas des GIN, GIST… bien plus complexes à évaluer.
-
Une page d’index B-Tree commence comme une table, avec un entête de 24 octets, et 4 octets par enregistrement dans la page.
-
Chaque entrée d’index a un entête de 6 octets (pointeur vers l’enregistrement). Il contient ensuite les données des colonnes de l’index (même taille et contraintes d’alignement que les données de l’enregistrement).
-
Chaque entrée d’index a un pointeur de 4 octets dans l’entête de la page, tout comme une entrée de page heap. Le « coût » d’entêtes pour un enregistrement d’index est donc de 10 octets.
-
Les contraintes d’alignement sont plus importantes pour les index que pour les données (une entrée d’index est bien plus compacte). La partie « données » d’une entrée d’index est alignée sur la taille de pointeur de l’architecture (8 octets en 64 bits)
Pour une taille moyenne d’enregistrement de 19 octets :
Il faudra donc arrondir la taille à 24 octets sur une machine 64 bits, et à 20 octets sur une machine 32 bits.
-
Le fillfactor d’un index est par défaut de 90%.
-
On ignore les pages intermédiaires de l’index, dont le nombre est extrêmement faible par rapport aux feuilles (c’est le principe des B-Tree). L’évolution du nombre de pages intermédiaires est fonction du logarithme du nombre de pages totales.
Un index a donc pour taille théorique :
(10 octets + somme des tailles (champs) alignée à l'architecture) × nb_enregistrements × 1,003 / 0,90
Là encore ce n’est qu’une approximation
Un index se fragment également au long de sa vie.
La déduplication des valeurs indexées identiques peut réduire drastiquement la taille, si les données se répètent. (Du moins depuis PostgreSQL et pour certains types : entiers, textes à collation déterministe…)
Fonctions de calculs de volumétrie¶
Ces fonctions permettent de connaître les volumétries des différentes parties d’une table :
SELECT
oid AS table_oid,
c.relnamespace::regnamespace || '.' || relname AS TABLE,
reltoastrelid,
reltoastrelid::regclass::text AS table_toast,
reltuples AS nb_lignes_estimees,
pg_size_pretty(pg_table_size(c.oid)) AS " Table (dont TOAST)",
pg_size_pretty(pg_relation_size (c.oid,'main')) AS " Heap",
pg_size_pretty(pg_relation_size (reltoastrelid,'main')) AS " Toast",
pg_size_pretty(pg_indexes_size (reltoastrelid)) AS " Toast (PK)",
pg_size_pretty(pg_relation_size (c.oid,'vm')) AS " Vis.map",
pg_size_pretty(pg_relation_size (c.oid,'fsm')) AS " FSM",
pg_size_pretty(pg_indexes_size (c.oid)) AS " Index",
pg_size_pretty(pg_total_relation_size(c.oid)) AS "Total"
FROM pg_class c
WHERE relkind = 'r'
AND relname = 'textes'
\gx
-[ RECORD 1 ]------+------------------------
table_oid ║ 220416
table ║ public.textes
reltoastrelid ║ 220419
table_toast ║ pg_toast.pg_toast_220416
nb_lignes_estimees ║ 2.0985848e+07
Table (dont TOAST) ║ 3187 MB
Heap ║ 3186 MB
Toast ║ 24 kB
Toast (PK) ║ 16 kB
Vis.map ║ 104 kB
FSM ║ 824 kB
Index ║ 310 MB
Total ║ 3497 MB
Exemples¶
Exemple 1¶
Pour la table ci-dessus :
On calcule :
ou plutôt 4309 Mo en surestimant l’alignement, ce qui est la valeur obtenue plus haut.Exemple 2¶
Cette table possède 3 index :
CREATE TABLE test_taille (a int, b varchar, c int);
INSERT INTO test_taille SELECT i, i::text, i FROM generate_series (50000,50000+1e6) i;
CREATE INDEX test_taille_idx1 on test_taille (a);
CREATE INDEX test_taille_idx2 on test_taille (a,b);
CREATE INDEX test_taille_idx3 on test_taille (c,a,b);
Le champ texte b
fait en moyenne 6 caractères.
La table contient 1 million d’enregistrement.
-
La taille de la table est donc de :
-
La taille de l’index
test_taille_idx1
(en 64 bits) est : -
Pour
test_taille_idx2
: -
Pour
test_taille_idx3
:
-
Vérification :
-
Par contre, si la déduplication peut opérer, ce sera beaucoup moins :
La taille de la table reste identique, mais les index descendent à 6 Mo chacun.
Exemple 2¶
Pour une base pgbench
de taille 100 :
- La table
pgbench_accounts
est définie par :
Elle fait 10 000 000 lignes de 28 + 4 + 4 + 4 + (1+84) = 125 octets (arrondi à 128 avec l’alignement).
- La taille attendue est :
128 × 10 000 000 × 1,003 / 1024 /1024 = 1224 Mo
- En réalité :
On va tenter d’expliquer ce petit écart.
-
La requête suivante permet de voir qu’il y a 61 lignes dans la plupart des blocs :
-
La requête suivante dans un bloc au hasard permet de voir la disposition de ces lignes :
SELECT *, length(t_data) AS taille_t_data
FROM heap_page_items (get_raw_page ('pgbench_accounts','main', 1111) )
WHERE lp <= 2 or lp >=61
ORDER BY lp
\gx
Résultat de la requête
-[ RECORD 1 ]-+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
lp | 1
lp_off | 8064
lp_flags | 1
lp_len | 121
t_xmin | 12538435
t_xmax | 0
t_field3 | 2
t_ctid | (1111,1)
t_infomask2 | 4
t_infomask | 2818
t_hoff | 24
t_bits |
t_oid |
t_data | \xbc0801000100000000000000ab202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
taille_t_data | 97
-[ RECORD 2 ]-+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
lp | 2
lp_off | 7936
lp_flags | 1
lp_len | 121
t_xmin | 12538435
t_xmax | 0
t_field3 | 2
t_ctid | (1111,2)
t_infomask2 | 4
t_infomask | 2818
t_hoff | 24
t_bits |
t_oid |
t_data | \xbd0801000100000000000000ab202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
taille_t_data | 97
-[ RECORD 3 ]-+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
lp | 61
lp_off | 384
lp_flags | 1
lp_len | 121
t_xmin | 12538435
t_xmax | 0
t_field3 | 2
t_ctid | (1111,61)
t_infomask2 | 4
t_infomask | 2818
t_hoff | 24
t_bits |
t_oid |
t_data | \xf80801000100000000000000ab202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
taille_t_data | 97
et on y lit :
- les données de la ligne (
t_data
) font bien 97 octets - le champ
lp_off
indique que la première ligne commence à l’octet 8064 (en fin de bloc, ce qui nous amène à 8064 + une ligne de 128 = 8192 octets) - celui de la deuxième à 7636 (et 7936 + 128 = 8064)
-
celui de la 61ᵉ et dernière à 384.
-
De plus, l’entête du bloc fait 24 + 4×61 = 268 octets. Cela explique qu’entre la fin de cet entête et l’octet 384, il n’y a pas la place de caser une 62ᵉ ligne, et on perd donc 116 octets par bloc.
-
On retombe bien sur :
Exemple 2 (suite)¶
Pour cette même table,
passer le fillfactor de 100 (défaut) à 80 augmente la taille de la table
de 1/0,80 (+25 %), soit progressivement lors des mises à jour,
soit d’un coup avec un VACUUM FULL
;
La table passe en conséquence de 1281 à 1594 Mo.
Pour aller plus loin¶
- Documentation officielle :
- Manuels de formation Dalibo :
- Code source de PostgreSQL :
- PostgreSQL - Architecture et notions avancées (Guillaume Lelarge), chapitres 3.2 et 3.3