我很好奇。众所周知,好奇心以杀死猫而闻名。
那么,给猫剥皮最快的方法是什么?
本次测试的猫皮环境:
- Debian Squeeze 上的PostgreSQL 9.0 ,具有不错的 RAM 和设置。
- 6,000 名学生,24,000 名俱乐部会员(数据从具有现实生活数据的类似数据库复制而来。)
- 从问题中的命名模式略有转移:
student.id
is student.stud_id
and club.id
is club.club_id
here。
- 我在这个线程中以他们的作者命名了这些查询。
- 我运行了几次所有查询来填充缓存,然后我选择了 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) 超立方体 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 的良好性能。非常奇特的查询计划。
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);
查询 2) 的 CTE 变体。令人惊讶的是,它可能会导致使用完全相同的数据的查询计划略有不同。我在 上找到了顺序扫描student
,其中子查询变体使用了索引。
11) 超立方体 3:101.482 毫秒
另一个后期添加的超级立方体。真是太神奇了,有多少种方法。
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
);
俱乐部会员的动态数量
换句话说:不同数量的过滤器。这个问题要求恰好有两个俱乐部会员资格。但是许多用例必须为不同的数量做准备。看: