Мені було цікаво. І як ми всі знаємо, цікавість має репутацію вбивства котів.
Отже, який найшвидший спосіб зібрати кішку?
Точне оточення для котів для цього тесту:
- PostgreSQL 9.0 на Debian Squeeze з гідною оперативною пам’яттю та налаштуваннями.
- 6 000 студентів, 24 000 членів клубу (дані скопійовані з подібної бази даних з реальними даними).
- Невелике відхилення від схеми іменування у питанні:
student.id
є student.stud_id
і club.id
є club.club_id
тут.
- Я назвав запити за їхнім автором у цій темі, з індексом, де їх два.
- Я кілька разів запускав усі запити, щоб заповнити кеш, тоді я вибрав найкраще з 5 за допомогою ПОЯСНЕНОГО АНАЛІЗУ.
Відповідні індекси (повинні бути оптимальними - якщо нам не вистачає попередніх знань, які клуби будуть запитуватися):
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-дерева може використовуватися з умовами запитів, що включають будь-який підмножина стовпців індексу, але індекс є найбільш ефективним, коли на провідних (крайніх лівих) стовпцях є обмеження.
Результати:
Загальний час виконання від ПОЯСНЕННЯ АНАЛІЗУ.
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,228 мс
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) іперкуба 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) іперкуба 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) іперкуба 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) erwin 3: 2,337 мс
@ 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) erwin 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
)
Динамічна кількість членів клубу
Іншими словами: різна кількість фільтрів. Це питання вимагало рівно двох членів клубу. Але у багатьох випадках використання доводиться готуватися до різної кількості.
Детальна дискусія в цій пов'язаній пізнішій відповіді: