Бях любопитен. И както всички знаем, любопитството има репутация за убиване на котки.
И така, кой е най-бързият начин да одирате котка?
Средата за отлепване на котка за този тест:
- PostgreSQL 9.0 на Debian Squeeze с прилична RAM и настройки.
- 6 000 студенти, 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 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) Мартин 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) Дерек: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) Ервин 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) Шон: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;
Последните три се представят почти по същия начин. 4) и 5) водят до същия план за заявка.
Късни допълнения
Изискан SQL, но производителността не може да се справи:
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
)
);
Както се очакваше, тези двамата се представят почти еднакво. Планът на заявката води до сканиране на таблици, плановникът не намира начин да използва индексите тук.
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;
Изискан SQL, прилична производителност за CTE. Много екзотичен план за заявка.
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);
CTE вариант на заявка 2). Изненадващо, това може да доведе до малко по-различен план за заявка с точно същите данни. Намерих последователно сканиране на student
, където вариантът на подзаявката използва индекса.
11) ypercube 3:101,482 ms
Още едно късно добавяне на 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,377 ms
11 на ypercube) всъщност е просто изкривяващият обратен подход на този по-опростен вариант, който също все още липсваше. Работи почти толкова бързо, колкото и топ котките.
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
Трудно е за вярване, но ето още един, наистина нов вариант. Виждам потенциал за повече от две членства, но също така се нарежда сред най-добрите котки само с две.
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
);
Динамичен брой клубни членства
С други думи:различен брой филтри. Този въпрос задават точнодва клубни членства. Но много случаи на употреба трябва да се подготвят за различен брой. Вижте: