Aller au contenu

Vue matérialisée

En SQL, une vue est le résultat d’une requête dont la définition est stockée dans le catalogue système. Elle peut être assimilée à une table virtuelle qui peut être consultée à l’aide d’un SELECT. Elle permet ainsi de masquer la complexité de la requête. Elle peut également être utile pour masquer certaines données à un utilisateur.

Chaque appel à une vue entraîne l’exécution de la requête associée, ce qui peut être coûteux lorsque la requête est complexe. Depuis la version 9.3, il est possible de créer des vues matérialisées. Le résultat de la requête est alors dupliqué dans une table. Les vues matérialisées ne sont spécifiées dans la norme SQL, il s’agit d’une extension.

C’est une forme de dé-normalisation puisque la lecture de la vue entraîne juste la lecture de la table sans exécuter la requête. Il n’y a ainsi aucune garantie que les données qu’elle contient sont bien à jour par rapport aux données réellement présentes dans les objets de la base.

Cette technique de mise en cache est utilisée principalement à des fins d’optimisation et de recherche de performance, lorsque l’on doit faire face à des requêtes particulièrement longues et complexes. Comme tout système de mise en cache, il faut prévoir un système d’invalidation du cache afin de le mettre à jour.

Avantages

Les vues matérialisées sont utiles :

  • pour apporter un gain notable en performance pour des requêtes spécifiques ;
  • lorsque les “données réelles” sont peu ou rarement modifiées ;
  • lorsque l’on peut travailler sur des données légèrement désynchronisées.

Inconvénients

Voici toutefois des objections majeures à l’utilisation des vues matérialisées dans PostgreSQL :

  • elles ne simplifient pas les problèmes, au contraire elles complexifient la gestion des données ;
  • il faut prévoir un moyen de les mettre à jour (contention possible).

Mise en oeuvre

Création

Pour créer une vue matérialisée, il faut utiliser l’ordre CREATE MATERIALIZED VIEW mymatview AS :

Créons un jeu de données :

CREATE TABLE t1 (c1 INT);
INSERT INTO t1 (c1)
SELECT *
FROM generate_series(1, 100);

CREATE TABLE t2 (c1 INT, c2 TEXT); 
INSERT INTO t2 (c1, c2)
SELECT i, md5(i::TEXT)
FROM generate_series(1, 100) i(i);

La requête suivante fait la joINTure entre t1 et t2 sur la colonne c1, puis applique un filtre sur la colonne c2 afin de ne sélectionner que les enregistrements se terminant par un chiffre pair :

SELECT t1.c1, t2.c2
FROM t1
JOIN t2 ON t1.c1 = t2.c1
WHERE t2.c2 ~ '[0|2|4|6|8]$';

Voici son plan d’exécution :

EXPLAIN VERBOSE SELECT t1.c1, t2.c2 FROM t1 JOIN t2 ON t1.c1 = t2.c1 WHERE t2.c2 ~ '[0|2|4|6|8]$';
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Hash Join  (cost=2.64..5.32 rows=31 width=37)
   Output: t1.c1, t2.c2
   Hash Cond: (t1.c1 = t2.c1)
   ->  Seq Scan on public.t1  (cost=0.00..2.00 rows=100 width=4)
         Output: t1.c1
   ->  Hash  (cost=2.25..2.25 rows=31 width=37)
         Output: t2.c2, t2.c1
         ->  Seq Scan on public.t2  (cost=0.00..2.25 rows=31 width=37)
               Output: t2.c2, t2.c1
               Filter: (t2.c2 ~ '[0|2|4|6|8]$'::text)
(10 rows)

Créons une vue non matérialisée :

CREATE VIEW vuefresh
AS
SELECT t1.c1, t2.c2
FROM t1
JOIN t2 ON t1.c1 = t2.c1
WHERE t2.c2 ~ '[0|2|4|6|8]$';

Voici le plan d’exécution lors d’une lecture :

EXPLAIN VERBOSE SELECT * FROM vuefresh;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Hash Join  (cost=2.64..5.32 rows=31 width=37)
   Output: t1.c1, t2.c2
   Hash Cond: (t1.c1 = t2.c1)
   ->  Seq Scan on public.t1  (cost=0.00..2.00 rows=100 width=4)
         Output: t1.c1
   ->  Hash  (cost=2.25..2.25 rows=31 width=37)
         Output: t2.c2, t2.c1
         ->  Seq Scan on public.t2  (cost=0.00..2.25 rows=31 width=37)
               Output: t2.c2, t2.c1
               Filter: (t2.c2 ~ '[0|2|4|6|8]$'::text)
(10 rows)

Une lecture de la vue entraîne l’exécution de la requête.

Pour éviter cela, on peut créer la vue matérialisée :

CREATE MATERIALIZED VIEW vuemat AS SELECT t1.c1,t2.c2 FROM t1 join t2 ON t1.c1=t2.c1 WHERE t2.c2 ~ '[0|2|4|6|8]$';

Voici le plan d’exécution lors de la lecture :

EXPLAIN VERBOSE SELECT * FROM vuemat ;
                            QUERY PLAN                            
------------------------------------------------------------------
 Seq Scan on public.vuemat  (cost=0.00..22.70 rows=1270 width=36)
   Output: c1, c2
(2 rows)

Le plan est bien moins coûteux puisqu’il consiste en une simple lecture de la table. L’utilisation d’une vue matérialisée évite d’exécuter la requête à chaque lecture.

Rafraîchissement

Supprimons le contenu de la table t1 et comparons les deux vues:

DELETE FROM t1;

SELECT * FROM vuefresh;
 c1 | c2 
----+----
(0 rows)

