如何在PostgreSQL中有效地设置减去连接表?

时间:2017-11-22 17:38:23

标签: sql postgresql performance relational-division set-operations

我有以下表格:

  • work_units - 自我解释
  • workers - 自我解释
  • skills - 如果你想要工作,每个工作单位都需要很多技能。每个工人都精通各种技能。
  • work_units_skills - 加入表格
  • workers_skills - 加入表格

工人可以请求下一个适当的免费最高优先级(无论这意味着)分配给她的工作单元。

目前我有:

SELECT work_units.*
FROM work_units
-- some joins
WHERE NOT EXISTS (
        SELECT skill_id
        FROM work_units_skills
        WHERE work_unit_id = work_units.id

        EXCEPT

        SELECT skill_id
        FROM workers_skills
        WHERE worker_id = 1 -- the worker id that made the request
      )
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1
FOR UPDATE SKIP LOCKED;

这种情况使查询慢了8-10倍。

是否有更好的方式来表达work_units的技能应该是workers技能的一部分或者某些内容来改善当前的查询?

更多背景信息:

  • skills表格相当小。
  • work_unitsworkers往往只有很少的相关技能。
  • work_units_skills的索引为work_unit_id
  • 我尝试将workers_skills上的查询移到CTE中。这略有改善(10-15%),但仍然太慢。
  • 任何用户都可以选择没有技能的工作单位。 Aka是一个空集是每一组的子集。

9 个答案:

答案 0 :(得分:9)

一个简单的加速就是使用EXCEPT ALL代替EXCEPT。后者删除重复项,这在这里是不必要的,可能很慢。

可能更快的替代方法是使用更多NOT EXISTS代替EXCEPT

...
WHERE NOT EXISTS (
        SELECT skill_id
        FROM work_units_skills wus
        WHERE work_unit_id = work_units.id
        AND NOT EXISTS (
            SELECT skill_id
            FROM workers_skills ws
            WHERE worker_id = 1 -- the worker id that made the request
              AND ws.skill_id = wus.skill_id
        )
      )

<强>演示

http://rextester.com/AGEIS52439 - 删除LIMIT进行测试

答案 1 :(得分:5)

