暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

挑战高级开发DBA面试题-SQL递归/动态PL双重解法

SQL之美 2022-10-24
820

上周先生给我出了道题,感觉非常具有挑战性,大半年没玩高难度SQL感觉水平下降不少,两种解法花了12个小时完成,脑细胞牺牲不少。

题目如下所示:


题目需要花几分钟时间仔细观察,说明:

将DEL和PM表合并插入到pay表,依据PM表的付款金额和DEL的送货金额进行数据拆分重组,

从首条开始,当付款金额>=送货金额时 第PAY表第一条数据的 送货日期 送货金额 付款日期 均为 DEL和PM表的第一行记录,只有付款金额变成送货金额 ,同时生成第二条记录

第二条记录的送货日期取DEL表的第二行记录 送货金额+付款金额=第一行的付款金额-送货金额的差值 付款日期取PM表的第一行记录


第三条记录 送货日期 +付款日期取 DEL+PM表第二行记录,送货金额+付款金额= 

第四条记录  送货日期 +付款日期取 DEL+PM表第三行记录,送货金额+付款金额=第二行付款金额-(送货金额第二行金额 -(第一行的付款金额-送货金额的差值))

第五条记录   送货日期 +付款日期取 DEL+PM表第三行记录,送货金额+付款金额=第三条付款金额

以上只是一种正常的数据理解说明,如果这里的付款金额与送货金额发生大小变动还需要调整,具体需要联想到业务改金额测试校验下。


---------------------先发简单的PL动态SQL解法-------------------------------

前置表建好:

create table del as
select 'A' as 合同号,'1.1' as 送货日期,100 as 送货金额 from dual
union
select 'A' as 合同号,'1.10' as 送货日期,200 as 送货金额 from dual
union
select 'A' as 合同号,'1.20' as 送货日期,300 as 送货金额 from dual ;
create table pm as
select 'A' as 合同号,'2.10' as 付款日期,250 as 付款金额 from dual
union
select 'A' as 合同号,'2.20' as 付款日期,100 as 付款金额 from dual
union
select 'A' as 合同号,'2.25' as 付款日期,250 as 付款金额 from dual ;
create table pay as
select del.合同号,del.送货日期, del.送货金额 ,pm.付款日期, pm.付款金额, rownum rn from
(select a.*,rownum rn from del a)del ,
( select a.*,rownum rn from pm a)pm
where del.合同号=pm.合同号 and del.rn=pm.rn;
truncate table pay;

接下来就是完成的PL代码实现:

create or replace procedure SH_TEST is 

type cur_mony is ref cursor;

cur_monye cur_mony;

  v_rn               number;

  v_ht               del.合同号%TYPE;

  v_elivery_date     del.送货日期%TYPE;

  v_elivery_date2    del.送货日期%TYPE;

  v_elivery_mony     del.送货金额%TYPE;

  v_elivery_mony2     del.送货金额%TYPE;

  v_rnmax               number;

  v_payment_date    pm.付款日期%TYPE;

  v_payment_date2    pm.付款日期%TYPE;

  v_payment_mony     pm.付款金额%TYPE;

  v_amountd          pm.付款金额%TYPE;

  v_amountd2         pm.付款金额%TYPE;

  v_nm               number ;

BEGIN

  --计数器

  v_nm :=0;

  open cur_monye for  select  del.rn  from (select a.*,rownum rn from del a)del ,( select a.*,rownum rn from pm a)pm 

where del.合同号=pm.合同号 and del.rn=pm.rn  ; 

loop 

   fetch cur_monye into v_rn;

   

  BEGIN

   SELECT A.合同号, A.送货日期,A.送货金额 ,B.付款日期, B.付款金额,A.送货日期2,A.送货金额2+A.送货金额-B.付款金额-b.付款金额2,

   nvl(送货金额2,0), (A.送货金额3- B.付款金额3),付款日期4,

   max(a.rn) over (partition by a.rn) 

     INTO v_ht,v_elivery_date, v_elivery_mony ,v_payment_date, v_payment_mony,v_elivery_date2,v_amountd,

     v_elivery_mony2,v_amountd2,v_payment_date2,v_rnmax

      FROM (select a.*,lead(a.送货日期) over (order by a.送货日期) as  送货日期2,

           lag(a.送货日期) over (order by a.送货日期) as  送货日期3,

           lag(a.送货金额) over (order by a.送货金额) as  送货金额2,

           lead(a.送货金额) over (order by a.送货金额) as 送货金额3,

         

      rownum RN from del a where 合同号='A' ) A, 

           (select b.*,

              lag(b.付款金额) over (order by b.付款金额) as  付款金额2,

              lag(B.付款日期) over (order by B.付款日期) as  付款日期4,

              lead(b.付款金额) over (order by b.付款金额) as  付款金额3,

           rownum RN from pm  b where 合同号='A' ) B

     WHERE A.合同号=B.合同号 AND A.RN = B.RN and a.rn=v_rn ;

     --解决游标尾数额外循环一次BUG

      v_nm := v_nm+1;

     end;

     ----计算付款大于送货金额头部数据

  IF v_payment_mony >= v_elivery_mony and   v_nm<v_rnmax+1 THEN 

  insert into pay  select  v_ht, v_elivery_date,v_elivery_mony,v_payment_date,v_elivery_mony,1 from dual;

   insert into pay  select  v_ht, v_elivery_date2,v_payment_mony-v_elivery_mony,v_payment_date,v_payment_mony-v_elivery_mony,1 from dual;

   commit;

  end if ;

 ----计算 计算付款小于送货金额且 尾部与中间差额数据 

   IF v_payment_mony < v_elivery_mony and   v_nm<v_rnmax+1  and  v_amountd is null

     THEN  

      insert into pay  select  v_ht, v_elivery_date,  v_payment_mony-v_amountd2 , v_payment_date,  

      v_payment_mony-v_amountd2 ,2 from dual;

       commit;

       

  END IF;

  -----计算付款小于送货金额且(本次+上行送货金额)-(本次+上行付款金额)>0 尾部数据

   IF v_payment_mony < v_elivery_mony and   v_nm<v_rnmax+1  and v_amountd IS NOT NULL  

     THEN  

      insert into pay  select  v_ht, v_elivery_date, v_elivery_mony-v_payment_mony ,v_payment_date2,  v_elivery_mony-v_payment_mony ,3

      from dual;

      insert into pay  select  v_ht, v_elivery_date, v_payment_mony ,v_payment_date, v_payment_mony ,3

      from dual;

       dbms_output.put_line(v_rn);

        commit;

   end if;

  exit when cur_monye%notfound;

   dbms_output.put_line('结束');

  end loop;

    close cur_monye;

