Search code examples
for-loopstored-proceduresplsqlcursor

Can't figure out cursor for loop in a plsql procedure


I am trying to write a procedure in plsql that takes in two parameters, month and year. The procedure generates data for a table - loanreport. The procedure generates for all loan types in the loan type table. When the procedure is run it should populate the table with:

  1. Month
  2. Year
  3. closed loan amount (sum of loan amounts of loans with status = 6) if no loans have status = 6 then closed loan amount is 0.

    4.Average Closing Period

Here are the pertinent tables:

CREATE TABLE LOANDETAILS 
(LOANNO VARCHAR2(11) primary key, 
    PROPERTYID VARCHAR2(10), 
    CUSTID CHAR(8), 
    LOANTYPE VARCHAR2(20), 
    LOANSTATUSCODE NUMBER(3,0), 
    LOANAMOUNT NUMBER(10,2), 
    RATE NUMBER(5,2), 
    LOANCREATIONDATE DATE, 
    LOANSTATUSDATE DATE,
    constraint loandet_prop_fk foreign key(PROPERTYID) references PROPERTIES(propertyid),
    constraint loandet_cust_fk foreign key(CUSTID) references customers(custid),
    constraint loandet_lt_fk foreign key (LOANTYPE) references loantypes(loantype)
   );
   --insert
Insert into LOANDETAILS values ('L1000000001','P1000001','C1000001','Conventional',1,87975,9,to_date('26-JUL-2016','DD-MON-YY'),to_date('02-AUG-2016','DD-MON-YYYY'));
Insert into LOANDETAILS values ('L1000000009','P1000009','C1000009','FHA',6,160055,4.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('07-DEC-2016','DD-MON-YYYY'));
Insert into LOANDETAILS values ('L1000000010','P1000010','C1000010','VA',2,217600,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('07-DEC-2016','DD-MON-YYYY'));


CREATE TABLE LOANTYPES
   (ltID char(5) constraint loantypes_pk primary key,
    loantype VARCHAR2(20) constraint loantypes_lt_unique UNIQUE, 
    description VARCHAR2(100),
    active char(1) constraint loantypes_active CHECK (active IN ('Y','N')) -- if loan type is currently being offered
    );
Insert into loantypes values ('LT001', 'VA', 'Service members, veterans or eligible family','Y');
Insert into loantypes values ('LT002', 'FHA', 'Federal Housing Administration eligible loans', 'Y');
Insert into loantypes values ('LT003', 'Conventional', 'Standard loan','Y');
Insert into loantypes values ('LT004', 'Employee', 'Eligible employees of the organization','Y');
Insert into loantypes values ('LT005', 'Reconstruct', 'Relief work reconstruction','N');

CREATE TABLE LOANTYPEREPORT 
   (LOANTYPE VARCHAR2(20), 
    MONTH number(2,0), 
    YEAR NUMBER(4,0),  
    CLOSEDLOANSAMOUNT NUMBER(15,2), 
    AVERAGECLOSINGPERIOD NUMBER(5,2),
    constraint loantr_pk PRIMARY KEY (LOANTYPE, RMONTH, RYEAR)
   );

I am new to sql and I clearly have some knowledge gaps. I am attempting to create a procedure then would like to make a cursor and iterate over the cursor with a for loop to create the desired report. Here is my incomplete code:

CREATE OR REPLACE PROCEDURE loan_type_report_procedure (Month loantypereport.month%type, Year loantypereport.year%type) AS

CURSOR C1 IS
SELECT l.loantype, 
       loanamount, 
       loancreationdate,  
       loanstatusdate,
       loanstatuscode,
       to_char(LOANCREATIONDATE, 'mm') AS rMonth, 
       to_char(LOANCREATIONDATE, 'YYYY') AS rYEAR
FROM LOANTYPES l
JOIN LOANDETAILS d
ON l.loantype = d.loantype
WHERE Month = to_char(LOANCREATIONDATE, 'mm') 
      AND Year = to_char(LOANCREATIONDATE, 'YYYY')

BEGIN
FOR loan_rec in C1 LOOP

As far as my understanding goes for loop goes row by row in the cursor. If I want a final table loan type report that contains loantype, month, year, closed loan amount, and average closing period - how do I make that work? Closed loan amount and average closing period are both aggregated on the loan type grouping. Would I use a group by and having in the cursor select statement?

