Как отфильтровать результаты SQL в сквозном отношении - PullRequest
92 голосов
/ 09 сентября 2011

Предполагая, что у меня есть таблицы student, club и student_club:

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

Я хочу знать, как найти всех студентов как в футболе (30), так и в бейсболе (50) club.
Хотя этот запрос не работает, это самое близкое, что у меня есть:

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

Ответы [ 13 ]

130 голосов
/ 15 октября 2011

Мне было любопытно. И как мы все знаем, любопытство имеет репутацию убийства кошек.

Итак, какой самый быстрый способ снять кожу с кошки?

Точная среда для кошачьей шкуры для этого теста:

  • PostgreSQL 9.0 в Debian Squeeze с приличной оперативной памятью и настройками.
  • 6000 студентов, 24 000 членов клуба (данные скопированы из аналогичной базы данных с данными из реальной жизни).
  • Небольшое отклонение от схемы именования в вопросе: student.id - это student.stud_id, а club.id - это club.club_id здесь.
  • Я назвал запросы в честь их автора в этой теме с индексом, где их два.
  • Я запустил все запросы пару раз, чтобы заполнить кэш, а затем выбрал лучшее из 5 с помощью EXPLAIN ANALYZE.
  • Соответствующие индексы (должны быть оптимальными - до тех пор, пока нам не хватает предварительных знаний о том, какие клубы будут опрошены):

    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_pkey не требуется для большинства запросов здесь.
    Первичные ключи автоматически реализуют уникальные индексы в PostgreSQL.
    Последний индекс должен восполнить этот известный недостаток многостолбцовых индексов в PostgreSQL:

Многоколонный индекс B-дерева можно использовать с условиями запроса, которые задействовать любое подмножество столбцов индекса, но индекс наиболее эффективен, когда есть ограничения на ведущую (крайнюю слева) столбцы.

Результаты:

Общее время выполнения из EXPLAIN ANALYZE.

1) Мартин 2: 44,594 мс

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) Эрвин 1: 33,217 мс

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) Мартин 1: 31,735 мс

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) Дерек: 2,287 мс

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) Эрвин 2: 2,181 мс

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) Шон: 2,043 мс

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;

Последние три работают почти одинаково. 4) и 5) приводят к одному и тому же плану запросов.

Поздние добавления:

Необычный SQL, но производительность не справляется.

7) ypercube 1: 148,649 мс

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 мс

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  
      )
   );

Как и ожидалось, эти двое работают почти одинаково. План запроса приводит к сканированию таблицы, планировщик не находит здесь способа использовать индексы.


9) wildplasser 1: 49,849 мс

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;

Необычный SQL, достойная производительность для CTE. Очень экзотический план запроса.
Опять же, было бы интересно, как 9.1 справляется с этим. Я собираюсь обновить кластер БД, используемый здесь, до 9.1 в ближайшее время. Может быть, я перезапущу весь Шебанг ...


10) wildplasser 2: 36,986 мс

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);

CTE вариант запроса 2). Удивительно, но это может привести к несколько другому плану запросов с точно такими же данными. Я нашел последовательное сканирование в student, где вариант подзапроса использовал индекс.


11) ypercube 3: 101,482 мс

Еще одно позднее добавление @ypercube. Удивительно, сколько существует способов.

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) Эрвин 3: 2,377 мс

@ ypercube's 11) на самом деле является просто извращающим обратным подходом этого более простого варианта, который также все еще отсутствовал. Выступает почти так же быстро, как и лучшие кошки.

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) Эрвин 4: 2,375 мс

Трудно поверить, но вот еще один, действительно новый вариант. Я вижу потенциал для более чем двух членов, но он также входит в число лучших кошек с двумя.

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
   )

Динамическое количество членов клуба

Другими словами: различное количество фильтров. Этот вопрос задал ровно два членства в клубах. Но во многих случаях нужно готовиться к изменению числа.

Подробное обсуждение в следующем ответе:

18 голосов
/ 09 сентября 2011
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
10 голосов
/ 09 сентября 2011
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)
5 голосов
/ 18 октября 2011

Если вам нужен только student_id, то:

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

Если вам также нужно имя от студента, то:

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 )

Если у вас более двух клубов в таблице club_selection, тогда:

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 ) )
4 голосов
/ 19 октября 2011

Еще один CTE. Выглядит чисто, но, вероятно, сгенерирует тот же план, что и groupby в обычном подзапросе.

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)
    ;

Для тех, кто хочет проверить, копия моего сгенерированного testdata штука:

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)
    ;
4 голосов
/ 09 сентября 2011
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)  

Или более общее решение, которое проще распространить на n клубы, и при этом исключаются INTERSECT (недоступно в MySQL) и IN (так как производительность этого отстой в 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  
3 голосов
/ 18 октября 2011

Поскольку никто не добавил эту (классическую) версию:

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  
              )
      )

или аналогичный:

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  
              )
      )

Еще одна попытка с немного другим подходом. Вдохновлен статьей в Объяснить расширенный: несколько атрибутов в таблице EAV: GROUP BY против 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  
              )
      )

Другой подход:

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 ;   
3 голосов
/ 15 октября 2011

Таким образом, существует более одного способа снятия шкуры с кошки .
Я добавлю еще два , чтобы сделать его, ну, более полным.

1) ГРУППА сначала, ПРИСОЕДИНЯЙТЕСЬ позже

Предполагая разумную модель данных, где (student_id, club_id) равно уникально в student_club.Вторая версия Мартина Смита похожа на что-то похожее, но сначала он присоединяется к группам позже.Это должно быть быстрее:

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) СУЩЕСТВУЕТ

И, конечно же, есть классический EXISTS.Аналогично варианту Дерека с IN.Просто и быстро.(В MySQL это должно быть немного быстрее, чем вариант с 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);
2 голосов
/ 18 октября 2011
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

    ;

Это, кажется, работает достаточно хорошо, поскольку сканирование CTE устраняет необходимость в двух отдельных подзапросах.

Всегда есть причина неправильно использовать рекурсивные запросы!

(Кстати: mysql, похоже, не имеет рекурсивных запросов)

1 голос
/ 18 мая 2012

@ erwin-brandstetter Пожалуйста, сравните это:

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;

Это как номер 6) @sean, просто чище, я думаю.

...