如何递归地创建谱系并查找匹配以进行近亲繁殖检测 (Oracle)

Posted

技术标签:

【中文标题】如何递归地创建谱系并查找匹配以进行近亲繁殖检测 (Oracle)【英文标题】:How to recursivly create pedigrees and find matches for inbreeding detection (Oracle) 【发布时间】:2020-05-05 13:36:01 【问题描述】:

我很难在 Oracle 中创建一个函数来确定 2 种交配动物是否会产生近亲繁殖。 函数需要3个参数:男性ID、女性ID和要查看的深度。

起初我认为我应该使用具有如下结构的表中的数据创建两个谱系:

    TABLE animal
    +-----+---------+--------+
    | ID  | SIRE_ID | DAM_ID |
    +-----+---------+--------+
    | 111 | 112     | 212    |
    | 112 | 113     | 213    |
    | 212 | 116     | 216    |
    +-----+---------+--------+

(不完全相关,但对于这个和未来的例子,我使用 ID-s 作为 1?? 是男性和 2?? 是女性。) 为此,我应该使用深度参数 - 可能是递归的。

这是我目前所拥有的:

function animal_pedigree (p_id number,
    p_max_pedigree_level number,
    p_pedigree_level number := 0,
    p_position varchar2 := '') return animal_ancestors_table
    pipelined
is
    v_sire_id number;
    v_dam_id number;
        v_row animal_ancestor;
begin
        v_row.id := p_id;
        v_row.pedigree_level := p_pedigree_level;
        v_row.position := p_position;
    pipe row (v_row);
    if p_pedigree_level < p_max_pedigree_level then
        select sire_id, dam_id
        into v_sire_id, v_dam_id
        from arc.animal
        where id = p_id;
        if v_sire_id is not null then
            for rec in (select id, pedigree_level, position
                from table(animal_pedigree (v_sire_id, p_max_pedigree_level, p_pedigree_level+1, p_position || 's'))) loop
                                v_row.id := rec.id;
                                v_row.pedigree_level := rec.pedigree_level;
                                v_row.position := rec.position;
                pipe row (v_row);
            end loop;
        end if;
        if v_dam_id is not null then
            for rec in (select id, pedigree_level, position
                from table(animal_pedigree (v_dam_id, p_max_pedigree_level, p_pedigree_level+1, p_position || 'd'))) loop
                                v_row.id := rec.id;
                                v_row.pedigree_level := rec.pedigree_level;
                                v_row.position := rec.position;
                pipe row (v_row);
            end loop;
        end if;
    end if;
    return;
end;

接下来是我最棘手的部分:比较谱系以找到匹配的 ID(并记住找到匹配的深度)。

最终我想返回发现近亲繁殖的最小深度,或者返回 0(如果没有发现)。

注意!我只想比较两个谱系,而不是比较一个内部的 ID。 (如果已经存在近亲繁殖,我希望它被忽略,只对新形成的近亲繁殖感兴趣。)

为了进一步说明,我添加了 3 个示例。 标有 *(星号)的匹配项。

示例 1:

男性血统

Depth           1       2       3

                            |--114
                    |--113--|
                    |       |--214
            |--112--|       
            |       |       |--115
            |       |--213--|
            |               |--215
       111--|
            |               |--117
            |       |--116--|
            |       |       |--217
            |--212--|
                    |       |--118
                    |--216--|
                            |--218

女性血统

Depth           1       2       3

                            |--124
                    |--123--|
                    |       |--224
            |--122--|       
            |       |       |--125
            |       |--223--|
            |               |--225
       211--|
            |               |--127
            |       |--126--|
            |       |       |--227
            |--222--|
                    |       |--128
                    |--226--|
                            |--228

[RETURN 0] 未找到相同的 ID

示例 2:

男性血统

Depth           1       2       3

                            |--114*
                    |--113--|
                    |       |--214
            |--112--|       
            |       |       |--115
            |       |--213--|
            |               |--215
       111--|
            |               |--117
            |       |--116--|
            |       |       |--217
            |--212--|
                    |       |--114*
                    |--216--|
                            |--218

女性血统

Depth           1       2       3

                            |--124
                    |--123--|
                    |       |--224
            |--122--|       
            |       |       |--125
            |       |--223--|
            |               |--225
       211--|
            |               |--127
            |       |--126--|
            |       |       |--227
            |--222--|
                    |       |--128
                    |--226--|
                            |--228

[RETURN 0] 匹配的 ID-s 都在男性谱系中找到。忽略。

示例 3:

男性血统

Depth           1       2       3

                            |--114*
                    |--113--|
                    |       |--214
            |--112--|       
            |       |       |--115
            |       |--213--|
            |               |--215
       111--|
            |               |--117
            |       |--116--|
            |       |       |--217
            |--212--|
                    |       |--118
                    |--216--|
                            |--218

女性血统

Depth           1       2       3

                            |--124
                    |--123--|
                    |       |--224
            |--122--|       
            |       |       |--125
            |       |--223--|
            |               |--225
       211--|
            |               |--127
            |       |--114*-|
            |       |       |--227
            |--222--|
                    |       |--128
                    |--226--|
                            |--228

