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

Améliorations du partitionnement dans PostgreSQL 11

Un système de partitionnement dans PostgreSQL a été ajouté pour la première fois dans PostgreSQL 8.1 par le fondateur de 2ndQuadrant Simon Riggs . Il était basé sur l'héritage des relations et utilisait une nouvelle technique pour exclure les tables de l'analyse par une requête, appelée « exclusion de contraintes ». Alors qu'il s'agissait d'un énorme pas en avant à l'époque, il est aujourd'hui considéré comme lourd à utiliser et lent, et doit donc être remplacé.

Dans la version 10, il a été remplacé grâce aux efforts héroïques de Amit Langote avec un « partitionnement déclaratif » de style moderne. Cette nouvelle technologie signifiait que vous n'aviez plus besoin d'écrire du code manuellement pour acheminer les tuples vers leurs partitions correctes, et plus besoin de déclarer manuellement les contraintes correctes pour chaque partition :le système faisait ces choses automatiquement pour vous.

Malheureusement, dans PostgreSQL 10, c'est à peu près tout ce qu'il a fait. En raison de la complexité et des contraintes de temps, il manquait beaucoup de choses dans l'implémentation de PostgreSQL 10. Robert Haas a donné une conférence à ce sujet dans le PGConf.EU de Varsovie.

De nombreuses personnes ont travaillé sur l'amélioration de la situation pour PostgreSQL 11; voici ma tentative de recomptage. Je les divise en trois domaines :

  1. Nouvelles fonctionnalités de partitionnement
  2. Meilleur support DDL pour les tables partitionnées
  3. Optimisations des performances.

Nouvelles fonctionnalités de partitionnement

Dans PostgreSQL 10, vos tables partitionnées peuvent l'être dans RANGE et LISTE modes. Ce sont des outils puissants sur lesquels baser de nombreuses bases de données du monde réel, mais pour de nombreuses autres conceptions, vous avez besoin du nouveau mode ajouté dans PostgreSQL 11 :HASH partitionnement . De nombreux clients en ont besoin, et Amul Sul travaillé dur pour le rendre possible. Voici un exemple simple :

CREATE TABLE clients (
client_id INTEGER, name TEXT
) PARTITION BY HASH (client_id);

CREATE TABLE clients_0 PARTITION OF clients
    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
CREATE TABLE clients_1 PARTITION OF clients
    FOR VALUES WITH (MODULUS 3, REMAINDER 1);
CREATE TABLE clients_2 PARTITION OF clients
    FOR VALUES WITH (MODULUS 3, REMAINDER 2);

Il n'est pas obligatoire d'utiliser la même valeur de module pour toutes les partitions ; cela vous permet de créer plus de partitions ultérieurement et de redistribuer les lignes une partition à la fois, si nécessaire.

Une autre fonctionnalité très utile, écrite par Amit Khandekar est la possibilité d'autoriser la MISE À JOUR pour déplacer des lignes d'une partition à une autre - c'est-à-dire que s'il y a un changement dans les valeurs de la colonne de partitionnement, la ligne est automatiquement déplacée vers la bonne partition. Auparavant, cette opération aurait généré une erreur.

Une autre nouveauté, écrite par Amit Langote et votre serviteur , est-ce que INSERT ON CONFLICT UPDATE peut être appliqué aux tables partitionnées . Auparavant, cette commande échouait si elle ciblait une table partitionnée. Vous pouvez le faire fonctionner en sachant exactement dans quelle partition la ligne se retrouvera, mais ce n'est pas très pratique. Je n'aborderai pas les détails de cette commande, mais si vous avez déjà souhaité avoir UPSERT dans Postgres, c'est ça. Une mise en garde est que la UPDATE l'action ne peut pas déplacer la ligne vers une autre partition.

Enfin, une autre nouvelle fonctionnalité mignonne dans PostgreSQL 11, cette fois par Jeevan Ladhe, Beena Emerson, Ashutosh Bapat, Rahila Syed, etRobert Haas est la prise en charge d'une partition par défaut dans une table partitionnée , c'est-à-dire une partition qui reçoit toutes les lignes qui ne rentrent dans aucune des partitions normales. Cependant, bien que sympathique sur le papier, cette fonctionnalité n'est pas très pratique sur les paramètres de production car certaines opérations nécessitent un verrouillage plus lourd avec les partitions par défaut que sans. Exemple :la création d'une nouvelle partition nécessite d'analyser la partition par défaut afin de déterminer qu'aucune ligne existante ne correspond aux limites de la nouvelle partition. Peut-être qu'à l'avenir, ces exigences de verrouillage seront réduites, mais en attendant, je suggère de ne pas l'utiliser.

Meilleur support DDL

Dans PostgreSQL 10, certains DDL refusaient de fonctionner lorsqu'ils étaient appliqués à une table partitionnée et vous obligeaient à traiter chaque partition individuellement. Dans PostgreSQL 11, nous avons corrigé quelques-unes de ces limitations, comme annoncé précédemment par Simon Riggs. Tout d'abord, vous pouvez maintenant utiliser CREATE INDEX sur une table partitionnée , une fonctionnalité écrite par votre serviteur. Celui-ci peut être vu comme une simple question de réduction de l'ennui :au lieu de répéter la commande pour chaque partition (et en veillant à ne jamais l'oublier pour chaque nouvelle partition), vous ne pouvez le faire qu'une seule fois pour la table partitionnée parente, et elle s'applique automatiquement à toutes les partitions, existantes et futures.

Une chose intéressante à garder à l'esprit est la correspondance des index existants dans les partitions. Comme vous le savez, la création d'un index est une proposition bloquante, donc moins cela prend de temps, mieux c'est. J'ai écrit cette fonctionnalité pour que les index existants dans la partition soient comparés aux index en cours de création, et s'il y a des correspondances, il n'est pas nécessaire d'analyser la partition pour créer de nouveaux index :les index existants seraient utilisés.

Parallèlement à cela, également par votre humble serviteur, vous pouvez également créer UNIQUE contraintes, ainsi que PRIMARY KEY contraintes . Deux mises en garde :premièrement, la clé de partition doit faire partie de la clé primaire. Cela permet aux vérifications uniques d'être effectuées localement par partition, en évitant les index globaux. Deuxièmement, il n'est pas encore possible d'avoir des clés étrangères faisant référence à ces clés primaires. Je travaille dessus pour PostgreSQL 12.

Une autre chose que vous pouvez faire (grâce à la même personne) est de créer POUR CHAQUE LIGNE déclencheurs sur une table partitionnée , et qu'il s'applique à toutes les partitions (existantes et futures). Comme effet secondaire, vous pouvez avoir différé unique contraintes sur les tables partitionnées. Une mise en garde :seulement APRÈS les déclencheurs sont autorisés, jusqu'à ce que nous trouvions comment gérer AVANT déclencheurs qui déplacent des lignes vers une autre partition.

Enfin, une table partitionnée peut avoir une CLÉ ÉTRANGÈRE contraintes . C'est très pratique pour partitionner de grandes tables de faits tout en évitant les références pendantes, que tout le monde déteste. Mon collègue Gabriele Bartolini m'a attrapé par les genoux quand il a découvert que j'avais écrit et commis cela, criant que cela changeait la donne et comment pouvais-je être si insensible pour ne pas l'en informer. Moi, je continue juste à pirater le code pour m'amuser.

Travail de performance

Auparavant, le pré-traitement des requêtes pour savoir quelles partitions ne pas scanner (exclusion de contraintes) était plutôt simpliste et lent. Cela a été amélioré par un travail d'équipe admirable réalisé par Amit Langote, David Rowley, Beena Emerson, Dilip Kumar pour introduire d'abord une "élagage plus rapide" et une "élagage d'exécution" basée sur celle-ci par la suite. Le résultat est beaucoup plus puissant et plus rapide (David Rowley déjà décrit dans un article précédent.) Après tous ces efforts, l'élagage des partitions est appliqué à trois moments de la vie d'une requête :

  1. Au moment du plan de requête,
  2. Lorsque les paramètres de la requête sont reçus,
  3. À chaque point où un nœud de requête transmet des valeurs en tant que paramètres à un autre nœud.

