19

我有以下表格:

  • 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 倍。

有没有更好的方法来表达 awork_units的技能应该是 s 技能的子集workers或改进当前查询的东西?


更多上下文:

  • skills桌子相当小。
  • 两者work_units并且workers往往具有很少的相关技能。
  • work_units_skills有索引work_unit_id
  • 我尝试将查询workers_skills移至 CTE。这带来了轻微的改善(10-15%),但仍然太慢了。
  • 任何用户都可以拾取没有技能的工作单元。又名空集是每个集合的子集。
4

9 回答 9

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删除测试

于 2017-11-27T09:10:04.890 回答
5

(见下面的更新

此查询work_unit使用简单的 LEFT JOIN 在请求工人拥有的较短技能表中查找缺少的技能。诀窍是,每当缺少技能时,连接中都会有一个 NULL 值,并将其转换为 a1work_unit通过保留具有所有0值的那些将其删除,即具有 a maxof 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;
于 2017-11-27T10:17:05.010 回答
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;  

演示(感谢史蒂夫钱伯斯提供大部分数据)

您绝对应该在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)避免访问这些表。

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

于 2017-11-27T08:23:20.940 回答
3

位掩码解决方案
无需对您之前的数据库设计进行任何更改,只需添加 2 个字段。
首先:一个 long 或 bigint(与您的 DBMS 相关)到 Workers
第二个:另一个 long 或 bigint 到 Work_Units

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

那么如果我们想将技能 1,3,6,7 设置为一个 work_unit,只需使用这个数字 01100101。
(我提出使用二进制 0,1 放置的反转版本来支持将来的附加技能。)

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

可以为工人生成类似的数字。任何工人都会选择一些技能。所以我们可以将选中的项目变成一个数字,并保存在 Worker 表的附加字段中。

最后,要为任何工作人员找到合适的 work_units 子集,只需从 work_units 中选择并使用按位与,如下所示。
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 TAG。

于 2017-11-27T10:03:59.503 回答
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_idskill_ids为工人获取 ,加入这些工作单元skill_ids并因此得到work_unit_ids。)

于 2017-11-28T12:27:42.030 回答
2

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

套用您的查询,您只对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代码版本(上面的第二个示例查询)应该可以在不需要修改这种极端情况的情况下工作(并且是我最初尝试用于实时性能指标的版本)

于 2017-11-27T19:08:55.433 回答
2

根据目前的信息,我只能凭直觉回答。尝试删除 EXCEPT 语句,看看它是否变得更快。如果是这样,您可以再次添加该部分,但使用 WHERE 条件。根据我的经验,集合运算符(MINUS/EXCEPT、UNION、INTERSECT)是性能杀手。

于 2017-11-27T08:23:14.807 回答
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 的所有技能 ID,并将其与使用<@运算符的特定工人的技能进行比较(“被包含”)。该条件返回所有 work_unit_ids,其中技能 ID 列表包含在单个工人的技能中。

以我的经验,这种方法通常比等效存在或相交解决方案更快。

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

于 2017-12-04T22:13:08.617 回答
2

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

顺便说一句,连接中涉及的所有列都被索引了吗?如果我忘记了,我的服务器会从 500k+ 表上的 2-3 秒查询到 10k 表上的崩溃

于 2017-12-01T09:41:47.003 回答