Formation SQL2
Dalibo SCOP
8 janvier 2018
Vous êtes libres de redistribuer et/ou modifier cette création selon les conditions suivantes :
Paternité
Pas d'utilisation commerciale
Partage des conditions initiales Ă l'identique
Principe de Pareto (dérivé) : 80% des effets sont produits par 20% des causes.
pgBadger, PoWA, pg_stat_statements, pg_stat_planstkprof, statspack, AWR…SQL Server ProfilerIl existe une définition mathématique précise de chacune des 7 formes normales.
3FNUn attribut (colonne) doit ĂŞtre atomique :
boolean abs, boolean volant_a_gauche, enum couleur, etc. DifficileWHERE| Immatriculation | Modèle | Caractéristiques | 
|---|---|---|
| TT-802-AX | Clio | 4 roues motrices, ABS, toit ouvrant, peinture verte | 
| QS-123-DB | AX | jantes aluminium, peinture bleu | 
INSERT INTO voiture
VALUES ('AD-057-GD','Clio','interieur bleu, anti-blocage des roues');    Column      |  Type   |            Description             
----------------+---------+------------------------------------
immatriculation | text    | 
modele          | text    | 
couleur         | color   | Couleur vehicule (bleu,rouge,vert)
toit_ouvrant    | boolean | Option toit ouvrant
abs             | boolean | Option anti-blocage des roues
type_roue       | boolean | tole/aluminium
motricite       | boolean | 2 roues motrices / 4 roues motricesNULL signifie habituellement :
Dans tous les cas, c'est une absence d'information. Ou du moins la seule information est qu'on ne sait pas.
Une table remplie de NULLs est habituellement signe d'un problème de modélisation.
Plus rarement, on rencontre aussi :
varcharNULLidentifiant, nom_attribut, valeurhstore)| id_pers | nom_attr | val_attr | 
|---|---|---|
| 1 | nom | Prunelle | 
| 1 | prenom | Léon | 
| 1 | telephone | 0123456789 | 
| 1 | fonction | dba | 
Comment lister tous les dba?
SELECT id, att_nom.val_attr nom , att_prenom.val_attr prenom,att_telephone.val_attr tel
FROM personnes p
JOIN personne_attributs att_nom 
 ON (p.id=att_nom.id_pers AND att_nom.nom_attr='nom')
JOIN personne_attributs att_prenom
 ON (p.id=att_prenom.id_pers AND att_prenom.nom_attr='prenom')
JOIN personne_attributs att_telephone
 ON (p.id=att_telephone.id_pers AND att_telephone.nom_attr='telephone')
JOIN personne_attributs att_fonction
 ON (p.id=att_fonction.id_pers AND att_fonction.nom_attr='fonction')
WHERE att_fonction.val_attr='dba';Tables Ă plusieurs dizaines, voire centaines de colonnes :
LEFT JOINCHECK pour exclure une partitionPour les performances, on envisage souvent de distribuer la base sur plusieurs nœuds.
SQL est un langage déclaratif :
PrologLes opérateurs purement relationnels sont les suivants :
SELECT (choix des colonnes)WHERE (choix des enregistrements)FROM/JOIN (choix des tables)ORDER BYGROUP BY/DISTINCTHAVINGLe volume de données récupéré a un impact sur les performances.
SQL : langage ensembliste et déclaratif
Une vue est simplement une requête pré-déclarée en base.
DISTINCT, GROUP BY etc.
Le problème est similaire à tout autre langage.
Si IN, limiter le nombre d'enregistrements grâce à DISTINCT
SELECT * FROM t1
  WHERE val IN (SELECT DISTINCT …)Éviter une requête liée :
SELECT a,b
  FROM t1
  WHERE val IN (SELECT f(b))Certaines sous-requĂŞtes sont l'expression de Semi-join ou Anti-join
SELECT * FROM t1 WHERE fk
   [NOT] IN (SELECT pk FROM t2 WHERE xxx)
SELECT * FROM t1 WHERE [NOT] EXISTS
   (SELECT 1 FROM t2 WHERE t2.pk=t1.fk AND xxx)
