Comment filtrer les résultats SQL dans une relation has-many-through

101

En supposant que j'ai les tables student, clubet student_club:

student {
    id
    name
}
club {
    id
    name
}
student_club {
    student_id
    club_id
}

Je veux savoir comment trouver tous les élèves du club de soccer (30) et de baseball (50).
Bien que cette requête ne fonctionne pas, c'est la chose la plus proche que j'ai jusqu'à présent:

SELECT student.*
FROM   student
INNER  JOIN student_club sc ON student.id = sc.student_id
LEFT   JOIN club c ON c.id = sc.club_id
WHERE  c.id = 30 AND c.id = 50
Xeoncross
la source

Réponses:

146

J'étais curieux. Et comme nous le savons tous, la curiosité a la réputation de tuer les chats.

Alors, quel est le moyen le plus rapide d'écorcher un chat?

L'environnement précis de peau de chat pour ce test:

  • PostgreSQL 9.0 sur Debian Squeeze avec une RAM et des paramètres décents.
  • 6.000 étudiants, 24.000 adhésions à des clubs (données copiées à partir d'une base de données similaire avec des données réelles.)
  • Léger détournement du schéma de dénomination dans la question: student.idest student.stud_idet club.idest club.club_idici.
  • J'ai nommé les requêtes d'après leur auteur dans ce fil, avec un index où il y en a deux.
  • J'ai exécuté toutes les requêtes plusieurs fois pour remplir le cache, puis j'ai choisi le meilleur des 5 avec EXPLAIN ANALYZE.
  • Index pertinents (devrait être l'optimum - tant que nous ne savons pas quels clubs seront interrogés):

    ALTER TABLE student ADD CONSTRAINT student_pkey PRIMARY KEY(stud_id );
    ALTER TABLE student_club ADD CONSTRAINT sc_pkey PRIMARY KEY(stud_id, club_id);
    ALTER TABLE club       ADD CONSTRAINT club_pkey PRIMARY KEY(club_id );
    CREATE INDEX sc_club_id_idx ON student_club (club_id);

    club_pkeyn'est pas requis par la plupart des requêtes ici.
    Les clés primaires implémentent automatiquement des index uniques dans PostgreSQL.
    Le dernier index est de compenser cette lacune connue des index multi-colonnes sur PostgreSQL:

Un index B-tree multicolonne peut être utilisé avec des conditions de requête qui impliquent n'importe quel sous-ensemble des colonnes de l'index, mais l'index est plus efficace lorsqu'il existe des contraintes sur les colonnes de début (les plus à gauche).

Résultats:

Durée totale d'exécution de EXPLAIN ANALYZE.

1) Martin 2: 44,594 ms

SELECT s.stud_id, s.name
FROM   student s
JOIN   student_club sc USING (stud_id)
WHERE  sc.club_id IN (30, 50)
GROUP  BY 1,2
HAVING COUNT(*) > 1;

2) Erwin 1: 33,217 ms

SELECT s.stud_id, s.name
FROM   student s
JOIN   (
   SELECT stud_id
   FROM   student_club
   WHERE  club_id IN (30, 50)
   GROUP  BY 1
   HAVING COUNT(*) > 1
   ) sc USING (stud_id);

3) Martin 1: 31,735 ms

SELECT s.stud_id, s.name
   FROM   student s
   WHERE  student_id IN (
   SELECT student_id
   FROM   student_club
   WHERE  club_id = 30
   INTERSECT
   SELECT stud_id
   FROM   student_club
   WHERE  club_id = 50);

4) Derek: 2,287 ms

SELECT s.stud_id,  s.name
FROM   student s
WHERE  s.stud_id IN (SELECT stud_id FROM student_club WHERE club_id = 30)
AND    s.stud_id IN (SELECT stud_id FROM student_club WHERE club_id = 50);

5) Erwin 2: 2,181 ms

