如何在PostgreSQL中有效地设置减法联接表?

如何在PostgreSQL中有效地设置减法联接表?,sql,postgresql,performance,relational-division,set-operations,Sql,Postgresql,Performance,Relational Division,Set Operations,我有以下表格: 工作单位-不言自明 工人-不言自明 技能-每个工作单元都需要一些技能,如果你想在上面工作的话。每个工人都精通许多技能 工作单元技能-联接表 工人技能-加入表 工人可以请求分配给她的下一个适当的免费最高优先级(无论这意味着什么)工作单元 目前我有: SELECT work_units.* FROM work_units -- some joins WHERE NOT EXISTS ( SELECT skill_id FROM work_unit

我有以下表格:

  • 工作单位
    -不言自明
  • 工人
    -不言自明
  • 技能
    -每个工作单元都需要一些技能,如果你想在上面工作的话。每个工人都精通许多技能
  • 工作单元技能
    -联接表
  • 工人技能
    -加入表
工人可以请求分配给她的下一个适当的免费最高优先级(无论这意味着什么)工作单元


目前我有:

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倍

有没有更好的方法来表示
工作单位的技能应该是
工人的技能的子集,或者改进当前查询的内容


更多背景:

  • skills
    表相当小
  • 工作单位
    工人
    的相关技能都很少
  • work\u units\u skills
    work\u unit\u id
    上有索引
  • 我尝试将
    workers\u skills
    上的查询移动到CTE中。这带来了轻微的改善(10-15%),但仍然太慢
  • 任何用户都可以拾取没有技能的工作单元。也就是说,空集是每个集合的子集

    • 根据目前的信息,我只能凭直觉回答。尝试删除EXCEPT语句,看看它是否会显著加快。如果有,您可以再次添加该零件,但使用WHERE条件。
      根据我的经验,集合运算符(减/除、并集、交集)是性能的杀手。

      您可以使用以下查询

      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;  
      
      (感谢史蒂夫·钱伯斯提供的大部分数据)

      你肯定应该有
      工作单位技能(技能id)
      工人技能(工人id)
      工作单位(id)
      的索引。 如果您想加快速度,甚至可以创建索引
      work\u units\u skills(skill\u id,work\u unit\u id)
      workers\u skills(worker\u id,skill\u id)
      ,避免访问这些表


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

      一个简单的加速方法是使用
      而不是
      ,除了
      。后者将删除重复项,这在这里是不必要的,并且可能会很慢

      另一种可能更快的方法是使用另一个
      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
              )
            )
      
      演示


      -去除测试的
      限制

      位掩码解决方案
      在以前的数据库设计中没有任何更改,只需添加2个字段
      第一个:将long或bigint(与您的DBMS相关)插入worker
      第二:另一个长的或大的单位进入工作单位

      这些字段显示工作单位的技能和工人的技能。例如,假设技能表中有8条记录。 (请注意,技能记录以小字体显示)
      1-一些技能1
      2-一些技能2
      ...
      8-一些技能8

      如果我们想将技能1,3,6,7设置为一个工作单元,只需使用这个数字01100101
      (我建议使用二进制0,1位置的反向版本,以支持将来的其他技能。)

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

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

      最后,要为任何工作人员找到合适的工作单位子集,只需从工作单位中选择,并按位使用,如下所示。
      A:我们正在搜索与他/她相关的工作单位的特定工作人员(显示每个工作人员的技能)的新字段。
      B:显示每个工作单元技能的新工作单元字段

      select * from work_units
      where A & B  = B
      
      注意:
      1:当然,这是最快的方法,但也有一些困难。
      2:当添加或删除新技能时,我们会遇到一些额外的困难。但这是一种权衡。添加或删除新技能很少发生。
      3:我们应该使用技能、工作单位技能和工人技能。但在搜索中,我们只使用新字段


      此外,此方法还可用于标签管理系统,如堆栈溢出标签。

      (请参见下文的更新)

      此查询使用简单的左连接查找良好的
      工作单元
      ,以在请求工作人员拥有的较短技能表中查找缺少的技能。诀窍是,每当缺少技能时,联接中就会有一个空值,该值被转换为
      1
      ,而
      工作单元
      将通过保留所有
      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;
      
      更新

      为了捕获没有技能的
      工作单位
      ,我们将
      工作单位
      表放入联接:

      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;
      

      相关子查询正在惩罚您,尤其是额外使用Exception

      为了解释您的查询,您只对
      工作单位id
      感兴趣,而指定的工人拥有该工作单位的所有技能?(如果某个工作单元具有与其关联的技能,但指定用户没有该技能,则排除该工作单元?)

      这可以通过连接和分组方式实现,而根本不需要关联

      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\u id
      NULL
      ,并且
      COUNT()
      将忽略
      NULL
      ,这意味着
      COUNT(ws.skill\u id)
      将低于
      COUNT(ws.skill\u 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
      
      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
      
      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);
      
      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 ...