SELECT * FROM vuemat;
 c1 |                c2                
----+----------------------------------
  9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
 10 | d3d9446802a44259755d38e6d163e820
 12 | c20ad4d76fe97759aa27a0c99bff6710
 14 | aab3238922bcc25a6f606eb525ffdc56
 19 | 1f0e3dad99908345f7439f8ffabdffc4
 20 | 98f13708210194c475687be6106a3b84
 27 | 02e74f10e0327ad868d138f2b4fdd6f0
 35 | 1c383cd30b7c298ab50293adfecb7b18
 36 | 19ca14e7ea6328a42e0eb13d585e4c22
 40 | d645920e395fedad7bbbed0eca3fe2e0
 42 | a1d0c6e83f027327d8461063f4ac58a6
 44 | f7177163c833dff4b38fc8d2872f1ec6
 48 | 642e92efb79421734881b53e1e1b18b6
 49 | f457c545a9ded88f18ecee47145a72c0
 51 | 2838023a778dfaecdc212708f721b788
 52 | 9a1158154dfa42caddbd0694a4e9bdc8
 53 | d82c8d1619ad8176d665453cfb2e55f0
 54 | a684eceee76fc522773286a895bc8436
 56 | 9f61408e3afb633e50cdf1b20de6f466
 59 | 093f65e080a295f8076b1c5722a46aa2
 60 | 072b030ba126b2f4b2374f342be9ed44
 63 | 03afdbd66e7929b125f8597834fa83a4
 67 | 735b90b4568125ed6c3f678819b6e058
 69 | 14bfa6bb14875e45bba028a21ed38046
 71 | e2c420d928d4bf8ce0ff2ec19b371514
 76 | fbd7939d674997cdb4692d34de8633c4
 88 | 2a38a4a9316c49e5a833517c45d31070
 89 | 7647966b7343c29048673252e490f736
 91 | 54229abfcfa5649e7003b83dd4755294
 92 | 92cc227532d17e56e07902b254dfad10
 96 | 26657d5ff9020d2abefe558796b99584
(31 rows)

La vue matérialisée vuemat contient des données périmées. Il est possible de mettre à jour la vue grâce à l’ordre REFRESH MATERIALIZED VIEW vuemat; :

REFRESH MATERIALIZED VIEW vuemat;
REFRESH MATERIALIZED VIEW

La vue matérialisée vuemat ne contient plus des données périmées :

SELECT * FROM vuemat;
 c1 | c2 
----+----
(0 rows)

Note

La mise à jour d’une vue matérialisée entraîne la pose d’un verrou de type ACCESS EXCLUSIVE sur la vue, ce qui empêche toute opération sur celle-ci (même une simple lecture).

La version 9.4 introduit la possibilité de rafraîchir une vue matérialisée sans blocage grâce à la clause CONCURRENTLY. Dès lors, le verrou utilisé est de type EXCLUSIVE ce qui autorise les lectures. Cependant, il n’est possible d’utiliser cette clause que s’il existe un index unique portant seulement sur des noms de colonne et sur l’intégralité des lignes de la vue :

REFRESH MATERIALIZED VIEW CONCURRENTLY vuemat ;
ERROR:  cannot refresh materialized view "public.vuemat" concurrently
HINT:  Create a unique index with no WHERE clause on one or more columns of the materialized view.

Le `REFRESH MATERIALIZED VIEW CONCURRENTLY` est impossible car il manque un index unique :

```sql
CREATE UNIQUE INDEX ON vuemat (c1);
CREATE INDEX

Avec la création du précédent index unique, il est alors maintenant possible de lancer un REFRESH MATERIALIZED VIEW CONCURRENTLY :

REFRESH MATERIALIZED VIEW CONCURRENTLY vuemat ;
REFRESH MATERIALIZED VIEW

Attention

Rien n’empêche d’insérer des lignes en doublon dans la table t1 qui pourrait rentrer en conflit avec l’index unique sur la vue matérialisée. L’erreur ne sera relevée que lors de la mise à jour de la vue :

INSERT INTO t1(c1) VALUES ('9');
REFRESH MATERIALIZED VIEW  CONCURRENTLY vuemat ;
INSERT INTO t1(c1) VALUES ('9');
REFRESH MATERIALIZED VIEW  CONCURRENTLY vuemat ;

Le deuxième rafraîchissement est alors impossible car l’index unique sur vuemat n’est plus respecté :

ERROR:  new data for materialized view "vuemat" contains duplicate rows without any null columns
DETAIL:  Row: (9,45c48cce2e2d7fbdea1afc51c7c6ad26)

Ainsi les deux vues peuvent différer :

SELECT * FROM vuemat WHERE c1 = 9;
c1 |                c2                
----+----------------------------------
9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
(1 row)

SELECT * FROM vuefresh WHERE c1 = 9;
c1 |                c2                
----+----------------------------------
9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
(3 rows)

Écrire dans les vues matérialisées ?

Contrairement aux vues classiques, il n’est pas possible de modifier directement le contenu d’une vue matérialisée :

INSERT INTO vuemat VALUES ('456', 'azerty0');
ERROR:  cannot change materialized view "vuemat"

Conclusion

À l’exception de cas très précis et maîtrisés, les vues matérialisées sont à déconseiller car elles sont complexes à mettre en place et à maintenir.

Elles peuvent être utiles pour des infocentres où il est nécessaire de manipuler de gros volumes de données. Elles permettent ainsi d’effectuer un calcul intermédiaire afin d’obtenir un résultat plus rapidement.

Info

Postgres n’effectue pas de réécriture transparente des vues matérialisées. Il faut explicitement faire une requête sur la vue pour l’exploiter.