SELECT s.stud_id,  s.name
FROM   student s
WHERE  EXISTS (SELECT * FROM student_club
               WHERE  stud_id = s.stud_id AND club_id = 30)
AND    EXISTS (SELECT * FROM student_club
               WHERE  stud_id = s.stud_id AND club_id = 50);

6) Sean: 2,043 ms

SELECT s.stud_id, s.name
FROM   student s
JOIN   student_club x ON s.stud_id = x.stud_id
JOIN   student_club y ON s.stud_id = y.stud_id
WHERE  x.club_id = 30
AND    y.club_id = 50;

Les trois derniers fonctionnent à peu près de la même manière. 4) et 5) aboutissent au même plan de requête.

Ajouts tardifs:

Fantaisie SQL, mais les performances ne peuvent pas suivre.

7) ypercube 1: 148,649 ms

SELECT s.stud_id,  s.name
FROM   student AS s
WHERE  NOT EXISTS (
   SELECT *
   FROM   club AS c 
   WHERE  c.club_id IN (30, 50)
   AND    NOT EXISTS (
      SELECT *
      FROM   student_club AS sc 
      WHERE  sc.stud_id = s.stud_id
      AND    sc.club_id = c.club_id  
      )
   );

8) ypercube 2: 147,497 ms

SELECT s.stud_id,  s.name
FROM   student AS s
WHERE  NOT EXISTS (
   SELECT *
   FROM  (
      SELECT 30 AS club_id  
      UNION  ALL
      SELECT 50
      ) AS c
   WHERE NOT EXISTS (
      SELECT *
      FROM   student_club AS sc 
      WHERE  sc.stud_id = s.stud_id
      AND    sc.club_id = c.club_id  
      )
   );

Comme prévu, ces deux performances sont presque identiques. Le plan de requête entraîne des analyses de table, le planificateur ne trouve pas un moyen d'utiliser les index ici.


9) wildplasser 1: 49.849 ms

WITH RECURSIVE two AS (
   SELECT 1::int AS level
        , stud_id
   FROM   student_club sc1
   WHERE  sc1.club_id = 30
   UNION
   SELECT two.level + 1 AS level
        , sc2.stud_id
   FROM   student_club sc2
   JOIN   two USING (stud_id)
   WHERE  sc2.club_id = 50
   AND    two.level = 1
   )
SELECT s.stud_id, s.student
FROM   student s
JOIN   two USING (studid)
WHERE  two.level > 1;

Fantaisie SQL, performances décentes pour un CTE. Plan de requête très exotique.
Encore une fois, il serait intéressant de savoir comment la 9.1 gère cela. Je vais bientôt mettre à niveau le cluster de base de données utilisé ici vers la version 9.1. Peut-être que je vais relancer tout le shebang ...


10) Wildplasser 2: 36,986 ms

WITH sc AS (
   SELECT stud_id
   FROM   student_club
   WHERE  club_id IN (30,50)
   GROUP  BY stud_id
   HAVING COUNT(*) > 1
   )
SELECT s.*
FROM   student s
JOIN   sc USING (stud_id);

Variante CTE de la requête 2). Étonnamment, cela peut entraîner un plan de requête légèrement différent avec exactement les mêmes données. J'ai trouvé une analyse séquentielle sur student, où la variante de sous-requête a utilisé l'index.


11) ypercube 3: 101,482 ms

Un autre ajout tardif @ypercube. C'est vraiment incroyable, combien il y a de façons.

SELECT s.stud_id, s.student
FROM   student s
JOIN   student_club sc USING (stud_id)
WHERE  sc.club_id = 10                 -- member in 1st club ...
AND    NOT EXISTS (
   SELECT *
   FROM  (SELECT 14 AS club_id) AS c  -- can't be excluded for missing the 2nd
   WHERE  NOT EXISTS (
      SELECT *
      FROM   student_club AS d
      WHERE  d.stud_id = sc.stud_id
      AND    d.club_id = c.club_id
      )
   )

12) erwin 3: 2,377 ms

