Мне было любопытно. И, как все мы знаем, любопытство имеет репутацию убийцы кошек.
Итак, какой самый быстрый способ снять шкуру с кошки?
Точная среда для снятия шкуры кошки для этого теста:
- PostgreSQL 9.0 на Debian Squeeze с приличной оперативной памятью и настройками.
- 6.000 студентов, 24000 членских взносов в клубы (данные скопированы из аналогичной базы данных с данными из реальной жизни).
- Небольшое отклонение от схемы именования в вопросе:
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 справляется с этим. Вскоре я собираюсь обновить используемый здесь кластер db до 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 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
)
Динамическое количество членств в клубах
Другими словами: разное количество фильтров. В этом вопросе задано ровно два членства в клубах. Но многие варианты использования должны быть готовы к разному количеству.
Подробное обсуждение в этом более позднем ответе:
(student_id, club_id)
(или наоборот) индексом.источник
источник
Если вам просто нужен student_id, тогда:
Если вам также нужно имя студента, то:
Если у вас более двух клубов в таблице club_selection, тогда:
источник
Или более общее решение, которое легче распространить на
n
клубы и которое позволяет избежатьINTERSECT
(недоступно в MySQL) иIN
(поскольку производительность этого отстойная в MySQL )источник
HAVING
делает в MySQL.Еще один CTE. Он выглядит чистым, но, вероятно, сгенерирует тот же план, что и groupby в обычном подзапросе.
Для тех, кто хочет протестировать, копия моей генерации тестовых данных:
источник
Итак, есть несколько способов снять шкуру с кошки .
Я добавлю еще два, чтобы сделать его, ну, более полным.
1) Сначала ГРУППА, позже ПРИСОЕДИНЯЙТЕСЬ
Предполагая вменяемый модель данных , где
(student_id, club_id)
находится уникальный вstudent_club
. Вторая версия Мартина Смита чем-то похожа, но он присоединяется к первым, а потом к группам. Это должно быть быстрее:2) СУЩЕСТВУЕТ
И, конечно же, классика
EXISTS
. Подобно варианту Дерека сIN
. Просто и быстро. (В MySQL это должно быть немного быстрее, чем вариант сIN
):источник
Поскольку эту (классическую) версию никто не добавил:
или похожие:
Еще одна попытка с немного другим подходом. На основе статьи в Explain Extended: несколько атрибутов в таблице EAV: GROUP BY vs. NOT EXISTS :
Другой подход:
источник
(stud_id, club_id)
и(club_id, stud_id)
(или Первичном и Уникальном)? Я все еще думаю, что для некоторых из этих запросов разница от 2 до 140 мс слишком велика, чтобы ее можно было объяснить различиями в планах выполнения.Кажется, это работает достаточно хорошо, так как CTE-сканирование позволяет избежать необходимости в двух отдельных подзапросах.
Всегда есть причина злоупотреблять рекурсивными запросами!
(Кстати: в mysql нет рекурсивных запросов)
источник
Различные планы запроса в запросе 2) и 10)
Я тестировал db в реальной жизни, поэтому имена отличаются от списка кошачьих шкур. Это резервная копия, поэтому во время всех тестовых запусков ничего не менялось (кроме незначительных изменений в каталогах).
Запрос 2)
Запрос 10)
источник
@ erwin-brandstetter Пожалуйста, сравните это:
Это как номер 6 от @sean, просто чище, я думаю.
источник
@
уведомление работает только в комментариях, но не в ответах. Я случайно наткнулся на этот пост. План запроса и производительность вашего запроса идентичны запросу Шона. Это фактически то же самое, но запрос Шона с явнымJOIN
синтаксисом является обычно предпочтительной формой, потому что это понятнее. +1 за еще один верный ответ!План запроса:
Так что, похоже, он все еще хочет сканировать seq для ученика.
источник
Использование самого быстрого варианта (мистер Шон в диаграмме мистера Брандштеттера). Может быть вариант только с одним присоединением только к матрице student_club, имеющей право на жизнь. Таким образом, самый длинный запрос будет иметь только два столбца для вычисления, идея состоит в том, чтобы сделать запрос тонким.
источник