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_plans
tkprof
, statspack
, AWR
…SQL Server Profiler
Il existe une définition mathématique précise de chacune des 7 formes normales.
3FN
Un 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 motrices
NULL
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 :
varchar
NULL
identifiant, nom_attribut, valeur
hstore
)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 JOIN
CHECK
pour exclure une partitionPour les performances, on envisage souvent de distribuer la base sur plusieurs nœuds.
SQL
est un langage déclaratif :
Prolog
Les opérateurs purement relationnels sont les suivants :
SELECT
(choix des colonnes)WHERE
(choix des enregistrements)FROM/JOIN
(choix des tables)ORDER BY
GROUP BY/DISTINCT
HAVING
Le 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 ms
Avec index :
=# CREATE INDEX idx_t1_i ON t1 (i);
=# SELECT * FROM t1 WHERE i = 10000;
Temps : 27,711 ms
La 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 ms
Compromis Ă 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)=C
Usage classique :
Avec un index fonctionnel, l'optimiseur sait utiliser un index :
CREATE INDEX index ON dictionnaire_fr(anglais(mot))
COMMIT
ROLLBACK
COMMIT
deadlocks
(verrous mortels)deadlock
deadlocks
est ralentieCe document s'appuie sur de nombreuses sources.
WHERE
Cette 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 reltuples
NULL
)pg_statistic
pg_stats
pg_class
relpages
et reltuples
CREATE STATISTICS
default_statistics_target = 100
Configurable 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=1
c1=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 ms
SELECT 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 BY
Prédicat incluant une transformation :
WHERE col1 + 2 > 5
Opérateur non-supporté par l'index :
WHERE col1 <> 'valeur';
effective_cache_size
NOT IN
avec une sous-requĂŞte
NOT EXISTS
UNION
au lieu de UNION ALL
SELECT
LATERAL
N'hésitez pas, c'est le moment !
SELECT c1 FROM t1 WHERE c1<10
SELECT pg_class.relname, pg_class.reltuples
FROM pg_class
LEFT JOIN pg_namespace
ON pg_class.relnamespace=pg_namespace.oid;
<
, <=
, =
, >=
, >
DML
Possibilité d'indexer plusieurs colonnes :
CREATE INDEX ON ma_table (id, name) ;
EXPLAIN (ANALYZE, BUFFERS)
CAST
)De nombreuses possibilités d'indexation avancée :
Covering indexes
GiN
GiST
BRIN
Hash
N'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))
IMMUTABLE
On trouve parfois « index couvrants » dans la littérature française.
CREATE INDEX idx1 on T1 (col1,col2)
WHERE
ET contiennent toutes les colonnes demandées par la requête :
SELECT col1,col2` FROM t1 WHERE col1>12
Tri 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
btree
btree
et rtree
KNN
= K-Nearest neighbours
, K plus proches voisinsRequĂŞtes de types
ORDER BY ma_colonne <-> une_référence LIMIT 10
BRIN
: 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
PgBadger
pg_stat_statements
PoWA
pg_qualstats
PoWa
PoWA
HypoPG
LIMIT
/OFFSET
LATERAL
UPSERT
: INSERT ou UPDATELIMIT
FETCH FIRST xx ROWS
SELECT *
FROM employes
LIMIT 2;
matricule | nom | service | salaire
-----------+----------+----------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00
(2 lignes)
LIMIT
SELECT *
FROM employes
LIMIT 2
ORDER BY matricule;
matricule | nom | service | salaire
-----------+----------+----------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00
SELECT *
FROM employes
ORDER BY matricule
LIMIT 2
OFFSET 2;
matricule | nom | service | salaire
-----------+----------+-------------+---------
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
RETURNING
INSERT
UPDATE
DELETE
CREATE 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 0
INSERT ....
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.00
INSERT 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.00
INSERT ....
ON CONFLICT (<colonne clé>)
DO UPDATE
SET colonne_a_modifier = excluded.colonne,
autre_colonne_a_modifier = excluded.autre_colonne,
...;
LATERAL
foreach
SELECT 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
/DELETE
RETURNING
WITH 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 UPDATE
SELECT FOR UPDATE SKIP LOCKED
SSI : Serializable Snapshot Isolation (9.1+)
serializable
default_transaction_isolation=serializable
dans la configurationGROUP BY
GROUP 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 xmlagg
SELECT 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)
FILTER
CASE
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.00
SUM(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.00
SUM(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.4
lag(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.3
SELECT 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 ROW
RANGE
ROWS
ORDER BY
UNBOUNDED 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 FOLLOWING
xxx 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 FOLLOWING
SELECT 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.3
WITHIN 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.8
GROUPING SETS/ROLLUP/CUBE
GROUP 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 | 90
GROUPING 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.↩