@ ypercube's 11) n'est en fait que l'approche inversée stupéfiante de cette variante plus simple, qui manquait également. Fonctionne presque aussi vite que les meilleurs chats.

SELECT s.*
FROM   student s
JOIN   student_club x USING (stud_id)
WHERE  sc.club_id = 10                 -- member in 1st club ...
AND    EXISTS (                        -- ... and membership in 2nd exists
   SELECT *
   FROM   student_club AS y
   WHERE  y.stud_id = s.stud_id
   AND    y.club_id = 14
   )

13) erwin 4: 2,375 ms

Difficile à croire, mais voici une autre variante véritablement nouvelle. Je vois un potentiel pour plus de deux adhésions, mais il se classe également parmi les meilleurs chats avec seulement deux.

SELECT s.*
FROM   student AS s
WHERE  EXISTS (
   SELECT *
   FROM   student_club AS x
   JOIN   student_club AS y USING (stud_id)
   WHERE  x.stud_id = s.stud_id
   AND    x.club_id = 14
   AND    y.club_id = 10
   )

Nombre dynamique d'adhésions au club

En d'autres termes: un nombre variable de filtres. Cette question demandait exactement deux adhésions au club. Mais de nombreux cas d'utilisation doivent se préparer à un nombre variable.

Discussion détaillée dans cette réponse ultérieure connexe:

