I have written a standard SQL Select Query to select the zip code in which the largest number of sales were. I now need to convert it to an anonymous PL/SQL block, however I'm still very "green" with PL/SQL and really don't have much of an idea as to how to accomplish this. Also, I need to incorporate a LIMIT into the PL/SQL anonymous block that will only display the lowest numeric zip code in the event of a tie.
Here are the tables w/some data:
CREATE TABLE CUSTOMERS
(customerID INT PRIMARY KEY,
customerZip VARCHAR(15) NOT NULL);
CREATE TABLE SALES
(saleID INT PRIMARY KEY,
customerID INT,
CONSTRAINT SALES_FK1 FOREIGN KEY (customerID) REFERENCES CUSTOMERS(customerID));
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (1, '20636');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (2, '20619');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (3, '20670');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (4, '20670');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (5, '20636');
INSERT INTO SALES (saleID, customerID) VALUES (1, 1);
INSERT INTO SALES (saleID, customerID) VALUES (2, 2);
INSERT INTO SALES (saleID, customerID) VALUES (3, 3);
INSERT INTO SALES (saleID, customerID) VALUES (4, 4);
INSERT INTO SALES (saleID, customerID) VALUES (5, 5);
And here's the SQL query I wrote:
SELECT C.customerZip, COUNT (*) AS "MOST_SALES_byZIP"
FROM SALES S
INNER JOIN CUSTOMERS C
ON S.customerID = C.customerID
GROUP BY C.customerZip
HAVING COUNT (*) >= ALL
(SELECT COUNT(*)
FROM SALES S
INNER JOIN CUSTOMERS C
ON S.customerID = C.customerID
GROUP BY C.customerZip)
ORDER BY C.customerZip;
Basically, I first need to know how to "convert" this into a PL/SQL anonymous block. Then, I need to know how I can limit the results to only show the lowest numeric zip code if there is a tie between two or more.
I have an SQL fiddle Schema built here, if it helps: http://sqlfiddle.com/#!4/ca18bf/2
Thank you!
80% of good PL/SQL programming is good SQL coding.
In your problem: first, in SQL, to select the lowest numeric zip code from among those tied for most sales, you can do a join followed by aggregation by zip code, as you did already - and then use the aggregate LAST function. Like so:
select min(customerzip) keep (dense_rank last order by count(*)) as selected_zip
from sales inner join customers using (customerid)
group by customerzip
;
SELECTED_ZIP
---------------
20636
Now it is easy to use this in an anonymous block (if you have to - for whatever reason). SET SERVEROUTPUT ON is not part of the PL/SQL code; it is a command to the interface program, to instruct it to display the content of the output buffer on screen.
set serveroutput on
declare
selected_zip integer;
begin
select min(customerzip) keep (dense_rank last order by count(*))
INTO selected_zip -- this is the PL/SQL part!
from sales inner join customers using (customerid)
group by customerzip
;
dbms_output.put_line('Selected zip is: ' || selected_zip);
end;
/
PL/SQL procedure successfully completed.
Selected zip is: 20636
Here's an option. Create a function since an anonymous block can only print to STDOUT, it can't return something into a variable
The having clause is remove and simply order by count,zip so that top count wins then top count + top zip based on the order. Added in fetch first 1 rows ONLY to only get the 1 row then returned it from the function.
SQL> CREATE OR REPLACE FUNCTION getlowest RETURN NUMBER AS
l_ret NUMBER;
BEGIN
FOR r IN (
SELECT
c.customerzip,
COUNT(*) AS "MOST_SALES_byZIP"
FROM
sales s
INNER JOIN customers c ON s.customerid = c.customerid
GROUP BY
c.customerzip
order by
COUNT(*), c.customerzip
fetch first 1 rows ONLY
) LOOP
l_ret := r.customerzip;
END LOOP;
RETURN l_ret;
END;
/
SQL> show errors;
SQL>
SQL> select getlowest from dual
2 /
20619
SQL>
If the aim is to return a result set, then the PL/SQL block to do that would be
-- [Declare the host ref cursor according to the calling tool/language]
-- e.g. in SQL*Plus
var resultset refcursor
begin
open :resultset for
select c.customerzip, count(*) as most_sales_byzip
from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip
having count(*) >= all
( select count(*) from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip )
order by c.customerzip;
end;
From Oracle 12.1 onwards you can use implicit result sets:
declare
rc sys_refcursor;
begin
open rc for
open :resultset for
select c.customerzip, count(*) as most_sales_byzip
from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip
having count(*) >= all
( select count(*) from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip )
order by c.customerzip;
dbms_sql.return_result(rc);
end;
However we already have SQL to do this so it seems a bit pointless.
Related
There are around 120k records in the database, and based on a few functions I calculate scores for all the records, weekly I have to update the table with new records and respective scores.
Below is a procedure that I am using to merge data into the table:
create or replace procedure scorecalc
AS
score1 number;
score2 number;
score3 number;
CURSOR cur IS
SELECT Id_number from tableA;
r_num cur%ROWTYPE;
BEGIN
--OPEN cur;
FOR r_num IN cur
LOOP
select functionA(r_num.id_number),functionb(r_num.id_number),functionc(r_num.id_number) into score1, score2,score3 from dual;
Merge into scores A USING
(Select
r_num.id_number as ID, score1 as scorea, score2 as scoreb, score3 as scorec, TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM DUAL) B
ON ( A.ID = B.ID and A.scoredate = B.scoredate)
WHEN NOT MATCHED THEN
INSERT (
ID, scorea, scoreb, scorec, scoredate)
VALUES (
B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate)
WHEN MATCHED THEN
UPDATE SET
A.scorea = B.scorea,
A.scoreb = B.scoreb,
A.scorec = B.scorec;
COMMIT;
END LOOP;
END;
whereas functionA/ B/ C has complex queries, joins in it to calculate the score.
Please suggest me any way to improve the performance because currently with this snippet of code I am only able to insert some 2k records in 1 hour? Can I use parallel DML here?
Thank you!
Why are you doing this in a procedure? This could all be done via DML:
MERGE INTO scores a USING
(SELECT ta.id_number AS ID,
functionA(ta.id) AS scoreA,
functionB(ta.id) AS scoreB,
functionC(ta.id) AS scoreC,
TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM tableA ta) b
ON (a.id = b.id AND a.scoredate = b.scoredate)
WHEN MATCHED THEN UPDATE SET
a.scorea = b.scorea,
a.scoreb = b.scoreb,
a.scorec = b.scorec
WHEN NOT MATCHED THEN INSERT (ID, scorea, scoreb, scorec, scoredate)
VALUES (B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate);
If you want to try using PARALLEL hint after that, feel free. But you should definitely get rid of that cursor and stop doing "Slow-by-slow" processing.
Something I've had some success with has been inserting from a select statement. It is pretty performant as it doesn't involve row by row inserting.
In your case, I'm thinking it would be something like:
INSERT INTO table (ID, scorea, scoreb, scorec, scoredate)
SELECT functionA(id_number), functionB(id_number), functionC(id_number)
FROM tableA
An example of this can be found at the link below:
https://docs.oracle.com/cd/B12037_01/appdev.101/b10807/13_elems025.htm
To schedule it just put the statement #Del in a procedure block;
create or replace procedure Saturday_Night_Merge is
begin
<Put the merge statement here>
end Saturday_Night_Merge;
I'm currently migrating data from legacy system to the current system.
I have this INSERT statement inside a stored procedure.
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
SELECT LEGACY_ID PRIMARY_ID
, (SELECT COUNT(*) + 1
FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = L1.LEGACY_ID) SEQUENCE_ID
, L1.DESCR
FROM LEGACY_TABLE L1;
However, whenever I have multiple values of LEGACY_ID from LEGACY_TABLE, the query for the SEQUENCE_ID doesn't increment.
Why is this so? I can't seem to find any documentation on how the INSERT INTO SELECT statement works behind the scenes. I am guessing that it selects all the values from the table you are selecting and then inserts them simultaneously after, that's why it doesn't increment the COUNT(*) value?
What other workarounds can I do? I cannot create a SEQUENCE because the SEQUENCE_ID must be based on the number of PRIMARY_ID that are present. They are both primary ids.
Thanks in advance.
Yes, The SELECT will be executed FIRST and only then the INSERT happens.
A Simple PL/SQL block below, will be a simpler approach, though not efficient.
DECLARE
V_SEQUENCE_ID NUMBER;
V_COMMIT_LIMIT:= 20000;
V_ITEM_COUNT := 0;
BEGIN
FOR REC IN (SELECT LEGACY_ID,DESCR FROM LEGACY_TABLE)
LOOP
V_SEQUENCE_ID:= 0;
SELECT COUNT(*)+1 INTO V_SEQUENCE_ID FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = REC.LEGACY_ID
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
VALUES
(REC.LEGACY_ID,V_SEQUENCE_ID,REC.DESCR);
V_ITEM_COUNT := V_ITEM_COUNT + 1;
IF(V_ITEM_COUNT >= V_COMMIT_LIMIT)
THEN
COMMIT;
V_ITEM_COUNT := 0;
END IF;
END LOOP;
COMMIT;
END;
/
EDIT: Using CTE:
WITH TABLE_1_PRIM_CT(PRIMARY_ID, SEQUENCE_ID) AS
(
SELECT L1.LEGACY_ID,COUNT(*)
FROM LEGACY_TABLE L1
LEFT OUTER JOIN TABLE_1 T1
ON(L1.LEGACY_ID = T1.PRIMARY_ID)
GROUP BY L1.LEGACY_ID
)
INSERT INTO TABLE_1
(SELECT L1.LEGACY_ID,
CTE.SEQUENCE_ID+ (ROW_NUMBER() OVER (PARTITION BY L1.LEGACY_ID ORDER BY null)),
L1.DESCR
FROM TABLE_1_PRIM_CT CTE, LEGACY_TABLE L1
WHERE L1.LEGACY_ID = CTE.PRIMARY_ID);
PS: With your Millions of Rows, this is going to create a temp table
of same size, during execution. Do Explain Plan before actual execution.
Ok this is a tricky one (for me at least) lets say I have the following tables--
TABLES
ORDERS
create table orders (
ono number(5) not null primary key,
cno number(5) references customers,
eno number(4) references employees,
received date,
shipped date);
ODETAILS
create table odetails (
ono number(5) not null references orders,
pno number(5) not null references parts,
qty integer check(qty > 0),
primary key (ono,pno));
PARTS
create table parts(
pno number(5) not null primary key,
pname varchar2(30),
qoh integer check(qoh >= 0),
price number(6,2) check(price >= 0.0),
olevel integer);
TABLES DATA
insert into orders values
(1020,1111,1000,'10-DEC-11','12-DEC-11');
insert into orders values
(1021,1111,1000,'12-JAN-12','15-JAN-12');
insert into orders values
(1022,2222,1001,'13-FEB-12','20-FEB-12');
insert into orders values
(1023,3333,1000,'12-MAR-12',null);
insert into odetails values
(1020,10506,1);
insert into odetails values
(1020,10507,1);
insert into odetails values
(1020,10508,2);
insert into odetails values
(1020,10509,3);
insert into odetails values
(1021,10601,4);
insert into odetails values
(1022,10601,1);
insert into odetails values
(1022,10701,1);
insert into odetails values
(1023,10800,1);
insert into odetails values
(1023,10900,1);
insert into parts values
(10506,'Land Before Time I',200,19.99,20);
insert into parts values
(10507,'Land Before Time II',156,19.99,20);
insert into parts values
(10508,'Land Before Time III',190,19.99,20);
insert into parts values
(10509,'Land Before Time IV',60,19.99,20);
insert into parts values
(10601,'Sleeping Beauty',300,24.99,20);
insert into parts values
(10701,'When Harry Met Sally',120,19.99,30);
insert into parts values
(10800,'Dirty Harry',140,14.99,30);
insert into parts values
(10900,'Dr. Zhivago',100,24.99,30);
Now I'm required to create a procedure which takes in a value as month and generates a report which contains the following--
a. Number of sales
b. Sales value
c. Most popular item
d. Least popular item
My approach
CREATE OR REPLACE PROCEDURE TEST_REPORT
(MONTH_NUMBER IN NUMBER )
AS
PARTS_NUMBER VARCHAR2(10);
SHIPPING_STATUS VARCHAR2(10);
V_ENO VARCHAR2(5);
V_PNO VARCHAR2(5);
SALES NUMBER(30);
V_MONTH VARCHAR2(10);
BEGIN
SELECT RECEIVED INTO V_MONTH FROM ORDERS WHERE
WHERE EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER;
SELECT SUM(PRICE*QTY)
INTO SALES
FROM ORDERS,ODETAILS,PARTS
WHERE
EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER
END TEST_REPORT;
Then bang my head explodes. I was thinking of using cursors.. but then I thought a SELECT query with different column alias would be simpler. But as it seems I cant get the syntax right and currently If I execute this procedure it gives me a an error
ORA-01422: exact fetch returns more than requested number of rows
As there is more than one record in the table. What approach would be right and logical? and also did I get the syntax right for the procedures? I would appreciate the help and guidance.
Many thanks
(P.S. I condemn the person's naming convention in the script which I had to run to create the tables)
Well, let's see:
Number of sales (I'm assuming "of each part"):
SELECT od.PNO, COUNT(*) AS SALE_COUNT
FROM ODETAILS od
INNER JOIN ORDERS o
ON od.ONO = o.ONO
WHERE EXTRACT(MONTH FROM o.RECEIVED) = &MONTH_NUMBER
GROUP BY od.PNO
Sales value:
SELECT od.PNO, p.PRICE, SUM(od.QTY) AS SALES_QTY, SUM(od.QTY * p.PRICE) AS SALES_VALUE
FROM ODETAILS od
INNER JOIN PARTS p
ON p.PNO = od.PNO
GROUP BY od.PNO, p.PRICE
Most popular item (just take the first row):
SELECT od.PNO, SUM(od.QTY) AS TOTAL_QTY
FROM ODETAILS od
GROUP BY od.PNO
ORDER BY SUM(od.QTY) DESC
Least popular item (which has any sales) (just take the first row):
SELECT od.PNO, SUM(od.QTY) AS TOTAL_QTY
FROM ODETAILS od
GROUP BY od.PNO
ORDER BY SUM(od.QTY) ASC
Number of order cancellations and value, number of returns and values, item that has highest return: there does not appear to be data in the question (at this point) which would supply this information. I'll add a comment to the question to this effect. If further information or explanations are forthcoming I'll include this in my answer.
Best of luck.
Both these queries have issues.
Below query will return more than one record from ORDERS table. INTO clause will work only when the query returns exactly one record at a time.
SELECT RECEIVED INTO V_MONTH FROM ORDERS WHERE
WHERE EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER;
This query have 2 issues, no join between ORDERS, ODETAILS and PARTS as well as INTO clause. Here also it will return more than one record.
SELECT SUM(PRICE*QTY)
INTO SALES
FROM ORDERS,ODETAILS,PARTS
WHERE
EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER
END TEST_REPORT;
You need to define your requirement to suggest approach for your application.
I am quite new to Oracle and I have an issue I have been struggelig With for some hours.
sample:
Create Table Accounts (Id number(10),Balance number(16,3), Status Varchar2(50),Owner_Id number(10));
Create Table Transactions (Id number(10),Amount number(16,3), Trxn_date date, Account_Id number(10));
Create Table Owner (Id number(10), Firstname varchar2(50),Lastname varchar2(50));
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (1,1000,'OPEN',10);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (2,5000,'CLOSED',11);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (3,1000,'OPEN',12);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (4,5000,'CLOSED',13);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (5,1000,'OPEN',14);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (6,5000,'CLOSED',15);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (7,1000,'OPEN',16);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (8,5000,'CLOSED',17);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (9,1000,'OPEN',18);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (10,5000,'CLOSED',19);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (11,1000,'OPEN',20);
Insert Into Accounts(Id,Balance,Status,Owner_Id) Values (12,5000,'CLOSED',21);
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST1');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST2');
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST3');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST4');
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST5');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST6');
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST7');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST8');
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST9');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST10');
Insert Into Owner(Id,Firstname,Lastname) Values (10,'John','TEST11');
Insert Into Owner(Id,Firstname,Lastname) Values (11,'John','TEST12');
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (1,10,'02-FEB-2015',5);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (2,10,'02-APR-2015',5);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (3,10,'02-JUN-2015',5);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (4,10,'02-AUG-2015',5);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (5,10,'02-FEB-2015',2);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (6,10,'02-APR-2015',2);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (7,10,'02-JUN-2015',2);
Insert Into Transactions(Id,Amount,Trxn_Date,Account_Id) Values (8,10,'02-AUG-2015',2);
Data Check:
Select Unique(Account_Id) From Accounts A
Inner Join Owner B on B.ID=A.OWNER_ID
Inner Join Transactions I on I.ACCOUNT_ID=A.Id
Where I.Trxn_date Between '01-FEB-2015' and '01-JUL-2015'
And A.Status='CLOSED'
and A.Balance=5000;/*1 Row Returned*/
The Loop must exit at first Id returned
Declare
l_NewDate date:='01-FEB-2015';
l_OldDate date:='01-JUL-2015';
l_pID number(10);
Begin
For I in (Select Account_Id From Transactions
Where Trxn_date Between l_NewDate and l_OldDate)
Loop
Select Id Into l_pID From
(Select B.Id From Accounts A
Inner Join Owner B on A.Owner_Id = B.Id
Where A.Status = 'CLOSED' And A.Balance = 5000 And A.Id=I.Account_Id)
Where rownum < 2;
dbms_output.put_line(l_pID);
Exit;
End Loop;
End;
ORA-01403: No data found
ORA-06512: at line 12
I fail to understand why no data is found when the data check above clearly states otherwise.
Regards
J. Olsen
Like you say, your data check query:
Select Unique(Account_Id)
From Accounts A
Inner Join Owner B on B.ID=A.OWNER_ID
Inner Join Transactions I on I.ACCOUNT_ID=A.Id
Where I.Trxn_date Between '01-FEB-2015' and '01-JUL-2015'
And A.Status='CLOSED'
and A.Balance=5000;
... returns a single row with a single Account_Id value of 2.
But then, your PL/SQL code basically splits the logic in 2 queries. The query you loop on is:
Select Account_Id
From Transactions
Where Trxn_date Between '01-FEB-2015' and '01-JUL-2015'
And, when I run it, it returns:
5
5
5
2
2
2
Now the above's order is not guaranteed, as you don't have an ORDER BY clause. But if you get the results in the same order as me, then your first loop iteration will execute the next query using 5 as input:
Select *
From Accounts A
Inner Join Owner B on A.Owner_Id = B.Id
Where A.Status = 'CLOSED'
And A.Balance = 5000
And A.Id = 5
... which doesn't return any data, which is why you get your error.
If you would have been lucky enough to have started with the value of 2:
Select *
From Accounts A
Inner Join Owner B on A.Owner_Id = B.Id
Where A.Status = 'CLOSED'
And A.Balance = 5000
And A.Id = 2
... it would have worked as expected.
I wish I could recommend a proper solution, but I just don't truly understand what you are trying to do. But it certainly feels like you shouldn't need PL/SQL loops to do what you want. A simple query should be sufficient. Your data check query seems like a good start.
EDIT
For what it's worth, I think this is a more straight forward way of doing the exact same thing you are intending to do (no loops):
Declare
l_NewDate date:='01-FEB-2015';
l_OldDate date:='01-JUL-2015';
l_pID number(10);
Begin
select o.id into l_pID
from transactions t
join accounts a
on a.id = t.account_id
and a.status = 'CLOSED'
and a.balance = 5000
join owner o
on o.id = a.owner_id
where t.trxn_date between l_NewDate and l_OldDate
and rownum < 2;
dbms_output.put_line(l_pID);
End;
Below is the code for PL/SQL query for which I have to grab the names of the instructors who are qualified to teach a particular course when name is supplied along with number of times they have taught this course and the last time (year and term) when they taught this course.
I have done most part of the question, but couldn't figure out how to get the corresponding details of the max(o.co_year) i.e. term
declare
gname varchar2(20);
count_id number(2);
id varchar(20);
year1 number(4);
cursor abc // cursor 1
is
SELECT i.i_gname
into gname
FROM INSTRUCTOR I
WHERE i.i_id in (
SELECT t.i_id FROM TeachingQualification T
WHERE t.c_id in (SELECT c.c_id FROM COURSE C
WHERE c.c_title = 'Advanced Database App')) ;
cursor bcd // cursor two
is
select o.i_id, count(o.i_id), max(o.co_year)
into id, count_id, year1
from courseoffering o
where (o.i_id = i_id and o.c_id = 1234567)
group by o.i_id;
Begin
open abc;
open bcd;
loop
FETCH abc into gname;
exit when abc%NOTFOUND;
FETCH bcd into id, count_id, year1;
exit when bcd%NOTFOUND;
DBMS_OUTPUT.PUT_LINE ('NAME: ' || gname || ' Number of times taught ' || count_id || ' Year ' || year1 || ); // want to output corresponding column details for the year1 attribute.
end loop;
close bcd;
close abc;
end;
PL/SQL I want the corresponding column of MAX(O.CO_YEAR). How to do it?
Yeah I know, but its an university task which I'm suppose to be implemented in PL/SQL.
Blah! I'd rather that your university teaches you about the proper use of PLSQL and SQL, and gave a meaningful task. If you can do something in SQL, then do it in SQL.
Also: Why are your cursors containing the INTO keyword? Why do you need a loop? It seems you only ever expect 1 returned value?
I dissected your code a bit:
--instructors who are qualified to teach a course
SELECT i.i_gname --instructor name
FROM INSTRUCTOR I
WHERE i.i_id IN ( SELECT t.i_id --instructor_id
FROM TeachingQualification T
WHERE t.c_id in (SELECT c.c_id --course_id
FROM COURSE C
WHERE c.c_title = 'Advanced Database App'));
--instructors who taught a course, with amount and last time
SELECT o.i_id instructor_id, count(o.i_id) times_taught, max(o.co_year) last_time_taught
FROM courseoffering o
WHERE o.c_id = 1234567 --course_id
GROUP BY o.i_id;
--all instructors (ID) who taught advanced database app, how many times, and last time
--consider that this may produce NO_DATA_FOUND
SELECT o.i_id instructor_id, count(o.i_id) times_taught, max(o.co_year) last_time_taught
FROM course c
JOIN courseoffering o
ON c.c_id = o.c_id
WHERE c.c_title = 'Advanced Database App'
GROUP BY o.i_id;
-- i don't think teachingqualification is required. The last select providers instructor IDs.
-- There is no need to go through that table, unless it would contain extra data you'd want to
-- filter by. Since courseoffering is being queried, and it has instructors, it stands to reason
-- that those instructor are qualified to teach the course.
SELECT (SELECT i_gname FROM instructor WHERE i_id = o.i_id) instructor
,count(o.i_id) times_taught
, max(o.co_year) last_time_taught
FROM course c
JOIN courseoffering o
ON c.c_id = o.c_id
WHERE c.c_title = 'Advanced Database App'
GROUP BY o.i_id;
-- Look, if you do want a PLSQL block for this, go ahead.
BEGIN
FOR r IN (SELECT (SELECT i_gname FROM instructor WHERE i_id = o.i_id) instructor
,count(o.i_id) times_taught
, max(o.co_year) last_time_taught
FROM course c
JOIN courseoffering o
ON c.c_id = o.c_id
WHERE c.c_title = 'Advanced Database App'
GROUP BY o.i_id)
LOOP
DBMS_OUTPUT.PUT_LINE ('NAME: ' || r.instructor || ' Number of times taught ' || r.times_taught || ' Year ' || r.last_time_taught);
END LOOP;
END;
Oh, and please give your columns meaningful names. Call it instructor_id instead of i_id for example.
I set up some example data:
create table course (id number(5,0), cname varchar2(50), constraint course_pk primary key (id))
/
create table courseoffering(id number(5,0), course_id number(5,0), instructor_id number(5,0), course_year number(5,0), constraint offering_pk primary key (id), constraint course_fk foreign key (course_id) references course (id))
/
insert into course values (1, 'Tech I');
insert into course values (2, 'Basic SQL');
insert into course values (3, 'Advanced SQL');
--Instructor 1
insert into courseoffering values (1, 1, 1, 2009); --Tech I
insert into courseoffering values (2, 1, 1, 2010); --Tech I
insert into courseoffering values (3, 1, 1, 2011); --Tech I
insert into courseoffering values (4, 2, 1, 2011); --Basic SQL
insert into courseoffering values (5, 2, 1, 2012); --Basic SQL
--Instructor 2
insert into courseoffering values (6, 2, 2, 2008); --Basic SQL
insert into courseoffering values (7, 2, 2, 2009); --Basic SQL
insert into courseoffering values (8, 2, 2, 2010); --Basic SQL
insert into courseoffering values (9, 3, 2, 2010); --Advanced SQL
insert into courseoffering values (10, 3, 2, 2011); --Advanced SQL
insert into courseoffering values (11, 3, 2, 2012); --Advanced SQL
insert into courseoffering values (12, 1, 2, 2009); --Tech I
insert into courseoffering values (13, 1, 2, 2010); --Tech I
commit;
Running this:
SELECT c.cname, o.instructor_id, count(o.instructor_id) times_taught, max(o.course_year) last_time_taught
FROM course c
JOIN courseoffering o
ON c.id = o.course_id
GROUP BY c.cname, o.instructor_id
ORDER BY c.cname, o.instructor_id;
Produces:
CNAME INSTRUCTOR_ID TIMES_TAUGHT LAST_TIME_TAUGHT
Advanced SQL 2 3 2012
Basic SQL 1 2 2012
Basic SQL 2 3 2010
Tech I 1 3 2011
Tech I 2 2 2010
You can even easily turn the required data into a view.
No PLSQL required. Only a couple of lines in SQL. And if you want it in PLSQL you can still use a loop to cover multiple instructors per course, or if you narrow it to one course and one instructor, some variables. Always minimize switching between SQL and PLSQL contexts.
Cursors in Oracle can have parameters. See http://docs.oracle.com/cd/B19306_01/appdev.102/b14261/sqloperations.htm#BABHICAF for details.
How it might work in your code:
cursor bcd(p_c_id courseoffering.c_id%type) // cursor two
is
select o.i_id, count(o.i_id), max(o.co_year)
into id, count_id, year1
from courseoffering o
where (o.i_id = i_id and o.c_id = p_c_id)
group by o.i_id;