Partitionner une table. Comparaison PostGreSQL / MS SQL Server

Comme nous l’avons dit dans ce billet (Partitionner vos tables pour améliorer les performances) le réel intérêt du partitionnement réside dans le gestion des espaces de stockage des différentes partitions. Bien que le partitionnement de table soit disponible dans PostGreSQL depuis quelques versions, il est loin d’être aussi intéressant que ce que propose Oracle ou MS SQL Server… Cet article à pour but de comparer la mise en Å“uvre des différentes solutions que proposent PostGreSQL et MS SQL Server.

NOTA : les SGBDR testés sont PostGreSQL version 9.1 et MS SQL Server version 2008 R2 Enterprise (le partitionnement n’étant disponible qu’à partir de l’édition Enterprise).

1 – LE PRINCIPE DU PARTITIONNEMENT

L’idée du partitionnement est basé sur le principe de divide et impera c’est à dire diviser pour régner, dans le sens ou il est plus facile de manipuler un plus faible volume de données lorsque c’est possible. Autrement dit, le partitionnement revient à saucissonner une table en plusieurs partitions afin que la majeur partie des requêtes sur cette table ne concerne et ne manipule qu’une seule partition. Il est encore plus intéressant si chaque partition est stockée sur un disque indépendant (ou une LUN d’un SAN alignée sur des disques physiques indépendant) permettant ainsi des opérations d’écriture et de lecture physique en parallèle.
Si le principe est très intéressant, son implémentation réserve de multiples surprises que nous allons découvrir à travers cette comparaison…

2 – L’EXEMPLE

Une table très simple nous servira de cobaye. Soit une table enregistrant des mesures à des dates précises. Nous voulons partitionner cette tables par années.
Voici la description de cette table en « pur » SQL :


CREATE TABLE T_MESURE_MSR  
(  
  MSR_ID      INT   NOT NULL,
  MSR_DATE    DATE  NOT NULL,  
  MSR_MESURE  FLOAT NOT NULL  
)

Pour laquelle nous désirons partitionner par années, sachant que nous bornes sont :

  • le premier janvier 2008
  • le premier janvier 2009
  • le premier janvier 2010
  • le premier janvier 2011.

Première surprise : la règle des piquets et des intervalles…
Si nous voulons partitionner en n valeurs pivot (dites « piquets« ) il nous faudra n+1 intervalles ! Sinon, nous ne pourrons prendre en compte toutes les valeurs.
Par exemple dans une table partitionnée par date, si nous voulons partitionner pour les années de 2008 à 2011, il faudra 5 partitions :

  • toutes les lignes de 2007 et avant
  • les lignes de l’année 2008
  • les lignes de l’année 2009
  • les lignes de l’année 2010
  • toutes les lignes de 2011 et après

Partitionnement de tables avec SQL

3 – TECHNIQUE DU PARTITIONNEMENT

Pour PostGreSQL, la méthode se déroule en 6 temps :

  • créer une table mère
  • créer les espaces de stockage des partitions (ici 5)
  • créer autant de tables filles qu’il existe de partitions (ici 5) avec une contrainte CHECK vérifiant que les données valident les bornes de la partition;
  • greffer sur la table mère ou la vue autant de règles pour chacun des ordres SQL de mise à jour de données qu’il existe de table fille (5 « rules » INSERT + 5 « rules » UPDATE + 5 « rules » DELETE).
  • rajouter un déclencheur pour interdire à la table mère de recevoir des lignes (2 commandes SQL)

Avec 4 valeurs pivots, soit 5 partitions, PostGreSQL nécessite donc de lancer 28 commandes SQL, mais nous allons voir que les règles UPDATE multiplient encore cette complexité par le nombre de partition. Au final nous arrivons à un nombre global de 58 requêtes SQL !
A noter : il n’existe pas d’IHM pour créer un partitionnement avec PostGreSQL.

Pour SQL Server, la méthode se déroule en 4 temps :

  • créer autant d’espaces de stockage que de partitions (FILEGROUP + FILE = 10 commandes)
  • créer une fonction de partition avec les valeurs pivot
  • créer un schéma de répartition des données faisant appel à la fonction de partition
  • créer la table sur le schéma de partition

Avec 4 valeurs pivots, soit 5 partitions, MS SQL Server nécessite donc de lancer 13 ordres SQL.
L’outil SSMS propose outre des IHM pour le partitionnement, un assistant.

Assistant de partitionnement dans SSMS pour SQL Server :

Partitionnement de tables SQL avec SQL Server : assitant graphique
Partitionnement de tables SQL avec SQL Server : assitant graphique

Bonne idée, mais hélas l’assistant est bugué !

NOTA : la différence est déjà significative en terme de nombre de commande et donc de complexité de mise en œuvre :
pour PostGreSQL il faut ((n+1) * 5) + 3 commandes SQL si l’on partitionne avec gestion des espaces de stockage, tandis que que pour SQL Server il faut ((n+1) * 2) + 3 commandes SQL, soit un gain d’environ 2,5 fois en faveur de MS SQL Server.
De plus, si l’on ne gère pas les espaces de stockage, la différence devient infinie ! En effet, SQL Server n’a besoin que de 3 commandes quelque soit le nombre de partitions, tandis que PostGreSQL à besoin de ((n+1) * 4) + 2 commandes…

Voyons maintenant le mise en Å“uvre dans les deux SGBDR…

3 – LA SOLUTION POSTGRESQL

3.1 – mise en Å“uvre

3.1.1 – création de la table mère :


CREATE TABLE T_MESURE_MSR
(  
  MSR_ID      INT   NOT NULL,  
  MSR_DATE    DATE  NOT NULL,  
  MSR_MESURE  FLOAT NOT NULL  
);

3.1.2 – création des espaces de stockage :

Les répertoires visés doivent préalablement exister.

CREATE TABLESPACE TS_PART0
  LOCATION 'E:\SQLDATA\PARTMSR0\';  
   
CREATE TABLESPACE TS_PART1  
  LOCATION 'F:\SQLDATA\PARTMSR1\';  
   
CREATE TABLESPACE TS_PART2  
  LOCATION 'G:\SQLDATA\PARTMSR2\';
   
CREATE TABLESPACE TS_PART3  
  LOCATION 'H:\SQLDATA\PARTMSR3\';
   
CREATE TABLESPACE TS_PART4  
  LOCATION 'I:\SQLDATA\PARTMSR4\';


3.1.3 – création des 5 tables filles de partitionnement :


CREATE TABLE T_MESURE_AVANT2008_MSR  
(  
CHECK ( MSR_DATE < DATE '2008-01-01')  
) INHERITS (T_MESURE_MSR) TABLESPACE TS_PART0;  
 
CREATE TABLE T_MESURE_AN2008_MSR  
(  
CHECK ( MSR_DATE >= DATE '2008-01-01' AND MSR_DATE < DATE '2009-01-01')  
) INHERITS (T_MESURE_MSR) TABLESPACE TS_PART1;  
 
CREATE TABLE T_MESURE_AN2009_MSR  
(  
CHECK ( MSR_DATE >= DATE '2009-01-01' AND MSR_DATE < DATE '2010-01-01')  
) INHERITS (T_MESURE_MSR) TABLESPACE TS_PART2;  
 
CREATE TABLE T_MESURE_AN2010_MSR  
(  
CHECK ( MSR_DATE >= DATE '2010-01-01' AND MSR_DATE < DATE '2011-01-01')  
) INHERITS (T_MESURE_MSR) TABLESPACE TS_PART3;  
 