Erwin Brandstetter
la source
1
Brandstetter, très beau travail. J'ai commencé une prime sur cette question pour vous donner un crédit supplémentaire (mais je dois attendre 24 heures). Quoi qu'il en soit, je me demande comment ces requêtes se passent lorsque vous commencez à ajouter plusieurs club_id au lieu de seulement deux ...
Xeoncross
@Xeoncross: Félicitations à votre geste généreux. :) Avec plus de club_ids, je soupçonne que 1) et 2) se rapprochent de la vitesse, mais il faudrait un plus grand nombre pour renverser le classement.
Erwin Brandstetter
si vous avez plus de deux clubs, créez une autre table contenant ces clubs. Ensuite, rejoignez cette table dans votre sélection.
Paul Morgan du
@Erwin: Thnx (pour les benchmarks). Pas pinailler, mais peut-être que vous pouvez essayer ces requêtes (je veux dire toutes, pas seulement la mienne) avec un (student_id, club_id)index (ou l'inverse).
ypercubeᵀᴹ
3
Ai-je tort de penser que tout ce qui est inférieur à 200 ms est une performance acceptable, compte tenu du domaine en question et de la taille de l'échantillon? Pour un intérêt personnel, j'ai effectué mes propres tests sur SQL Server 2008 R2 en utilisant les mêmes index de structure et (je pense) la diffusion des données mais en augmentant à un million d'étudiants (un ensemble raisonnablement grand pour le domaine donné, je pense) et il n'y avait toujours pas Pas grand chose pour séparer les différentes approches, OMI. Bien sûr, ceux basés sur la division relationnelle pourraient cibler une table de base, leur donnant l'avantage de «l'extensibilité».
jour du
18
SELECT s.*
FROM student s
INNER JOIN student_club sc_soccer ON s.id = sc_soccer.student_id
INNER JOIN student_club sc_baseball ON s.id = sc_baseball.student_id
WHERE 
 sc_baseball.club_id = 50 AND 
 sc_soccer.club_id = 30
Sean
la source
10
select *
from student
where id in (select student_id from student_club where club_id = 30)
and id in (select student_id from student_club where club_id = 50)
Derek Kromm
la source
Cette requête fonctionne bien, mais quelque chose me dérange de devoir demander au SGBDR de vérifier autant d'index * le nombre de clubs.
Xeoncross
6
J'aime le plus cette requête parce qu'elle ressemble à un style propre, c'est comme python dans sql. J'échangerais volontiers 0,44 ms (diff avec la requête de Sean) pour ce type de code.
MGP
5

Si vous voulez juste student_id alors:

    Select student_id
      from student_club
     where club_id in ( 30, 50 )
  group by student_id
    having count( student_id ) = 2

Si vous avez également besoin du nom de l'élève, alors:

Select student_id, name
  from student s
 where exists( select *
                 from student_club sc
                where s.student_id = sc.student_id
                  and club_id in ( 30, 50 )
             group by sc.student_id
               having count( sc.student_id ) = 2 )

Si vous avez plus de deux clubs dans une table club_selection, alors:

Select student_id, name
  from student s
 where exists( select *
                 from student_club sc
                where s.student_id = sc.student_id
                  and exists( select * 
                                from club_selection cs
                               where sc.club_id = cs.club_id )
             group by sc.student_id
               having count( sc.student_id ) = ( select count( * )
                                                   from club_selection ) )
Paul Morgan
la source
Les deux premiers sont inclus dans / identiques à ma requête 1. Mais le troisième répond à la question ajoutée de @Xeoncross dans les commentaires ci-dessus. Je voterais pour cette partie sans les dupes.
Erwin Brandstetter
Merci pour le commentaire mais je montre également une mise en forme. Je vais le laisser «tel quel».
Paul Morgan
4
SELECT *
FROM   student
WHERE  id IN (SELECT student_id
              FROM   student_club
              WHERE  club_id = 30
              INTERSECT
              SELECT student_id
              FROM   student_club
              WHERE  club_id = 50)  

Ou une solution plus générale plus facile à étendre aux nclubs et qui évite INTERSECT(non disponible dans MySQL) et IN(car les performances de cela sont nulles dans MySQL )

SELECT s.id,
       s.name
FROM   student s
       join student_club sc
         ON s.id = sc.student_id
WHERE  sc.club_id IN ( 30, 50 )
GROUP  BY s.id,
          s.name
HAVING COUNT(DISTINCT sc.club_id) = 2  
Martin Smith
la source
Sans aucun doute, votre deuxième réponse est la meilleure pour les requêtes générées par code. Vais-je sérieusement écrire 10 jointures ou sous-requêtes pour trouver la division relationnelle de 10 critères? Heck non, j'utiliserai plutôt cette solution brillante. Merci de m'avoir appris ce que HAVINGfait MySQL.
Eric L.
4

Un autre CTE. Il semble propre, mais il générera probablement le même plan qu'un groupby dans une sous-requête normale.

WITH two AS (
    SELECT student_id FROM tmp.student_club
    WHERE club_id IN (30,50)
    GROUP BY student_id
    HAVING COUNT(*) > 1
    )
SELECT st.* FROM tmp.student st
JOIN two ON (two.student_id=st.id)
    ;

Pour ceux qui veulent tester, une copie de mon truc de générer des données de test:

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp;

CREATE TABLE tmp.student
    ( id INTEGER NOT NULL PRIMARY KEY
    , sname VARCHAR
    );

CREATE TABLE tmp.club
    ( id INTEGER NOT NULL PRIMARY KEY
    , cname VARCHAR
    );

CREATE TABLE tmp.student_club
    ( student_id INTEGER NOT NULL  REFERENCES tmp.student(id)
    , club_id INTEGER NOT NULL  REFERENCES tmp.club(id)
    );

INSERT INTO tmp.student(id)
    SELECT generate_series(1,1000)
    ;

INSERT INTO tmp.club(id)
    SELECT generate_series(1,100)
    ;

INSERT INTO tmp.student_club(student_id,club_id)
    SELECT st.id  , cl.id
    FROM tmp.student st, tmp.club cl
    ;

DELETE FROM tmp.student_club
WHERE random() < 0.8
    ;

UPDATE tmp.student SET sname = 'Student#' || id::text ;
UPDATE tmp.club SET cname = 'Soccer' WHERE id = 30;
UPDATE tmp.club SET cname = 'Baseball' WHERE id = 50;

ALTER TABLE tmp.student_club
    ADD PRIMARY KEY (student_id,club_id)
    ;
wildplasser
la source
Ouais, c'est en fait juste une sous-requête avec group by comme dans ma première version. Le même plan de requête + la surcharge CTE entraîne les mêmes performances + un peu pour le CTE. Belle configuration de test, cependant.
Erwin Brandstetter
Je ne sais pas s'il y a un overhead CTE. La distribution des données de test est très importante. Il en va de même pour la disponibilité des statistiques: après VACUUM ANALYZE, le temps d'exécution est passé de 67,4 à 1,56 ms. Seuls les hash et les bitmaps impliqués dans le QP.
wildplasser
C'est spécial dans votre cas, après avoir supprimé 80% d'une grande table et mis à jour beaucoup, vous aviez plus de tuples morts qu'autre chose. Pas étonnant, l'analyse sous vide aide beaucoup. J'ai exécuté les deux variantes avec et sans CTE, et étonnamment, les plans de requête n'étaient pas identiques. ou mieux encore, je vais ouvrir une salle de chat pour ça.
Erwin Brandstetter
Ne vous inquiétez pas, je connaissais les 80% de lignes mortes ... Je pense que les statistiques importent aussi. Mais l'histogramme est plutôt «plat», étant donné la suppression aléatoire. C'est peut-être simplement l'estimation des pages nécessaires qui change suffisamment pour que le planificateur décide de changer de plan.
wildplasser
3

Il y a donc plus d'une façon d'écorcher un chat .
Je vais en ajouter deux autres pour le rendre plus complet.

1) GROUPE d'abord, REJOIGNEZ-VOUS plus tard