SELECT t1.* FROM t1 LEFT JOIN t2 ON (t1.fk=t2.pk)
   WHERE t2.id IS [NOT] NULL`NOT IN)L'accès aux données est coûteux.
gram.y de 14000 lignesSe connecter coûte cher :
=> Maintenir les connexions coté applicatif ou utiliser un pooler.
Les spécifications sont souvent procédurales, voire objet !
CTE (SQL:1999)Un ordre SQL peut effectuer de nombreuses choses :
CASE, etc.-- et /* */Encore une fois, prendre de la distance vis-à -vis des spécifications fonctionnelles :
COUNT(*), éventuellement un test de l'existence d'UN enregistrementUn index améliore les SELECT
Sans index :
=# SELECT * FROM t1 WHERE i = 10000;
Temps : 1760,017 msAvec index :
=# CREATE INDEX idx_t1_i ON t1 (i);
=# SELECT * FROM t1 WHERE i = 10000;
Temps : 27,711 msLa présence d'un index ralentit les mises à jour :
=# INSERT INTO t1 SELECT i FROM generate_series(1, 10000000) i;
Temps : 39674,079 ms=# CREATE INDEX idx_t1_i ON t1 (i);
=# INSERT INTO t1 SELECT i FROM generate_series(1, 10000000) i;
Temps : 94925,140 msCompromis Ă trouver :
GIN (Generalized Inverted Index)GiST (Generalized Search Tree)BRIN (Block Range INdex)BRIN sont utiles pour les grosses volumétriesSi une fonction est appliquée à une colonne dans un prédicat :
SELECT ... FROM table WHERE f(colonne)=CUsage classique :
Avec un index fonctionnel, l'optimiseur sait utiliser un index :
CREATE INDEX index ON dictionnaire_fr(anglais(mot))COMMITROLLBACKCOMMIT
deadlocks (verrous mortels)deadlockdeadlocks est ralentieCe document s'appuie sur de nombreuses sources.
WHERECette requĂŞte d'exemple :
SELECT matricule, nom, prenom, nom_service, fonction, localisation
  FROM employes emp
  JOIN services ser ON (emp.num_service = ser.num_service)
 WHERE ser.localisation = 'Nantes';L'objet de ce module est de comprendre son plan d'exécution :
 Hash Join  (cost=1.06..2.29 rows=4 width=48)
   Hash Cond: (emp.num_service = ser.num_service)
   ->  Seq Scan on employes emp  (cost=0.00..1.14 rows=14 width=35)
   ->  Hash  (cost=1.05..1.05 rows=1 width=21)
         ->  Seq Scan on services ser  (cost=0.00..1.05 rows=1 width=21)
               Filter: ((localisation)::text = 'Nantes'::text)pg_class
relpages et reltuplesNULL)pg_statistic
pg_statspg_class
relpages et reltuplesCREATE STATISTICSdefault_statistics_target = 100Configurable par colonne
ALTER TABLE nom ALTER [ COLUMN ] colonne SET STATISTICS valeur;300 * default_statistics_target
EXISTS, IN et certaines jointures externes :
PostgreSQL dispose de la parallélisation depuis la version 9.6. Cela ne concernait que les jointures de type Nested Loop et Hash Join. Quant au Merge Join, il a fallu attendre la version 10 pour que la parallélisation soit supportée.
SELECT * FROM t1 WHERE c1=1 AND c2=1c1=1 est vrai pour 20% des lignesc2=1 est vrai pour 10% des lignesCREATE STATISTICS pour des statistiques multi-colonnesSELECT *
FROM commandes
WHERE extract('year' from date_commande) = 2014;extract
SELECT * FROM t1 WHERE c2 LIKE 'x%';Delete  (actual time=111.251..111.251 rows=0 loops=1)
  ->  Hash Join  (actual time=1.094..21.402 rows=9347 loops=1)
        ->  Seq Scan on lot_a30_descr_lot
            (actual time=0.007..11.248 rows=34934 loops=1)
        ->  Hash  (actual time=0.501..0.501 rows=561 loops=1)
              ->  Bitmap Heap Scan on lot_a10_pdl
                  (actual time=0.121..0.326 rows=561 loops=1)
                    Recheck Cond: (id_fantoir_commune = 320013)
                    ->  Bitmap Index Scan on...
                        (actual time=0.101..0.101 rows=561 loops=1)
                          Index Cond: (id_fantoir_commune = 320013)
Trigger for constraint fk_lotlocal_lota30descrlot:
  time=1010.358 calls=9347
Trigger for constraint fk_nonbatia21descrsuf_lota30descrlot:
  time=2311695.025 calls=9347
Total runtime: 2312835.032 msSELECT DISTINCT t1.* FROM t1 JOIN t2 ON (t1.id=t2.t1_id);DISTINCT est souvent utilisé pour dédoublonner les lignes de t1
GROUP BY est plus rapideGROUP BYPrédicat incluant une transformation :
WHERE col1 + 2 > 5Opérateur non-supporté par l'index :
WHERE col1 <> 'valeur';effective_cache_sizeNOT IN avec une sous-requĂŞte
NOT EXISTSUNION au lieu de UNION ALL
SELECT
LATERALN'hésitez pas, c'est le moment !
SELECT c1 FROM t1 WHERE c1<10SELECT pg_class.relname, pg_class.reltuples
FROM pg_class
LEFT JOIN pg_namespace
       ON pg_class.relnamespace=pg_namespace.oid;
<, <=, =, >=, >
DMLPossibilité d'indexer plusieurs colonnes :
CREATE INDEX ON ma_table (id, name) ;EXPLAIN (ANALYZE, BUFFERS)CAST)De nombreuses possibilités d'indexation avancée :
Covering indexesGiNGiSTBRINHashN'indexe qu'une partie des données :
CREATE INDEX ON table (colonne) WHERE condition;CREATE INDEX ON index(une_colonne) WHERE une_colonne = 'test`Il s'agit d'un index sur le résultat d'une fonction :
WHERE upper(a)='DUPOND'l'index classique ne fonctionne pas
CREATE INDEX mon_idx ON ma_table ((upper(a))IMMUTABLEOn trouve parfois « index couvrants » dans la littérature française.
CREATE INDEX idx1 on T1 (col1,col2)WHEREET contiennent toutes les colonnes demandées par la requête :
SELECT col1,col2` FROM t1 WHERE col1>12Tri sans collation (pour LIKE)
CREATE INDEX idx1 ON ma_table (col_varchar varchar_pattern_ops)Permet :
SELECT ... FROM ma_table WHERE col_varchar LIKE 'chaine%'GIN : Generalized Inverted iNdex
TID) oĂą le trouverGiST : Generalized Search Tree
btreebtree et rtreeKNN = K-Nearest neighbours, K plus proches voisinsRequĂŞtes de types
ORDER BY ma_colonne <-> une_référence LIMIT 10BRIN : Block Range INdex (9.5+)
min/max)Index Hash : * Journalisés uniquement depuis la version 10 * donc facilement corrompus sur les versions antérieures * Moins performants que les btree * Ne gèrent que les égalités, pas « < » et « > » * Mais plus compacts * À ne pas utiliser
PgBadgerpg_stat_statementsPoWApg_qualstats
PoWaPoWAHypoPGLIMIT/OFFSETLATERALUPSERT : INSERT ou UPDATELIMITFETCH FIRST xx ROWSSELECT *
  FROM employes
 LIMIT 2;
 matricule |   nom    | service  | salaire
-----------+----------+----------+----------
 00000001  | Dupuis   |          | 10000.00
 00000004  | Fantasio | Courrier |  4500.00
(2 lignes)LIMITSELECT *
  FROM employes
 LIMIT 2
 ORDER BY matricule;
 matricule |   nom    | service  | salaire
-----------+----------+----------+----------
 00000001  | Dupuis   |          | 10000.00
 00000004  | Fantasio | Courrier |  4500.00SELECT *
  FROM employes
 ORDER BY matricule
 LIMIT 2
 OFFSET 2;
 matricule |   nom    |   service   | salaire
-----------+----------+-------------+---------
 00000006  | Prunelle | Publication | 4000.00
 00000020  | Lagaffe  | Courrier    | 3000.00RETURNINGINSERTUPDATEDELETECREATE TABLE test_returning (id serial primary key, val integer);
INSERT INTO test_returning (val)
  VALUES (10)
RETURNING id, val;
 id | val
----+-----
  1 |  10
(1 ligne)INSERT ... ON CONFLICT DO { NOTHING | UPDATE }INSERT INTO employes (matricule, nom, service, salaire)
 VALUES ('00000001', 'Marsupilami', 'Direction', 50000.00);
ERROR:  duplicate key value violates unique constraint
        "employes_pkey"
DETAIL:  Key (matricule)=(00000001) already exists.ON CONFLICT DO NOTHING évite d'insérer une ligne existante :=# INSERT INTO employes (matricule, nom, service, salaire)
   VALUES ('00000001', 'Marsupilami', 'Direction', 50000.00)
   ON CONFLICT DO NOTHING;
INSERT 0 0INSERT ....
ON CONFLICT
  DO NOTHING;INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000001', 'M. Pirate', 'Direction', 0.00)
ON CONFLICT (matricule)
   DO UPDATE SET salaire = employes.salaire,
                 nom = excluded.nom
RETURNING *;
 matricule |    nom    |   service   | salaire
-----------+-----------+-------------+----------
 00000001  | M. Pirate | Direction   | 50000.00INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000002', 'Moizelle Jeanne', 'Publication', 3000.00),
       ('00000040', 'Lebrac', 'Publication', 3100.00)
ON CONFLICT (matricule)
   DO UPDATE SET salaire = employes.salaire,
                 nom = excluded.nom
RETURNING *;
 matricule |       nom       |   service   | salaire
-----------+-----------------+-------------+----------
 00000002  | Moizelle Jeanne | Publication |  3000.00
 00000040  | Lebrac          | Publication |  3000.00INSERT ....
ON CONFLICT (<colonne clé>)
  DO UPDATE
        SET colonne_a_modifier = excluded.colonne,
            autre_colonne_a_modifier = excluded.autre_colonne,
            ...;LATERAL
foreachSELECT titre,
       top_5_messages.date_publication,
       top_5_messages.extrait
  FROM sujets,
  LATERAL(SELECT date_publication,
                 substr(message, 0, 100) AS extrait
          FROM messages
         WHERE sujets.sujet_id = messages.sujet_id
         ORDER BY date_publication DESC
         LIMIT 5) top_5_messages
 ORDER BY sujets.date_modification DESC,
          top_5_messages.date_publication DESC
 LIMIT 25;
SELECT titre,
       top_5_messages.date_publication,
       top_5_messages.extrait
  FROM sujets,
       get_top_5_messages(sujet_id) AS top_5_messages
 ORDER BY sujets.date_modification DESC
 LIMIT 25;WITH resultat AS (
   /* requĂŞte complexe */
)
SELECT *
  FROM resultat
 WHERE nb < 5;WITH nom_vue1 AS (
 <requête pour générer la vue 1>
)
SELECT *
  FROM nom_vue1;INSERT/UPDATE/DELETERETURNINGWITH donnees_a_archiver AS (
DELETE FROM donnes_courantes
 WHERE date < '2015-01-01'
 RETURNING *
)
INSERT INTO donnes_archivees
SELECT * FROM donnees_a_archiver;WITH RECURSIVE suite AS (
SELECT 1 AS valeur
UNION ALL
SELECT valeur + 1
  FROM suite
 WHERE valeur < 10
)
SELECT * FROM suite;
WITH RECURSIVE parcours_menu AS (
SELECT menu_id, libelle, parent_id,
       libelle AS arborescence
  FROM entrees_menu
 WHERE libelle = 'Terminal'
   AND parent_id IS NULL
UNION ALL
SELECT menu.menu_id, menu.libelle, menu.parent_id,
       arborescence || '/' || menu.libelle
  FROM entrees_menu menu
  JOIN parcours_menu parent
    ON (menu.parent_id = parent.menu_id)
)
SELECT * FROM parcours_menu;SELECT FOR UPDATESELECT FOR UPDATE SKIP LOCKED
SSI : Serializable Snapshot Isolation (9.1+)
serializabledefault_transaction_isolation=serializable dans la configurationGROUP BYGROUP BY
SELECT service,
       sum(salaire) AS salaires_par_service
  FROM employes
 GROUP BY service;
   service   | salaires_par_service
-------------+----------------------
 Courrier    |              7500.00
 Direction   |             10000.00
 Publication |              7000.00
(3 lignes)ORDER BY dans la fonction d'agrégatarray_agg, string_agg et xmlaggSELECT service,
       string_agg(nom, ', ' ORDER BY nom) AS liste_employes
  FROM employes
 GROUP BY service;
   service   |  liste_employes
-------------+-------------------
 Courrier    | Fantasio, Lagaffe
 Direction   | Dupuis
 Publication | Lebrac, Prunelle
(3 lignes)FILTERCASE complexesSELECT count(*) AS compte_pays,
       count(CASE WHEN r.nom_region='Europe' THEN 1
                  ELSE 0
              END) AS compte_pays_europeens
  FROM pays p
  JOIN regions r
    ON (p.region_id = r.region_id);SELECT count(*) AS compte_pays,
       count(*) FILTER (WHERE r.nom_region='Europe')
                AS compte_pays_europeens
  FROM pays p
  JOIN regions r
    ON (p.region_id = r.region_id);OVER (PARTITION BY ...)SELECT matricule, salaire, service,
       SUM(salaire) OVER (PARTITION BY service)
                 AS total_salaire_service
  FROM employes;
 matricule | salaire  |   service   | total_salaire_service
-----------+----------+-------------+-----------------------
 00000004  |  4500.00 | Courrier    |               7500.00
 00000020  |  3000.00 | Courrier    |               7500.00
 00000001  | 10000.00 | Direction   |              10000.00
 00000006  |  4000.00 | Publication |               7000.00
 00000040  |  3000.00 | Publication |               7000.00SUM(salaire) OVER (PARTITION BY service)
SELECT ...
 agregation OVER (PARTITION BY <colonnes>)
  FROM <liste_tables>
 WHERE <predicats>OVER (ORDER BY …)row_number()rank(), dense_rank()lead(), lag()SELECT row_number() OVER (ORDER BY matricule),
       matricule, nom
  FROM employes;
 row_number | matricule |   nom
------------+-----------+----------
          1 | 00000001  | Dupuis
          2 | 00000004  | Fantasio
          3 | 00000006  | Prunelle
          4 | 00000020  | Lagaffe
          5 | 00000040  | Lebrac
(5 lignes)SELECT matricule, salaire,
       SUM(salaire) OVER (ORDER BY matricule)
  FROM employes;
 matricule | salaire  |   sum
-----------+----------+----------
 00000001  | 10000.00 | 10000.00
 00000004  |  4500.00 | 14500.00
 00000006  |  4000.00 | 18500.00
 00000020  |  3000.00 | 21500.00
 00000040  |  3000.00 | 24500.00SUM(salaire) OVER (ORDER BY matricule)
SELECT ...
 agregation OVER (ORDER BY >colonnes>)
  FROM <liste_tables>
 WHERE <predicats>OVER (PARTITION BY .. ORDER BY ..)SELECT continent, pays, population,
       rank() OVER (PARTITION BY continent
                    ORDER BY population DESC)
              AS rang
  FROM population;
    continent     |       pays         | population | rang
------------------+--------------------+------------+------
 Afrique          | Nigéria            |      173.6 |    1
 Afrique          | Éthiopie           |       94.1 |    2
 Afrique          | Égypte             |       82.1 |    3
 Afrique          | Rép. dém. du Congo |       67.5 |    4
(...)
 Amérique du Nord | États-Unis         |      320.1 |    1
 Amérique du Nord | Canada             |       35.2 |    2
(...)OVER (PARTITION BY continent
      ORDER BY population DESC)
SELECT ...
 <agregation> OVER (PARTITION BY <colonnes>
                  ORDER BY <colonnes>)
  FROM <liste_tables>
 WHERE <predicats>lead(colonne, n)
lag(colonne, n)
SELECT pays, continent, population,
       lag(population) OVER (PARTITION BY continent
                             ORDER BY population DESC)
  FROM population;
         pays          | continent | population |  lag
-----------------------+-----------+------------+--------
 Chine                 | Asie      |     1385.6 |
 Iraq                  | Asie      |       33.8 | 1385.6
 Ouzbékistan           | Asie      |       28.9 |   33.8
 Arabie Saoudite       | Asie      |       28.8 |   28.9
 France métropolitaine | Europe    |       64.3 |
 Finlande              | Europe    |        5.4 |   64.3
 Lettonie              | Europe    |        2.1 |    5.4lag(population) OVER (PARTITION BY continent
                      ORDER BY population DESC)
first_value(colonne)
last_value(colonne)
nth_value(colonne, n)
SELECT pays, continent, population,
       first_value(population)
           OVER (PARTITION BY continent
                 ORDER BY population DESC)
  FROM population;
       pays      | continent | population | first_value
-----------------+-----------+------------+-------------
 Chine           | Asie      |     1385.6 |      1385.6
 Iraq            | Asie      |       33.8 |      1385.6
 Ouzbékistan     | Asie      |       28.9 |      1385.6
 Arabie Saoudite | Asie      |       28.8 |      1385.6
 France          | Europe    |       64.3 |        64.3
 Finlande        | Europe    |        5.4 |        64.3
 Lettonie        | Europe    |        2.1 |        64.3SELECT matricule, nom, salaire, service,
       rank() OVER w,
       dense_rank() OVER w
  FROM employes
 WINDOW w AS (ORDER BY salaire);SELECT fonction_agregat OVER nom,
       fonction_agregat_2 OVER nom ...
       ...
  FROM <liste_tables>
 WHERE <predicats>
 WINDOW nom AS (PARTITION BY ... ORDER BY ...)RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWRANGEROWSORDER BYUNBOUNDED PRECEDING: depuis le début de la partitionCURRENT ROW : depuis la ligne couranteUNBOUNDED FOLLOWING : jusqu'à la fin de la partitionCURRENT ROW : jusqu'à la ligne courante
OVER (PARTITION BY ...
  ORDER BY ...
  RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGxxx PRECEDING : depuis les xxx valeurs devant la ligne couranteCURRENT ROW : depuis la ligne courantexxx FOLLOWING : depuis les xxx valeurs derrière la ligne couranteCURRENT ROW : jusqu'à la ligne courante
OVER (PARTITION BY ...
  ORDER BY ...
  ROWS BETWEEN 2 PRECEDING AND 1 FOLLOWINGSELECT pays, continent, population,
       last_value(population)
        OVER (PARTITION BY continent ORDER BY population
              RANGE BETWEEN UNBOUNDED PRECEDING
                        AND UNBOUNDED FOLLOWING)
  FROM population;
         pays          | continent | population | last_value
-----------------------+-----------+------------+------------
 Arabie Saoudite       | Asie      |       28.8 |     1385.6
 Ouzbékistan           | Asie      |       28.9 |     1385.6
 Iraq                  | Asie      |       33.8 |     1385.6
 Chine (4)             | Asie      |     1385.6 |     1385.6
 Lettonie              | Europe    |        2.1 |       64.3
 Finlande              | Europe    |        5.4 |       64.3
 France métropolitaine | Europe    |       64.3 |       64.3WITHIN GROUP
SELECT continent,
  percentile_disc(0.5)
    WITHIN GROUP (ORDER BY population) AS "mediane",
  percentile_disc(0.95)
    WITHIN GROUP (ORDER BY population) AS "95pct",
  ROUND(AVG(population), 1) AS moyenne
FROM population
 GROUP BY continent;
         continent         | mediane | 95pct  | moyenne
---------------------------+---------+--------+---------
 Afrique                   |    33.0 |  173.6 |    44.3
 Amérique du Nord          |    35.2 |  320.1 |   177.7
 Amérique latine. Caraïbes |    30.4 |  200.4 |    53.3
 Asie                      |    53.3 | 1252.1 |   179.9
 Europe                    |     9.4 |   82.7 |    21.8GROUPING SETS/ROLLUP/CUBEGROUP BY  
SELECT piece,region,sum(quantite)
FROM stock GROUP BY GROUPING SETS (piece,region);
 piece  | region | sum 
--------+--------+-----
 clous  |        |  70
 ecrous |        |  90
 vis    |        | 160
        | est    | 120
        | nord   |  60
        | ouest  |  50
        | sud    |  90GROUPING SETS
SELECT piece,NULL as region,sum(quantite)
  FROM stock
  GROUP BY piece
UNION ALL
SELECT NULL, region,sum(quantite)
  FROM STOCK
  GROUP BY region;ROLLUP  
SELECT piece,region,sum(quantite)
FROM stock GROUP BY ROLLUP (piece,region);Cette requête est équivalente à la requête suivante utilisant GROUPING SETS :
SELECT piece,region,sum(quantite)
FROM stock 
GROUP BY GROUPING SETS ((),(piece),(piece,region));CUBE
  
SELECT piece,region,sum(quantite)
FROM stock GROUP BY CUBE (piece,region);Cette requête est équivalente à la requête suivante utilisant GROUPING SETS :
SELECT piece,region,sum(quantite)
FROM stock                              
GROUP BY GROUPING SETS (
  (),
  (piece),
  (region),
  (piece,region) 
  ); Situation où deux sessions ou plus modifient des données en tables au même moment.↩
La solution actuelle semble techniquement meilleure et la solution actuelle a donc été choisie. Le wiki du projet PostgreSQL montre que l'ordre MERGE a été étudié et qu'un certains nombres d' aspects cruciaux n'ont pas été spécifiés, amenant le projet PostgreSQL a utiliser sa propre version. Voir la documentation : https://wiki.postgresql.org/wiki/UPSERT#MERGE_disadvantages.↩