PostgreSQL 10 apportait enfin un support natif du partitionnement. Cependant, il souffrait de plusieurs limitations qui ne le rendaient pas si séduisant que cela. La version 11, qui va bientôt arriver en version bêta, corrige un certain nombre de ces défauts. Mais pas tous. Commençons par voir les différents changements. Ce premier article nous permet d'explorer l'élimination dynamique des partitions.
Commençons par nous créer un premier jeu d'essai :
CREATE TABLE orders ( num_order INTEGER NOT NULL, date_order DATE NOT NULL, ) PARTITION BY RANGE (date_order); CREATE TABLE orders_201804 PARTITION OF orders FOR VALUES FROM ('2018-04-01') TO ('2018-05-01'); CREATE TABLE orders_201805 PARTITION OF orders FOR VALUES FROM ('2018-05-01') TO ('2018-06-01');
Ajoutons quelques données :
INSERT INTO orders VALUES (1, '2018-04-22'), (2, '2018-05-01'), (3, '2018-05-11');
On termine la création du jeu d'essai en mettant à jour les statistiques pour l'optimiseur :
ANALYZE;
Vérifions déjà que l'optimiseur est toujours capable de retirer les partitions inutiles à la planification. Ici le prédicat est connu avant l'exécution, l'optimiseur peut donc lire uniquement la partition du mois de mai :
SELECT * FROM orders WHERE date_order = '2018-05-11';
On vérifie que la partition du mois d'avril est bien éliminée à la planification en observant que le plan obtenu ne contient pas aucune lecture de cette partition :
Append (actual rows=1 loops=1) -> Seq Scan on orders_201805 (actual rows=1 loops=1) Filter: (date_order = '2018-05-11'::date) Rows Removed by Filter: 1
Une forme de construction souvent vue sur le terrain utilise la fonction
to_date
pour calculer un prédicat sur une date :
SELECT * FROM orders WHERE date_order = to_date('2018-05-11', 'YYYY-MM-DD');
Avec l'élimination dynamique des partitions, on pourrait penser que cette requête en bénéficiera bien. Mais ce n'est pas le cas, le prédicat est utilisé comme clause de filtrage à la lecture et PostgreSQL 11 ne sait pas l'utiliser directement pour exclure dynamiquement la partition d'avril. La lecture de cette partition est bien réalisée et ne ramène aucune ligne :
Append (actual rows=1 loops=1) -> Seq Scan on orders_201804 (actual rows=0 loops=1) Filter: (date_order = to_date('2018-05-11'::text, 'YYYY-MM-DD'::text)) Rows Removed by Filter: 2 -> Seq Scan on orders_201805 (actual rows=1 loops=1) Filter: (date_order = to_date('2018-05-11'::text, 'YYYY-MM-DD'::text)) Rows Removed by Filter: 1
On peut forcer la création d'un InitPlan
, donc en utilisant une
sous-requête pour calculer le prédicat :
SELECT * FROM orders WHERE date_order = (SELECT to_date('2018-05-11', 'YYYY-MM-DD'));
Le plan d'exécution obtenu est un peu différent : l'InitPlan correspondant à notre sous-requête apparaît. Mais ce qui nous intéresse le plus concerne la lecture de la partition d'avril. Elle apparaît dans le plan car elle n'est pas exclue à la planification, mais elle n'est pas exécutée car exclue à l'exécution.
Append (actual rows=1 loops=1) InitPlan 1 (returns $0) -> Result (actual rows=1 loops=1) -> Seq Scan on orders_201804 (never executed) Filter: (date_order = $0) -> Seq Scan on orders_201805 (actual rows=1 loops=1) Filter: (date_order = $0) Rows Removed by Filter: 1
Ce mécanisme est également fonctionnel si l'on avait utilisé une
sous-requête sans la fonction to_date
:
SELECT * FROM orders WHERE date_order = (SELECT date '2018-05-11');
On peut aussi vérifier que le mécanisme fonctionne avec une requête préparée :
PREPARE q0 (date) AS SELECT * FROM orders WHERE date_order = $1;
On exécutera au moins 6 fois la requête suivante. Les 5 premières exécutions vont servir à PostgreSQL à se fixer sur un plan générique :
EXECUTE q0 ('2018-05-11');
Le plan d'exécution de la requête montre que seule la partition du mois de mai est accédée et nous montre aussi qu'un sous-plan a été éliminé, il s'agit de la lecture sur la partition d'avril :
Append (actual rows=1 loops=1) Subplans Removed: 1 -> Seq Scan on orders_201805 (actual rows=1 loops=1) Filter: (date_order = $1) Rows Removed by Filter: 1
On pourra aussi s'en assurer avec la requête préparée suivante :
PREPARE q1 AS SELECT * FROM orders WHERE date_order IN ($1, $2, $3);
Et l'execute suivant, où l'on fera varier les paramètres pour tantôt viser les deux partitions, tantôt une seule :
EXECUTE q1 ('2018-05-11', '2018-05-11', '2018-05-10');
Le mécanisme d'élimination dynamique est donc fonctionnel pour les requêtes préparées.
Ajoutons une table pour pouvoir réaliser une jointure :
CREATE TABLE bills ( num_bill SERIAL NOT NULL PRIMARY KEY, num_order INTEGER NOT NULL, date_order DATE NOT NULL ); INSERT INTO bills (num_order, date_order) VALUES (1, '2018-04-22');
On met également à jour les statistiques pour l'optimiseur, surtout pour que l'optimiseur sache qu'une Nested Loop sera plus adaptée pour la jointure que l'on va faire :
ANALYZE bills;
La jointure suivante ne concernera donc que des données du mois d'avril, du
fait des données présentes dans la table bills
:
SELECT * FROM bills b JOIN orders o ON ( b.num_order = o.num_order AND b.date_order = o.date_order);
La jointure est donc bien réalisée avec une Nested Loop. L'optimiseur n'a fait aucune élimination de partition, il n'a aucun élément pour y parvenir. En revanche, l'étage d'exécution a tout simplement éliminé la partition de mai de la jointure :
Nested Loop (actual rows=1 loops=1) -> Seq Scan on bills b (actual rows=1 loops=1) -> Append (actual rows=1 loops=1) -> Seq Scan on orders_201804 o (actual rows=1 loops=1) Filter: ((b.num_order = num_order) AND (b.date_order = date_order)) Rows Removed by Filter: 1 -> Seq Scan on orders_201805 o_1 (never executed) Filter: ((b.num_order = num_order) AND (b.date_order = date_order))
Que se passe-t-il si l'on force l'optimiseur à ne pas utiliser une Nested Loop :
SET enable_nestloop = off; explain (analyze, costs off, timing off) SELECT * FROM bills b JOIN orders o ON ( b.num_order = o.num_order AND b.date_order = o.date_order);
Le plan d'exécution montre que l'optimiseur préfère une jointure par
hachage. Mais les deux partitions sont lues dans ce cas. Le mécanisme
d'élimination dynamique de partition de PostgreSQL 11 n'est effectif que sur
les Nested Loop
(ça ne marchera pas non plus avec un Merge
Join
) :
Hash Join (actual rows=1 loops=1) Hash Cond: ((o.num_order = b.num_order) AND (o.date_order = b.date_order)) -> Append (actual rows=4 loops=1) -> Seq Scan on orders_201804 o (actual rows=2 loops=1) -> Seq Scan on orders_201805 o_1 (actual rows=2 loops=1) -> Hash (actual rows=1 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on bills b (actual rows=1 loops=1)
Le mécanisme d'élimination dynamique de partitions est donc fonctionnel dans un certain nombre de cas, mais reste encore perfectible.
Tous les plans d'exécution de cet article ont été obtenus avec EXPLAIN
(analyze, costs off, timing off)
.