[RETURN 2] 在男性谱系的深度 3 和女性谱系的深度 2 中找到匹配 ID

【问题讨论】:

您的示例有助于理解问题。为了帮助我们所有人测试我们的暂定解决方案,添加create table as select ... 语句以在实际表中重现示例数据也会有所帮助。 您可以使用递归 CTE 找到结果。它应该比程序简单得多。 抱歉,忘了说我正在运行 oracle 10(g) 【参考方案1】:

由于您使用的是 10g 而不是更新版本,因此您需要使用 oracle 的分层查询而不是 The Impaler 显示的递归公用表表达式。为了使我的解决方案正常工作,将动物性别编码为单独的列而不是将其嵌入动物 ID 中会很有帮助,因此我将使用下表定义。 (注意:我没有 10g 的实例来尝试这个,所以我不确定 10g 中是否有可递减约束。如果不只是删除这些子句。它们使加载示例数据变得更容易。) :

CREATE TABLE animal
    ( ID number not null primary key
    , GENDER varchar2(1) not null
    , SIRE_ID number
    , DAM_ID number
    , constraint animal_gender check (gender in ('M','F'))
    , constraint animal_sire_fk FOREIGN KEY (sire_id) REFERENCES animal(id) DEFERRABLE INITIALLY DEFERRED
    , constraint animal_dam_fk FOREIGN KEY (dam_id) REFERENCES animal(id) DEFERRABLE INITIALLY DEFERRED
    );

从那里生成任何给定动物的所有祖先的映射很有帮助,这称为闭包表,如果需要,您可以在谷歌上搜索更多关于闭包表的信息。这可以通过递归 SQL 或在我们的例子中使用 oracle 分层表来完成,因为您使用的是 10g。在这个例子中,我将它命名为 Ancestry:

with ancestry as (
select CONNECT_BY_ROOT id id
     , CONNECT_BY_ROOT gender gender
     , id ancestor_id
     , gender ancestor_gender
     , level-1 lvl
  from animal
  connect by id in (prior sire_id, prior dam_id)
)

从那里您可以通过适度简单的连接找到所有具有共同祖先的动物:

select m.id sire_id
     , f.id dam_id
     , m.ancestor_id
     , m.ancestor_gender
     , m.lvl sire_lvl
     , f.lvl dam_lvl
  from ancestry m
  join ancestry f
    on m.ancestor_id = f.ancestor_id
   and m.gender = 'M'
   and f.gender = 'F';

该查询列出了所有成对的雄性和雌性动物,以及它们所有的共同祖先。这有点多,我们希望将其缩减为您感兴趣的配对,并将其限制为仅第一个共同祖先。为此,我们将添加一个 where 子句,将其限制为感兴趣的配对,并使用聚合使我们只找到第一个祖先:

select m.id sire_id
     , f.id dam_id
     , max(m.ancestor_id) keep (dense_rank first order by least(m.lvl,f.lvl)) first_ancestor
     , max(m.ancestor_gender) keep (dense_rank first order by least(m.lvl,f.lvl)) ancestor_gnder
     , min(m.lvl) sire_lvl
     , min(f.lvl) dam_lvl
  from ancestry m
  join ancestry f
    on m.ancestor_id = f.ancestor_id
   and m.gender = 'M'
   and f.gender = 'F'
 where (m.id, f.id) in ((111,211))
 group by m.id, f.id;

将所有这些放在一起是最终的查询:

with ancestry as (
select CONNECT_BY_ROOT id id
     , CONNECT_BY_ROOT gender gender
     , id ancestor_id
     , gender ancestor_gender
     , level-1 lvl
  from animal
  connect by id in (prior sire_id, prior dam_id)
)
select m.id sire_id
     , f.id dam_id
     , max(m.ancestor_id) keep (dense_rank first order by least(m.lvl,f.lvl)) first_ancestor
     , max(m.ancestor_gender) keep (dense_rank first order by least(m.lvl,f.lvl)) ancestor_gnder
     , min(m.lvl) sire_lvl
     , min(f.lvl) dam_lvl
  from ancestry m
  join ancestry f
    on m.ancestor_id = f.ancestor_id
   and m.gender = 'M'
   and f.gender = 'F'
 where (m.id, f.id) in ((111,211))
 group by m.id, f.id;

您可以在小提琴示例中通过db<>fiddle 看到它的实际作用2 和 3,因此例如 2 仅添加了男性线,而在示例 3 中仅添加了女性线,产生以下配对 (1111, 1121), (2111, 1211) 和 (1111,3211) 对于示例 1,分别为 2 和 3。

这只会在这对动物有共同祖先时返回记录。它还预先生成了整个祖先关闭,这对于大型邻接列表可能很耗时。为了更有效的查询,祖先关闭可以仅限于具有 START 条件的感兴趣的动物。此外,可以将搜索深度限制在两个位置之一(或两者),在输出查询中的 where 子句或祖先查询中的 where 子句中。此外,为了满足您的要求,即当没有共同祖先时,配对返回显示零级别的行,需要进行一些细微的修改。首先,需要修改祖先 CTE 以使自连接具有空级别(深度为零)。这对于使聚合工作很重要。然后需要稍微更新聚合列和连接条件以允许没有共同祖先的记录。这是修改后的查询:

