【问题标题】:Using a JOIN instead of HAVING(COUNT > n) to improve performance使用 JOIN 而不是 HAVING(COUNT > n) 来提高性能
【发布时间】: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;

所以基本上,我的问题是:

  1. 我的 JOIN 解决方案是否正确?
  2. 有没有更好/规范的方法来解决这个问题?

索引friend_fb_id,以及更改架构,被认为是禁区。我需要尽我所能。

【问题讨论】:

  • 我没有强加这些限制,这只是我必须处理的情况。所以这里没有什么“魔法”,问题是查询是否可以以更有效的方式重新表述。我找不到任何关于这种 JOIN 策略的示例,我想从其他开发者那里获得反馈。
  • 如果没有索引 - 这将是全扫描。全扫描慢。如果您想提高性能 - 第一步是正确索引。您不能更改架构?在完成第一步之前,没有第二步。我坚持:你想要的是“魔法”。您无法从任何地方神奇地获得性能(除非您购买更昂贵的硬件)
  • 郑重声明,JOIN方案不进行SEQ扫描;如果是这样,它将与 GROUP BY 一样高效,我不会问这个问题。在一个有 1 亿行的 prod DB 中,GROUP BY 策略需要 1-30 分钟,而 JOIN 大约需要 3 秒。
  • "作为记录,JOIN 解决方案不执行 SEQ 扫描" --- 1. 显示EXPLAIN 2. 如果没有索引怎么办? mysql如何在不扫描的情况下知道关于数据集的任何信息?
  • 为什么LEFT OUTER 到处加入?只需更改为 INNER 即可为优化器提供更多选择。请为这两个查询发布explain(没有它,任何 SQL 问题都是无用的)。 _users 表到底有多大,您是否考虑过将 JOIN 重写为 EXISTS

标签: sql performance postgresql join group-by


【解决方案1】:

我没有足够大的数据集来检查,但看看这是否执行得更快。

select me.*
from _users me
where 2=(select count(1) from
          (select 1 from _fb_friends ff 
           join _users friend on friend.fb_id=ff.friend_fb_id
           where ff.user_name=me.user_name
           limit 2) x
         )

【讨论】:

    【解决方案2】:

    你可以使用临时表吗?如果是这样,试试这个...

    drop table if exists friend_count; 
    
    create temporary table friend_count ( 
      user_name varchar not null primary key, 
      friend_count int not null
    ); 
    
    create index on friend_count (friend_count);
    
    insert into friend_count select 
      user_name,
      count(*)
    from _fb_friends
    /* place more code here necessary to count only the firends within a smaller
      group of users */ 
    group by user_name; 
    
    select 
      me.user_name,
      me.fb_id
    from _users me
    join friend_count fc on fc.user_name = me.user_name
    where fc.friend_count >= 2; 
    

    【讨论】:

      猜你喜欢
      • 2012-11-12
      • 2011-08-24
      • 1970-01-01
      • 1970-01-01
      • 2013-01-26
      • 1970-01-01
      • 2013-04-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多