PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

PostgreSQL 9.6 :analyse séquentielle parallèle

Pendant longtemps, l'un des défauts les plus connus de PostgreSQL était la possibilité de paralléliser les requêtes. Avec la sortie de la version 9.6, ce ne sera plus un problème. Un gros travail a été fait sur ce sujet, à commencer par le commit 80558c1, l'introduction du balayage séquentiel parallèle, que nous verrons au cours de cet article.

Tout d'abord, vous devez prendre note :le développement de cette fonctionnalité a été continu et certains paramètres ont changé de nom entre un commit et un autre. Cet article a été rédigé à partir d'un checkout effectué le 17 juin et certaines fonctionnalités ici illustrées ne seront présentes que dans la version 9.6 beta2.

Par rapport à la version 9.5, de nouveaux paramètres ont été introduits dans le fichier de configuration. Ce sont :

  • max_parallel_workers_per_gather  :le nombre de travailleurs pouvant assister à un parcours séquentiel d'une table ;
  • min_parallel_relation_size  :la taille minimale qu'une relation doit avoir pour que le planificateur envisage l'utilisation de travailleurs supplémentaires ;
  • parallel_setup_cost  :le paramètre du planificateur qui estime le coût d'instanciation d'un travailleur ;
  • parallel_tuple_cost :le paramètre du planificateur qui estime le coût de transfert d'un tuple d'un worker à un autre ;
  • force_parallel_mode :paramètre utile pour les tests, le parallélisme fort et aussi une requête dans laquelle le planificateur fonctionnerait autrement.

Voyons comment les travailleurs supplémentaires peuvent être utilisés pour accélérer nos requêtes. Nous créons une table de test avec un champ INT et cent millions d'enregistrements :

postgres=# CREATE TABLE test (i int);
CREATE TABLE
postgres=# INSERT INTO test SELECT generate_series(1,100000000);
INSERT 0 100000000
postgres=# ANALYSE test;
ANALYZE

PostgreSQL a max_parallel_workers_per_gather défini sur 2 par défaut, pour lequel deux workers seront activés lors d'un scan séquentiel.

Un simple scan séquentiel ne présente aucune nouveauté :

postgres=# EXPLAIN ANALYSE SELECT * FROM test;
                                                       QUERY PLAN                         
------------------------------------------------------------------------------------------------------------------------
 Seq Scan on test  (cost=0.00..1442478.32 rows=100000032 width=4) (actual time=0.081..21051.918 rows=100000000 loops=1)
 Planning time: 0.077 ms
 Execution time: 28055.993 ms
(3 rows)

En effet, la présence d'un WHERE clause est requise pour la parallélisation :

postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..964311.60 rows=1 width=4) (actual time=3.381..9799.942 rows=1 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Seq Scan on test  (cost=0.00..963311.50 rows=0 width=4) (actual time=6525.595..9791.066 rows=0 loops=3)
         Filter: (i = 1)
         Rows Removed by Filter: 33333333
 Planning time: 0.130 ms
 Execution time: 9804.484 ms
(8 rows)

Nous pouvons revenir à l'action précédente et observer les différences en définissant max_parallel_workers_per_gather à 0 :

postgres=# SET max_parallel_workers_per_gather TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Seq Scan on test  (cost=0.00..1692478.40 rows=1 width=4) (actual time=0.123..25003.221 rows=1 loops=1)
   Filter: (i = 1)
   Rows Removed by Filter: 99999999
 Planning time: 0.105 ms
 Execution time: 25003.263 ms
(5 rows)

Un temps 2,5 fois supérieur.

Le planificateur ne considère pas toujours un balayage séquentiel parallèle comme la meilleure option. Si une requête n'est pas assez sélective et qu'il y a beaucoup de tuples à transférer d'un worker à un worker, elle peut préférer un parcours séquentiel "classique" :

postgres=# SET max_parallel_workers_per_gather TO 2;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Seq Scan on test  (cost=0.00..1692478.40 rows=90116088 width=4) (actual time=0.073..31410.276 rows=89999999 loops=1)
   Filter: (i < 90000000)
   Rows Removed by Filter: 10000001
 Planning time: 0.133 ms
 Execution time: 37939.401 ms
(5 rows)

En fait, si nous essayons de forcer un scan séquentiel parallèle, nous obtenons un moins bon résultat :

postgres=# SET parallel_tuple_cost TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
                                                             QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..964311.50 rows=90116088 width=4) (actual time=0.454..75546.078 rows=89999999 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Seq Scan on test  (cost=0.00..1338795.20 rows=37548370 width=4) (actual time=0.088..20294.670 rows=30000000 loops=3)
         Filter: (i < 90000000)
         Rows Removed by Filter: 3333334
 Planning time: 0.128 ms
 Execution time: 83423.577 ms
(8 rows)

Le nombre de nœuds de calcul peut être augmenté jusqu'à max_worker_processes (par défaut :8). Nous restaurons la valeur de parallel_tuple_cost et nous voyons ce qui se passe en augmentant max_parallel_workers_per_gather à 8.

postgres=# SET parallel_tuple_cost TO DEFAULT ;
SET
postgres=# SET max_parallel_workers_per_gather TO 8;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..651811.50 rows=1 width=4) (actual time=3.684..8248.307 rows=1 loops=1)
   Workers Planned: 6
   Workers Launched: 6
   ->  Parallel Seq Scan on test  (cost=0.00..650811.40 rows=0 width=4) (actual time=7053.761..8231.174 rows=0 loops=7)
         Filter: (i = 1)
         Rows Removed by Filter: 14285714
 Planning time: 0.124 ms
 Execution time: 8250.461 ms
(8 rows)

Même si PostgreSQL pouvait utiliser jusqu'à 8 travailleurs, il n'en a instancié que six. En effet, Postgres optimise également le nombre de travailleurs en fonction de la taille de la table et de la min_parallel_relation_size . Le nombre de workers mis à disposition par postgres est basé sur une progression géométrique avec 3 comme rapport commun 3 et min_parallel_relation_size comme facteur d'échelle. Voici un exemple. Compte tenu des 8 Mo de paramètre par défaut :

Taille Ouvrier
<8MB 0
<24MB 1
<72Mo 2
<216MB 3
<648MB 4
<1944MB 5
<5822MB 6

La taille de notre table est de 3458 Mo, donc 6 est le nombre maximum de nœuds de calcul disponibles.

postgres=# \dt+ test
                    List of relations
 Schema | Name | Type  |  Owner   |  Size   | Description
--------+------+-------+----------+---------+-------------
 public | test | table | postgres | 3458 MB |
(1 row)

Enfin, je ferai une brève démonstration des améliorations obtenues grâce à ce patch. En exécutant notre requête avec un nombre croissant de travailleurs en croissance, nous obtenons les résultats suivants :

Ouvriers Heure
0 24767,848 ms
1 14855,961 ms
2 10 415,661 ms
3 8041,187 ms
4 8090.855 ms
5 8082,937 ms
6 8061,939 ms

Nous pouvons voir que les temps s'améliorent considérablement, jusqu'à ce que vous atteigniez un tiers de la valeur initiale. Il est aussi simple d'expliquer le fait qu'on ne voit pas d'améliorations entre l'utilisation de 3 et 6 workers :la machine sur laquelle le test a été exécuté a 4 CPU, donc les résultats sont stables après avoir ajouté 3 workers supplémentaires au processus d'origine .

Enfin, PostgreSQL 9.6 a préparé le terrain pour la parallélisation des requêtes, dans laquelle l'analyse séquentielle parallèle n'est que le premier grand résultat. On verra aussi qu'en 9.6, les agrégations ont été parallélisées, mais c'est une information pour un autre article qui sortira dans les prochaines semaines !