En supposant un modèle de données sain où (student_id, club_id)est unique dans student_club. La deuxième version de Martin Smith est un peu similaire, mais il rejoint le premier, les groupes plus tard. Cela devrait être plus rapide:

SELECT s.id, s.name
  FROM student s
  JOIN (
   SELECT student_id
     FROM student_club
    WHERE club_id IN (30, 50)
    GROUP BY 1
   HAVING COUNT(*) > 1
       ) sc USING (student_id);

2) EXISTE

Et bien sûr, il y a le classique EXISTS. Similaire à la variante de Derek avec IN. Simple et rapide. (Dans MySQL, cela devrait être un peu plus rapide que la variante avec IN):

SELECT s.id, s.name
  FROM student s
 WHERE EXISTS (SELECT 1 FROM student_club
               WHERE  student_id = s.student_id AND club_id = 30)
   AND EXISTS (SELECT 1 FROM student_club
               WHERE  student_id = s.student_id AND club_id = 50);
Erwin Brandstetter
la source
3

Puisque personne n'a ajouté cette version (classique):

SELECT s.*
FROM student AS s
WHERE NOT EXISTS
      ( SELECT *
        FROM club AS c 
        WHERE c.id IN (30, 50)
          AND NOT EXISTS
              ( SELECT *
                FROM student_club AS sc 
                WHERE sc.student_id = s.id
                  AND sc.club_id = c.id  
              )
      )

ou similaire:

SELECT s.*
FROM student AS s
WHERE NOT EXISTS
      ( SELECT *
        FROM
          ( SELECT 30 AS club_id  
          UNION ALL
            SELECT 50
          ) AS c
        WHERE NOT EXISTS
              ( SELECT *
                FROM student_club AS sc 
                WHERE sc.student_id = s.id
                  AND sc.club_id = c.club_id  
              )
      )

Encore un essai avec une approche légèrement différente. Inspiré d'un article dans Explain Extended: Multiple attributes in a EAV table: GROUP BY vs. NOT EXISTS :

SELECT s.*
FROM student_club AS sc
  JOIN student AS s
    ON s.student_id = sc.student_id
