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

Posted

技术标签:

【中文标题】如何在 PostgreSQL 中有效地设置减去连接表?【英文标题】:How to efficiently set subtract a join table in PostgreSQL? 【发布时间】:2018-05-06 13:19:28 【问题描述】:

我有以下表格:

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_skillswork_unit_id 上有索引。 我尝试将workers_skills 上的查询移至CTE。这带来了轻微的改善 (10-15%),但仍然太慢了。 任何用户都可以拾取没有技能的工作单元。也就是空集是每个集的子集。

【问题讨论】:

我认为魔鬼可能在 cmets 中缺失的细节中(如 ORDER BY something complex bunch of conditions 等)。所以,如果您可以发布EXPLAIN,它可能会有所帮助 @KaushikNayak,我尝试删除个别条件并使用更简单的东西进行订购。查询仍然慢得多。所以它不是这个和其他一些条件的组合。可能是这个和其他两个+条件,但不太可能。不幸的是,我无法发布EXPLAIN,因为该项目是私人项目,但如果您有任何预感,我可以回答您的问题。 Edit您的问题并添加使用explain (analyze, verbose, buffers)生成的执行计划。 Formatted text 请no screen shots。如果您不想(或不能)共享表名,请将其上传到explain.depesz.com 并启用混淆计划的选项(尽管执行计划很少会泄露任何机密信息) 2 个问题? 1. 我可以对您的数据库设计进行一些更改吗?例如添加 1 或 2 个额外字段。 2. 您使用哪种 DBMS? @g.Irani,对于前者 - 你可以。事实上,我正在考虑两个这样的解决方案,其中包含一个额外的列 - 一个涉及位掩码,另一个涉及哈希。两者都快得多(在转储基准上提高了约 60%),但似乎仍然不够快。对于后者 - 根据标签 - postgres. 【参考方案1】:

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

【讨论】:

【参考方案2】:

您可以使用以下查询

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】:

一个简单的加速方法是使用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 进行测试

【讨论】:

不错的一个。简单和明显的变化都会导致约 50% 的提升。不过,我会更多地尝试其他解决方案,因为这仍然不够快。可能我会将它与其他东西结合使用。【参考方案4】:

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

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

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

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

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

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

select * from work_units
where A & B  = B

注意: 1:当然,这是最快的方法,但有一些困难。 2:添加或删除新技能时,我们会遇到一些额外的困难。但这是一个权衡。添加或删除新技能的情况较少发生。 3:我们也应该使用技能和 work_unit_skills 和 workers_skills。但在搜索中,我们只是使用新字段

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

【讨论】:

【参考方案5】:

(见下文更新

此查询使用简单的 LEFT JOIN 找到一个好的 work_unit,以在请求工人拥有的较短技能表中查找缺失的技能。诀窍是每当缺少技能时,连接中都会有一个 NULL 值,这将被转换为 1 并且 work_unit 通过保留所有 0 值的那些被删除,即具有 @987654325 @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;

【讨论】:

COUNT(t.skill_id) = COUNT(s.skil_id) 也会产生相同的逻辑。正如@RadimBača 所注意到的,这需要稍作修改,以允许没有技能的工作单元被视为有效(前提是空集是所有其他集的子集) 也不错。这比我原来的查询快了大约 6-8 倍,在可接受的范围内。但是,它不包括没有任何技能的工作单位。如果没有OR,我想不出一种方法来包含它们,这使得它变得更慢(仅比原始查询快 2 倍)。如果您能想出一种方法来包含没有技能的文档并保持接近当前的性能可能就是这样。 @ndn 除了 HAVING 子句之外,这与下面我的答案第一部分中的子查询相同(在功能上它们是相同的,只是用于计算缺失技能的代数不同)。通过首先加入工作 _units 表,这可以为没有技能的工作单位工作;请参阅我的答案中的第三个查询。 @ndn 尝试将子查询加入您的文档表,而不是使用 IN(),您可能会提高性能。 (另外,documents table 是从哪里来的?你是第一次提到它;)) @ndn [我已经用一个捕获非熟练工作单位的查询更新了答案]。查询错过没有技能的工作单元的原因可能是这些没有出现在work_units_skills 表中。要解决此问题,请在创建 work_units_skills 表时执行 LEFT JOIN。这将为每个 work_unit 留下一行,如果没有技能,则 Skill_id 中将有一个 NULL。否则,另一种解决方案是将 work_units 表添加到 JOIN 查询,而不是执行 OR 或 UNION。但是扩展 work_units_skills 表似乎是一个更好的选择。【参考方案6】:

相关的子查询正在惩罚您,尤其是在额外使用 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,并且NULLCOUNT() 忽略,这意味着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,因为数据不再是原子的。 (这会导致未来的痛苦,尤其是当你到达一个你拥有如此多技能的世界时,它们不再适合为位掩码选择的数据类型,或者当涉及到管理频繁或技能组合的复杂变化。)

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

编辑:示例修订始终包含没有技能的工作单元...

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 版本的代码(上面的第二个示例查询) 无需修改这个极端案例即可工作(并且是我最初试用的版本)性能指标)

【讨论】:

我想你错过了没有技能分配的work_units:dbfiddle.uk/… 确实如此。这是OP所说的要求吗?空集是否被视为非空集的子集?自从 uni 以来已经很长时间了;)无论如何,如果这是 OP 需要的,这是一个简单的改变...... 进行了修改,但它在哪里显示his 查询返回这些? 位掩码不符合规范化并导致 SQL-code-smell / SQL-anti-pattern。是真的。但是当我们的数据很大时,我们可以使用一些异常策略来达到期望的性能。所有大数据技术都无视规范化。我们应该通过编程来控制这种非规范化,而不仅仅是通过数据库概念和特性。 @Radim Bača:我绝对没有否决这个答案,我永远不会做这样的事情。 (为了证明这一点,我可以在你想要的时候对这个答案投反对票,然后再投票 1 小时。)这个答案很优雅,我正在尝试通过这个问题和答案来学习一些东西。【参考方案7】:

您可以在聚合中获取工人技能所涵盖的工作单元,如前所述。你通常会在这组工作单元上使用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。)

【讨论】:

根据其他类似的答案,这需要稍作调整以适应没有相关技能的工作单位。基本上,这与我在第一个建议中使用的前提相同,@DanGetz 在他的建议中使用。 我还建议应该反转第二个索引。就目前而言,可以最佳地结合技能,但聚合需要排序。当反转(到work_unit_id, skill_id时,数据已经在聚合之前排序,右侧表已经减少到一个工作人员,已经非常小,因此很容易保存在内存中。 【参考方案8】:

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

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

【讨论】:

【参考方案9】:

使用 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,并将其与使用 &lt;@ 运算符(“被包含”)的特定工人的技能进行比较。该条件返回所有 work_unit_ids,其中 Skill_ids 列表包含在单个工人的技能中。

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

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

【讨论】:

以上是关于如何在 PostgreSQL 中有效地设置减去连接表?的主要内容,如果未能解决你的问题,请参考以下文章

有效地减去不同形状的numpy数组

如何使用 postgresql/netezza 从日期时间中减去天数或月数

在 PostgreSQL 中有效地合并最近日期的两个数据集

如何在 JavaEE 应用程序中为 PostgreSQL 热备设置配置连接故障转移?

勺子与 PostgreSql 的连接问题

postgresql pg.Pool 连接池凭据的安全性