with ancestry as (
select CONNECT_BY_ROOT id id
     , CONNECT_BY_ROOT gender gender
     , id ancestor_id
     , gender ancestor_gender
     , case level when 1 then null else level-1 end lvl
     , level-1 lvl0
  from animal

 -- Limit depth to 3 generations
 where level-1 <= 3 

 connect by id in (prior sire_id, prior dam_id)

 -- Only build ancestry closure for these animals
 start with id in (1111,1211,2111,3211)
)
select m.id sire_id
     , f.id dam_id
     , max(nvl2(m.lvl,m.ancestor_id,null)) keep (dense_rank first order by least(m.lvl,f.lvl) nulls last) first_ancestor
     , max(nvl2(m.lvl,m.ancestor_gender,null)) keep (dense_rank first order by least(m.lvl,f.lvl) nulls last) ancestor_gnder
     , nvl(min(m.lvl),0) sire_lvl
     , nvl(min(f.lvl),0) dam_lvl
  from ancestry m
  join ancestry f
    on (m.ancestor_id = f.ancestor_id or (m.id, f.id) in ((m.ancestor_id, f.ancestor_id)))
   and m.gender = 'M'
   and f.gender = 'F'
 where (m.id, f.id) in ((1111,1211) -- First example no common ancestors
                       ,(2111,1211) -- 2nd ex common ancesters in male line
                       ,(1111,3211))-- 3rd ex common ancestry of sire & dam

   -- Limit to at most 3 generations
   and greatest(m.lvl0, f.lvl0) <= 3

 group by m.id, f.id;

【讨论】:

我不明白。这不会在 SQL Fiddle 或我的数据库中给出任何结果。它只是继续运行……原因可能是它试图建立整个血统吗?我的数据库非常大(数百万只动物),祖先的深度有 10 甚至更多。也许我读错了查询,但我没有看到所构建祖先深度的限制,它是否在从中选择之前为所有动物创建了祖先? 我不确定为什么 SQL Fiddle 不起作用,我现在也遇到了问题,所以也许这是他们的问题。除此之外,是的,当前查询构建了整个祖先封闭,并且也不限制搜索的深度。您可以通过添加深度限制并限制它搜索的动物来优化祖先。我已经用这些增强功能更新了上面的最后一个查询。另一种选择是将祖先闭包创建为物化视图,这样就不需要为每个查询重新构建它。 我将示例从 SQL Fiddle 移到了 dbfiddle,这似乎更稳定,至少目前如此。 谢谢哨兵。该解决方案有效,但我认为我必须利用您构建物化查询的想法,即使深度为 4,并将查询限制为仅 2 只动物,大约需要 10 秒才能获得结果。在我的例子中,我需要在一个页面上用结果数字填充大约一百个配对的列表。所以我会接受你的回答,因为它给出了预期的结果。感谢您引导我走向正确的道路:)【参考方案2】:

您可以使用递归 CTE 来查找匹配的祖先。

此示例未经测试,因为您没有提供创建示例数据的脚本。无论如何,这个查询应该可以工作:

with
l (id, aid, sire_id, dam_id, lvl) as (
  select id, id, sire_id, dam_id, 0 from animal where id = 111 -- male ID
  union all
  select l.id, a.id, l.lvl + 1
  from l
  join animal a on a.id in (l.sire_id, l.dam_id)
),
r (id, aid, sire_id, dam_id, lvl) as (
  select id, id, sire_id, dam_id, 0 from animal where id = 211 -- female ID
  union all
  select r.id, a.id, r.lvl + 1
  from r
  join animal a on a.id in (r.sire_id, r.dam_id)
)
select 
  l.id as male_id, l.aid as male_ancestor_id, l.lvl as male_ancestor_depth,
  r.id as female_id, r.aid as female_ancestor_id, r.lvl as female_ancestor_depth
from l
join r on r.aid = l.aid

此查询返回所有匹配项(可以有多个)及其所有组合。您可以添加额外的更改来删除重复的匹配项,因为动物可以是每棵树上已有多个动物的祖先。

此外,主查询显示匹配的所有详细信息,包括匹配的祖先及其对应的深度。您可以轻松修改它以仅显示深度(如您所愿)。或者...您可以扩展它以向您显示到达每个祖先的“完整路径”。这取决于您喜欢的确切输出。我敢打赌,一旦你看到结果,你就会想知道更多关于它的信息。

【讨论】:

抱歉,忘了说我正在运行 oracle 10(g)

以上是关于如何递归地创建谱系并查找匹配以进行近亲繁殖检测 (Oracle)的主要内容,如果未能解决你的问题,请参考以下文章

何时创建 RDD 沿袭?如何找到谱系图?

如何通过回溯递归地修改数组元素?

以递归方式查找最后在PHP中修改的文件和文件夹

如何加快递归搜索功能?

如何创建自定义查找功能?

如何使用 Bash 递归地创建不存在的子目录?