WHERE sc.club_id = 50                      --- one option here
  AND NOT EXISTS
      ( SELECT *
        FROM
          ( SELECT 30 AS club_id           --- all the rest in here
                                           --- as in previous query
          ) AS c
        WHERE NOT EXISTS
              ( SELECT *
                FROM student_club AS scc 
                WHERE scc.student_id = sc.id
                  AND scc.club_id = c.club_id  
              )
      )

Une autre approche:

SELECT s.stud_id
FROM   student s

EXCEPT

SELECT stud_id
FROM 
  ( SELECT s.stud_id, c.club_id
    FROM student s 
      CROSS JOIN (VALUES (30),(50)) c (club_id)
  EXCEPT
    SELECT stud_id, club_id
    FROM student_club
    WHERE club_id IN (30, 50)   -- optional. Not needed but may affect performance
  ) x ;   
ypercubeᵀᴹ
la source
+1 .. de beaux ajouts à la collection de peaux de chat pas si complète! :) Je les ai ajoutés au benchmark.
Erwin Brandstetter
Ce n'est pas un combat équitable :) Le gros avantage d'une division relationnelle comme celle-ci est que le diviseur peut être une table de base de sorte que modifier le diviseur est très bon marché, c'est-à-dire mettre à jour les lignes d'une table de base ciblée par la même requête en changeant le SQL interroger à chaque fois.
jour du
@ErwinBrandstetter: Serait-il possible d'ajouter la 3ème variante à vos tests?
ypercubeᵀᴹ
@ypercube: Vous l'avez. Jolie version tordue. :)
Erwin Brandstetter
1
@Erwin: Lorsque vous parvenez à perdre du temps à ce sujet, pouvez-vous également essayer d'avoir deux clés UNIQUES, sur les deux (stud_id, club_id)et (club_id, stud_id)(ou principale et unique)? Je pense toujours que pour certaines de ces requêtes, la différence entre 2 et 140 ms est trop élevée pour être expliquée par les différences dans les plans d'exécution.
ypercubeᵀᴹ
2
WITH RECURSIVE two AS
    ( SELECT 1::integer AS level
    , student_id
    FROM tmp.student_club sc0
    WHERE sc0.club_id = 30
    UNION
    SELECT 1+two.level AS level
    , sc1.student_id
    FROM tmp.student_club sc1
    JOIN two ON (two.student_id = sc1.student_id)
    WHERE sc1.club_id = 50
    AND two.level=1
    )
SELECT st.* FROM tmp.student st
JOIN two ON (two.student_id=st.id)
WHERE two.level> 1

    ;

Cela semble fonctionner raisonnablement bien, car l'analyse CTE évite le besoin de deux sous-requêtes distinctes.

Il y a toujours une raison d'abuser des requêtes récursives!

(BTW: mysql ne semble pas avoir de requêtes récursives)

wildplasser
la source
+1 pour avoir trouvé un autre moyen décent à mi-chemin! J'ai ajouté votre requête au benchmark. J'espère que ça vous va. :)
Erwin Brandstetter
C'est bon. Mais c'était une plaisanterie, bien sûr. Le CTE fonctionne bien si plus de disques d'étudiants * errants sont ajoutés. (Pour les tests, j'ai utilisé 1000 étudiants * 100 clubs et supprimé 80% au hasard)
wildplasser
1

Différents plans de requête dans les requêtes 2) et 10)

J'ai testé dans une vraie base de données, donc les noms diffèrent de la liste des peaux de chat. C'est une copie de sauvegarde, donc rien n'a changé pendant toutes les exécutions de test (à l'exception des modifications mineures apportées aux catalogues).

Requête 2)

SELECT a.*
FROM   ef.adr a
JOIN (
    SELECT adr_id
    FROM   ef.adratt
    WHERE  att_id IN (10,14)
    GROUP  BY adr_id
    HAVING COUNT(*) > 1) t using (adr_id);