(请参阅下面的更新

此查询使用简单的LEFT JOIN找到一个好的work_unit,以便在请求工人拥有的较短技能表中找到缺少的技能。诀窍是,只要缺少技能,连接中就会有一个NULL值,这会被转换为1,而work_unit将被删除0值{即max 0

作为经典SQL,这将是引擎优化的最有针对性的查询:

SELECT work_unit_id
FROM
  work_units_skills s
LEFT JOIN
  (SELECT skill_id FROM workers_skills WHERE worker_id = 1) t
ON (s.skill_id=t.skill_id)
GROUP BY work_unit_id
HAVING max(CASE WHEN t.skill_id IS NULL THEN 1 ELSE 0 END)=0
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1
FOR UPDATE SKIP LOCKED;

<强>更新

为了捕捉没有技能的work_units,我们将work_units表扔进了JOIN:

SELECT r.id AS work_unit_id
FROM
  work_units r
LEFT JOIN
  work_units_skills s ON (r.id=s.work_unit_id)
LEFT JOIN
  (SELECT skill_id FROM workers_skills WHERE worker_id = 1) t
ON (s.skill_id=t.skill_id)
GROUP BY r.id
HAVING bool_or(s.skill_id IS NULL) OR bool_and(t.skill_id IS NOT NULL)
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1
FOR UPDATE SKIP LOCKED;

答案 2 :(得分:3)

您可以使用以下查询

SELECT wu.*
FROM work_units wu
LEFT JOIN work_units_skills wus ON wus.work_unit_id = wu.id and wus.skill_id IN (
    SELECT id
    FROM skills
    EXCEPT
    SELECT skill_id
    FROM workers_skills
    WHERE worker_id = 1 -- the worker id that made the request
)
WHERE wus.work_unit_id IS NULL;  

demo(谢谢Steve Chambers的大部分数据)

您绝对应该拥有work_units_skills(skill_id)workers_skills(worker_id)work_units(id)的索引。 如果您想加快速度,甚至更多,请创建索引work_units_skills(skill_id, work_unit_id)workers_skills(worker_id, skill_id),以避免访问这些表。

子查询是独立的,如果结果不大,外连接应该相对较快。

答案 3 :(得分:3)

比特掩码解决方案
如果您以前的数据库设计没有任何更改,只需添加2个字段。
第一种:长或大(与您的DBMS相关)进入工人 第二:Work_Units中的另一个long或bigint

这些字段显示work_units技能和工人技能。例如,假设您在技能表中有8条记录。 (注意技能记录小)
1-一些技巧1 2-一些技巧2
...... 8-一些技巧8

然后如果我们想将技能1,3,6,7设置为一个work_unit,只需使用此号码01100101.
(我提议使用反向版本的二进制0,1位置以支持将来的其他技能。)

在实践中,你可以使用10个基数来添加数据库(101而不是01100101)

可以为工人生成类似的数字。任何工人都选择一些技能。因此,我们可以将所选项目转换为数字,并将其保存在工作表中的其他字段中。

最后,为任何工作者找到合适的work_units子集,必须从work_units中选择并使用按位AND,如下所示。
A: new_field_of_specific_worker(显示每个工人的技能)我们正在搜索与他/她相关的works_units。
B: new_field_of_work_units,显示每个work_unit的技能

select * from work_units
where A & B  = B

注意:
1:绝对,这是最快的方式,但有一些困难。
2:添加新技能或删除新技能时会遇到一些额外的困难。但这是一种权衡。添加或删除新技能的可能性较小。
3:我们也应该使用技能和work_unit_skills和workers_skills。但在搜索中,我们只使用新字段

另外,这种方法可用于TAG管理系统,如Stack Overflow TAGs。

答案 4 :(得分:2)

根据目前的信息,我只能预感回答。尝试删除EXCEPT语句,看看它是否明显更快。如果是,您可以再次添加该部分,但使用WHERE条件。 根据我的经验,操作员(MINUS / EXCEPT,UNION,INTERSECT)是性能杀手。

答案 5 :(得分:2)

相关的子查询正在惩罚你,尤其是额外使用EXCEPT。

为了解释您的查询,当您指定的工作人员拥有所有work_unit的技能时,您只对work_unit_id感兴趣? (如果work_unit具有与之关联的技能,但指定的用户没有该技能,请排除该work_unit?)

这可以通过JOIN和GROUP BY来实现,而根本不需要相关性。

SELECT
    work_units.*
FROM
    work_units
--
-- some joins
--
INNER JOIN
(
    SELECT
        wus.work_unit_id
    FROM
        work_unit_skills   wus
    LEFT JOIN
        workers_skills     ws
            ON  ws.skill_id  = wus.skill_id
            AND ws.worker_id = 1
    GROUP BY
        wus.work_unit_id
    HAVING
        COUNT(wus.skill_id) = COUNT(ws.skill_id)
)
     applicable_work_units
         ON  applicable_work_units.work_unit_id = work_units.id
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1

子查询将工人的技能组与每个工作单位的技能组进行比较。如果工作单位有任何技能,则工作人员不会ws.skill_id为该行NULL,而NULL会忽略COUNT()表示COUNT(ws.skill_id)将低于COUNT(wus.skill_id),因此work_unit将被排除在子查询的结果之外。

这假定workers_skills表在(work_id, skill_id)上是唯一的,work_unit_skills表在(work_unit_id, skill_id)上是唯一的。如果情况并非如此,那么您可能需要修改HAVING子句(例如COUNT(DISTINT wus.skill_id)等)


<强> 编辑:

上述查询假设只有相对较少数量的工作单位才能匹配特定工作人员的匹配标准。

如果您假设相对大量的工作单位匹配,则相反的逻辑会更快。

(基本上,尝试使子查询返回的行数尽可能低。)

SELECT
    work_units.*
FROM
    work_units
--
-- some joins
--
LEFT JOIN
(
    SELECT
        wus.work_unit_id
    FROM
        work_unit_skills   wus
    LEFT JOIN
        workers_skills     ws
            ON  ws.skill_id  = wus.skill_id
            AND ws.worker_id = 1
    WHERE
        ws.skill_id IS NULL
    GROUP BY
        wus.work_unit_id
)
     excluded_work_units
         ON  excluded_work_units.work_unit_id = work_units.id
WHERE
    excluded_work_units.work_unit_id IS NULL
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1

这个比较所有工作单位技能与工人的技能,并且只保留工作单位具有工人没有技能的行。

然后,GROUP BY工作单位获取需要忽略的工作单位清单。

通过LEFT将这些内容加入到现有结果中,您可以规定只有 时才会包含工作单元在子查询中指定excluded_work_units.work_unit_id IS NULL

有用的在线指南会引用anti-joinanti-semi-join


<强> 编辑:

一般情况下,我建议不要使用位掩码。

不是因为它很慢,而是因为它无法正常化。表示多个数据项的单个字段的存在是一般的sql-code-smell / sql-anti-pattern,因为数据不再是原子的。 (这会导致痛苦,特别是如果你到达一个你拥有如此多技能的世界,以至于他们不再适合所选择的比特掩码的数据类型,或者当涉及到频繁管理或技能组的复杂变化。)

也就是说,如果性能仍然是一个问题,那么去标准化往往是一个非常有用的选择。我建议将位掩码保存在单独的表中,以明确它们的反规范化/缓存计算结果。但总的来说,这些选择应该是最后的选择,而不是第一反应。


编辑: 示例修订总是包含没有技能的work_units ...

SELECT
    work_units.*
FROM
    work_units
--
-- some joins
--
INNER JOIN
(
    SELECT
        w.id   AS work_unit_id
    FROM
        work_units          w
    LEFT JOIN
        work_units_skills   wus
            ON wus.work_unit_id = w.id
    LEFT JOIN
        workers_skills      ws
            ON  ws.skill_id  = wus.skill_id
            AND ws.worker_id = 1
    GROUP BY
        w.id
    HAVING
        COUNT(wus.skill_id) = COUNT(ws.skill_id)
)
     applicable_work_units
         ON  applicable_work_units.work_unit_id = work_units.id

代码excluded_work_units版本(上面的第二个示例查询)应该无需修改此角落案例(并且是我的那个)最初试用现场表现指标)

答案 6 :(得分:2)

如已经显示的那样,您可以在聚合中获得工人技能所涵盖的工作单位。您通常会在这组工作单位上使用IN

SELECT wu.*
FROM work_units wu
-- some joins
WHERE wu.id IN
(
  SELECT wus.work_unit_id
  FROM work_units_skills wus
  LEFT JOIN workers_skills ws ON ws.skill_id = wus.skill_id AND ws.worker_id = 1
  GROUP BY wus.work_unit_id
  HAVING COUNT(*) = COUNT(ws.skill_id)
)
-- AND a bunch of other conditions
-- ORDER BY something complex
LIMIT 1
FOR UPDATE SKIP LOCKED;

当谈到加速查询时,主要部分通常是提供适当的索引。 (使用完美的优化器,重写一个查询以获得相同的结果将完全没有效果,因为优化器将达到相同的执行计划。)

您需要以下索引(列的顺序很重要):

create index idx_ws on workers_skills (worker_id, skill_id);
create index idx_wus on work_units_skills (skill_id, work_unit_id);

(请按照以下方式阅读:我们附带worker_id,获取工作人员skill_ids,加入这些skill_ids上的工作单位,然后获取work_unit_ids。 )

答案 7 :(得分:2)

可能不适用于你,但我有一个类似的问题,我解决了简单地将main和sub合并到同一列中,使用主数字和sub的字母。

顺便说一下,联接中涉及的所有列都已编入索引吗? 如果我忘了

,我的服务器从500k +表的2-3秒查询到10k表崩溃

答案 8 :(得分:2)

使用Postgres,通常可以使用数组更有效地表达关系除法。

在你的情况下,我以下将做你想做的事:

select *
from work_units
where id in (select work_unit_id
             from work_units_skills
             group by work_unit_id
             having array_agg(skill_id) <@ array(select skill_id 
                                                 from workers_skills 
                                                 where worker_id = 6))
and ... other conditions here ...
order by ...

array_agg(skill_id)收集每个work_unit的所有skill_id,并使用<@运算符将其与特定工作人员的技能进行比较(&#34;包含在&#34;中)。该条件返回所有work_unit_ids,其中skill_ids列表包含在单个工作人员的技能中。

根据我的经验,这种方法通常比同等存在或交叉解决方案更快。

在线示例:http://rextester.com/WUPA82849