Il s'agit d'une amélioration remarquable par rapport au système d'origine qui ne pouvait être appliquée qu'au moment du plan de requête, et je pense que cela plaira à beaucoup.

Vous pouvez voir cette fonctionnalité en action en comparant la sortie EXPLAIN pour une requête avant et après la désactivation de enable_partition_pruning option. Comme exemple très simpliste, comparez ce plan sans élagage :

SET enable_partition_pruning TO off;
EXPLAIN (ANALYZE, COSTS off)
SELECT * FROM clientes
WHERE cliente_id = 1234;
                                QUERY PLAN                                
-------------------------------------------------------------------------
 Append (actual time=6.658..10.549 rows=1 loops=1)
   ->  Seq Scan on clientes_1 (actual time=4.724..4.724 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 24978
   ->  Seq Scan on clientes_00 (actual time=1.914..1.914 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12644
   ->  Seq Scan on clientes_2 (actual time=0.017..1.021 rows=1 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12570
   ->  Seq Scan on clientes_3 (actual time=0.746..0.746 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12448
   ->  Seq Scan on clientes_01 (actual time=0.648..0.648 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12482
   ->  Seq Scan on clientes_4 (actual time=0.774..0.774 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12400
   ->  Seq Scan on clientes_5 (actual time=0.717..0.717 rows=0 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12477
 Planning Time: 0.375 ms
 Execution Time: 10.603 ms

avec celui produit avec la taille :

EXPLAIN (ANALYZE, COSTS off)
SELECT * FROM clientes
WHERE cliente_id = 1234;
                                QUERY PLAN                               
----------------------------------------------------------------------
 Append (actual time=0.054..2.787 rows=1 loops=1)
   ->  Seq Scan on clientes_2 (actual time=0.052..2.785 rows=1 loops=1)
         Filter: (cliente_id = 1234)
         Rows Removed by Filter: 12570
 Planning Time: 0.292 ms
 Execution Time: 2.822 ms

Je suis sûr que vous trouverez cela convaincant. Vous pouvez voir une tonne d'exemples plus sophistiqués en parcourant le fichier attendu des tests de régression.

Un autre élément était l'introduction des jointures par partition, par Ashutosh Bapat . L'idée ici est que si vous avez deux tables partitionnées et qu'elles sont partitionnées de manière identique, alors lorsqu'elles sont jointes, vous pouvez joindre chaque partition d'un côté à sa partition correspondante de l'autre côté ; c'est bien mieux que de joindre chaque partition d'un côté à chaque partition de l'autre côté. Le fait que les schémas de partition doivent correspondre exactement cela peut sembler peu susceptible d'être utilisé dans le monde réel, mais en réalité, il existe de nombreuses situations où cela s'applique. Exemple :une table commandes et sa table commandes_items correspondante. Heureusement, il y a déjà beaucoup de travail pour assouplir cette restriction.

Le dernier élément que je veux mentionner est les agrégats par partition, par Jeevan Chalke, Ashutosh Bapat, etRobert Haas . Cette optimisation signifie qu'une agrégation qui inclut les clés de partition dans le GROUP BY La clause peut être exécutée en agrégeant les lignes de chaque partition séparément, ce qui est beaucoup plus rapide.

Réflexions finales

Après les développements significatifs de ce cycle, PostgreSQL a une histoire de partitionnement beaucoup plus convaincante. Bien qu'il reste encore de nombreuses améliorations à apporter, notamment pour améliorer les performances et la simultanéité de diverses opérations impliquant des tables partitionnées, nous sommes maintenant à un point où le partitionnement déclaratif est devenu un outil très précieux pour servir de nombreux cas d'utilisation. Chez 2ndQuadrant, nous continuerons à contribuer au code pour améliorer PostgreSQL dans ce domaine et dans d'autres, comme nous l'avons fait pour chaque version depuis la 8.0.