Merge Join  (cost=630.10..1248.78 rows=627 width=295) (actual time=13.025..34.726 rows=67 loops=1)
  Merge Cond: (a.adr_id = adratt.adr_id)
  ->  Index Scan using adr_pkey on adr a  (cost=0.00..523.39 rows=5767 width=295) (actual time=0.023..11.308 rows=5356 loops=1)
  ->  Sort  (cost=630.10..636.37 rows=627 width=4) (actual time=12.891..13.004 rows=67 loops=1)
        Sort Key: adratt.adr_id
        Sort Method:  quicksort  Memory: 28kB
        ->  HashAggregate  (cost=450.87..488.49 rows=627 width=4) (actual time=12.386..12.710 rows=67 loops=1)
              Filter: (count(*) > 1)
              ->  Bitmap Heap Scan on adratt  (cost=97.66..394.81 rows=2803 width=4) (actual time=0.245..5.958 rows=2811 loops=1)
                    Recheck Cond: (att_id = ANY ('{10,14}'::integer[]))
                    ->  Bitmap Index Scan on adratt_att_id_idx  (cost=0.00..94.86 rows=2803 width=0) (actual time=0.217..0.217 rows=2811 loops=1)
                          Index Cond: (att_id = ANY ('{10,14}'::integer[]))
Total runtime: 34.928 ms

Requête 10)

WITH two AS (
    SELECT adr_id
    FROM   ef.adratt
    WHERE  att_id IN (10,14)
    GROUP  BY adr_id
    HAVING COUNT(*) > 1
    )
SELECT a.*
FROM   ef.adr a
JOIN   two using (adr_id);

Hash Join  (cost=1161.52..1261.84 rows=627 width=295) (actual time=36.188..37.269 rows=67 loops=1)
  Hash Cond: (two.adr_id = a.adr_id)
  CTE two
    ->  HashAggregate  (cost=450.87..488.49 rows=627 width=4) (actual time=13.059..13.447 rows=67 loops=1)
          Filter: (count(*) > 1)
          ->  Bitmap Heap Scan on adratt  (cost=97.66..394.81 rows=2803 width=4) (actual time=0.252..6.252 rows=2811 loops=1)
                Recheck Cond: (att_id = ANY ('{10,14}'::integer[]))
                ->  Bitmap Index Scan on adratt_att_id_idx  (cost=0.00..94.86 rows=2803 width=0) (actual time=0.226..0.226 rows=2811 loops=1)
                      Index Cond: (att_id = ANY ('{10,14}'::integer[]))
  ->  CTE Scan on two  (cost=0.00..50.16 rows=627 width=4) (actual time=13.065..13.677 rows=67 loops=1)
  ->  Hash  (cost=384.68..384.68 rows=5767 width=295) (actual time=23.097..23.097 rows=5767 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 1153kB
        ->  Seq Scan on adr a  (cost=0.00..384.68 rows=5767 width=295) (actual time=0.005..10.955 rows=5767 loops=1)
Total runtime: 37.482 ms
Erwin Brandstetter
la source
@wildplasser: voyez les plans de requêtes divergents! Inattendu pour moi. p. 9.0. La salle de chat était peu maniable, alors j'abuse d'une réponse ici.
Erwin Brandstetter
Scènes étranges. Fondamentalement, le même QP ici (9.0.1-beta-quelque chose) pour le CTE: seq scan + bitmap au lieu d'un index scan + merge. Peut-être une faille dans l'heuristique de coût de l'optimiseur? Je vais produire encore un autre abus CTE ...
wildplasser
1

@ erwin-brandstetter S'il vous plaît, comparez ceci:

SELECT s.stud_id, s.name
FROM   student s, student_club x, student_club y
WHERE  x.club_id = 30
AND    s.stud_id = x.stud_id
AND    y.club_id = 50
AND    s.stud_id = y.stud_id;

C'est comme le numéro 6) de @sean, juste plus propre, je suppose.

Taai
la source
2
Vous devez savoir que @-notifying ne fonctionne que dans les commentaires, pas dans les réponses. Je suis tombé sur ce message par hasard. Le plan de requête et les performances de votre requête sont identiques à la requête de Sean. C'est effectivement la même chose, mais la requête de Sean avec une JOINsyntaxe explicite est la forme généralement préférée, car elle est plus claire. +1 pour une autre réponse valide, cependant!
Erwin Brandstetter le
0
-- EXPLAIN ANALYZE
WITH two AS (
    SELECT c0.student_id
    FROM tmp.student_club c0
    , tmp.student_club c1
    WHERE c0.student_id = c1.student_id
    AND c0.club_id = 30
    AND c1.club_id = 50
    )
SELECT st.* FROM tmp.student st
JOIN two ON (two.student_id=st.id)
    ;

Le plan de requête:

 Hash Join  (cost=1904.76..1919.09 rows=337 width=15) (actual time=6.937..8.771 rows=324 loops=1)
   Hash Cond: (two.student_id = st.id)
   CTE two
     ->  Hash Join  (cost=849.97..1645.76 rows=337 width=4) (actual time=4.932..6.488 rows=324 loops=1)
           Hash Cond: (c1.student_id = c0.student_id)
           ->  Bitmap Heap Scan on student_club c1  (cost=32.76..796.94 rows=1614 width=4) (actual time=0.667..1.835 rows=1646 loops=1)
                 Recheck Cond: (club_id = 50)
                 ->  Bitmap Index Scan on sc_club_id_idx  (cost=0.00..32.36 rows=1614 width=0) (actual time=0.473..0.473 rows=1646 loops=1)                     
                       Index Cond: (club_id = 50)
           ->  Hash  (cost=797.00..797.00 rows=1617 width=4) (actual time=4.203..4.203 rows=1620 loops=1)
                 Buckets: 1024  Batches: 1  Memory Usage: 57kB
                 ->  Bitmap Heap Scan on student_club c0  (cost=32.79..797.00 rows=1617 width=4) (actual time=0.663..3.596 rows=1620 loops=1)                   
                       Recheck Cond: (club_id = 30)
                       ->  Bitmap Index Scan on sc_club_id_idx  (cost=0.00..32.38 rows=1617 width=0) (actual time=0.469..0.469 rows=1620 loops=1)
                             Index Cond: (club_id = 30)
   ->  CTE Scan on two  (cost=0.00..6.74 rows=337 width=4) (actual time=4.935..6.591 rows=324 loops=1)
   ->  Hash  (cost=159.00..159.00 rows=8000 width=15) (actual time=1.979..1.979 rows=8000 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 374kB
         ->  Seq Scan on student st  (cost=0.00..159.00 rows=8000 width=15) (actual time=0.093..0.759 rows=8000 loops=1)
 Total runtime: 8.989 ms
(20 rows)

Donc, il semble toujours vouloir le balayage séquentiel sur l'étudiant.

wildplasser
la source
J'ai hâte de voir si cela a été corrigé dans la version 9.1.
Erwin Brandstetter
0
SELECT s.stud_id, s.name
FROM   student s,
(
select x.stud_id from 
student_club x 
JOIN   student_club y ON x.stud_id = y.stud_id
WHERE  x.club_id = 30
AND    y.club_id = 50
) tmp_tbl
where tmp_tbl.stud_id = s.stud_id
;

Utilisation de la variante la plus rapide (M. Sean dans le graphique de M. Brandstetter). Peut être une variante avec une seule jointure, seule la matrice student_club a le droit de vivre. Ainsi, la requête la plus longue n'aura que deux colonnes à calculer, l'idée est de rendre la requête fine.

Stépan Pavlov
la source
1
Bien que cet extrait de code puisse résoudre la question, inclure une explication aide vraiment à améliorer la qualité de votre message. N'oubliez pas que vous répondez à la question des lecteurs à l'avenir, pas seulement à la personne qui la pose maintenant! Veuillez modifier votre réponse pour ajouter une explication et donner une indication des limites et des hypothèses applicables.
BrokenBinary