END SH_TEST;

最终效果如下图:


耗时4h
RN仅类似于标记便与测试整体可以看到,没有特别高深的代码,其实整体思路还是比较顺畅的,难点在于作者 各种基础PL写法忘了,1年多没写存储过程很多东西属实不太熟练了。
下面SQL实现可谓是写的头发都掉了,我第一时间想到的递归,然而单纯的递归并不能满足实际需求,需要不断进行业务规则魔改,感兴趣可以先自行挑战下。

---------------------下面来分享下SQL 递归实现-------------------------------

with  del as (

select 'A' as 合同号,'1.1'  as 送货日期,100 as 送货金额 from dual 

union 

select 'A' as 合同号,'1.10'  as 送货日期,200 as 送货金额 from dual 

union 

select 'A' as 合同号,'1.20'  as 送货日期,300 as 送货金额 from dual

 )

 ,pm as (

select 'A' as 合同号,'2.10'  as 付款日期,250 as 付款金额 from dual 

union 

select 'A' as 合同号,'2.20'  as 付款日期,100 as 付款金额 from dual 

union 

select 'A' as 合同号,'2.25'  as 付款日期,250 as 付款金额 from dual


)

,v1 as ( select del.合同号,del.送货日期,

 del.送货金额 ,pm.付款日期, pm.付款金额,

  pm.付款金额-del.送货金额 付款金额差 ,rownum rn  from

 (select a.*,rownum rn from del a)del ,

 (select a.*,rownum rn from pm a)pm

  where del.合同号=pm.合同号 and del.rn=pm.rn )


,tmony  (合同号,送货日期,送货金额, 付款日期, 付款金额,付款金额差,送货日期2,付款日期2,付款金额2,送货金额2,付款金额差2,送货金额ZS,ITERATER) AS 

( select 合同号,送货日期,送货金额, 付款日期, 付款金额,付款金额差,'1','1',1,1,1,1,1 from v1 

  UNION ALL

  SELECT

    t1.合同号, 

    t1.送货日期,

    t1.送货金额,

    t1.付款日期,

    t1.付款金额,

    t1.付款金额差 ,

    t2.送货日期 送货日期2,

    t2.付款日期 付款日期2,

    t2.付款金额 付款金额2,

    t2.送货金额 送货金额2,

    t2.付款金额差 付款金额差2,

    t1.付款金额-t2.送货金额 送货金额ZS,

    ITERATER + 1        

  FROM v1 t1, tmony t2

  WHERE t1.rn = t2.ITERATER

)

---冗余金额首

select 合同号,送货日期,送货金额, 付款日期, 送货金额 as 付款金额 ,1

 from tmony where ITERATER=2  and 付款金额差2>0 and 送货金额ZS>0

union all

----增补金额

select 合同号,

case when 付款金额差2>0 then lead(送货日期2) over (order by 送货日期2)   else 送货日期2 end 送货日期,

case when 付款金额差2>0 and 送货金额ZS >0 then 付款金额差 

     when 付款金额差2>0 and 送货金额ZS <0 then 送货金额2

     when 付款金额差2<0 and  送货金额ZS>0 then 送货金额ZS

     when 付款金额差2<0 and  送货金额ZS<0 then 送货金额2-付款金额2  else 送货金额 end 送货金额  ,      

case when 付款金额差2>0 then 付款日期 

     when 付款金额差2<0 and  送货金额ZS<0 then lag(付款日期2) over (order by 付款日期2)  else  付款日期2  end   付款日期,

case when 付款金额差2>0 and 送货金额ZS >0 then 付款金额差 

     when 付款金额差2>0 and 送货金额ZS <0 then 送货金额2

      when 付款金额差2<0 and  送货金额ZS>0 then 送货金额ZS

     when 付款金额差2<0 and  送货金额ZS<0 then 送货金额2-付款金额2  else  送货金额 end  付款金额

 ,2 from tmony where ITERATER=2 

---冗余金额尾

union all

select 合同号,送货日期2 as 送货日期,付款金额2 as 送货金额,  付款日期2 as 付款日期,付款金额2 as 付款金额 ,3

 from tmony where ITERATER=2  and 送货金额ZS<0 and 付款金额差2<0

以下是结果:


这个没有过多的说明,无需建表,一条SQL搞定,主要是调试太多次了,长达8小时的测试成果,其实这里还有很大的美观精简优化空间,感兴趣可以自行优化。。

通过RN字段的值,我们可以看到 PL动态SQL与SQL递归的实现思路还是有所不同的 ,PL动态SQL可以做到单行数据直接拆分两行结果,但是单独的静态SQL是有局限的,只能通过递归实现这类计算。
如 各位有更好的方式,欢迎加好友联系,共同学习,体验SQL的极致调优!







文章转载自SQL之美,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论