【问题标题】:Oracle Query - Join with comma separated dataOracle 查询 - 使用逗号分隔的数据连接
【发布时间】:2019-10-28 21:23:15
【问题描述】:

表名称:crm_mrdetails

 id | mr_name | me_email     | mr_mobile  | mr_doctor|
 -----------------------------------------------------
 1  | John    |abc@gmail.com | 1234555555 | ,1,2,3   |

表名称:crm_mr_doctor

id | dr_name     | specialization|  
----------------------------------  
1  | Abhishek    | cordiologist  |
2  | Krishnan    | Physician     |
3  | Krishnan    | Nurse         |

mrdetails.mr_doctor 中的连接值是mr_doctor.id 的外键。我需要加入它们以产生如下输出:

 id | mr_name | me_email     |Doctor_Specialization|
 -------------------------------------------------
 1  | John    |abc@gmail.com |cordiologist,Physician,Nurse|

我是 Oracle 新手,我使用的是 Oracle 12C。非常感谢任何帮助。

【问题讨论】:

  • 我怎样才能加入两个表,我提供的样本,加入必须在同一列上完成。
  • 你不能在这里交叉加入,它不是我的模型,有人创造了我们需要照原样使用。加入应该发生在 Mr_Doctor 和 ID 上
  • 你用来和数据库交互的前端是SQL Developer,它的最新版本是4.3(我认为)。 12c 是您的 Oracle 数据库的版本,它是一个不同的软件产品。 SQL Developer 与您的问题无关;数据库版本是。我编辑了您的标签,我将编辑问题以反映这一点。
  • 请注意:mr_doctor 列中的输入字符串以逗号开头(在数字 1 之前),但输出不应以逗号开头?
  • 是的正确,逗号是存在的(前导和尾随),我不知道为什么数据模型会这样创建。我提供的例子

标签: string oracle oracle12c


【解决方案1】:

首先我们必须承认这是一个糟糕的数据模型。 mr_doctor 列违反 First Normal Form。这不是什么深奥的理论观点。不在 1NF 中意味着我们必须编写更多代码来查找键的含义,而不是使用标准的 SQL 连接语法。这也意味着我们不能依赖包含有效 ID 的列:mr_doctor 可以包含任何旧的废话,我们必须编写一个可以处理它的查询。有关更多信息,请参阅Is storing a delimited list in a database column really that bad?

无论如何。这是一个解决方案,它使用正则表达式将 mr_doctor 列拆分为 ID,然后将它们连接到 mr_doctor 表。连接specialization 列以产生所需的输出。

select mrdet.id, 
       mrdet.mr_name,
       mrdet.me_email,
       listagg(mrdoc.specialization, ',') 
                     within group (order by mrdoc.specialization) as doctor_specialization
from mr_details mrdet
     join (
        select distinct id, 
               regexp_substr(mr_doctor, '(,?)([0-9]+)(,?)', 1, level, null, 2) as dr_id
        from mr_details 
        connect by level <= regexp_count(mr_doctor, '(,?)([0-9]+)')
       ) as mrids
    on mrids.id = mrdet.id
    left outer join mr_doctor mrdoc
       on mrids.dr_id = mr_doc.id
group by mrdet.id, 
       mrdet.mr_name,
       mrdet.me_email
/

尽管数据模型很脆弱,但该解决方案具有相当的弹性。如果字符串中有太多逗号或空格,它将返回结果。它将忽略字母或其他不是数字的值。如果提取的数字与mr_doctor 表中的 ID 不匹配,它不会抛出异常。显然,由于这些原因,结果是不可信的,但这是不可靠的数据模型代价的一部分。

你能解释一下吗:(,?)([0-9]+)(,?)

模式匹配零个或一个逗号,后跟一个或多个数字,后跟零个或一个逗号。也许匹配模式中的(,?) 并不是绝对必要的。但是,如果没有它们,这个字符串 2 3 4 将匹配与这个字符串 2,3,4 相同的三个 ID。也许这是正确的也许不是。当外键存储在 CSV 列中而不是通过适当的约束强制执行时,“正确”甚至意味着什么?

【讨论】:

  • 嗨@APC,您能否解释一下:(,?)([0-9]+)(,?) 您在 SUBSTR 中使用过。我无法理解它。第一部分(,?) 表示开头出现 0 次或多次问号,最后表示最后出现 0 次或多次问号但是如果我们在两种情况下都考虑 0 并尝试匹配数字,那么 ,99 应该是分别考虑为 9 和 9!现在我将注意力转移到[0-9+] 范围内的+,这意味着出现1 次或多次。即使最后一个参数为 2,我似乎也有点困惑。我是 regexp_substr 的新手!
  • @pOrinG - 好问题。我扩大了我的答案。问题是,我在使用正则表达式进行数据清理时有过糟糕的经历,所以我倾向于冗长并过度指定匹配项。许多正则表达式从业者喜欢简洁和优雅。双方都有争论。
  • 感谢您的解释。
