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

How to recursivly create pedigrees and find matches for inbreeding detection (Oracle)

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

起初我认为我应该使用来自 table 的数据创建两个谱系,其结构如下:

    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-s(并记住找到匹配的深度)。

最终我想 return 发现近亲繁殖的最小深度或当 none 被发现时为 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 都在男性血统中找到。忽略。

示例 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-s

您可以使用递归 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

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

此外,主查询显示匹配的所有详细信息,包括匹配的祖先及其对应的深度。您可以轻松修改它以仅显示深度(如您所愿)。或者...您可以展开它以向您显示 "full path" 以到达每个祖先。这取决于您想要的确切输出。我敢打赌,一旦您看到结果,您就会想要了解更多相关信息。

由于您使用的是 10g 而不是较新的版本,因此您需要使用 oracle 的分层查询而不是 The Impaler 所示的递归通用 table 表达式。为了使我的解决方案起作用,将动物性别编码为单独的列而不是将其嵌入动物 ID 中会很有帮助,因此我将使用以下 table 定义。 (注意:我没有 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
    );

从那里为任何给定动物生成所有祖先的映射很有帮助,这称为闭包 table 如果需要,您可以 google 更多关于闭包 tables .这是通过递归 SQL 或在我们的例子中使用 oracle 分层 tables 完成的,因为您使用的是 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;

并且您可以在 fiddle 示例中使用此 db<>fiddle 看到它的实际效果示例 2 和示例 3 与示例 1 不同,因此示例 2 仅添加了雄性系,示例 3 中仅添加了雌性系,产生了以下配对 (1111, 1121)、(2111, 1211) 和 ( 1111,3211) 分别用于示例 1、2 和 3。

这只会 return 记录这对动物有共同祖先的情况。它还会预生成整个祖先闭包,这对于大型邻接列表来说可能很耗时。为了更有效的查询,祖先闭包可以仅限于具有 START 条件的感兴趣的动物。此外,搜索深度可以限制在两个地方之一(或两者),在输出查询中的 where 子句中,或在祖先查询中的 where 子句中。此外,为了满足您的要求,即在没有共同祖先时配对 return 显示零级别的行,需要进行一些细微的修改。首先,祖先 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;