Thank you for your insight


Solution

  • If I understand your question correctly, you'd like to summarize the number of loans by their LOANTYPE, for each month, and for the loans that were closed (LOANSTATUSCODE = 6), you'd like to SUM their amount and record an AVERAGE of their loan time span.
    From the look of LOANTYPEREPORT, it looks like you are planning for this average span to be in a number of days.

    To accomplish this, you do not need to use PL/SQL. This can be done with traditional SQL. I'm only guessing at what criteria go into deciding a loan's duration, so I'll outline an example below with a couple variations.

    In this example, I needed to modify your tables a little, since they reference other tables not included in your post (I dropped LOANDET_PROP_FK and LOANDET_CUST_FK).

    After creating your example tables, create the LOANTYPEREPORT table (modified slightly from your example to compile):

    CREATE TABLE LOANTYPEREPORT
    (LOANTYPE VARCHAR2(20),
     MONTH NUMBER(2,0),
     YEAR NUMBER(4,0),
     CLOSEDLOANSAMOUNT NUMBER(15,2),
     AVERAGECLOSINGPERIOD NUMBER(5,2),
      CONSTRAINT LOANTR_PK PRIMARY KEY (LOANTYPE, MONTH, YEAR)
    );
    

    Also, I added a little extra data for some FHA and Conventional loans, to help differentiate the SUMs and durations in the examples below.

    Insert into LOANDETAILS values ('L1000000001','P1000001','C1000001','Conventional',1,87975,9,to_date('26-JUL-2016','DD-MON-YY'),to_date('02-AUG-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000009','P1000009','C1000009','FHA',6,160055,4.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('07-DEC-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000010','P1000010','C1000010','VA',2,217600,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('07-DEC-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000011','P1000010','C1000010','VA',6,217600,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('07-DEC-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000012','P1000010','C1000010','VA',6,111111,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('17-DEC-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000013','P1000010','C1000010','VA',2,222222,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-DEC-2016','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000014','P1000010','C1000010','Conventional',6,333333,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-JAN-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000015','P1000010','C1000010','Conventional',5,333333,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-FEB-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000016','P1000010','C1000010','Conventional',4,333333,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-MAR-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000017','P1000010','C1000010','FHA',4,444444,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-APR-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000018','P1000010','C1000010','FHA',6,200000,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-APR-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000019','P1000010','C1000010','FHA',6,300000,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('27-MAY-2017','DD-MON-YYYY'));
    Insert into LOANDETAILS values ('L1000000020','P1000010','C1000010','FHA',6,300000,7.5,to_date('30-NOV-2016','DD-MON-YYYY'),to_date('22-MAY-2017','DD-MON-YYYY'));
    

    Then, you can load the report table.
    Example 1 This assumes that only closed loans should be included in the loan-duration average, that the loan-duration is between LOANCREATIONDATE and LOANSTATUSDATE, and that you only want data for months and loan-types where loans were actually closed. This means july 2016 will not be included at all, since no loans were closed in that month.

    INSERT INTO LOANTYPEREPORT
      SELECT
        LOANDETAILS.LOANTYPE,
        EXTRACT(MONTH FROM LOANDETAILS.LOANSTATUSDATE) AS MONTH,
        EXTRACT(YEAR FROM LOANDETAILS.LOANSTATUSDATE) AS YEAR,
        SUM(LOANDETAILS.LOANAMOUNT) AS CLOSED_LOAN_AMOUNT,
        AVG(LOANDETAILS.LOANSTATUSDATE - LOANDETAILS.LOANCREATIONDATE) AS AVERAGE_LOAN_DURATION
      FROM LOANDETAILS
        WHERE LOANDETAILS.LOANSTATUSCODE = 6
      GROUP BY
        LOANDETAILS.LOANTYPE,
        EXTRACT(YEAR FROM LOANDETAILS.LOANSTATUSDATE),
        EXTRACT(MONTH FROM LOANDETAILS.LOANSTATUSDATE);
    

    Then see what it did:

    SELECT YEAR,MONTH,LOANTYPE,CLOSEDLOANSAMOUNT,AVERAGECLOSINGPERIOD
    FROM LOANTYPEREPORT
    ORDER BY YEAR, MONTH, LOANTYPE;
    
    
    YEAR  MONTH  LOANTYPE      CLOSEDLOANSAMOUNT  AVERAGECLOSINGPERIOD  
    2016  12     FHA           160055             7                     
    2016  12     VA            328711             12                    
    2017  1      Conventional  333333             58                    
    2017  4      FHA           200000             148                   
    2017  5      FHA           600000             175.5                 
    

    Example 2: But if you want to include data for months where no loans were closed for a given loan-type (I'm not sure but this might be the third item in your post), then you'll need to enumerate the months.
    You can do this several ways, but I'll just include an extra query in this example that sets boundaries for the for 2016 - 2018.

    Run the insert:

    INSERT INTO LOANTYPEREPORT
      WITH YEAR_MONTH AS(
         SELECT THE_MONTH.MONTH_NUMBER,
           THE_YEAR.YEAR_NUMBER
         FROM
           (SELECT LEVEL AS MONTH_NUMBER FROM DUAL CONNECT BY LEVEL < 13) THE_MONTH
           CROSS JOIN
           (SELECT YEAR_NUMBER FROM
             (SELECT LEVEL AS YEAR_NUMBER FROM DUAL CONNECT BY LEVEL < 2019)
           WHERE YEAR_NUMBER BETWEEN 2016 AND 2018) THE_YEAR
      )
      SELECT
        LOANTYPES.LOANTYPE,
        YEAR_MONTH.MONTH_NUMBER,
        YEAR_MONTH.YEAR_NUMBER,
        SUM(COALESCE(CLOSED_LOAN.LOANAMOUNT,0)) AS CLOSED_LOAN_AMOUNT,
        AVG(CLOSED_LOAN.LOAN_DURATION) AS AVERAGE_LOAN_DURATION
      FROM
        YEAR_MONTH
        CROSS JOIN LOANTYPES
          LEFT OUTER JOIN (SELECT EXTRACT(MONTH FROM LOANDETAILS.LOANSTATUSDATE) AS MONTH,
                                  EXTRACT(YEAR FROM LOANDETAILS.LOANSTATUSDATE) AS YEAR,
                             LOANDETAILS.LOANTYPE,
                             LOANDETAILS.LOANAMOUNT,
                             LOANDETAILS.LOANSTATUSDATE - LOANDETAILS.LOANCREATIONDATE AS LOAN_DURATION
                           FROM LOANDETAILS
      WHERE LOANDETAILS.LOANSTATUSCODE = 6) CLOSED_LOAN
        ON YEAR_MONTH.YEAR_NUMBER = CLOSED_LOAN.YEAR
    AND YEAR_MONTH.MONTH_NUMBER = CLOSED_LOAN.MONTH
        AND LOANTYPES.LOANTYPE = CLOSED_LOAN.LOANTYPE
        GROUP BY YEAR_NUMBER, MONTH_NUMBER, LOANTYPES.LOANTYPE
    ORDER BY YEAR_NUMBER, MONTH_NUMBER, LOANTYPES.LOANTYPE;
    

    And see what data is generated:

    YEAR  MONTH  LOANTYPE      CLOSEDLOANSAMOUNT  AVERAGECLOSINGPERIOD  
    2016  1      Conventional  0                                        
    2016  1      Employee      0                                        
    2016  1      FHA           0                                        
    2016  1      Reconstruct   0                                        
    2016  1      VA            0                                        
    2016  2      Conventional  0                                        
    ...
    ...
    ...
    2016  12     Conventional  0                                        
    2016  12     Employee      0                                        
    2016  12     FHA           160055             7                     
    2016  12     Reconstruct   0                                        
    2016  12     VA            328711             12                    
    2017  1      Conventional  333333             58                    
    2017  1      Employee      0                                        
    2017  1      FHA           0                      
    2017  4      FHA           200000             148                   
    2017  4      Reconstruct   0                                        
    2017  4      VA            0                                  
    2017  5      Conventional  0                                        
    2017  5      Employee      0                                        
    2017  5      FHA           600000             175.5                 
    2017  5      Reconstruct   0                                        
    2017  5      VA            0                                        
    2017  6      Conventional  0                                        
    2017  6      Employee      0                                        
    2017  6      FHA           0                               
    

    This way you get sum of closing amounts for loans closed each month of each type, or zero if none were closed.

    EDIT with an example of returning from a function.

    Just to reiterate, you do not need to use a function to do this kind of reporting.
    But if you have a requirement to use a function, here is an example:

    First, create your return type:

    CREATE TYPE LOAN_TYPE_MONTH_REPORT IS OBJECT (
      LOANTYPE VARCHAR2(20),
      MONTH number(2,0),
      YEAR NUMBER(4,0),
      CLOSEDLOANSAMOUNT NUMBER(15,2),
      AVERAGECLOSINGPERIOD NUMBER(5,2)
    );
    /
    
    CREATE TYPE LOAN_TYPE_MONTH_REPORT_LIST IS TABLE OF LOAN_TYPE_MONTH_REPORT;
    /
    

    Then create your function:

    CREATE FUNCTION GET_LOAN_TYPE_REPORT_FOR_MONTH(P_YEAR IN NUMBER, P_MONTH IN NUMBER)
      RETURN LOAN_TYPE_MONTH_REPORT_LIST
      IS
      V_MONTH_REPORT LOAN_TYPE_MONTH_REPORT_LIST;
      BEGIN
        SELECT
          LOAN_TYPE_MONTH_REPORT(
             THE_MONTH_YEAR.LOANTYPE,
             THE_MONTH_YEAR.THE_MONTH,
             THE_MONTH_YEAR.THE_YEAR,
             COALESCE(CLOSED_LOAN_SUMMARY.CLOSED_LOAN_AMOUNT,0),
             CLOSED_LOAN_SUMMARY.AVERAGE_LOAN_DURATION)
        BULK COLLECT INTO V_MONTH_REPORT
        FROM
          (SELECT P_YEAR AS THE_YEAR, P_MONTH AS THE_MONTH, LOANTYPES.LOANTYPE FROM LOANTYPES) THE_MONTH_YEAR
          LEFT OUTER JOIN
          (SELECT
             LOANDETAILS.LOANTYPE,
             EXTRACT(MONTH FROM LOANDETAILS.LOANSTATUSDATE) AS THE_MONTH,
             EXTRACT(YEAR FROM LOANDETAILS.LOANSTATUSDATE) AS THE_YEAR,
             SUM(LOANDETAILS.LOANAMOUNT) AS CLOSED_LOAN_AMOUNT,
             AVG(LOANDETAILS.LOANSTATUSDATE - LOANDETAILS.LOANCREATIONDATE) AS AVERAGE_LOAN_DURATION
           FROM LOANDETAILS
           WHERE LOANDETAILS.LOANSTATUSCODE = 6
           GROUP BY
             LOANDETAILS.LOANTYPE,
             EXTRACT(YEAR FROM LOANDETAILS.LOANSTATUSDATE),
             EXTRACT(MONTH FROM LOANDETAILS.LOANSTATUSDATE)) CLOSED_LOAN_SUMMARY
            ON THE_MONTH_YEAR.THE_YEAR = CLOSED_LOAN_SUMMARY.THE_YEAR
               AND THE_MONTH_YEAR.THE_MONTH = CLOSED_LOAN_SUMMARY.THE_MONTH
               AND THE_MONTH_YEAR.LOANTYPE = CLOSED_LOAN_SUMMARY.LOANTYPE;
        RETURN V_MONTH_REPORT;
      END;
      /
    

    Then test it: Here's a month with two loan-types with closings:

    SELECT * FROM TABLE(GET_LOAN_TYPE_REPORT_FOR_MONTH(2016,12));
    LOANTYPE      MONTH  YEAR  CLOSEDLOANSAMOUNT  AVERAGECLOSINGPERIOD  
    Employee      12     2016  0                                        
    VA            12     2016  328711             12                    
    Reconstruct   12     2016  0                                        
    FHA           12     2016  160055             7                     
    Conventional  12     2016  0                                      
    

    Or a month with just one loan-type with a closing:

    SELECT * FROM TABLE(GET_LOAN_TYPE_REPORT_FOR_MONTH(2017,01));
    LOANTYPE      MONTH  YEAR  CLOSEDLOANSAMOUNT  AVERAGECLOSINGPERIOD  
    Employee      1      2017  0                                        
    Reconstruct   1      2017  0                                        
    FHA           1      2017  0                                        
    VA            1      2017  0                                        
    Conventional  1      2017  333333             58