【解决方案2】:

您必须将mr_doctor 列中的数据拆分为行,连接表crm_mrdoctor,然后使用listagg()。 如何拆分数据? Splitting string into multiple rows in Oracle

select t.id, max(mr_name) mr_name, 
       listagg(specialization, ', ') within group (order by rn) specs
  from (
    select id, mr_name, levels.column_value rn, 
           trim(regexp_substr(mr_doctor, '[^,]+', 1, levels.column_value)) as did
      from crm_mrdetails t,
           table(cast(multiset(select level 
                                 from dual 
                                 connect by level <= 
                                     length(regexp_replace(t.mr_doctor, '[^,]+')) + 1) 
                      as sys.odcinumberlist)) levels) t
  left join crm_mr_doctor d on t.did = d.id
  group by t.id

演示和结果:

with crm_mrdetails (id, mr_name, mr_doctor) as (
    select 1, 'John', ',1,2,3'   from dual union all
    select 2, 'Anne', ',4,2,6,5' from dual union all
    select 3, 'Dave', ',4'       from dual),
crm_mr_doctor (id, dr_name, specialization) as (
    select 1, 'Abhishek', 'cordiologist' from dual union all
    select 2, 'Krishnan', 'Physician'    from dual union all
    select 3, 'Krishnan', 'Nurse'        from dual union all
    select 4, 'Krishnan', 'Onkologist'   from dual union all
    select 5, 'Krishnan', 'Surgeon'      from dual union all
    select 6, 'Krishnan', 'Nurse'        from dual
    )
select t.id, max(mr_name) mr_name, 
       listagg(specialization, ', ') within group (order by rn) specs
  from (
    select id, mr_name, levels.column_value rn, 
           trim(regexp_substr(mr_doctor, '[^,]+', 1, levels.column_value)) as did
      from crm_mrdetails t,
           table(cast(multiset(select level 
                                 from dual 
                                 connect by level <= 
                                     length(regexp_replace(t.mr_doctor, '[^,]+')) + 1) 
                      as sys.odcinumberlist)) levels) t
  left join crm_mr_doctor d on t.did = d.id
  group by t.id

输出:

    ID MR_NAME SPECS
------ ------- -------------------------------------
     1 John    cordiologist, Physician, Nurse
     2 Anne    Onkologist, Physician, Nurse, Surgeon
     3 Dave    Onkologist

【讨论】:

  • 为什么t被用了两次?
【解决方案3】:

您可以使用递归子查询和简单的字符串函数(这可能比使用正则表达式和相关的分层查询更快):

Oracle 设置

CREATE TABLE crm_mrdetails (id, mr_name, mr_doctor) as
    select 1, 'John', ',1,2,3'   from dual union all
    select 2, 'Anne', ',4,2,6,5' from dual union all
    select 3, 'Dave', ',4'       from dual;

CREATE TABLE crm_mr_doctor (id, dr_name, specialization) as
    select 1, 'Abhishek', 'cordiologist' from dual union all
    select 2, 'Krishnan', 'Physician'    from dual union all
    select 3, 'Krishnan', 'Nurse'        from dual union all
    select 4, 'Krishnan', 'Onkologist'   from dual union all
    select 5, 'Krishnan', 'Surgeon'      from dual union all
    select 6, 'Krishnan', 'Nurse'        from dual;

查询

WITH crm_mrdetails_bounds ( id, mr_name, mr_doctor, start_pos, end_pos ) AS (
  SELECT id,
         mr_name,
         mr_doctor,
         2,
         INSTR( mr_doctor, ',', 2 )
  FROM   crm_mrdetails
UNION ALL
  SELECT id,
         mr_name,
         mr_doctor,
         end_pos + 1,
         INSTR( mr_doctor, ',', end_pos + 1 )
  FROM   crm_mrdetails_bounds
  WHERE  end_pos > 0
),
crm_mrdetails_specs ( id, mr_name, start_pos, specialization_id ) AS (
  SELECT id,
         mr_name,
         start_pos,
         TO_NUMBER(
           CASE end_pos
           WHEN 0
           THEN SUBSTR( mr_doctor, start_pos )
           ELSE SUBSTR( mr_doctor, start_pos, end_pos - start_pos )
           END
         )
  FROM   crm_mrdetails_bounds
)
SELECT s.id,
       MAX( s.mr_name ) AS mr_name,
       LISTAGG( d.specialization, ',' )
         WITHIN GROUP ( ORDER BY s.start_pos )
         AS doctor_specialization
