PostgreSQLLa base de données la plus sophistiquée au monde.

Version anglaise

5.10. Partitionnement de tables

PostgreSQL™ offre un support basique du partitionnement de table. Cette section explique pourquoi et comment implanter le partitionnement lors de la conception de la base de données.

5.10.1. Aperçu

Le partitionnement fait référence à la division d'une table logique volumineuse en plusieurs parties physiques plus petites. Le partitionnement comporte de nombreux avantages :

  • les performances des requêtes peuvent être significativement améliorées dans certaines situations, particulièrement lorsque la plupart des lignes fortement accédées d'une table se trouvent sur une seule partition ou sur un petit nombre de partitions. Le partitionnement se substitue aux colonnes principales des index, réduisant ainsi la taille des index et facilitant la tenue en mémoire des parties les plus utilisées de l'index ;

  • lorsque les requêtes ou les mises à jour accèdent à un important pourcentage d'une seule partition, les performances peuvent être grandement améliorées par l'utilisation avantageuse de parcours séquentiels sur cette partition plutôt que d'utiliser un index et des lectures aléatoires réparties sur toute la table ;

  • les chargements et suppressions importants de données peuvent être obtenus par l'ajout ou la suppression de partitions, sous réserve que ce besoin ait été pris en compte lors de la conception du partitionnement. Exécuter ALTER TABLE DETACH PARTITION ou supprimer une partition individuelle en utilisant DROP TABLE est bien plus rapide qu'une opération de masse. Cela supprime également la surcharge due au VACUUM causé par un DELETE massif ;

  • les données peu utilisées peuvent être déplacées sur un média de stockage moins cher et plus lent.

Les bénéfices ne sont réellement intéressants que si cela permet d'éviter une table autrement plus volumineuse. Le point d'équilibre exact à partir duquel une table tire des bénéfices du partitionnement dépend de l'application. Toutefois, le partitionnement doit être envisagé si la taille de la table peut être amenée à dépasser la taille de la mémoire physique du serveur.

PostgreSQL™ offre un support natif pour les formes suivantes de partitionnement :

Partitionnement par intervalles

La table est partitionnée en « intervalles » (ou échelles) définis par une colonne clé ou par un ensemble de colonnes, sans recouvrement entre les intervalles de valeurs affectées aux différentes partitions. Il est possible, par exemple, de partitionner par échelles de date ou par intervalles d'identifiants pour des objets métier particuliers.

Partitionnement par liste

La table est partitionnée en listant explicitement les valeurs clés qui apparaissent dans chaque partition.

Partitionnement par hachage

La table est partitionnée en spécifiant un module et un reste pour chaque partition. Chaque partition contiendra les lignes pour lesquelles la valeur de hachage de la clé de partition divisée par le module spécifié produira le reste spécifié.

Si votre application nécessite d'utiliser d'autres formes de partitionnement qui ne sont pas listées au-dessus, des méthodes alternatives comme l'héritage et des vues UNION ALL peuvent être utilisées à la place. De telles méthodes offrent de la flexibilité, mais n'ont pas certains des bénéfices de performance du partitionnement déclaratif natif.

5.10.2. Partitionnement Declaratif

PostgreSQL™ donne un moyen de spécifier comment diviser une table en morceaux appelés partitions. La table qui est divisée est appelée table partitionnée. La spécification consiste en une méthode de partitionnement et une liste de colonnes ou expressions à utiliser comme la clé de partitionnement.

Toutes les lignes insérées dans la table partitionnée seront redirigées vers une des partitions en se basant sur la valeur de la clé de partitionnement. Chaque partition a un sous-ensemble des données défini par ses limites de partition. Les méthodes de partitionnement actuellement supportées sont le partitionnement par intervalles, par liste et par hachage.

Les partitions peuvent elles-mêmes être définies comme des tables partitionnées, en utilisant ce qu'on appelle du sous-partitionnement. Les partitions peuvent avoir leurs propres index, contraintes et valeurs par défaut, différents de ceux des autres partitions. Voir CREATE TABLE(7) pour plus de détails sur la création des tables partitionnées et des partitions.

Il n'est pas possible de transformer une table standard en table partitionnée et inversement. Par contre, il est possible d'ajouter une table standard ou une table partitionnée contenant des données comme une partition d'une table partitionnée, ou de supprimer une partition d'une table partitionnée, la transformant en table standard; voir ALTER TABLE(7) pour en apprendre plus sur les sous-commandes ATTACH PARTITION et DETACH PARTITION.

