对于多对多关系,从多中搜索一个

With a many-to-many relationship, search by the many for the one

我在版本和工件之间存在多对多关系,其中给定版本与多个工件相关联,而给定工件与多个版本相关联。

我知道如何建模:我有一个 releases table 和一个 ID 列:

CREATE TABLE releases (
    release_uuid uuid PRIMARY KEY
);

和带有 ID 列的 artifacts table:

CREATE TABLE artifacts (
    artifact_uuid uuid PRIMARY KEY,
    hash          bytea
    -- other data
);

和一个连接 table release_artifacts 具有来自其他每个的外键列:

CREATE TABLE release_artifacts (
    id            serial PRIMARY KEY,
    release_uuid  uuid REFERENCES releases(release_uuid) NOT NULL,
    artifact_uuid uuid REFERENCES artifacts(artifact_uuid) NOT NULL,
    UNIQUE (release_uuid, artifact_uuid)
);

我想做的是找到一个版本 "containing" 一组给定的工件,这样我就可以警告重复版本。也就是说,对于工件 A1A2A3,这三个工件究竟定义了哪些版本 Rx?更形象地说,给定 release_artifacts table:

release ID | artifact ID
-----------+------------
R1         | A1
R1         | A2
R1         | A3
R2         | A4
R2         | A2
R2         | A3

我可以使用 A1A2A3 作为输入来执行什么搜索,然后返回 R1?在 A2A3 上搜索会 return NULL。或者我需要不同的型号吗?我认为如果 release_artifacts table 将一个版本映射到一组工件 ID 会更容易,但随后我失去了 artifact table.[= 的参照完整性31=]

我不需要最大性能或最大并发保护,但如果这些不会显着增加查询的复杂性,我会很高兴。这是在 Postgres 9.6 数据库中,尽管我认为这是一个版本。

您可以使用聚合:

select release_id
from release_artifacts
group by release_id
having sum( artifact_id in ('A1', 'A2', 'A3') ) = 3 and
       count(*) = 3;

假设没有重复项。

或者您可以使用字符串或数组聚合:

select release_id
from release_artifacts
group by release_id
having string_agg(artifact_id order by artifact_id) = 'A1,A2,A3';

这是的一个例子。这是基本技术库:

  • How to filter SQL results in a has-many-through relation

对于给定的(典型的)many-to-many 设置,这是最快的查询之一:

SELECT release_id
FROM   release_artifacts ra1
JOIN   release_artifacts ra2 USING (release_id)
JOIN   release_artifacts ra3 USING (release_id)
WHERE  ra1.artifact_id = 'A1' 
AND    ra2.artifact_id = 'A2' 
AND    ra3.artifact_id = 'A3';

此查询的缺点:您必须根据要查找的工件数量调整构建。如果一直是3,那就完全没有缺点了。

对于动态数量的工件,您可以动态构建查询。或者按照此处的说明使用递归 CTE(推荐!):

  • Using same column multiple times in WHERE clause

(artifact_id, release_id) 上设置约束(及其实现 index)对性能有很大帮助,而不是在 (release_id, artifact_id) 上相反,因为第一个也是(希望)最具选择性的谓词在 artifact_id 上。在反向组合上增加一个索引以覆盖所有碱基通常是值得的。参见:

另外 将搜索限制为具有确切给定工件集的版本(没有其他工件)- :

SELECT release_id
FROM   release_artifacts ra1
JOIN   release_artifacts ra2 USING (release_uuid)
JOIN   release_artifacts ra3 USING (release_uuid)
WHERE  ra1.artifact_uuid = 'A1' 
AND    ra2.artifact_uuid = 'A2'
AND    ra2.artifact_uuid = 'A3'
AND    NOT EXISTS (      -- no other artifacts
   SELECT FROM release_artifacts rax
   WHERE  rax.release_uuid   = ra1.release_uuid
   AND    rax.artifact_uuid <> ra1.artifact_uuid
   AND    rax.artifact_uuid <> ra2.artifact_uuid
   AND    rax.artifact_uuid <> ra3.artifact_uuid
   );

或者:

   ...
   AND    rax.artifact_uuid <> ALL ('{A1, A2, A3}'::uuid[])
   );

LEFT JOIN / IS NULL。参见:

  • Select rows which are not present in other table

应该只会稍微多花点钱,并以类似的方式扩展。