FROM   crm_mrdetails_specs s
       INNER JOIN crm_mr_doctor d
       ON ( s.specialization_id = d.id )
GROUP BY s.id

输出

身份证 | MR_NAME | DOCTOR_SPECIALIZATION -: | :-------- | :-------------------------------- 1 |约翰 |心内科医师、医师、护士 2 |安妮 |肿瘤学家、内科医生、护士、外科医生 3 |戴夫 |肿瘤学家

db小提琴here

【讨论】:

    【解决方案4】:

    请根据您的要求更改列名。

    CREATE OR REPLACE Function ReplaceSpec
        (String_Inside IN Varchar2)
        Return Varchar2 Is
    
            outputString Varchar2(5000);
            tempOutputString crm_doc.specialization%TYPE;
    
        Begin
    
            FOR i in 1..(LENGTH(String_Inside)-LENGTH(REPLACE(String_Inside,',',''))+1)
            LOOP
    
                Select specialization into tempOutputString From crm_doc 
                Where id = PARSING_STRING(String_Inside,i);
    
                If i != 1 Then
                    outputString := outputString || ',';
                end if;
                outputString := outputString || tempOutputString;
    
            END LOOP;
    
            Return outputString;
    
    
        End;
    /
    

    Parsing_String 函数有助于拆分逗号分隔值。

    CREATE OR REPLACE Function PARSING_STRING
    (String_Inside IN Varchar2, Position_No IN Number) 
    Return Varchar2 Is
        OurEnd   Number; Beginn Number;
    Begin
    
    If Position_No < 1 Then 
    Return Null; 
    End If;
    
    OurEnd := Instr(String_Inside, ',', 1, Position_No);
    
    If OurEnd = 0 Then
        OurEnd := Length(String_Inside) + 1;
    End If;
    
    If Position_No = 1 Then
        Beginn := 1;
    Else
        Beginn := Instr(String_Inside, ',', 1, Position_No-1) + 1;
    End If;
    
    Return Substr(String_Inside, Beginn, OurEnd-Beginn);
    
    End;
    /
    

    请注意,我只提供了一个基本功能来获取您的输出。您可能需要添加一些例外等。

    Eg. When the doc_id [mr_doctor] is empty, what to do.
    

    用法

    select t1.*,ReplaceSpec(doc_id) from crm_details t1
    

    如果您的 mr_doctor 数据始终以逗号开头:

    Select t1.*,ReplaceSpec(Substr(doc_id,2)) from crm_details t1
    

    【讨论】:

    • 重建轮子的大量工作LISTAGG
    • @JorgeCampos 我无法想象只有 LISTAGG 和单个查询可以完成全部所需工作的工作流程。我可能错了!!
    • 我只有只读权限,我不能创建任何表/过程/函数,将数据提取到 SQL 并报告它
    • @APC 我真的很高兴有人做到了。会帮助我成长。干杯!
    【解决方案5】:

    请转至https://oracle-base.com/articles/misc/string-aggregation-techniques 字符串聚合技术

    选择部门号, LTRIM(MAX(SYS_CONNECT_BY_PATH(ename,',')) 保持 (DENSE_RANK LAST ORDER BY curr),',') 作为员工 从(选择部门号, 姓名, ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY ename) AS curr, ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY ename) -1 AS prev 来自雇员) 按部门分组 CONNECT BY prev = PRIOR curr AND deptno = PRIOR deptno 从 curr = 1 开始

    listagg 和 wm_concat 也可以像其他人一样使用

    【讨论】:

    • 您能否将问题中的代码(即查询)格式化为代码?
    【解决方案6】:

    这个怎么样?我没有测试过,所以可能有语法错误。

    select id,mr_name,me_email,listagg(specialization,',') within group (order by specialization) as Doctor_Specialization
    from
    (select dtls.id,dtls.mr_name,dtls.me_email,dr.specialization
    from crm_mrdetails dtls,
    crm_mr_doctor dr
    where INSTR(','||dtls.mr_doctor||',' , ','||dr.id||',') > 0
    ) group by id,mr_name,me_email;
    

    【讨论】:

      猜你喜欢
      • 2020-09-14
      • 2013-05-31
      • 2015-03-25
      • 2014-05-30
      • 2019-05-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多