Dans les faits, les partitions individuelles sont liées à la table partitionnée grâce à l'héritage  ; il n'est pas possible d'utiliser les fonctionnalités génériques de l'héritage (discuté ci-dessous) avec des tables déclarées partitionnées et leurs partitions. Par exemple, une partition ne peut pas avoir d'autre parent qu'une table partitionnée dont elle est une partition, et une table standard ne peut pas hériter d'une table partitionnée faisant d'elle son parent. Cela veut dire que les tables partitionnées et les partitions ne participent pas à l'héritage avec les tables standard. Puisqu'une hiérarchie de partition est constituée d'une table partitionnée et de ses partitions, il s'agit toujours d'une hiérarchie d'héritage, toutes les règles normales de l'héritage s'appliquent comme décrit dans Section 5.9, « L'héritage » avec quelques exceptions, les plus notables étant :

  • Les contraintes CHECK et NOT NULL d'une table partitionnée sont toujours héritées par toutes les partitions. La création de contraintes CHECK qui sont marquées comme NO INHERIT n'est pas autorisée sur des tables partitionnées.

  • Utiliser ONLY pour ajouter ou supprimer une contrainte sur la table partitionnée uniquement est supporté tant qu'il n'y a pas de partition. Une fois qu'il existe des partitions, utiliser ONLY remontera une erreur puisqu'ajouter ou supprimer des contraintes sur uniquement la table partitionnée, quand des partitions existent, n'est pas supporté. À la place, des contraintes sur les partitions elles-mêmes peuvent être ajoutées ou (si elles ne sont pas présentes sur la table parente) supprimées directement sur les partitions.

  • Comme une table partitionnée ne contient jamais de données directement, essayer d'utiliser TRUNCATE ONLY sur une table partitionnée retournera toujours une erreur.

  • Les partitions ne peuvent pas avoir de colonnes qui ne sont pas présentes dans le parent. Il n'est pas non plus possible de spécifier des colonnes quand une partition est créée avec CREATE TABLE, pas plus qu'il n'est possible d'ajouter des colonnes aux partitions une fois celles-ci créées en utilisant ALTER TABLE. Des tables peuvent être ajoutées comme des partitions avec ALTER TABLE ... ATTACH PARTITION seulement si leurs colonnes correspondent exactement à celles du parent, en incluant toutes les colonnes oid.

  • Vous ne pouvez pas supprimer la contrainte NOT NULL d'une colonne d'une partition si la contrainte est présente dans la table parente.

Les partitions peuvent également être des tables étrangères, bien que ce cas ait des limitations qui n'existent pas avec des tables normales ; see CREATE FOREIGN TABLE(7) for more information.

La mise à jour de la clé de partition d'une ligne peut entraîner son déplacement vers une autre partition si cette ligne satisfait aux limites de la partition.

5.10.2.1. Exemple

Imaginons que nous soyons en train de construire une base de données pour une grande société de crème glacée. La société mesure les pics de températures chaque jour, ainsi que les ventes de crème glacée dans chaque région. Conceptuellement, nous voulons une table comme ceci :

CREATE TABLE mesure (
    id_ville        int not null,
    date_trace      date not null,
    temperature     int,
    ventes          int
);

La plupart des requêtes n'accèdent qu'aux données de la dernière semaine, du dernier mois ou du dernier trimestre, car cette table est essentiellement utilisée pour préparer des rapports en ligne pour la direction. Pour réduire le nombre de données anciennes à stocker, seules les trois dernières années sont conservées. Au début de chaque mois, les données du mois le plus ancien sont supprimées. Dans cette situation, le partitionnement permet de répondre aux différents besoins identifiés sur la table des mesures.

Pour utiliser le partitionnement déclaratif dans ce cas d'utilisation, il faut suivre les étapes suivantes :

  1. Créer une table measurement comme une table partitionnée en spécifiant la clause PARTITION BY, ce qui inclut la méthode de partitionnement ( RANGE dans ce cas) ainsi que la liste de la ou des colonnes à utiliser comme clé de partitionnement.

    +CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    ) PARTITION BY RANGE (logdate);
    

    Vous pourriez décider d'utiliser plusieurs colonnes dans la clé de partitionnement pour le partitionnement par intervalles, si vous le souhaitez. Bien sûr, cela aura souvent pour conséquence un plus grand nombre de partitions, chacune étant individuellement plus petite. D'un autre côté, utiliser moins de partitions pourrait entraîner un critère de partitionnement plus grossier et moins de partitions. Une requête accédant à la table partitionnée aura à parcourir moins de partitions si les conditions impliquent une partie ou la totalité de ces colonnes. Par exemple, imaginez une table partitionnée par intervalles utilisant les colonnes lastname et firstname (dans cet ordre) comme clé de partitionnement.

  2. Créez les partitions. La définition de chaque partition doit spécifier les limites qui correspondent à la méthode de partitionnement ainsi qu'à la clé de partitionnement du parent. Veuillez noter que spécifier des limites telles que les valeurs de la nouvelle partition pourront se chevaucher avec celles d'une ou plusieurs autres partitions retournera une erreur. Insérer des données dans la table parent qui ne correspondent pas à une des partitions existantes remontera une erreur  ; la partition appropriée doit être ajoutée manuellement.

    Les partitions ainsi créées sont de tous les points de vue des tables PostgreSQL™ normales (ou, potentiellement, des tables étrangères). Il est possible de spécifier un tablespace et des paramètres de stockage pour chacune des partitions séparément.

    Il n'est pas nécessaire de créer les contraintes de table décrivant les conditions limites de la partition pour les partitions. À la place, des contraintes de partitions sont générées implicitement à partir de la spécification de limite de partition chaque fois qu'il est nécessaire de s'y référer.

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
        FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
    
    CREATE TABLE measurement_y2006m03 PARTITION OF measurement
        FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
    
     ...
    CREATE TABLE measurement_y2007m11 PARTITION OF measurement
        FOR VALUES FROM ('2007-11-01') TO ('2007-12-01');
    
    CREATE TABLE measurement_y2007m12 PARTITION OF measurement
        FOR VALUES FROM ('2007-12-01') TO ('2008-01-01')
        TABLESPACE fasttablespace;
    
    CREATE TABLE measurement_y2008m01 PARTITION OF measurement
        FOR VALUES FROM ('2008-01-01') TO ('2008-02-01')
        WITH (parallel_workers = 4)
        TABLESPACE fasttablespace;
     

    Pour implémenter le sous-partitionnement, spécifiez la clause PARTITION BY dans les commandes utilisées pour créer des partitions individuelles, par exemple :

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
        FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
        PARTITION BY RANGE (peaktemp);
    

    Après avoir créé les partitions de measurement_y2006m02, toute donnée insérée dans measurement qui correspond à measurement_y2006m02 (ou donnée qui est directement insérée dans measurement_y2006m02, sous réserve qu'elle satisfasse sa contrainte de partition) sera redirigée vers l'une de ses partitions en se basant sur la colonne peaktemp. La clé de partition spécifiée pourrait se chevaucher avec la clé de partition du parent, il faut donc faire spécialement attention lorsque les limites d'une sous-partition sont spécifiées afin que l'ensemble de données qu'elle accepte constitue un sous-ensemble de ce que les propres limites de la partition acceptent  ; le système n'essayera pas de vérifier si c'est vraiment le cas.

  3. Créez un index sur la ou les colonnes de la clé, ainsi que tout autre index que vous pourriez vouloir pour chaque partition. (L'index sur la clé n'est pas strictement nécessaire, mais c'est utile dans la plupart des scénarios.) Ceci crée automatiquement un index sur chaque partition, et toutes les partitions que vous créerez ou attacherez plus tard contiendront également l'index.

    CREATE INDEX ON measurement (logdate);

    Assurez-vous que le paramètre de configuration enable_partition_pruning ne soit pas désactivé dans postgresql.conf. S'il l'est, les requêtes ne seront pas optimisées comme voulu.

Dans l'exemple ci-dessus, nous créerions une nouvelle partition chaque mois, il serait donc avisé d'écrire un script qui génère le DDL nécessaire automatiquement.

5.10.2.2. Maintenance des partitions

Normalement, l'ensemble des partitions établies lors de la définition initiale de la table n'a pas vocation à demeurer statique. Il est normal de vouloir supprimer d'anciennes partitions de données et périodiquement ajouter de nouvelles partitions pour les nouvelles données. Un des avantages les plus importants du partitionnement est précisément qu'il permet d'exécuter cette tâche de maintenance normalement pénible instantanément en manipulant la structure de la partition, plutôt que de physiquement bouger de grands ensembles de données.

Le moyen le plus simple pour supprimer d'anciennes données est de supprimer la partition qui n'est plus nécessaire :

DROP TABLE measurement_y2006m02;

Cela peut supprimer des millions d'enregistrements très rapidement, car il n'est pas nécessaire de supprimer chaque enregistrement séparément. Veuillez noter toutefois que la commande ci-dessus nécessite de prendre un verrou de type ACCESS EXCLUSIVE sur la table parente.

Une autre possibilité, qui est généralement préférable, est de ne pas supprimer la partition de la table partitionnée, mais de la conserver en tant que table :

ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;

Cela permet d'effectuer ensuite d'autres opérations sur les données avant de la supprimer. Par exemple, il s'agit souvent du moment idéal pour sauvegarder les données en utilisant COPY, pg_dump, ou des outils similaires. Cela pourrait également être le bon moment pour agréger les données dans un format moins volumineux, effectuer d'autres manipulations de données ou exécuter des rapports.

De la même manière, nous pouvons ajouter une nouvelle partition pour gérer les nouvelles données. Nous pouvons créer une partition vide dans la table partitionnée exactement comme la première partition a été créée précédemment :

CREATE TABLE measurement_y2008m02 PARTITION OF measurement
    FOR VALUES FROM ('2008-02-01') TO ('2008-03-01')
    TABLESPACE fasttablespace;

De manière alternative, il est parfois plus utile de créer la nouvelle table en dehors de la structure de la partition, et d'en faire une partition plus tard. Cela permet de charger des données, les vérifier et effectuer des transformations avant que les données apparaissent dans la table partitionnée :

CREATE TABLE measurement_y2008m02
  (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS)
  TABLESPACE fasttablespace;

ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
   CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );

\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work

ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
    FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );

Avant d'exécuter une commande ATTACH PARTITION, il est recommandé de créer une contrainte CHECK sur la table qui doit être attachée décrivant la contrainte de partition désirée. De cette manière, le système n'aura pas besoin d'effectuer un parcours de la table pour valider la contrainte de partition implicite. Sans la contrainte CHECK, la table sera parcourue pour valider la contrainte de partition tout en ayant un verrou de niveau ACCESS EXCLUSIVE sur la table parente. Vous pouvez alors supprimer la contrainte CHECK redondante après que ATTACH PARTITION soit fini.

Comme expliqué ci-dessus, il est possible de créer des index sur les tables partitionnées, et ils seront automatiquement appliqués à la hiérarchie complète. Ceci est très pratique car, non seulement les partitions existantes seront indexées, mais aussi toute nouvelle partition le sera. La seule limitation est qu'il n'est pas possible d'utiliser la clause CONCURRENTLY lors de la création d'un tel index partitionné. Pour éviter de longues périodes de verrous, il est possible d'utiliser CREATE INDEX ON ONLY sur la table partitionnée ; un tel index est marqué invalide et les partitions l'obtiennent pas automatiquement l'index. Les index sur les partitions peuvent être créés séparément en utilisant CONCURRENTLY, puis être attachés à l'index sur le parent en utilisant ALTER INDEX .. ATTACH PARTITION. Une fois que les index des partitions sont attachés à l'index parent, l'index parent est marqué valide automatiquement. Par exemple :

CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales);

CREATE INDEX measurement_usls_200602_idx
    ON measurement_y2006m02 (unitsales);
ALTER INDEX measurement_usls_idx
    ATTACH PARTITION measurement_usls_200602_idx;
...

Cette technique peut aussi être utilisée avec les contraintes UNIQUE et PRIMARY KEY ; les index sont créés implicitement quand la contrainte est créer. Par exemple :

ALTER TABLE ONLY measurement ADD UNIQUE (city_id, logdate);

ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
ALTER INDEX measurement_city_id_logdate_key
    ATTACH PARTITION measurement_y2006m02_city_id_logdate_key;
...

5.10.2.3. Limitations

Les limitations suivantes s'appliquent aux tables partitionnées :

  • Il n'y a aucun moyen de créer une contrainte d'exclusion couvrant toutes les partitions ; il est seulement possible de contraindre chaque partition individuellement.

  • Les contraintes d'unicité sur les tables partitionnées doivent inclure toutes les colonnes de la clé de partitionnement. Cette limitation existe parce que PostgreSQL™ peut seulement garantir l'unicité au niveau de chaque partition individuelle.

  • Alors que les clés primaires sont prises en charge sur les tables partitionnées, les clés étrangères faisant référence à des tables partitionnées ne sont pas prises en charge. (Les références de clés étrangères d'une table partitionnée vers une autre table sont supportées.)

  • en cas de besoin, les triggers BEFORE ROW doivent être définies sur des partitions individuelles, et non sur la table partitionnée.

  • Mélanger des relations temporaires et permanentes dans la même arborescence de partitions n'est pas autorisé. Par conséquent, si une table partitionnée est permanente, ses partitions doivent l'être aussi ; de même si la table partitionnée est temporaire, ses partitions doivent l'être aussi. Lors de l'utilisation de relations temporaires, tous les membres de l'arborescence des partitions doivent être issus de la même session.

5.10.3. Implémentation utilisant l'héritage

Bien que le partitionnement déclaratif natif soit adapté pour la plupart des cas d'usage courant, il y a certains cas où une approche plus flexible pourrait être utile. Le partitionnement peut être implémenté en utilisant l'héritage de table, ce qui permet plusieurs autres fonctionnalités qui ne sont pas supportées par le partitionnement déclaratif, comme :

  • Pour le partitionnement déclaratif, les partitions doivent avoir exactement les mêmes colonnes que la table partitionnée, alors qu'avec l'héritage de table, les tables filles peuvent avoir des colonnes supplémentaires non présentes dans la table parente.

  • L'héritage de table permet de multiples héritages.

  • Le partitionnement déclaratif ne prend en charge que le partitionnement par intervalle, par liste et par hachage, tandis que l'héritage de table permet de diviser les données de la manière choisie par l'utilisateur. (Notez, cependant, que si l'exclusion de contrainte n'est pas en mesure de tailler efficacement les tables filles, la performance de la requête peut être faible).

  • Certaines opérations nécessitent un verrou plus fort en utilisant le partitionnement déclaratif qu'en utilisant l'héritage de table. Par exemple, ajouter ou supprimer une partition d'une table partitionnée nécessite de prendre un verrou de type ACCESS EXCLUSIVE sur la table parente, alors qu'un verrou de type SHARE UPDATE EXCLUSIVE est suffisant dans le cas de l'héritage classique.

5.10.3.1. Exemple

Nous utilisons la même table measurement non partitionnée que nous avons déjà utilisée au-dessus. Pour l'implémenter le partitionnement en utilisant l'héritage, utilisez les étapes suivantes :

  1. Créez la table « master », à partir de laquelle toutes les tables « filles » hériteront. Cette table ne contiendra aucune donnée. Ne définissez aucune contrainte de vérification sur cette table, à moins que vous n'ayez l'intention de l'appliquer de manière identique sur toutes les tables filles. Il n'y a aucun intérêt à définir d'index ou de contrainte unique sur elle non plus. Pour notre exemple, la table master correspond à la table measurement définie à l'origine.

  2. Créez plusieurs tables « enfant » qui chacune hérite de la table master. Normalement, ces tables n'auront aucune colonne supplémentaire par rapport à celles héritées de la table master. Tout comme avec le partitionnement déclaratif, ces tables filles ont tous les aspects des tables (ou tables étrangères) PostgreSQL™ normales.

    CREATE TABLE measurement_y2006m02 () INHERITS (measurement);
    CREATE TABLE measurement_y2006m03 () INHERITS (measurement);
    ...
    CREATE TABLE measurement_y2007m11 () INHERITS (measurement);
    CREATE TABLE measurement_y2007m12 () INHERITS (measurement);
    CREATE TABLE measurement_y2008m01 () INHERITS (measurement);
    
  3. Ajoutez les contraintes de tables qui ne se chevauchent pas sur les tables filles pour définir les valeurs de clé autorisées dans chaque table fille.

    Les exemples typiques seraient :

    CHECK ( x = 1 )
    CHECK ( county IN ( 'Oxfordshire', 'Buckinghamshire', 'Warwickshire' ))
    CHECK ( outletID >= 100 AND outletID < 200 )
    

    Assurez-vous que les contraintes garantissent qu'il n'y a pas de chevauchement entre les valeurs de clés permises dans différentes tables filles. Une erreur fréquente est de mettre en place des contraintes d'intervalle comme ceci :

    CHECK ( outletID BETWEEN 100 AND 200 )
    CHECK ( outletID BETWEEN 200 AND 300 )
    

    Cet exemple est faux puisqu'on ne peut pas savoir à quelle table fille appartient la valeur de clé 200.

    Il serait préférable de créer à la place les tables filles comme cela :

    CREATE TABLE measurement_y2006m02 (
        CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2006m03 (
        CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' )
    ) INHERITS (measurement);
    
    ...
    CREATE TABLE measurement_y2007m11 (
        CHECK ( logdate >= DATE '2007-11-01' AND logdate < DATE '2007-12-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2007m12 (
        CHECK ( logdate >= DATE '2007-12-01' AND logdate < DATE '2008-01-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2008m01 (
        CHECK ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    ) INHERITS (measurement);
    
  4. Pour chaque table fille, créez un index sur la ou les colonnes de la clé, ainsi que tout autre index que vous voudriez.

    CREATE INDEX measurement_y2006m02_logdate ON measurement_y2006m02 (logdate);
    CREATE INDEX measurement_y2006m03_logdate ON measurement_y2006m03 (logdate);
    CREATE INDEX measurement_y2007m11_logdate ON measurement_y2007m11 (logdate);
    CREATE INDEX measurement_y2007m12_logdate ON measurement_y2007m12 (logdate);
    CREATE INDEX measurement_y2008m01_logdate ON measurement_y2008m01 (logdate);
    
  5. Nous voulons que notre application soit capable de dire INSERT INTO measurement ... et d'avoir les données redirigées dans la table fille appropriée. Nous pouvons réaliser cela en attachant une fonction de déclencheur convenable sur la table master. Si les données doivent être ajoutées sur la dernière table fille uniquement, nous pouvons utiliser une fonction de déclencheur très simple :

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
    

    Après avoir créé la fonction, nous créons un déclencheur qui appelle la fonction de déclencheur :

    CREATE TRIGGER insert_mesure_trigger
        BEFORE INSERT ON mesure
        FOR EACH ROW EXECUTE FUNCTION mesure_insert_trigger();
         

    La fonction déclencheur doit être redéfinie chaque mois pour qu'elle pointe toujours sur la table fille active. La définition du déclencheur n'a pas besoin d'être redéfinie.

    Il est également possible de laisser le serveur localiser la table fille dans laquelle doit être insérée la ligne proposée en entrée. Une fonction déclencheur plus complexe peut être utilisée pour cela  :

    CREATE OR REPLACE FUNCTION mesure_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        IF ( NEW.date_trace >= DATE '2006-02-01' AND
             NEW.date_trace < DATE '2006-03-01' ) THEN
            INSERT INTO mesure_a2006m02 VALUES (NEW.*);
        ELSIF ( NEW.date_trace >= DATE '2006-03-01' AND
                NEW.date_trace < DATE '2006-04-01' ) THEN
            INSERT INTO mesure_a2006m03 VALUES (NEW.*);
        ...
        ELSIF ( NEW.date_trace >= DATE '2008-01-01' AND
                NEW.date_trace < DATE '2008-02-01' ) THEN
            INSERT INTO mesure_a2008m01 VALUES (NEW.*);
        ELSE
            RAISE EXCEPTION 'Date en dehors de l''intervalle. Corrigez la fonction mesure_insert_trigger() !';
        END IF;
        RETURN NULL;
    END;
    $$
    LANGUAGE plpgsql;
         

    La définition du déclencheur ne change pas. Chaque test IF doit correspondre exactement à la contrainte CHECK de cette table fille.

    Bien que cette fonction soit plus complexe que celle du mois seul, il n'est pas nécessaire de l'actualiser aussi fréquemment, les branches pouvant être ajoutées avant d'être utiles.

    [Note]

    Note

    En pratique, il pourrait être préférable de vérifier prioritairement la dernière table fille créée si la plupart des insertions lui sont destinées. Pour des raisons de simplicité, les tests du déclencheur sont présentés dans le même ordre que les autres parties de l'exemple.

    Une approche différente est de rediriger les insertions dans la table fille appropriée à l'aide de règles, plutôt qu'un déclencheur, sur la table master. Par exemple :

    CREATE RULE measurement_insert_y2006m02 AS
    ON INSERT TO measurement WHERE
        ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    DO INSTEAD
        INSERT INTO measurement_y2006m02 VALUES (NEW.*);
    ...
    CREATE RULE measurement_insert_y2008m01 AS
    ON INSERT TO measurement WHERE
        ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    DO INSTEAD
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
    

    Une règle à un surcoût bien plus important qu'un trigger, mais le surcoût n'est payé qu'une fois par requête plutôt qu'une fois par ligne, cette méthode peut être avantageuse pour les situations d'insertions en masse. Toutefois, dans la plupart des cas, la méthode du déclencheur offrira de meilleures performances.

    Soyez conscient que COPY ignore les règles. Si vous voulez utiliser COPY pour insérer des données, vous devrez les copier dans la bonne table fille plutôt que dans la table master. COPY déclenche les triggers, vous pouvez donc l'utiliser normalement si vous utilisez l'approche par déclencheur.

    Un autre inconvénient à l'approche par règle est qu'il n'y a pas de moyen simple de forcer une erreur si l'ensemble de règles ne couvre pas la date d'insertion; les données iront silencieusement dans la table master à la place.

  6. Assurez-vous que le paramètre de configuration constraint_exclusion ne soit pas désactivé dans postgresql.conf. S'il l'est, les requêtes ne seront pas optimisées comme voulu.

Comme nous pouvons le voir, une hiérarchie complexe de tables peut nécessiter une quantité de DDL non négligeable. Dans l'exemple du dessus, nous créerions une nouvelle table fille chaque mois, il serait donc sage d'écrire un script qui génère le DDL requis automatiquement.

5.10.3.2. Maintenance du partitionnement par héritage

Pour supprimer les anciennes données rapidement, il suffit de supprimer la table fille qui n'est plus nécessaire :

DROP TABLE mesure_a2006m02;
  

Pour supprimer la table fille de la hiérarchie de tables, mais pour garder l'accès à la table en tant que telle :

ALTER TABLE mesure_a2006m02 NO INHERIT mesure;
  

Pour ajouter une nouvelle table fille pour gérer les nouvelles données, créez une table fille vide tout comme les tables filles originales ont été créées ci-dessus :

CREATE TABLE mesure_a2008m02 (
    CHECK ( date_trace >= DATE '2008-02-01' AND date_trace < DATE '2008-03-01' )
    ) INHERITS (mesure);
  

Une autre alternative est de créer et de remplir la nouvelle table enfant avant de l'ajouter à la hiérarchie de la table. Ceci pourrait permettre aux données d'être chargées, vérifiées et transformées avant d'être rendues visibles aux requêtes sur la table parente.

CREATE TABLE mesure_a2008m02
  (LIKE mesure INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
ALTER TABLE mesure_a2008m02 ADD CONSTRAINT y2008m02
   CHECK ( date_trace >= DATE '2008-02-01' AND date_trace < DATE '2008-03-01' );
\copy mesure_a2008m02 from 'mesure_a2008m02'
-- quelques travaux de préparation des données
ALTER TABLE mesure_a2008m02 INHERIT mesure;
  

5.10.3.3. Restrictions

Les restrictions suivantes s'appliquent au partitionnement par héritage :

  • Il n'existe pas de moyen automatique de vérifier que toutes les contraintes de vérification (CHECK) sont mutuellement exclusives. Il est plus sûr de créer un code qui fabrique les tables filles et crée et/ou modifie les objets associés plutôt que de les créer manuellement ;

  • les schémas montrés ici supposent que les colonnes clés du partitionnement d'une ligne ne changent jamais ou, tout du moins, ne changent pas suffisamment pour nécessiter un déplacement vers une autre partition. Une commande UPDATE qui tente de le faire échoue à cause des contraintes CHECK. Pour gérer ce type de cas, des déclencheurs peuvent être convenablement positionnés pour la mise à jour sur les tables filles, mais cela rend la gestion de la structure beaucoup plus complexe.

  • Si VACUUM ou ANALYZE sont lancés manuellement, il est obligatoire de les utiliser sur chaque table fille. Une commande comme :

    ANALYZE mesure;
    

    ne traite que la table maître.

  • Les commandes INSERT avec des clauses ON CONFLICT ont probablement peu de chances de fonctionner comme attendu, dans la mesure où l'action du ON CONFLICT est uniquement effectuée dans le cas de violations qui sont uniques à la table cible, pas à ses tables filles.

  • Les déclencheurs ou les règles seront nécessaires pour rediriger les lignes vers la table fille voulue, à moins que l'application ne soit explicitement au courant du schéma de partitionnement. Les déclencheurs peuvent être plus compliqués à écrire, et seront bien plus lents que la redirection de ligne effectuée en interne par le partitionnement déclaratif.

5.10.4. Élagage de partition

L'élagage des partitions (Partition pruning) est une technique d'optimisation des requêtes qui vise à améliorer les performances des tables à partitionnement déclaratif. A titre d'exemple :

SET enable_partition_pruning = on;                 -- the default
SELECT count(*) FROM mesure WHERE date_trace >= DATE '2008-01-01';

Sans l'élagage de partition, la requête ci-dessus parcourerait chacune des partitions de la table mesure. Avec l'élagage de partition activé, le planificateur examinera la définition de chaque partition et montrera qu'il n'est pas nécessaire de la parcourir car elle ne contient aucune ligne respectant la clause WHERE de la requête. Lorsque le planificateur peut le confirmer, il exclut (élague) la partition du plan de recherche.

En utilisant la commande EXPLAIN et le paramètre de configuration enable_partition_pruning, il est possible de voir la différence entre un plan pour lequel des partitions ont été élaguées et celui pour lequel elles ne l'ont pas été. Un plan typique non optimisé pour ce type de configuration de table serait :

SET enable_partition_pruning = off;
EXPLAIN SELECT count(*) FROM mesure WHERE date_trace >= DATE '2008-01-01';

                                    QUERY PLAN
-----------------------------------------------------------------------------------
 Aggregate  (cost=188.76..188.77 rows=1 width=8)
   ->  Append  (cost=0.00..181.05 rows=3085 width=0)
         ->  Seq Scan on measurement_y2006m02  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m03  (cost=0.00..33.12 rows=617 width=0)
                Filter: (logdate >= '2008-01-01'::date)
 ...
         ->  Seq Scan on measurement_y2007m11  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2007m12  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)

Quelques partitions, voire toutes, peuvent utiliser des parcours d'index à la place des parcours séquentiels de la table complète, mais le fait est qu'il n'est pas besoin de parcourir les anciennes partitions pour répondre à cette requête. Lorsque l'exclusion de contrainte est activée, un plan significativement moins coûteux est obtenu, qui délivre la même réponse :

SET enable_partition_pruning = on;
EXPLAIN SELECT count(*) FROM mesure WHERE date_trace >= DATE '2008-01-01';

                                    QUERY PLAN
-----------------------------------------------------------------------------------
 Aggregate  (cost=37.75..37.76 rows=1 width=8)
   ->  Append  (cost=0.00..36.21 rows=617 width=0)
         ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)

Il est à noter que l'élagage des partitions n'est piloté que par les contraintes définies implicitement par les clés de partition, et non par la présence d'index : il n'est donc pas nécessaire de définir des index sur les colonnes clés. Si un index doit être créé pour une partition donnée, ceci dépendra du fait que vous vous attendez à ce que les requêtes qui parcourent la partition parcourent généralement une grande partie de la partition ou seulement une petite partie. Un index sera utile dans ce dernier cas, mais pas dans le premier.

L'élagage des partitions peut être effectuée non seulement lors de la planification d'une requête, mais aussi lors de son exécution. Ceci est utile car cela peut permettre d'élaguer plus de partitions lorsque les clauses contiennent des expressions dont les valeurs ne sont pas connues au moment de la planification de la requête ; par exemple, des paramètres définis dans une instruction PREPARE, utilisant une valeur obtenue d'une sous-requête ou utilisant une valeur paramétrée sur la partie interne du nœud de boucle imbriqué (nested loop join. L'élagage de la partition pendant l'exécution peut être réalisé à l'un des moments suivant :

  • Lors de l'initialisation du plan d'exécution, l'élagage de partition peut être effectué pour les valeurs de paramètres qui sont connues pendant cette phase. Les partitions qui ont été élaguées pendant cette étape n'apparaîtront pas dans l'EXPLAIN ou l'EXPLAIN ANALYZE de la requête. Il est tout de même possible de déterminer le nombre de partitions qui ont été supprimées pendant cette phase en observant la propriété « Subplans Removed » (sous-plans supprimés) dans la sortie d'EXPLAIN.

  • Pendant l'exécution réelle du plan d'exécution. L'élagage des partitions peut également être effectué pour supprimer des partitions en utilisant des valeurs qui ne sont connues que pendant l'exécution de la requête. Cela inclut les valeurs des sous-requêtes et les valeurs utilisées pendant la recherche comme lors des nœuds de boucle imbriqué (nested loop join). Comme la valeur de ces paramètres peut changer plusieurs fois pendant l'exécution de la requête, l'élagage de la partition est effectué chaque fois que l'un des paramètres d'exécution utilisés pour celui-ci change. Déterminer si les partitions ont été élaguées pendant cette phase nécessite une inspection minutieuse de la propriété nloops de la sortie d'EXPLAIN ANALYZE. Les sous-plans correspondant aux différentes partitions pourraient avoir différentes valeurs dépendant du nombre de fois chacun d'entre eux a été évité lors de l'exécution. Certains pourraient être affichés comme (never executed) (littéralement, jamais exécuté) s'ils sont évités à chaque fois.

L'élagage des partitions peut être désactivée à l'aide du paramètre enable_partition_pruning.

[Note]

Note

L'élagage de partitions au moment de l'exécution survient seulement pour le type de nœud Append, mais pas pour les nœuds MergeAppend et ModifyTable. Ceci pourrait changer dans une prochaine version de PostgreSQL™.

5.10.5. Partitionnement et Contrainte d'exclusion

Les contraintes d'exclusion sont une technique d'optimisation de requêtes similaire à l'élagage de partitions. Bien qu'il soit principalement utilisé pour les tables partitionnées à l'aide de la méthode par héritage, il peut être utilisé à d'autres fins, y compris avec le partitionnement déclaratif.

Les contraintes d'exclusion fonctionnent d'une manière très similaire à l'élagage de partitions, sauf qu'elle utilise les contraintes CHECK de chaque table (d'où son nom) alors que l'élagage de partition utilise les limites de partition de la table, qui n'existe que dans le cas d'un partitionnement déclaratif. Une autre différence est que la contrainte d'exclusion n'est appliquée qu'au moment de la planification du plan d'excution ; il n'y a donc pas de tentative de suppression de partitions au moment de l'exécution.

Du fait que les contraintes d'exclusion utilisent la contrainte CHECK, ceci la rend plus lente par rapport à l'élagage de partitions. Néamoins, il peut parfois être utilisé comme un avantage : puisque les contraintes peuvent être définies même sur des tables avec partitionnement déclaratif, en plus de leurs limites internes, les contraintes d'exclusion peuvent être capables de supprimer des partitions supplémentaires pendant la phase de planification de la requête.

La valeur par défaut (et donc recommandée) de constraint_exclusion n'est ni on ni off, mais un état intermédiaire appelé partition, qui fait que la technique est appliquée seulement aux requêtes qui semblent fonctionner avec des tables partitionnées. La valeur on fait que le planificateur examine les contraintes CHECK dans chaque requête, y compris les requêtes simples qui ont peu de chance d'en profiter.

Les restrictions suivantes s'appliquent à l'exclusion de contraintes :

  • Les contraintes d'exclusion ne sont appliquées que lors de la phase de planification de la requête, contrairement à l'élagage de partition, qui peut être appliqué lors de la phase d'exécution de la requête.

  • La contrainte d'exclusion ne fonctionne que si la clause WHERE de la requête contient des constantes (ou des paramètres externes). Par exemple, une comparaison entre une fonction non immutable telle que CURRENT_TIMESTAMP ne peut pas être optimisée, car le planificateur ne peut pas savoir dans quelle table fille la valeur de la fonction ira lors de l'exécution.

  • Les contraintes de partitionnement doivent rester simples. Dans le cas contraire, le planificateur peut rencontrer des difficultés à déterminer les tables filles qu'il n'est pas nécessaire de parcourir. Des conditions simples d'égalité pour le partitionnement de liste ou des tests d'intervalle simples lors de partitionnement par intervalles sont recommandées, comme cela est illustré dans les exemples précédents. Une bonne règle consiste à s'assurer que les comparaisons entre colonnes de partitionnement et constantes utilisées par les contraintes de partitionnement se fassent uniquement à l'aide d'opérateurs utilisables par les index B-tree car seules les colonnes indexables avec un index B-tree sont autorisées dans la clé de partitionnement.

  • Toutes les contraintes sur toutes les filles de la table parente sont examinées lors de l'exclusion de contraintes. De ce fait, un grand nombre des filles augmente considérablement le temps de planification de la requête. Ainsi, le partitionnement basé sur l'héritage fonctionnera bien jusqu'à une centaine de tables enfant ; n'essayez pas d'utiliser plusieurs milliers d'enfants.

5.10.6. Bonnes pratiques pour le partitionnement déclaratif

Le choix de la méthode de partitionnement d'une table doit être fait avec beaucoup d'attention car les performances de l'optimisation des requêtes et leur exécution peuvent être fortement affectées négativement par un mauvais design.

Une des décisions les plus critiques au niveau du design est le choix de la clé (ou des clés) de partitionnement. Souvent, le meilleur choix revient à partitionner par la (ou les) colonne(s) qui apparaissent le plus fréquemment dans les clauses WHERE des requêtes en cours d'exécution sur la table partitionnée. Les éléments de la clause WHERE qui correspondent ou sont compatibles avec la clé de partitionnement peuvent être utilisés pour ignorer les partitions inutiles. La suppression des données inutiles est aussi un facteur à considérer lors de la conception de votre stratégie de partitionnement. Une partition entière peut être détachée rapidement, donc il peut être bénéfique de concevoir la stratégie de partitionnement d'une telle façon que tout les données à supprimer d'un coup soient concentrées sur une seule partition.

Choisir le nombre cible de partitions pour la table est aussi une décision critique à prendre. Ne pas avoir suffisamment de partitions pourrait avoir pour conséquence des index trop gros, et un emplacement des données pauvre qui résulterait en un ratio bas de lecture en cache. Néanmoins, diviser la table en trop de partitions pourrait aussi causer des problèmes. Trop de partitions pourrait signifier une optimisation plus longue des requêtes et une consommation mémoire plus importante durant l'optimisation et l'exécution. Lors de la conception du partitionnement de votre table, il est aussi important de prendre compte les changements pouvant survenir dans le futur. Par exemple, si vous choisissez d'avoir une partition par client et que vous avez un petit nombre de gros clients, il est important de réfléchir aux implications si, dans quelques années, vous vous trouvez avec un grand nombre de petits clients. Dans ce cas, il serait mieux de choisir de partitionner par RANGE et de choisir un nombre raisonnable de partitions, chacune contenant un nombre fixe de clients, plutôt que d'essayer de partitionner par LIST en espérant que le nombre de clients ne dépasse pas ce qui est possible au niveau du partitionnement des données.

Le sous-partitionnement peut aussi être utile pour diviser encore plus les partitions pour lesquelles on s'attend à ce qu'elles deviennent bien plus grosses que les autres partitions. Un sous-partitionnement excessif peut facilement amener à un grand nombre de partitions et peut causer les problèmes mentionnés dans le paragraphe précédent.

Il est aussi important de considérer la surcharge du partitionnement lors de l'optimisation et de l'exécution. L'optimiseur est généralement capble de gérer les hiérarchies de partitions qui montent à quelques centaines de partitions. Les durées d'optimisation deviennent plus longues et la consommation de mémoire devient plus importante au fur et à mesure de l'ajout de partitions. Ceci est tout particulièrement vrai pour les commandes UPDATE et DELETE. Une autre raison de se soucier d'un grand nombre de partitions est que la consommation mémoire du serveur pourrait grossir de façon significative sur une période de temps, et tout spécialement si beaucoup de sessions touchent un grand nombre de partitions. Ceci est dû au chargement des métadonnées nécessaires pour chaque partition en mémoire locale.

Avec une charge de type entrepôt de données, il peut être sensé d'utiliser un plus grand nombre de partitions que pour une charge de type OLTP. En général, dans les entrepôts de données, le temps d'optimisation d'une requête est peu importante parce que la majorité du temps de traitement est passée sur l'exécution de la requête. Avec l'une de ces deux types de charges, il est important de prendre les bonnes décisions dès le début, car le re-partitionnement de grosses quantités de données peut être très lent. Les simulations de la charge attendue sont souvent bénéfiques pour optimiser la stratégie de partitionnement. Ne jamais supposer qu'un plus grand nombre de partitions est toujours mieux qu'un petit nombre de partitions, et vice-versa.