Jointures latérales

(sujet préalablement traité avec la version 9.5)

Les 8 champions...le top du top !

      L’affaire est d'importance. Le clan des Géants des Pierres des Monts du Vieil Arbre a reçu une convocation pour le grand concours des lanceurs de rocs organisé chaque décade par le clan des Ogres-Mages d’Astragor. Les Géants se relaient en permanence, passant leur journée à lancer des blocs spécialement taillés de 637kg à plusieurs centaines de mètres dans la Vallée des Ecrasés, les écrasés étant les voyageurs ne connaissant pas la tradition du clan. Le Scribe Margiono, un gobelin mineur ayant été l’apprenti du Sage Marifel, grave scrupuleusement les résultats relevés au centimètre près par son corbeau. Oumpfor le Chef ordonne donc à Margiono de lui trouver les 8 meilleurs parmi les membres du clan, à savoir les Géants ayant réalisé les meilleures performances en considérant les plus récentes en cas d’égalité. Oumfor précise qu’il veut des membres actifs car les Géants sont un peu turbulents, se battent entre eux et une partie non négligeable du clan est en permanence sur le flanc.
      Margiono s’exécute, tentant d’abord une approche classique, aidé par Vincent :

select version(); version ----------------------------------------------------------------------------------------------------- PostgreSQL 10beta1 on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18) 6.3.0 20170516, 64-bit create table geants(idg smallint generated always as identity primary key, nmg character varying(32), actif boolean); CREATE TABLE insert into geants(nmg, actif) select upper(md5(random()::text)), case when random() < 0.7 then true else false end from generate_series(1,54); INSERT 0 54 create table lancers(dtl timestamp, idg smallint, perf integer); CREATE TABLE with recursive serie(i) as (select 576000 UNION ALL select i - 1 from serie where i > 1) insert into lancers(dtl, idg, perf) select current_timestamp - (i || ' minutes')::interval, trunc(random() * 54 + 1), case when random() <= 0.0001 then 50000 + trunc(random() * 50000 + 1) else trunc(random() * 50000 + 1) end from serie ; INSERT 0 576000 create index lancers_i1 on lancers(idg); CREATE INDEX create index lancers_i2 on lancers(perf, dtl); CREATE INDEX analyze geants; ANALYZE analyze lancers; ANALYZE explain select l.idg, l.dtl, l.perf from lancers l join (select g.idg idg, max(l.perf) perf from geants g join lancers l on (g.idg = l.idg) where g.actif = 't' group by g.idg) perf_max on (l.idg = perf_max.idg and l.perf = perf_max.perf) order by 3 desc, 2 desc limit 8; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------- Limit (cost=11963.49..11963.51 rows=8 width=14) -> Sort (cost=11963.49..11963.51 rows=8 width=14) Sort Key: l.perf DESC, l.dtl DESC -> Nested Loop (cost=10271.71..11963.37 rows=8 width=14) -> Finalize GroupAggregate (cost=10271.29..10272.14 rows=34 width=6) Group Key: g.idg -> Sort (cost=10271.29..10271.46 rows=68 width=6) Sort Key: g.idg -> Gather (cost=10262.08..10269.22 rows=68 width=6) Workers Planned: 2 -> Partial HashAggregate (cost=9262.08..9262.42 rows=34 width=6) Group Key: g.idg -> Hash Join (cost=1.97..8506.52 rows=151111 width=6) Hash Cond: (l_1.idg = g.idg) -> Parallel Seq Scan on lancers l_1 (cost=0.00..5514.00 rows=240000 width=6) -> Hash (cost=1.54..1.54 rows=34 width=2) -> Seq Scan on geants g (cost=0.00..1.54 rows=34 width=2) Filter: actif -> Index Scan using lancers_i2 on lancers l (cost=0.42..49.72 rows=1 width=14) Index Cond: (perf = (max(l_1.perf))) Filter: (g.idg = idg) select l.idg, l.dtl, l.perf from lancers l join (select g.idg idg, max(l.perf) perf from geants g join lancers l on (g.idg = l.idg) where g.actif = 't' group by g.idg) perf_max on (l.idg = perf_max.idg and l.perf = perf_max.perf) order by 3 desc, 2 desc limit 8; idg | dtl | perf -----+----------------------------+------- 49 | 2016-10-17 00:05:17.882661 | 99568 17 | 2016-09-01 15:45:17.882661 | 95927 22 | 2017-04-02 22:55:17.882661 | 94996 50 | 2017-04-27 11:06:17.882661 | 94866 35 | 2016-10-26 23:45:17.882661 | 93456 52 | 2016-07-14 23:39:17.882661 | 93417 26 | 2017-05-09 15:09:17.882661 | 93126 5 | 2017-03-22 18:41:17.882661 | 91619 (8 lignes) Temps : 166,649 ms ... Temps : 166,399 ms ... Temps : 166,593 ms

      Pas mal mais cette requête semble lente. Margiono se demande si une indexation différente pourrait donner de meilleures performances. Il sait en effet qu’Oumpfor n’espt pas spécialement patient. Mais avant d’ajouter des index, ce qui est toujours coûteux, il décide de tenter d’utiliser les possibilités offertes par SQL-1999 et les jointures LATÉRALES. PostgreSQL permet en effet de réaliser ce type de jointure depuis la version 9.3.

explain select g.idg, g.nmg, l.dtl, l.perf from (select idg, nmg from geants where actif = true) g join lateral (select perf, dtl from lancers where g.idg = idg order by perf desc, dtl desc fetch first 1 rows only) l on (true) order by perf desc, dtl desc fetch first 8 rows only; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Limit (cost=117.48..117.50 rows=8 width=47) -> Sort (cost=117.48..117.57 rows=34 width=47) Sort Key: lancers.perf DESC, lancers.dtl DESC -> Nested Loop (cost=0.42..116.80 rows=34 width=47) -> Seq Scan on geants (cost=0.00..1.54 rows=34 width=35) Filter: actif -> Limit (cost=0.42..3.37 rows=1 width=12) -> Index Scan Backward using lancers_i2 on lancers (cost=0.42..31416.05 rows=10667 width=12) Filter: (geants.idg = idg) select g.idg, g.nmg, l.dtl, l.perf from (select idg, nmg from geants where actif = true) g join lateral (select perf, dtl from lancers where g.idg = idg order by perf desc, dtl desc fetch first 1 rows only) l on (true) order by perf desc, dtl desc fetch first 8 rows only; idg | nmg | dtl | perf -----+----------------------------------+----------------------------+------- 49 | 11EC664FD0731BE3EADA1707AB329572 | 2016-10-17 00:05:17.882661 | 99568 17 | 9CD1EABF13DFF625969A75990C18E19A | 2016-09-01 15:45:17.882661 | 95927 22 | BD45A90206F2649324059CDBC6B684C9 | 2017-04-02 22:55:17.882661 | 94996 50 | 88C6BF69C6A63FB1A6DEA2FA947797D7 | 2017-04-27 11:06:17.882661 | 94866 35 | 80AB0A1955A03C47C7310305D82974DA | 2016-10-26 23:45:17.882661 | 93456 52 | 1D9591CCF91453EC1DD97635AADDC9B1 | 2016-07-14 23:39:17.882661 | 93417 26 | 73E6AEE91CEC598BE03D497AECDADD85 | 2017-05-09 15:09:17.882661 | 93126 5 | AE61D65062B970AF52723492B03DC229 | 2017-03-22 18:41:17.882661 | 91619 (8 lignes) Temps : 3,216 ms ... Temps : 3,327 ms ... Temps : 3,175 ms

      Bonne pioche de la part de Margiono. PostgreSQL estimait que le coût du plan de la requête utilisant LATERAL serait largement inférieur à celui du plan de la requête "classique". Et, à l’exécution, les temps de réponse oternus se sont en effet révélés être largement meilleurs.
      Qu’apporte ce type de jointure par rapport à une jointure habituelle ? Le mot clé LATERAL autorise ici à faire référence à une colonne de GEANTS dans la sous-requête effectuée sur la table LANCERS. Il s’agit donc de fait d’une boucle de type FOR EACH. Pour chaque géant actif, nous cherchons la meilleure performance et la date à laquelle elle a été réalisée via l’index lancers_i2 : c’est bien le fait de limiter le jeu de résultats de la sous-requête à la "top perf" de chaque géant qui rend ici la jointure latérale si efficace.

Mise à jour : 30/05/2017