【发布时间】:2023-03-28 17:37:01
【问题描述】:
我有一张用户表,以及一张他们之间的“Facebook 好友”关系表。给定一个(已知的)用户列表,我想快速找到该组中有 2 个或更多用户的 Facebook 好友的所有用户。
(这基本上归结为一个问题:我可以重写 GROUP BY/HAVING 以使用 JOIN 吗?)
这是我正在使用的架构的简化版本。我在这里使用了 VARCHAR 来使我的示例数据(如下)中的用户名更容易理解; IRL 相关列是 INT:
-- Simplified Schema
CREATE TABLE _users (
user_name VARCHAR NOT NULL PRIMARY KEY,
fb_id VARCHAR NULL UNIQUE
);
CREATE TABLE _fb_friends (
id SERIAL PRIMARY KEY,
user_name VARCHAR NULL REFERENCES _users(user_name),
friend_fb_id VARCHAR NULL REFERENCES _users(fb_id),
UNIQUE (user_name, friend_fb_id)
);
请注意,friend_fb_id 上没有(可访问的)索引。
还请注意,_fb_friends 表非常庞大 - 比 _users 表大几个数量级 - 使得明显的 GROUP BY/HAVING 解决方案非常慢。 IE。这是不可行的:
-- Using GROUP BY/HAVING: Obvious solution, but way too slow.
-- Does a SEQ SCAN on the gigantic table
SELECT me.*
FROM
_users me
LEFT OUTER JOIN _fb_friends ff ON (
ff.user_name = me.user_name
)
LEFT OUTER JOIN _users friend ON (
friend.fb_id = ff.friend_fb_id
)
GROUP BY me.user_name
HAVING COUNT(friend.user_name) >= 2;
我重写了这个以使用 JOIN,但我不确定我提出的解决方案是否有效或最佳:
-- Using JOINs: Way faster, but is it correct? Better way?
SELECT DISTINCT me.*
FROM (
_users me
LEFT OUTER JOIN _fb_friends ff1 ON (
ff1.user_name = me.user_name
)
LEFT OUTER JOIN _fb_friends ff2 ON (
ff2.user_name = me.user_name
AND ff2.friend_fb_id <> ff1.friend_fb_id
)
LEFT OUTER JOIN _users friend ON (
friend.fb_id = ff1.friend_fb_id
)
LEFT OUTER JOIN _users friend_2 ON (
friend_2.fb_id = ff2.friend_fb_id
)
)
WHERE (
friend.user_name IS NOT NULL
AND friend_2.user_name IS NOT NULL
);
为了它的价值,我写了一个简单的测试示例,似乎可以正常工作。但我真的不确定它是否正确,或者我是否会以最好的方式解决这个问题。两种策略都返回相同的用户:
BEGIN;
CREATE TABLE _users (
user_name VARCHAR NOT NULL PRIMARY KEY,
fb_id VARCHAR NULL UNIQUE
);
CREATE TABLE _fb_friends (
id SERIAL PRIMARY KEY,
user_name VARCHAR NULL REFERENCES _users(user_name),
friend_fb_id VARCHAR NULL REFERENCES _users(fb_id)
);
INSERT INTO _users (user_name, fb_id) VALUES
('Bob', 'bob'),
('Joe', 'joe'),
('Will', 'will'),
('Marcus', 'marcus'),
('Mitch', 'mitch'),
('Rick', 'rick');
INSERT INTO _fb_friends (user_name, friend_fb_id) VALUES
('Bob', 'joe'),
('Will', 'marcus'),
('Joe', 'bob'),
('Joe', 'marcus'),
('Joe', 'mitch'),
('Marcus', 'will'),
('Marcus', 'joe'),
('Mitch', 'joe');
SELECT 'GROUP BY/HAVING' AS Strategy, me.*
FROM
_users me
LEFT OUTER JOIN _fb_friends ff ON (
ff.user_name = me.user_name
)
LEFT OUTER JOIN _users friend ON (
friend.fb_id = ff.friend_fb_id
)
GROUP BY me.user_name
HAVING COUNT(friend.user_name) >= 2;
SELECT DISTINCT 'JOIN' AS Strategy, me.*
FROM (
_users me
LEFT OUTER JOIN _fb_friends ff1 ON (
ff1.user_name = me.user_name
)
LEFT OUTER JOIN _fb_friends ff2 ON (
ff2.user_name = me.user_name
AND ff2.friend_fb_id <> ff1.friend_fb_id
)
LEFT OUTER JOIN _users friend ON (
friend.fb_id = ff1.friend_fb_id
)
LEFT OUTER JOIN _users friend_2 ON (
friend_2.fb_id = ff2.friend_fb_id
)
)
WHERE (
friend.user_name IS NOT NULL
AND friend_2.user_name IS NOT NULL
);
DROP TABLE _fb_friends;
DROP TABLE _users;
COMMIT;
所以基本上,我的问题是:
- 我的 JOIN 解决方案是否正确?
- 有没有更好/规范的方法来解决这个问题?
索引friend_fb_id,以及更改架构,被认为是禁区。我需要尽我所能。
【问题讨论】:
-
我没有强加这些限制,这只是我必须处理的情况。所以这里没有什么“魔法”,问题是查询是否可以以更有效的方式重新表述。我找不到任何关于这种 JOIN 策略的示例,我想从其他开发者那里获得反馈。
-
如果没有索引 - 这将是全扫描。全扫描慢。如果您想提高性能 - 第一步是正确索引。您不能更改架构?在完成第一步之前,没有第二步。我坚持:你想要的是“魔法”。您无法从任何地方神奇地获得性能(除非您购买更昂贵的硬件)
-
郑重声明,JOIN方案不进行SEQ扫描;如果是这样,它将与 GROUP BY 一样高效,我不会问这个问题。在一个有 1 亿行的 prod DB 中,GROUP BY 策略需要 1-30 分钟,而 JOIN 大约需要 3 秒。
-
"作为记录,JOIN 解决方案不执行 SEQ 扫描" --- 1. 显示
EXPLAIN2. 如果没有索引怎么办? mysql如何在不扫描的情况下知道关于数据集的任何信息? -
为什么
LEFT OUTER到处加入?只需更改为INNER即可为优化器提供更多选择。请为这两个查询发布explain(没有它,任何 SQL 问题都是无用的)。 _users 表到底有多大,您是否考虑过将 JOIN 重写为EXISTS?
标签: sql performance postgresql join group-by