CREATE TABLE T_MESURE_APRES2010_MSR  
(  
CHECK ( MSR_DATE >= DATE '2011-01-01')  
) INHERITS (T_MESURE_MSR) TABLESPACE TS_PART4;

Notez qu’à ce stade il n’existe aucun moyen de contrôle de ne pas faire d’erreur, par exemple en créant deux tables filles dont les contraintes de domaine se superposent ou crée un « vide »…

3.1.4 – création des 15 règles de reroutage de mise à jour

Reroutage des INSERTs :

INSERT avant 2008 :


CREATE RULE R_I_MSR_AVANT2008
AS  
ON INSERT TO T_MESURE_MSR  
   WHERE ( MSR_DATE < DATE '2008-01-01' )  
DO INSTEAD  
  INSERT INTO T_MESURE_AVANT2008_MSR  
  VALUES ( NEW.MSR_ID,  
           NEW.MSR_DATE,  
           NEW.MSR_MESURE );

INSERT de 2008 :


CREATE RULE R_I_MSR_AN2008 AS  
ON INSERT TO T_MESURE_MSR  
   WHERE ( MSR_DATE >= DATE '2008-01-01'  
       AND MSR_DATE <  DATE '2009-01-01' )  
DO INSTEAD  
  INSERT INTO T_MESURE_AN2008_MSR  
  VALUES ( NEW.MSR_ID,  
           NEW.MSR_DATE,  
           NEW.MSR_MESURE );

INSERT de 2009 :


CREATE RULE R_I_MSR_AN2009 AS  
ON INSERT TO T_MESURE_MSR  
   WHERE ( MSR_DATE >= DATE '2009-01-01'  
       AND MSR_DATE <  DATE '2010-01-01' )  
DO INSTEAD  
  INSERT INTO T_MESURE_AN2009_MSR  
  VALUES ( NEW.MSR_ID,  
           NEW.MSR_DATE,  
           NEW.MSR_MESURE );

INSERT de 2010 :


CREATE RULE R_I_MSR_AN2010 AS  
ON INSERT TO T_MESURE_MSR  
   WHERE ( MSR_DATE >= DATE '2010-01-01'  
       AND MSR_DATE <  DATE '2011-01-01' )  
DO INSTEAD  
  INSERT INTO T_MESURE_AN2010_MSR  
  VALUES ( NEW.MSR_ID,  
           NEW.MSR_DATE,  
           NEW.MSR_MESURE );

INSERT après 2010 :


CREATE RULE R_I_MSR_APRES2010 AS  
ON INSERT TO T_MESURE_MSR  
   WHERE ( MSR_DATE >= DATE '2011-01-01' )  
DO INSTEAD  
  INSERT INTO T_MESURE_APRES2010_MSR  
  VALUES ( NEW.MSR_ID,  
           NEW.MSR_DATE,  
           NEW.MSR_MESURE );

Reroutage des UPDATEs :

UPDATE avant 2008 :


CREATE RULE R_U_MSR_AVANT2008
AS
ON UPDATE TO T_MESURE_MSR
   WHERE ( NEW.MSR_DATE < DATE '2008-01-01' )
DO INSTEAD
(
-- lignes ne changeant pas de partition
  UPDATE T_MESURE_AVANT2008_MSR
  SET MSR_ID     = NEW.MSR_ID,
      MSR_DATE   = NEW.MSR_DATE,
      MSR_MESURE = NEW.MSR_MESURE
  WHERE ( OLD.MSR_DATE < DATE '2008-01-01' );
-- lignes migrant d'une partition à l'autre
-- on insére d'abord les lignes ayant migrées
  INSERT INTO T_MESURE_AVANT2008_MSR
  SELECT  NEW.MSR_ID,
          NEW.MSR_DATE,
          NEW.MSR_MESURE
  WHERE NOT ( OLD.MSR_DATE < DATE '2008-01-01' ) ;      
-- puis supprime les lignes en migration dans chacune des partitions
-- d'abord dans la table de l'an 2008
  DELETE FROM T_MESURE_AN2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2008-01-01'  
    AND  OLD.MSR_DATE <  '2009-01-01';      
-- ensuite dans la table de l'an 2009
  DELETE FROM T_MESURE_AN2009_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2009-01-01'  
    AND  OLD.MSR_DATE <  '2010-01-01';      
-- ensuite dans la table de l'an 2010
  DELETE FROM T_MESURE_AN2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2010-01-01'  
    AND  OLD.MSR_DATE <  '2011-01-01';  
-- et enfin dans la table d'après 2010
  DELETE FROM T_MESURE_APRES2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2011-01-01';      
 );

UPDATE de 2008 :


CREATE RULE R_U_MSR_AN2008
AS
ON UPDATE TO T_MESURE_MSR
   WHERE ( NEW.MSR_DATE >= DATE '2008-01-01'  
       AND NEW.MSR_DATE <  DATE '2009-01-01' )
DO INSTEAD
(
-- lignes ne changeant pas de partition
  UPDATE T_MESURE_AN2008_MSR
  SET MSR_ID     = NEW.MSR_ID,
      MSR_DATE   = NEW.MSR_DATE,
      MSR_MESURE = NEW.MSR_MESURE
  WHERE ( OLD.MSR_DATE >= DATE '2008-01-01'  
      AND OLD.MSR_DATE <  DATE '2009-01-01'  );
-- lignes migrant d'une partition à l'autre
-- on insére d'abord les lignes ayant migrées
  INSERT INTO T_MESURE_AN2008_MSR
  SELECT  NEW.MSR_ID,
          NEW.MSR_DATE,
          NEW.MSR_MESURE
  WHERE NOT ( OLD.MSR_DATE >= DATE '2008-01-01'  
          AND OLD.MSR_DATE <  DATE '2009-01-01'  );          
-- puis on supprime les lignes en migration dans chacune des partitions
-- d'abord dans la table d'avant l'an 2008
  DELETE FROM T_MESURE_AVANT2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE < '2008-01-01';      
-- ensuite dans la table de l'an 2009
  DELETE FROM T_MESURE_AN2009_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2009-01-01'  
    AND  OLD.MSR_DATE <  '2010-01-01';      
-- ensuite dans la table de l'an 2010
  DELETE FROM T_MESURE_AN2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2010-01-01'  
    AND  OLD.MSR_DATE <  '2011-01-01';  
-- et enfin dans la table d'après 2010
  DELETE FROM T_MESURE_APRES2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2011-01-01';  
 );

UPDATE de 2009 :


CREATE RULE R_U_MSR_AN2009
AS
ON UPDATE TO T_MESURE_MSR
   WHERE ( NEW.MSR_DATE >= DATE '2009-01-01'  
       AND NEW.MSR_DATE <  DATE '2010-01-01' )
DO INSTEAD
(
-- lignes ne changeant pas de partition
  UPDATE T_MESURE_AN2009_MSR
  SET MSR_ID     = NEW.MSR_ID,
      MSR_DATE   = NEW.MSR_DATE,
      MSR_MESURE = NEW.MSR_MESURE
  WHERE ( OLD.MSR_DATE >= DATE '2009-01-01'  
      AND OLD.MSR_DATE <  DATE '2010-01-01'  );
-- lignes migrant d'une partition à l'autre
-- on insére d'abord les lignes ayant migrées
  INSERT INTO T_MESURE_AN2009_MSR
  SELECT  NEW.MSR_ID,
          NEW.MSR_DATE,
          NEW.MSR_MESURE
  WHERE NOT ( OLD.MSR_DATE >= DATE '2009-01-01'  
          AND OLD.MSR_DATE <  DATE '2010-01-01' );        
-- puis on supprime les lignes en migration dans chacune des partitions
-- d'abord dans la table d'avant l'an 2008
  DELETE FROM T_MESURE_AVANT2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE < '2008-01-01';      
-- d'abord dans la table de l'an 2008
  DELETE FROM T_MESURE_AN2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2008-01-01'  
    AND  OLD.MSR_DATE <  '2009-01-01';      
-- ensuite dans la table de l'an 2010
  DELETE FROM T_MESURE_AN2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2010-01-01'  
    AND  OLD.MSR_DATE <  '2011-01-01';  
-- et enfin dans la table d'après 2010
  DELETE FROM T_MESURE_APRES2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2011-01-01';  
 );

UPDATE de 2010 :


CREATE RULE R_U_MSR_AN2010
AS
ON UPDATE TO T_MESURE_MSR
   WHERE ( NEW.MSR_DATE >= DATE '2010-01-01'  
       AND NEW.MSR_DATE <  DATE '2011-01-01' )
DO INSTEAD
(
-- lignes ne changeant pas de partition
  UPDATE T_MESURE_AN2010_MSR
  SET MSR_ID     = NEW.MSR_ID,
      MSR_DATE   = NEW.MSR_DATE,
      MSR_MESURE = NEW.MSR_MESURE
  WHERE ( OLD.MSR_DATE >= DATE '2010-01-01'  
      AND OLD.MSR_DATE <  DATE '2011-01-01'  );
-- lignes migrant d'une partition à l'autre
-- on insére d'abord les lignes ayant migrées
  INSERT INTO T_MESURE_AN2010_MSR
  SELECT  NEW.MSR_ID,
          NEW.MSR_DATE,
          NEW.MSR_MESURE
  WHERE NOT ( OLD.MSR_DATE >= DATE '2010-01-01'  
          AND OLD.MSR_DATE <  DATE '2011-01-01' );        
-- puis on supprime les lignes en migration dans chacune des partitions
-- d'abord dans la table d'avant l'an 2008
  DELETE FROM T_MESURE_AVANT2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE < '2008-01-01';      
-- d'abord dans la table de l'an 2008
  DELETE FROM T_MESURE_AN2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2008-01-01'  
    AND  OLD.MSR_DATE <  '2009-01-01';      
-- ensuite dans la table de l'an 2009
  DELETE FROM T_MESURE_AN2009_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2009-01-01'  
    AND  OLD.MSR_DATE <  '2010-01-01';      
-- et enfin dans la table d'après 2010
  DELETE FROM T_MESURE_APRES2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2011-01-01';  
 );

UPDATE après 2010 :


REATE RULE R_U_MSR_APRES2010
AS
ON UPDATE TO T_MESURE_MSR
   WHERE ( NEW.MSR_DATE >= DATE '2011-01-01' )
DO INSTEAD
(
-- lignes ne changeant pas de partition
  UPDATE T_MESURE_APRES2010_MSR
  SET MSR_ID     = NEW.MSR_ID,
      MSR_DATE   = NEW.MSR_DATE,
      MSR_MESURE = NEW.MSR_MESURE
  WHERE ( OLD.MSR_DATE >= DATE '2011-01-01' );
-- lignes migrant d'une partition à l'autre
-- on insére d'abord les lignes ayant migrées
  INSERT INTO T_MESURE_APRES2010_MSR
  SELECT  NEW.MSR_ID,
          NEW.MSR_DATE,
          NEW.MSR_MESURE
  WHERE NOT ( OLD.MSR_DATE < DATE '2011-01-01' );        
-- puis supprime les lignes en migration dans chacune des partitions
-- d'abord dans la table d'avant l'an 2008
  DELETE FROM T_MESURE_AVANT2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE < '2008-01-01';      
-- d'abord dans la table de l'an 2008
  DELETE FROM T_MESURE_AN2008_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2008-01-01'  
    AND  OLD.MSR_DATE <  '2009-01-01';      
-- ensuite dans la table de l'an 2009
  DELETE FROM T_MESURE_AN2009_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2009-01-01'  
    AND  OLD.MSR_DATE <  '2010-01-01';      
-- et enfin dans la table de l'an 2010
  DELETE FROM T_MESURE_AN2010_MSR
  WHERE  MSR_ID = OLD.MSR_ID
    AND  MSR_DATE = OLD.MSR_DATE
    AND  MSR_MESURE = OLD.MSR_MESURE
    AND  OLD.MSR_DATE >= '2010-01-01'  
    AND  OLD.MSR_DATE <  '2011-01-01';  
 );

A ce stade force est de constater l’extrême complexité des règles dans le cas d’une possible migration de lignes d’une partition à l’autre… En effet le nombre d’ordre SQL dans chaque règle UPDATE est multiplié par le nombre de partitions… Pour 5 partitions, chacune des règles UPDATE doit inclure 7 commandes SQL… Bref, c’est l’explosion cartésienne. Ainsi avec 100 partitions, le nombre d’ordre SQL dans l’ensemble des règles à créer est de 104 x 100 = 10 400 ! Impensable…
De plus, comme PostGreSQL ne gère pas correctement les contraintes d’unicité (PRIMARY KEY, FOREIGN KEY) dans les mises à jour (pas de contraintes ensemblistes sauf à déroger en contraintes déferrée prévues dans une version future) il est hasardeux de partitionner sur une colonne susceptible de changer de valeur (en particulier erreur de saisie).

Reroutage des DELETEs :

DELETE avant 2008 :


CREATE RULE R_D_MSR_AVANT2008
AS  
ON DELETE TO T_MESURE_MSR  
   WHERE ( OLD.MSR_DATE < DATE '2008-01-01' )  
DO INSTEAD  
  DELETE FROM T_MESURE_AVANT2008_MSR  
  WHERE MSR_ID = OLD.MSR_ID
    AND MSR_DATE = OLD.MSR_DATE
    AND MSR_MESURE  = OLD.MSR_MESURE ;

DELETE de 2008 :


CREATE RULE R_D_MSR_AN2008
AS  
ON DELETE TO T_MESURE_MSR  
   WHERE ( OLD.MSR_DATE >= DATE '2008-01-01'  
       AND OLD.MSR_DATE <  DATE '2009-01-01' )  
DO INSTEAD  
  DELETE FROM T_MESURE_AN2008_MSR  
  WHERE MSR_ID = OLD.MSR_ID
    AND MSR_DATE = OLD.MSR_DATE
    AND MSR_MESURE  = OLD.MSR_MESURE ;

DELETE de 2009 :


CREATE RULE R_D_MSR_AN2009
AS  
ON DELETE TO T_MESURE_MSR  
   WHERE ( OLD.MSR_DATE >= DATE '2009-01-01'  
       AND OLD.MSR_DATE <  DATE '2010-01-01' )  
DO INSTEAD  
  DELETE FROM T_MESURE_AN2009_MSR  
  WHERE MSR_ID = OLD.MSR_ID
    AND MSR_DATE = OLD.MSR_DATE
    AND MSR_MESURE  = OLD.MSR_MESURE ;

DELETE de 2010 :


CREATE RULE R_D_MSR_AN2010
AS  
ON DELETE TO T_MESURE_MSR  
   WHERE ( OLD.MSR_DATE >= DATE '2010-01-01'  
       AND OLD.MSR_DATE <  DATE '2011-01-01' )  
DO INSTEAD  
  DELETE FROM T_MESURE_AN2010_MSR  
  WHERE MSR_ID = OLD.MSR_ID
    AND MSR_DATE = OLD.MSR_DATE
    AND MSR_MESURE  = OLD.MSR_MESURE ;

DELETE après 2010 :


CREATE RULE R_D_MSR_APRES2010
AS  
ON DELETE TO T_MESURE_MSR  
   WHERE ( OLD.MSR_DATE >= DATE '2011-01-01' )  
DO INSTEAD  
  DELETE FROM T_MESURE_APRES2010_MSR  
  WHERE MSR_ID = OLD.MSR_ID
    AND MSR_DATE = OLD.MSR_DATE
    AND MSR_MESURE  = OLD.MSR_MESURE ;

A ce stade tout semble parfait, sauf que si nous essayons d’insérer des données dans la table maître, alors nous sommes immédiatement informé d’une erreur particulièrement perverse… Par exemple avec l’insertion suivante :


INSERT INTO T_MESURE_MSR  
VALUES (1, '2007-01-01', 33),  
  (2, '2007-03-01', 22),  
  (3, '2007-04-01', 44),  
  (4, '2008-02-01', 55),  
  (5, '2008-06-01', 66),  
  (6, '2008-10-01', 77),  
  (7, '2009-01-01', 99),  
  (8, '2009-06-01', 11),  
  (9, '2010-01-01', 88),  
  (10, '2010-06-01', 0);

PostGreSQL retourne l’erreur suivante :
ERREUR: récursion infinie détectée dans les règles de la relation « t_mesure_an2008_msr »
Que se passe t-il ?
En fait dans chaque règle, PostGreSQL, lorsqu’il ne sait pas quoi faire des lignes, tente de les insérer dans la table mère. Tant est si bien que chaque ligne qui n’est pas insérée par la règle dans les tables filles est renvoyée à la table mère, relançant ainsi l’insertion dans les tables fille par le biais des règles ad infinitum !
Il faut donc en sus interdire à la table mère de recevoir une quelconque ligne et cela ne peut se faire par une contrainte CHECK, car ce type de contrainte se propage dans les filles. Seule solution : rajouter un trigger dans la table mère !

3.1.5 – création de la fonction trigger et du trigger pour empêcher la récursion de l’insertion parasite

CREATE FUNCTION P_E_I_MSR()
RETURNS TRIGGER  AS  
'
   BEGIN
      RAISE EXCEPTION ''Aucune ligne insérée dans la table mère'';
      RETURN NULL;
   END;
'
LANGUAGE 'plpgsql';
 
CREATE TRIGGER E_I_MSR  
AFTER INSERT
ON T_MESURE_MSR
FOR EACH ROW EXECUTE PROCEDURE P_E_I_MSR ();

PARTITIONNEMENT DE TABLE version PostGreSQL – PREMIÈRE CONCLUSION

Comme on le voit, il faut répéter les prédicats de filtrage qui ont déjà été mis en place au niveau des contraintes CHECK.
Au niveau des règles pour update, la complexité est telle qu’aucun exemple sur le web ne m’a permis d’avancer sur le sujet et il m’a fallut une journée de tatonnerment après de multiples posts dans les forums restés sans réponse pour entrevoir une solution aucunement documentée.
Par conséquent, là aussi, le risque de se tromper est extrémement élevé, d’autant que le nombre de commandes et leur complexité est gigantesque et le code verbeux. Inutile de dire à quel point il est difficilement pensable de faire de nombreuses partitions avec la solution proposée par PostGreSQL.
En sus, il n’y a toujours aucun moyen de vérifier que les valeurs de partitionnement sont non recouvrantes (donc redondante) et ne laisse aucune valeur exclue.
Enfin, la réentrance des règles et l’obligation d’un trigger sur la table mère, pénalise fortement le traitement des mises à jour de données a tel point qu’on est en droit de se demander si il existe réellement un gain global quelconque dans le partitionnement toutes commandes SQL confondues (INSERT, UPDATE, DELETE, SELECT)…

4 – LA SOLUTION MS SQL SERVER

Nous avons laissé dans l’ombre le nom de la base SQL et sa création au niveau de PostGreSQL. Voici ce qu’il faut faire dans MS SQL Server, pour créer une base et se placer dans son contexte :

CREATE DATABASE DB_PART  
GO  
USE DB_PART;  
GO

4.1 – Création des espaces de stockage

4.1.1 – création des FILEGROUPs :

ALTER DATABASE DB_PART ADD FILEGROUP FG_PART0;  
ALTER DATABASE DB_PART ADD FILEGROUP FG_PART1;  
ALTER DATABASE DB_PART ADD FILEGROUP FG_PART2;  
ALTER DATABASE DB_PART ADD FILEGROUP FG_PART3;  
ALTER DATABASE DB_PART ADD FILEGROUP FG_PART4;

4.1.2 – création des fichiers de stockage, dans les espaces préalablement créées :


ALTER DATABASE DB_PART  
  ADD FILE (NAME       = 'F_PART0',  
            FILENAME   = 'E:\SQLDATA\PARTMSR0\F_DB_PART_0.ndf',  
            SIZE       = 1 GB,  
            FILEGROWTH = 50 MB)  
  TO  FILEGROUP FG_PART0;  
ALTER DATABASE DB_PART  
  ADD FILE (NAME       = 'F_PART1',  
            FILENAME   = 'F:\SQLDATA\PARTMSR1\F_DB_PART_1.ndf',  
            SIZE       = 1 GB,  
            FILEGROWTH = 50 MB)  
  TO  FILEGROUP FG_PART1;  
ALTER DATABASE DB_PART  
  ADD FILE (NAME       = 'F_PART2',  
            FILENAME   = 'G:\SQLDATA\PARTMSR2\F_DB_PART_2.ndf',  
            SIZE       = 1 GB,  
            FILEGROWTH = 50 MB)  
  TO  FILEGROUP FG_PART2;  
ALTER DATABASE DB_PART  
  ADD FILE (NAME       = 'F_PART3',  
            FILENAME   = 'H:\SQLDATA\PARTMSR3\F_DB_PART_3.ndf',  
            SIZE       = 1 GB,  
            FILEGROWTH = 50 MB)  
  TO  FILEGROUP FG_PART3;  
ALTER DATABASE DB_PART  
  ADD FILE (NAME       = 'F_PART4',  
            FILENAME   = 'I:\SQLDATA\PARTMSR4\F_DB_PART_4.ndf',  
            SIZE       = 1 GB,  
            FILEGROWTH = 50 MB)  
  TO  FILEGROUP FG_PART4;

4.2 – création de la fonction de partitionnement :

C’est celle qui fixe les piquets….


CREATE PARTITION FUNCTION F_P_MSR_DATE (date)  
AS RANGE LEFT  
FOR VALUES ('2008-01-01', '2009-01-01', '2010-01-01', '2011-01-01');

La fonction intègre 4 valeurs pivot et nécessitera donc 5 espaces de stockage pour les données de -∞ à +∞
Son paramètre est un type de données et non une variable. La variables sera la colonne de partitionnement de la table.
Notez que les valeurs piquet seront stockées à gauche (RANGE LEFT) dans chacun des intervalles.

4.3 – création du schéma de partitionnement

C’est celle qui fixe les espaces de stockage pour chacun des intervalles de valeurs.
Le schéma de partitionnement fait référence à la fonction de partitionnment définie ci avant et indique les espaces de stockage :


CREATE PARTITION SCHEME F_S_MSR_DATE  
AS PARTITION F_P_MSR_DATE  
TO (FG_PART0, FG_PART1, FG_PART2, FG_PART3, FG_PART4);

4.4 – Création de la table partitionnée


CREATE TABLE T_MESURE_MSR
(  
  MSR_ID   INT not null,  
  MSR_DATE date not null,  
  MSR_MESURE int  
) ON F_S_MSR_DATE (MSR_DATE);

La table est créée en indiquant dans quoi elle réside (ON) et cette résidence est un ensemble de partitions déterminées par la fonction de partitionnement (F_P_MSR_DATE) associé au schéma de partitionnement (F_S_MSR_DATE) appliqué à la colonne MSR_DATE.

PARTITIONNEMENT DE TABLE version MS SQL Server – PREMIÈRE CONCLUSION

Comme on le voit, la solution de mise en Å“uvre sous MS SQL Server est d’une grande simplicité et ne laisse aucune zone d’ombre. Il est impossible de faire des partitions trouées ou se chevauchant et le code à mettre en oeuvre est on ne peut plus laconique.
Aucune requête ni trigger ni règles d’aucune sorte n’est à coder, car tout est fait en interne par le moteur SQL.
Seule différence notable, la gestion des espaces de stockage, plus complexe car beaucoup plus précise (réservation d’espace, méthode de croisssance…) que sous PostGreSQL ou elle est embryonnaire, mais là encore nous avons voulu être exhaustif en supposant que l’intérêt du partitionnement réside grandement dans la ventilation des IO sur différents espaces de stockage. Si tel n’était pas l’intérêt de notre utilisateur, alors la partie 4.1 du code est à supprimer et la création du schéma de partitionnement peut se résumer à :

CREATE PARTITION SCHEME F_S_MSR_DATE  
AS PARTITION F_P_MSR_DATE  
ALL TO (PRIMARY);

Ou PRIMARY est le nom de l’espace de stockage par défaut, mais on peut spécifier tout autre espace.

5 – MANIPULER LES DONNÉES D’UNE TABLE PARTITIONNÉE

Ce n’est pas tout de créer une table partitionnée, encore faut-il qu’elle vive. Nous allons donc étudier différentes opérations sur une telle table et leur conséquence sur la table.

5.1 – les insertions


INSERT INTO T_MESURE_MSR  
VALUES (1, '2007-01-01', 33),  
  (2, '2007-03-01', 22),  
  (3, '2007-04-01', 44),  
  (4, '2008-02-01', 55),  
  (5, '2008-06-01', 66),  
  (6, '2008-10-01', 77),  
  (7, '2009-01-01', 99),  
  (8, '2009-06-01', 11),  
  (9, '2010-01-01', 88),  
  (10, '2010-06-01', 0);

Dans PostGreSQL, les lignes sont bien ventilées dans les tables filles, mais elle apparaissent aussi dans la table mère sans qu’il soit possible de dire si ces lignes sont physiquement présente dans la table mère ou si elles n’existent réellement que dans les filles.
Seul le plan de requête d’un SELECT * FROM T_MESURE_MSR nous indique ce que fait réellement PG, c’est à dire une collecte des lignes de toutes les tables filles :
Plan de requête du SELECT lorsque la table est partitionnée
Si les lignes étaient aussi stockées dans la table mère nous verrions une redondance de ces lignes, ce qui n’est pas le cas.

Dans SQL Server, les lignes sont bien ventilées dans les différentes partitions, et il est possible directement dans la requête SQL de savoir à l’aide d’un appel particulier à la fonction de partitionnement de savoir ou chacune des lignes figurent :


SELECT *,  
       $PARTITION.F_P_MSR_DATE(MSR_DATE) AS NUMERO_PARTITION
FROM   T_MESURE_MSR

Pour ce faire on apelle la fonction de partition sur la colonne de partitionnement depuis un schéma virtuel de nom $PARTITION.


MSR_ID      MSR_DATE   MSR_MESURE  NUMERO_PARTITION
----------- ---------- ----------- ----------------
1           2007-01-01 33          1
2           2007-03-01 22          1
3           2007-04-01 44          1
4           2008-02-01 55          2
5           2008-06-01 66          2
6           2008-10-01 77          2
7           2009-01-01 99          2
8           2009-06-01 11          3
9           2010-01-01 88          3
10          2010-06-01 0           4

5.2 – Les extractions

5.1.1 – Recherche de ligne unique

C’est le cas de la requête retrouvant une ligne précisée sur le critère de partitionnement.
C’est par exemple la requête suivante :


SELECT *  
FROM   T_MESURE_MSR  
WHERE  MSR_DATE = '2009-06-01'

Que le SGBDR doit retrouver dans la bonne partition.

Pour SQL Server le plan de requête indique bien qu’il cherche dans une seule partition.
Plan de requête SQL Server du SELECT lorsque la table est partitionnée
PtnId1001 = Opérateur scalaire(RangePartitionNew(… signifie qu’il cherche la valeur dans une partition.

Pour postGreSQL et à condition qu’il soit correctement paramétré (à défaut depuis la version 8.4), le plan montre que PostGreSQL va scruter deux éléments : la bonne partition, mais aussi la table mère…
Plan de requête SELECT sur table PostGreSQL partitionnée avec CONSTRAINT_EXCLUSION à ON
Dans le cas ou le paramétrage est incorrect :
SET CONSTRAINT_EXCLUSION = OFF;
L’optimiseur est incapable de trouver directement la partition adéquate…
Plan de requête SELECT sur table PostGreSQL partitionnée

Bien entendu la recherche d’une ligne unique sur autre chose que le critère de partitionnement nécessite le parcours de toutes les partitions, que ce soit avec PostGreSQL ou MS SQL Server.

5.1.2 – Recherche de plusieurs lignes

C’est le cas d’une requête indiquant une plage de valeur, par exemple avec un IN ou un BETWEEN.
Par exemple, la requête suivante :


SELECT *  
FROM   T_MESURE_MSR  
WHERE  MSR_DATE BETWEEN '2008-02-01' AND '2009-06-01'

Le plan de requête PostGreSQL prend bien en compte les deux partitions, mais y ajoute toujours inutilement la table mère :
Plan de requête SELECT multiligne sur table PostGreSQL partitionnée avec CONSTRAINT_EXCLUSION à ON
Rappelons pour information que cette table ne peut contenir aucune ligne puisqu’un trigger l’y empêche. La scrutation de cette table est donc parfaitement inutile et fait perdre du temps de traitement !

La plan de requête MS SQL Server est plus direct, ne s’intéressant qu’aux partitions de l’intervalle :
Plan de requête SELECT multiligne sur table MS SQL Server partitionnée
C’est plus explicite sur le détail du plan de requête fournit en XML (On l’obtient en cliquant droit sur le plan graphique et en choisissant « afficher le fichier XML du plan d’exécution »)

5.3 – Les suppressions

Avec la simple requête suivante :

DELETE FROM T_MESURE_MSR WHERE MSR_DATE = '2008-02-01'

PostGreSQL semble perdu. Le plan graphique est incompréhensible :
Plan de requête PostGreSQL DELETE mono ligne table partitionnée
Et sa version textuelle, montre un comportement vraiment bizarre :


Delete on t_mesure_an2008_msr  (cost=0.00..114.40 rows=1 width=16)
  ->  Nested Loop  (cost=0.00..114.40 rows=1 width=16)
        Join Filter: ((t_mesure_an2008_msr.msr_id = public.t_mesure_msr.msr_id) AND (t_mesure_an2008_msr.msr_mesure = public.t_mesure_msr.msr_mesure))
        ->  Seq Scan on t_mesure_an2008_msr  (cost=0.00..32.13 rows=9 width=22)
              Filter: (msr_date = '2008-02-01'::date)
        ->  Materialize  (cost=0.00..81.96 rows=2 width=26)
              ->  Append  (cost=0.00..81.95 rows=2 width=26)
                    ->  Seq Scan on t_mesure_msr  (cost=0.00..40.98 rows=1 width=26)
                          Filter: ((msr_date >= '2008-01-01'::date) AND (msr_date < '2009-01-01'::date) AND (msr_date = '2008-02-01'::date))
                    ->  Seq Scan on t_mesure_an2008_msr t_mesure_msr  (cost=0.00..40.98 rows=1 width=26)
                          Filter: ((msr_date >= '2008-01-01'::date) AND (msr_date < '2009-01-01'::date) AND (msr_date = '2008-02-01'::date))
 
Delete on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Result  (cost=0.00..0.01 rows=1 width=0)
  One-Time Filter: false

En fait on s’aperçoit que le plan scrute toutes les partitions… Dès lors quel est le gain d’un DELETE ?

Le plan de requête de MS SQL Server est plus conforme à ce à quoi nous nous atendions puisqu’il ne scrute qu’une seule partition. Le voici sous forme textuelle :


StmtText
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  |--Table Delete(OBJECT:([DB_PART].[dbo].[T_MESURE_MSR]))
       |--Top(ROWCOUNT est 0)
            |--Table Scan(OBJECT:([DB_PART].[dbo].[T_MESURE_MSR]), SEEK:([PtnId1001]=RangePartitionNew(CONVERT_IMPLICIT(date,[@1],0),(0),'2008-01-01','2009-01-01','2010-01-01','2011-01-01')),  WHERE:([DB_PART].[dbo].[T_MESURE_MSR].[MSR_DATE]=CONVERT_IMPLICIT(date,[@1],0)) ORDERED FORWARD)

Et son image graphique :
Plan de requête MS SQL Server DELETE mono ligne table partitionnée

5.4 – les modifications

Voyons maintenant le cas d’un UPDATE et analysons ce qui se passe sans changement de partitions puis avec.

Premier exemple :


UPDATE T_MESURE_MSR
SET    MSR_DATE = '2007-06-20'
WHERE  MSR_DATE = '2007-01-01'

À nouveau PostGreSQL semble perdu alors qu’il n’a qu’une seule partition à scruter. Le plan graphique est encore plus immonde :
Plan de requête PostGreSQL UPDATE sur table partitionné
C’est le résultat hélas catastrophique des règles nécessaires à la mise en place du partitionnement. Le plan textuel est monstrueux :


Update on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Insert on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Update on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Insert on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Update on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Insert on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Update on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Insert on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Update on t_mesure_avant2008_msr  (cost=0.00..38.59 rows=4 width=28)
  ->  Nested Loop  (cost=0.00..38.59 rows=4 width=28)
        ->  Seq Scan on t_mesure_avant2008_msr  (cost=0.00..1.00 rows=1 width=6)
        ->  Append  (cost=0.00..37.55 rows=4 width=22)
              ->  Seq Scan on t_mesure_msr  (cost=0.00..36.55 rows=3 width=22)
                    Filter: ((msr_date < '2008-01-01'::date) AND (msr_date = '2007-01-01'::date))
              ->  Seq Scan on t_mesure_avant2008_msr t_mesure_msr  (cost=0.00..1.00 rows=1 width=22)
                    Filter: ((msr_date < '2008-01-01'::date) AND (msr_date = '2007-01-01'::date))
 
Delete on t_mesure_an2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2009_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_an2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Delete on t_mesure_apres2010_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Insert on t_mesure_avant2008_msr  (cost=0.00..0.01 rows=1 width=0)
  ->  Result  (cost=0.00..0.01 rows=1 width=0)
        One-Time Filter: false
 
Result  (cost=0.00..0.01 rows=1 width=0)
  One-Time Filter: false

Tandis que sous MS SQL Server il reste relativement simple :
Plan de requête MS SQL Server UPDATE monoligne

Deuxième exemple :


UPDATE T_MESURE_MSR
SET    MSR_DATE = '2009-06-20'
WHERE  MSR_DATE = '2007-01-01'

Là il y a changement de partition.

PostGreSQL se complique encore plus…
Plan de requête PostGreSQL UPDATE avec changement de partition
Je vous passerai les détails du plan textuel qui est comparable au précédent avec une instruction de plus…

Tandis que celui de SQL Server ne change pas d’un iota !

6 – LA VIE D’UNE TABLE PARTITIONNÉE

Dans la vraie vie, il arrive qu’une table ne soit pas parfaite du premier coup. Il faut donc en remanier la structure afin de la rendre plus fonctionnelle qu’elle ne l’était auparavant. De telles manipulations sont fréquentes et même les grosses tables partitionnées n’y échappent pas.
Parmi les choses les plus couramment entreprises, il y a :

  • l’ajout ou la suppression d’un index
  • l’ajout d’une colonne
  • l’ajout d’une clef (primaire ou unique)
  • l’ajout d’une contrainte de validation
  • l’ajout d’une contrainte d’intégrité référentielle.

C’est ce que nous allons maintenant tester…

6.1 – ajouter un index

Dans MS SQL Server on peut avoir une table partitionnée, sans que les index le soit, tout comme on peut choisir d’aligner les index et encore pour certains et pas tous.
Voici le création d’un index non partitionné sur la colonne MSR_MESURE :
CREATE INDEX X_MSR_MSR ON T_MESURE_MSR (MSR_MESURE);
Voici maintenant un index similaire, partitionné (il doit inclure la colonne de partitionnement dans la clause INCLUDE) :
CREATE INDEX X_MSR_MSR_P ON T_MESURE_MSR (MSR_MESURE) INCLUDE (MSR_DATE) ON F_S_MSR_DATE (MSR_DATE);

Pour PostGreSQL, la création d’un index au niveau de la table mère ne propage pas l’index dans les filles.
Pour le voir, il suffit de lister les index des tables à l’aide de la requête suivante :


SELECT S.nspname as TABLE_SCHEMA,  
       T.relname as TABLE_NAME,  
       CI.relname as INDEX_NAME  
FROM   pg_catalog.pg_class AS CI  
       INNER JOIN pg_catalog.pg_index I  
             ON I.indexrelid = CI.oid  
       INNER JOIN pg_catalog.pg_class AS T  
             ON I.indrelid = T.oid  
       LEFT OUTER JOIN pg_catalog.pg_namespace AS S  
             ON S.oid = CI.relnamespace  
WHERE  S.nspname NOT IN ('pg_catalog', 'pg_toast')  
  AND  pg_catalog.pg_table_is_visible(CI.oid)  
ORDER  BY 1,2;

Il faut donc créer autant d’index qu’il y a de tables filles…

CREATE INDEX X_MSR2007_MSR_DATE ON T_MESURE_AVANT2008_MSR (MSR_DATE);  
CREATE INDEX X_MSR2008_MSR_DATE ON T_MESURE_AN2008_MSR (MSR_DATE);  
CREATE INDEX X_MSR2009_MSR_DATE ON T_MESURE_AN2009_MSR (MSR_DATE);  
CREATE INDEX X_MSR2010_MSR_DATE ON T_MESURE_AN2010_MSR (MSR_DATE);  
CREATE INDEX X_MSR2011_MSR_DATE ON T_MESURE_APRES2010_MSR (MSR_DATE);

Encore une fois on multiplie les commandes par le nombre de partitions.
Le seul avantage est qu’il est possible d’indexer une partition et pas l’autre. C’est pratique lorsque l’on a des données anciennes en quantité qui sont rarement scrutées. Mais SQL Server permet de faire la même chose via un index filtré. Par exemple celui-ci :
CREATE INDEX X_MSR_MSR ON T_MESURE_MSR (MSR_MESURE) WHERE ( MSR_DATE > ‘2011-01-01′ );
Et vous noterez que c’est encore plus simple, car il n’y a besoin que de créer toujours qu’un seul index.

6.2 – Ajouter une colonne

ALTER TABLE T_MESURE_MSR  
ADD COLUMN MSR_HEURE TIME;

Pour PostGreSQL, la nouvelle colonne MSR_TIME, créée au niveau de la mère est propagée dans toutes les tables filles. Mais il en résulte au final 6 exécutions de commandes ALTER.
Pour le voir :


SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME  
FROM   INFORMATION_SCHEMA.COLUMNS  
WHERE  COLUMN_NAME = 'msr_heure'  
ORDER  BY 1, 2, 3;

Ou encore :


SELECT S.nspname AS TABLE_SCHEMA,  
       T.relname AS TABLE_NAME,  
       C.attname AS COLUMN_NAME  
FROM   pg_catalog.pg_class AS T  
       INNER JOIN pg_catalog.pg_namespace AS S  
             ON T.relnamespace = S.oid  
       INNER JOIN pg_catalog.pg_attribute AS C  
             ON T.oid = C.attrelid  
WHERE  relkind = 'r'  
  AND  nspname = 'public'  
  AND  C.attname = 'msr_heure'

Sous SQL Server s’agissant d’une seule table il n’y a aucune ambiguïté et un seul ALTER est déclenché.

6.3 – Ajout d’une clef

ALTER TABLE T_MESURE_MSR  
  ADD CONSTRAINT PK_MSR_ID  
  PRIMARY KEY (MSR_ID);

L’ajout d’une clef primaire se passe bien dans PostGreSQL, trop bien même… Car cette clef est purement cosmétique. En effet elle est bien créée dans la table mère, mais n’est pas propagée dans les filles !
Et bien entendu elle ne sert à rien….
Exemple :


INSERT INTO T_MESURE_MSR  
VALUES (1, '2007-01-01', 33),
       (1, '2008-01-01', 22),
       (1, '2009-01-01', 11);

Dans SQL Server, la création d’une clef se passe mal :
Msg 1908, Niveau 16, État 1, Ligne 1
‘MSR_DATE’ est une colonne de partition de l’index ‘PK_MSR_ID’. Les colonnes de partition d’un index unique doivent être un sous-ensemble de la clé d’index.
Msg 1750, Niveau 16, État 0, Ligne 1
Impossible de créer la contrainte. Voir les erreurs précédentes.

En fait SQL Server exige que la clef fasse partie du critère de partitionnement. En fait il est même fortement recommandées de faire sa partition sur la clef de la table.
C’est une contrainte lourde dans le sens ou il faut le prendre en compte au moment de la conception de la base…

6.4 – Ajout d’une contrainte de validation


ALTER TABLE T_MESURE_MSR  
  ADD CONSTRAINT CK_MSR_MSR_POSITIF  
  CHECK(MSR_MESURE >= 0);

Dans PostGreSQL, la nouvelle contrainte CHECK se propage dans les tables filles. Il en résulte aussi 6 exécutions de commandes ALTER.

Sous SQL Server s’agissant d’une seule table il n’y a aucune ambiguïté et un seul ALTER est déclenché.

6.5 – Ajout d’une contrainte d’intégrité


CREATE TABLE T_UNITE_UNT
(UNT_ID      INT PRIMARY KEY,
 UNT_LIBELLE VARCHAR(32))
 
ALTER TABLE T_MESURE_MSR
  ADD COLUMN UNT_ID INT  
  REFERENCES T_UNITE_UNT (UNT_ID);

Sous PostGreSQL, l’ajout complet (colonne et contrainte d’IR) ne se fait qu’au niveau de la table mère. Cependant la colonne est créée dans toutes les tables filles. Il faut donc rajouter la contrainte d’intégrité manuellement à toutes les filles.


ALTER T_MESURE_AVANT2008_MSR
  ADD CONSTRAINT FK_MSR_UNTAV8  
  FOREIGN KEY (UNT_ID)
  REFERENCES T_UNITE_UNT (UNT_ID);
ALTER T_MESURE_AN2008_MSR
  ADD CONSTRAINT FK_MSR_UNTA8  
  FOREIGN KEY (UNT_ID)
  REFERENCES T_UNITE_UNT (UNT_ID);
ALTER T_MESURE_AN2009_MSR
  ADD CONSTRAINT FK_MSR_UNTA9  
  FOREIGN KEY (UNT_ID)
  REFERENCES T_UNITE_UNT (UNT_ID);
ALTER T_MESURE_AN2010_MSR
  ADD CONSTRAINT FK_MSR_UNTAN10  
  FOREIGN KEY (UNT_ID)
  REFERENCES T_UNITE_UNT (UNT_ID);
ALTER T_MESURE_APRES2010_MSR
  ADD CONSTRAINT FK_MSR_UNTAP10  
  FOREIGN KEY (UNT_ID)
  REFERENCES T_UNITE_UNT (UNT_ID);

Sous SQL Server s’agissant d’une seule table il n’y a aucune ambiguïté et un seul ALTER est déclenché.

7 – PARTITIONNEMENT MULTICOLONNE

La partitionnement sur de multiples colonnes ne pose pas de problème sous PostGreSQL, mais il en résulte des requêtes très complexes à écrire pour les règles de mise à jour, comme pour les filtres de partitionnement, car il faut tenir compte de la vectorisation des informations (cela est facilité par le Row Value Constructor). Il est en revanche un peu plus limité sous MS SQL Server, car il faut créer une colonne calculée persistante, pour « réduire » la composition du critère de partitionnement à une seule valeur.

Voici un exemple, avec un partitionnement sur deux colonnes, l’une de type DATE l’autre de type TIME :

Création de la fonction de partitionnement :


CREATE PARTITION FUNCTION F_P_MSR_DATETIME (datetime)  
AS RANGE LEFT  
FOR VALUES ('2000-01-01', '2001-01-01', '2002-01-01', '2003-01-01');

Création du schéma de paritionnement, faisant référence à la fonction de partitionnment définie ci avant :


CREATE PARTITION SCHEME F_S_MSR_DATETIME  
AS PARTITION F_P_MSR_DATETIME  
TO (FG_PART0, FG_PART1, FG_PART2, FG_PART3, FG_PART4);

Création de la table partitionnée :


CREATE TABLE T_MESURE_2_MSR  
(  
  MSR_ID INT not null,  
  MSR_DATE date not null,  
  MSR_TIME TIME NOT NULL,  
  MSR_MESURE int,  
  MSR_DATETIME AS CAST(MSR_DATE AS DATETIME) + MSR_TIME PERSISTED  
) ON F_S_MSR_DATETIME (MSR_DATETIME);

8 – CONCLUSION PARTITIONNEMENT PostGreSQL vs MS SQL Server

Il faut bien constater que la mise en Å“uvre du partitionnement sous PostGreSQL est très complexe et quasi infaisable en production si l’on veut plus d’une dizaine de partitions. En outre, le risque de se tromper dans les très nombreuses requêtes et pire encore par un mauvais découpage des partitions entrainant des chevauchements ou des trous, est extrêmement élevé.

Il semble bien que le partitionnement sur PG n’est encore qu’embryonnaire et brouillon, et tout doit être fait à la main. En effet contrairement aux solutions de SQL Server ou Oracle, chacune des partitions est en fait une table indépendant sur laquelle il faut appliquer certaines commandes DDL sur toutes les partitions, si vous voulez que vos différentes partitions (en fait des tables) aient la même structure (colonnes, contraintes, index…). Avec un grand nombre de partitions, le nombre de commande à passer explose très vite. Nous avons dénombré plus de 10 000 ordres SQL pour la mise en place de 100 partitions !

De plus, du fait des règles, les plans de requête de PostGreSQL, concernant les ordres de mise à jour des données, sont d’une grande complexité et il en résulte fatalement des performances à la baisse alors que c’est l’inverse que l’on cherche dans le partitionnement !

L’oubli de paramétrer le serveur pour qu’il tienne compte des avantages des contraintes de validation pour utiliser les partition, n’est qu’un petit dommage que le DBA réparera bien vite :
SET CONSTRAINT_EXCLUSION = ON (ou PARTITION qui est la valeur par défaut à partir de la version 8.4).

Plus embêtant sont les scrutations inutiles de la table mère alors qu’elle est toujours vides par le fait du trigger;

Le seul point négatif que nous avons relevé chez MS SQL Server, en dehors du bug de l’assistant de mise en place des partitions dans l’interface SSMS (SQL Server Management Studio) est l’incapacité à placer une clef primaire ou unique indépendante du critère de partitionnement. Ce n’est pas un point bloquant, mais mieux vaut le prendre en compte au niveau de la conception de la base qu’après la mise en production.

Nous avons dit que la gestion des espaces de stockage PostGreSQL était encore aussi embryonnaire. C’est bien dommage car un partitionnement sans cela présente en définitive peu d’intérêt. Dans une bonne gestion des espaces de stockage, comme c’est le cas d’Oracle ou SQL Server, il est possible par exemple de pré réserver de l’espace pour stocker les données sur les fichiers de stockage des différentes tables de partitionnement. Cela permet de ventiler les IO et d’éviter les opérations de croissances de fichiers qui sont horriblement couteuses pour les transactions. Ce manque fait que PostGreSQL fera de l’autocroissance, entrainant des délais erratiques pour certaines requêtes de mise à jour en sus de n’être pas folichon du fait de la fragmentation physique des fichiers qui en résultera !

Pour terminer il est extrêmement difficile de gérer les partitions au niveau du stockage sous PostGreSQL. Par exemple pour migrer les données d’une partition d’un espace de stockage à l’autre dans PostGreSQL, il faut :
1) démarrez une transaction au niveau SERIALIZABLE
2) créer une nouvelle table fille dans cet espace de stockage
3) migrer les données par un UPDATE
4) supprimer les données de l’ancienne table
5) ajouter les index
6) ajouter les règles
7) modifier la vue
8) terminez la transaction
Et j’avoue ne pas avoir testé ce qui se passerait si une telle table est une référence d’intégrité…
Bien entendu s’il s’agit de migrer toutes les données, c’est faisable par un seule ALTER TABLE… Mais le cas dont nous parlons est par exemple celui du scindage d’une partition en deux…

En comparaison, sous MS SQL Server toute manipulation du stockage des partitions se fait en une seule commande :
ALTER TABLE … SWITCH PARTITION …
En sus on peut en une seule commande sous MS SQL Server fusionner ou scinder une partition :
ALTER PARTITION … SPLIT RANGE …
ALTER PARTITION … MERGE RANGE …
Et ces commandes agissent à bas niveau en arrière plan, sans que les utilisateurs en soient grandement incommodés, ce qui n’est pas le cas dans PostGreSQL, puisque les lignes devant être physiquement détruites, aucune transaction ni lecture ne peut s’effectuer pendant une telle migration.

Bref, pour ma part je considère que la partitionnement selon PostGreSQL n’est pas loin d’être un fiasco. C’est pour le moins une chose à éviter…
D’ailleurs conscient que la méthode de partitionnement n’est pas des plus aisée et présente de nombreuses failles, PostGreSQL envisage une autre solution plus proche de celle adoptée par SQL Server. À lire : « Table partitioning ». Mais comme le dit cet article « They are hard to fix in 9.0, but should continue to be improved in the future releases.« . En tout cas, cette technique n’est toujours pas présente dans la version 9.1 de PostGreSQL…

Pour information voici les déboires qui arrive en production à qui veut s’aventurer dans le partitionnement sous PG : Partitions and joins lead to index lookups on all partitions. Bref, dans ce cas pratique, le partitionnement conduit à des requêtes 36 fois moins rapide que sans partitionnement, alors que c’est l’effet inverse qui devrait en résulter !

--------
Frédéric Brouard, SQLpro - ARCHITECTE DE DONNÉES, http://sqlpro.developpez.com/
Expert bases de données relationnelles et langage SQL. MVP Microsoft SQL Server
www.sqlspot.com : modélisation, conseil, audit, optimisation, tuning, formation
* * * * * Enseignant CNAM PACA - ISEN Toulon - CESI Aix en Provence *

Une réflexion au sujet de « Partitionner une table. Comparaison PostGreSQL / MS SQL Server »

  1. Avatar de sqlprosqlpro Auteur de l’article

    Ci joint les commentaires de scheu postés à l’origine dans ce message

    Le peu que j’avais testé du partitionnement sous Postgresql m’avait amené à la même conclusion que toi : une usine à gaz difficile à maintenir et impensable dans un environnement de production

    Pour compléter ton article, tu aurais aussi pu parler des performances en insertion : j’avais fait il y a quelques années un bench entre Oracle 10g et Postgresql 8.2, et pour une boucle d’insert ligne à ligne dans une table partitionnée, c’était 2 fois plus long sous Postgresql (sans doute à cause de tous les triggers et contraintes à vérifier à chaque insert)
    Ca pourrait être intéressant de comparer les temps entre Postgresql et SQL Server

    Également pour aller plus loin sur les plans d’exécution, sous Postgresql pour que le plan d’exécution ne lise que la bonne partition (et pas toutes les autres), il faut faire un filtre direct sur la table (ex : « where col_date = ‘2010-01-05′ »).
    Mais dès l’instant où le filtre sur ta clé de partitionnement est issu d’une condition de jointure, ça ne marche pas la plupart du temps, et Postgresql parcourt toutes les partitions.
    Exemple avec « table1″ partitionnée par mois sur col_date et une requête du style « where table1.col_date = table2.col_date and table2.col_date = ‘2010-01-05′ » : Postgresql 8.2 parcourt toutes les partitions de « table1″ et non pas seulement celle correspondante à janvier 2010
    Alors qu’Oracle (et aussi SQL Server j’imagine) est capable par association d’en déduire table1.col_date=’2010-01-05′ et de ne lire que la partition de janvier 2010
    J’ai rencontré plusieurs fois ces problèmes de mauvais plans d’exécution en Postgresql 8.2, je n’ai pas testé si c’était toujours le cas dans les dernières versions

Laisser un commentaire