Complicated Crosstabbing in PostgreSQL involving Overlapping Dates - sql

I am trying to query the following data:
Student_ID Site Start End Primary_or_Secondary
1 A 1/1/19 2/28/19 Primary
1 B 2/1/19 6/30/19 Secondary
1 C 3/1/19 6/30/19 Primary
and get a result that looks like the following:
Student_ID Primary Secondary Start End
1 A null 1/1/19 1/31/19
1 A B 2/1/19 2/28/19
1 C B 3/1/19 6/30/19
So basically, a site can be primary or secondary site for a student, and I want to be able to see all the time frames the student are enrolled separately instead of any time frame overlapping.
I have wracked my brain about how I might do this in PostgreSQL, and have even looked at the crosstab function, but the dates are making it hard for my brain :-)
Any help with a query or set of queries, including some CTEs would be really helpful!

This isn't trivial. A mix of crosstab with overlapping and intersecting ranges, plus corner cases (merge same start / end dates) on top. Very hard to solve with set-based operations i.e. pure SQL.
I suggest a procedural solution in PL/pgSQL instead. Should perform nicely, too, as it only needs a single (bitmap-index) scan over the table:
CREATE OR REPLACE FUNCTION f_student_xtab(VARIADIC _student_ids int[])
RETURNS TABLE (
student_id int
, "primary" text
, secondary text
, start_date date
, end_date date) AS
$func$
DECLARE
r record;
BEGIN
student_id := -1; -- init with impossible value
FOR r IN
SELECT t.student_id, t.site, t.primary_or_secondary = 'Primary' AS prim, l.range_end, l.date
FROM tbl t
CROSS JOIN LATERAL (
VALUES (false, t.start_date)
, (true , t.end_date)
) AS l(range_end, date)
WHERE t.student_id = ANY (_student_ids)
ORDER BY t.student_id, l.date, range_end -- start of range first
LOOP
IF r.student_id <> student_id THEN
student_id := r.student_id;
IF r.prim THEN "primary" := r.site;
ELSE secondary := r.site;
END IF;
start_date := r.date;
ELSIF r.range_end THEN
IF r.date < start_date THEN
-- range already reported
IF r.prim THEN "primary" := NULL;
ELSE secondary := NULL;
END IF;
start_date := NULL;
ELSE
end_date := r.date;
RETURN NEXT;
IF r.prim THEN
"primary" := NULL;
IF secondary IS NULL THEN start_date := NULL;
ELSE start_date := r.date + 1;
END IF;
ELSE
secondary := NULL;
IF "primary" IS NULL THEN start_date := NULL;
ELSE start_date := r.date + 1;
END IF;
END IF;
end_date := NULL;
END IF;
ELSE -- range starts
IF r.date > start_date THEN
-- range already running
end_date := r.date - 1;
RETURN NEXT;
END IF;
start_date := r.date;
end_date := NULL;
IF r.prim THEN "primary" := r.site;
ELSE secondary := r.site;
END IF;
END IF;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM f_student_xtab(1,2,3);
Or:
SELECT * FROM f_student_xtab(VARIADIC '{1,2,3}');
db<>fiddle here - with extended test case
About VARIADIC:
Pass multiple values in single parameter
Return rows matching elements of input array in plpgsql function

Related

Nested Loops In Oracle Not Work Correctly (Exceeds the maximum)

I wrote These Two queries For Sample Table to insert into "z_exp14_resualt" From "z_exp14_main ". The First Query Works Correctly (Null) But Second That For rows have due_date on the main table (Not Null), not works correctly! . I Think Problem is The loop. It started before the due date and even goes beyond that . For NOT Nulls Insert must start from today(year and monts from sysdate and day from opening_date) and continue until due_date
Calculate For Null DUE_DATES
declare
i number := 1;
BEGIN
for i in 1..12 loop
insert into z_exp14_resualt
select dep_id,ADD_MONTHS(ADD_MONTHS(opening_date,trunc( months_between (sysdate,opening_date))),i),rate*balance
from z_exp14_main
WHERE due_date IS null;
end loop;
END;
/
And For Not Null Due_dates
DECLARE
diff number;
x number :=1;
BEGIN
for i in (select * FROM z_exp14_main WHERE due_date IS NOT null) loop
diff :=trunc( months_between (sysdate,i.opening_date));
WHILE (ADD_MONTHS(ADD_MONTHS(i.opening_date,diff),x)<i.due_date) LOOP
insert into z_exp14_resualt
select dep_id,ADD_MONTHS(ADD_MONTHS(i.opening_date,diff),x),rate*balance
from z_exp14_main WHERE due_date is not null ;
x :=x+1;
end loop;
end loop;
end;
/
sample Date On Main (z_exp14_main)
--
DEP_ID
DUE_DATE
BALANCE
RATE
OPENING_DATE
--
20056634
null
283428
10
15-SEP-16
--
20056637
null
180222
10
07-NOV-14
--
20056639
null
58741
10
28-AUG-14
--
40000020
27-NOV-21
5000000
22
31-MAR-14
--
40000023
23-APR-21
63000000
22
25-AUG-18
The Problem Is In the "While" condition. I correct it And This Is Final Answer:
DECLARE
diff number;
x number ;
BEGIN
for i in (select * FROM z_exp14_main WHERE due_date IS NOT null) loop
diff :=trunc( months_between (sysdate,i.opening_date));
x:=0;
WHILE (add_months(sysdate,x)<i.due_date) LOOP
insert into z_exp14_resualt values (i.dep_id,ADD_MONTHS(ADD_MONTHS(i.opening_date,diff),x),(i.rate*i.balance) );
x :=x+1;
end loop;
end loop;
end;
/

Is there an oracle spatial function for finding self-intersecting linestrings?

I need to find all self-intersecting linestrings in table. SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT finds only self-intersecting polygons, because self-intersecting linestrings are allowed. Any ideas?
It takes a bit of work, but it's doable.
Since Oracle(11.2) failed to provide, the only option we have is to brake the line into segments and use RELATE on the segments' pairs.
Following is my own implementation (used in production code for over 3 years, over millions of geometries). I chose the pipelined approach to cover for overly big or complex geometries.
Prerequisit 1, database type:
CREATE OR REPLACE TYPE ElemGeom as object
(eid integer, egeom mdsys.sdo_geometry, egtype integer, eelemnum integer, evertnum integer, earea number, elength number);
CREATE OR REPLACE TYPE ElemGeomTbl as table of ElemGeom;
Prerequisit 2, splitting function:
create or replace FUNCTION LineSegments (igeom in mdsys.sdo_geometry)
RETURN ElemGeomTbl pipelined
is
seg ElemGeom := ElemGeom(null,null,null,null,null,null,null);
cursor c is select T.id, T.X ,T.Y from table(SDO_UTIL.GETVERTICES(iGEOM)) T order by 1;
type ctbl is table of c%rowtype;
carr ctbl;
seg_geom mdsys.sdo_geometry;
cnt integer:=0;
segid integer; x1 number; y1 number; x2 number; y2 number;
begin
--if igeom.sdo_gtype not in (2002,2006)
--then... if you need to catch non-linears here...
--end if;
open c;
loop
fetch c
bulk collect into carr ;
for i in carr.first .. carr.last -1
loop cnt:=cnt+1;
segid := cnt;
x1 := carr(i).X; y1 := carr(i).Y;
x2 := carr(i+1).X; y2 := carr(i+1).Y;
seg_geom:= (mdsys.sdo_geometry(2002,2100,null
,mdsys.sdo_elem_info_array(1,2,1)
,mdsys.sdo_ordinate_array(x1,y1, x2,y2)));
seg.eid:=segid;
seg.egeom:=seg_geom;
seg.egtype:=seg_geom.sdo_gtype;
pipe row(seg);
end loop;
exit when c%notfound;
end loop;
close c;
end LineSegments;
You can test its output with something like (my GEOMs are SRID 2100):
with t1 as (
select
SDO_GEOMETRY(2002,2100,NULL,
SDO_ELEM_INFO_ARRAY(1,2,1),
SDO_ORDINATE_ARRAY(290161.697,4206385.413, 290161.901,4206388.095, 290162.684,4206385.188, 290163.188,4206388.041,
290163.51,4206385.22, 290164.357,4206388.159, 290166.879,4206387.108, 290161.397,4206387.366,
290166.331,4206386.067, 290165.763,4206388.052))
as G from DUAL
)
select * from t1,table(LineSegments(g));
And the main function:
create or replace FUNCTION validate_Line
(igeom in mdsys.sdo_geometry, itol in number default null)
RETURN varchar2
is
vtol number:= nvl(itol, 1/power(10,6));
verd1 varchar2(256); verd2 varchar2(256); v varchar2(256);
begin
verd1:= sdo_geom.validate_geometry_with_context(igeom,vtol);
for r1 in ( select a.eid seg1, a.egeom geom1, b.eid seg2, b.egeom geom2
from table(LineSegments(igeom)) a, table(LineSegments(igeom)) b
where a.eid < b.eid
order by a.eid, b.eid )
loop
--I hate outputting long words, so:
v:= replace(replace(sdo_geom.relate(r1.geom1,'determine',r1.geom2, vtol)
,'OVERLAPBDYDISJOINT','OVR-BDIS'),'OVERLAPBDYINTERSECT','OVR-BINT');
if instr('EQUAL,TOUCH,DISJOINT',v) = 0 then
verd2:= verd2|| case when verd2 is not null
then ', '||r1.seg1||'-'||r1.seg2||'='||v
else r1.seg1||'-'||r1.seg2||'='||v end;
end if;
end loop;
verd1:= nvl(verd1,'NULL')
|| case when verd1 ='TRUE' and verd2 is null then null
when verd1 ='TRUE' and verd2 is not null then ' *+: '||verd2
end;
return verd1;
end validate_Line;
And its test:
with t1 as (
select
SDO_GEOMETRY(2002,2100,NULL,
SDO_ELEM_INFO_ARRAY(1,2,1),
SDO_ORDINATE_ARRAY(290161.697,4206385.413, 290161.901,4206388.095, 290162.684,4206385.188, 290163.188,4206388.041,
290163.51,4206385.22, 290164.357,4206388.159, 290166.879,4206387.108, 290161.397,4206387.366,
290166.331,4206386.067, 290165.763,4206388.052))
as G from DUAL
)
select t1.*,validate_Line(g) from t1;
This returns:
*TRUE *+: 1-7=OVR-BDIS, 1-8=OVR-BDIS, 2-7=OVR-BDIS, 2-8=OVR-BDIS, 3-7=OVR-BDIS, 3-8=OVR-BDIS, 4-7=OVR-BDIS, 4-8=OVR-BDIS, 5-7=OVR-BDIS, 5-8=OVR-BDIS, 6-9=OVR-BDIS, 7-9=OVR-BDIS*
Of course, you can modify the output to be just a flag or anything else - this is just what suited my own needs.
HTH

Common Table Expressions: Relation does not exist

I am using postgresql version 10.3
I am working with a Common Table Expressions inside a function. The function code is following:
CREATE OR REPLACE FUNCTION subscriptions.to_supply_patients(
character varying,
timestamp with time zone)
RETURNS void
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
declare
aws_sub_pro alias for $1; -- health professional aws_sub
tour_date alias for $2; -- tour date to manage patients covered accounts
number_cover int; -- the number of account to manage
sub_list integer[]; -- array list for subscribers covered
no_sub_list integer[];-- array list for no subscribers covered
date_tour timestamp with time zone;--tour date to manage patients covered accounts converted with time zone converted to 23:59:59
begin
select subscriptions.convert_date_to_datetime((select date(tour_date)),'23:59:59','0 days') into date_tour; -- function to convert time to 23:59:59
select count(*) from subscriptions.cover where aws_sub = aws_sub_pro into number_cover; -- global number of patient to cover
if number_cover > 0 then
begin
if tour_date >= current_timestamp then -- if tour date is later than today date
begin
with cover_list as (
select id_cover from subscriptions.cover where aws_sub = aws_sub_pro and cover_duration >0 and is_covered_auto = true
-- we selectionned here the patients list to cover for health pro with aws_sub = aws_sub_pro
),
sub_cover_list as (
select id_subs from subscriptions.subscribers_cover inner join cover_list on cover_list.id_cover = subscribers_cover.id_cover
-- list of subscribers covered
),
no_sub_cover_list as (
select id_free_trial from subscriptions.no_subscribers_cover inner join cover_list on cover_list.id_cover = no_subscribers_cover.id_cover
-- list of no subscribers covered
)
-----------------------------------------------------------------------------------------------------------------------------
select array( select id_subs from subscriptions.subscribers_cover inner join cover_list on cover_list.id_cover = subscribers_cover.id_cover
) into sub_list; -- convert list of subscribers covered into array list
if array_upper(sub_list,1) <>0 then -- if array list is not empty
begin
for i in 1 .. array_upper (sub_list,1) loop -- for every one in this list
if date_tour = (select sub_deadline_date from subscriptions.subscription where id_subs =sub_list[i] ) then -- if tour date is equals to
-- the deadline date
begin
update subscriptions.subscription
set
sub_expiration_date = sub_expiration_date + interval'30 days', -- add 30 days to the exp date
sub_deadline_date = sub_deadline_date + interval'30 days', -- add 30 date to deadline date
sub_source = aws_sub_pro, -- supply source is no the professional
is_sub_activ = false -- we turn off patients subscription
where id_subs = (select id_subs from subscriptions.subscription where id_subs =sub_list[i] );
end;
end if;
end loop;
end;
end if;
--------------------------------------------------------------------------------------------------------------------------------
select array(select id_free_trial from subscriptions.no_subscribers_cover inner join cover_list on cover_list.id_cover = no_subscribers_cover.id_cover
) into no_sub_list;
if array_upper(no_sub_list,1) <>0 then
begin
for i in 1 .. array_upper (no_sub_list,1) loop
if date_tour = (select expiration_date from subscriptions.free_trial where id_free_trial =no_sub_list[i] and is_free_trial_activ = true ) then
begin
update subscriptions.free_trial
set
expiration_date = expiration_date + interval'30 days'
where id_free_trial = (select id_free_trial from subscriptions.free_trial where id_free_trial =no_sub_list[i] );
end;
end if;
end loop;
end;
end if;
end;
else
raise 'tour date must be later than today''s date' using errcode='71';
end if;
end;
else
raise notice 'your cover list is empty. you don''t have any patient to cover' using errcode='70';
end if;
end;
$BODY$;
ALTER FUNCTION subscriptions.to_supply_patients(character varying, timestamp with time zone)
OWNER TO master_pgsql_hygeexv2;
When I run this function, I get the following error:
ERROR: ERREUR: la relation « cover_list » n'existe pas
LINE 1: ...rom subscriptions.no_subscribers_cover inner join cover_list...
Translated:
(the relation « cover_list » does not exist )
I tried to run only the CTE in a query window and I get the same error message.
Is there something I am missing?
The CTE is part of the SQL statement and not visible anywhere outside of it.
So you can use cover_list only in the SELECT statement with the WITH clause.
Either repeat the WITH clause in the second SELECT statement or refractor the code so you need only a single query.
An alternative would be to use a temporary table.

Check all the value in table and return one value oracle

I have below query which i want use in stored procedure and which is returning the status with value 0 or 1 or 2.
CREATE OR REPLACE PROCEDURE TEST_CHECK
(NAME IN VARCHAR2, IN_ID IN NUMBER,
IN_STATUS OUT NUMBER)
AS
CURSOR CHK_STATUS IS
select STATUS
from TEST_LOG
where NAME = 'TT'
and ID = 19
and CHK_DATE >= to_date( TRUNC ( SYSDATE - 1 , 'MM' ) , 'YYYYMMDD' )
and CHK_DATE < to_date ( TRUNC ( SYSDATE), 'YYYYMMDD' );
BEGIN
FOR CHK_STAT IN CHK_STATUS
LOOP
IF CHK_STAT.STATUS = 0
THEN
IN_STATUS := 0;
ELSE
IF CHK_STAT.STATUS = 1
THEN
IN_STATUS := 1;
ELSE
IN_STATUS := 2;
END IF;
END IF;
END LOOP;
END TEST_CHECK;
But I want to use logic such that if any of the status from this query returns 2 then it should return as 2. If all the status returns 0 then only return 0. If all the status return 1 then only return 1. If status return between 0 and 1 then return 1. If status return between 1 and 2 then return 2.
So instead of returning too many values I want to restrict the result to only one value status which I want to pass as OUT paramter to another function. I have written above procedure but it's not giving expected output. I am not sure whether I need to use cursor or writing just query instead will work.
Just use CEIL function when returning your value, i.e. instead of this statement :
IF (CHK_STAT.STATUS =0)
THEN
IN_STATUS := 0;
else if (CHK_STAT.STATUS =1)
THEN
IN_STATUS := 1;
else
IN_STATUS := 2;
END IF;
Just use
IN_STATUS := CEIL(CHK_STAT.STATUS);
CEIL means ceiling as name implies, rounds up to consecutive bigger integer,
i.e. ceil(0)->0, ceil(0.5)->1, ceil(1)->1, ceil(1.3)->2, ceil(2)->2
It sounds like you just want the biggest status returned as an integer so something like this should work:
Select MAX(CEIL(STATUS))
FROM TEST_LOG
where SYS_DB_NAME = 'TT' and ENTITY_ID = 19
AND CHK_DATE >= to_date( TRUNC ( SYSDATE - 1 , 'MM' ) , 'YYYYMMDD' )
AND CHK_DATE < to_date ( TRUNC ( SYSDATE), 'YYYYMMDD' );
I think you can also use the code below,
FOR CHK_STAT IN CHK_STATUS
LOOP
IF CHK_STAT.STATUS > IN_STATUS OR IN_STATUS IS NULL THEN
IN_STATUS := CEIL(CHK_STAT.STATUS);
END IF;
END LOOP;

PL/SQL counting by looping?

I have an assignment asking me to rewrite this PL/SQL code I wrote for a previous assignment:
DECLARE
-- Variables used to count a, b, c, d, and f grades:
na integer := 0;
nb integer := 0;
nc integer := 0;
nd integer := 0;
nf integer := 0;
BEGIN
select count(*) into na
from gradeReport1
where grade = 'A';
select count(*) into nb
from gradeReport1
where grade = 'B';
select count(*) into nc
from gradeReport1
where grade = 'C';
select count(*) into nd
from gradeReport1
where grade = 'D';
select count(*) into nf
from gradeReport1
where grade = 'F';
if na > 0 then
DBMS_OUTPUT.PUT_LINE('There are total ' || na || ' A''s');
else
DBMS_OUTPUT.PUT_LINE('There are no As');
end if;
if nb > 0 then
DBMS_OUTPUT.PUT_LINE('There are total ' || nb || ' B''s');
else
DBMS_OUTPUT.PUT_LINE('There are no Bs');
end if;
if nc > 0 then
DBMS_OUTPUT.PUT_LINE('There are total ' || nc || ' C''s');
else
DBMS_OUTPUT.PUT_LINE('There are no Cs');
end if;
if nd > 0 then
DBMS_OUTPUT.PUT_LINE('There are total ' || nd || ' D''s');
else
DBMS_OUTPUT.PUT_LINE('There are no Ds');
end if;
if nf > 0 then
DBMS_OUTPUT.PUT_LINE('There are total ' || nf || ' F''s');
else
DBMS_OUTPUT.PUT_LINE('There are no Fs');
end if;
END;
All it does is search a table I made called gradeReport that stores studentID's and associates them with a grade. The PL/SQL counts all instances of a grade A through F. The question wants me to rewrite this solution using looping and VARRAYS. Could anyone give me a hint to help get the ball rolling for me? I've only been using PL/SQL for a few weeks and don't have much more than a basic understanding of the syntax so I'm completely lost and have no idea where to start.
Not looking for any answers here, just some ideas.
Thank You
How about starting with the doc. http://docs.oracle.com/database/122/LNPLS/plsql-control-statements.htm#LNPLS004
DECLARE
-- Need to ensure the array size will hold all the grades
TYPE grade_tab IS VARRAY(200) OF gradeReport1.grade%TYPE;
-- variable used to store the grades:
t_grades grade_tab;
-- Variables used to count a, b, c, d, and f grades:
na INTEGER;
nb INTEGER;
nc INTEGER;
nd INTEGER;
nf INTEGER;
BEGIN
-- Store the grades in an array:
SELECT grade
BULK COLLECT INTO t_grades
FROM gradeReport1
WHERE grade IN ( 'A', 'B', 'C', 'D', 'F' );
-- Loop through the grades and count how many of each:
FOR i IN 1 .. t_grades.COUNT LOOP
IF t_grades(i) = 'A' THEN na := na + 1;
ELSIF t_grades(i) = 'B' THEN nb := nb + 1;
ELSIF t_grades(i) = 'C' THEN nc := nc + 1;
ELSIF t_grades(i) = 'D' THEN nd := nd + 1;
ELSIF t_grades(i) = 'F' THEN nf := nf + 1;
END IF;
END LOOP;
-- Output grade counts
END;
/
However, a much simpler solution would be to do the counting in a single SQL query (although this doesn't meet the assessment's requirements of using a VARRAY):
DECLARE
-- Variables used to count a, b, c, d, and f grades:
na INTEGER;
nb INTEGER;
nc INTEGER;
nd INTEGER;
nf INTEGER;
BEGIN
SELECT COUNT( CASE grade WHEN 'A' THEN 1 END ),
COUNT( CASE grade WHEN 'B' THEN 1 END ),
COUNT( CASE grade WHEN 'C' THEN 1 END ),
COUNT( CASE grade WHEN 'D' THEN 1 END ),
COUNT( CASE grade WHEN 'F' THEN 1 END )
INTO na,
nb,
nc,
nd,
nf
FROM gradeReport1;
-- Output grade counts...
END;
/
Edit: as the requirement is specifically for varrays, see replies by AmmoQ and MTO. As they both point out, though, you'd be unlikely to need arrays for this type of task in practice, and even if you did, you would use a nested table or an associative array and not a varray.
You'll want a Cursor FOR loop, along the lines of
for r in (
select grade from gradereport1
)
loop
...
end loop;
In real code you'd probably make that a group by query and have SQL do the counting for you.
Then just conditionally increment the counters in the loop depending in the value of r.grade.
You can rationalise all of the if statements for reporting the totals by writing a procedure that takes a grade and a total, as the logic is the same for all of them.
procedure showgrade
( p_grade gradereport1.grade%type
, p_count integer )
is
begin
...
end showgrade;
I'll leave the details as an exercise.
Just for fun, here is another approach, using arrays and looping (but not a varray - they really are a bit useless):
declare
type gradereport_tt is table of pls_integer index by gradereport.grade%type;
gradecounts gradereport_tt;
g gradereport.grade%type;
begin
-- Initialise counts:
gradecounts('A') := 0;
gradecounts('B') := 0;
gradecounts('C') := 0;
gradecounts('D') := 0;
gradecounts('E') := 0;
gradecounts('F') := 0;
-- Count grades:
for r in (
select grade from gradereport
)
loop
gradecounts(r.grade) := gradecounts(r.grade) +1;
end loop;
-- Report counts:
g := gradecounts.first;
while g is not null loop
dbms_output.put_line(g || ': ' || gradecounts(g));
g := gradecounts.next(g);
end loop;
end;
btw there is no need to put brackets after if as in some other languages, unless the condition contains a mixture of and and or conditions that need separating.
There is also no need to write anything in uppercase. It's quite common and Steven Feuerstein does it all the time, but they had this debate in the HTML/CSS world and settled on lowercase for readability. And if you are going to have an uppercase rule, at least use it consistently. Your code example has end if; but END; not to mention Select (which I've fixed). Some people seem to be able to read code like this without it driving them nuts, but I'm afraid I'm not one of them.
set SERVEROUTPUT ON
declare
type number_array is VARRAY(5) OF integer;
total integer :=0;
i number :=1;
begin
numbers :=number_array(14,45,67,89,21);
arr_size := numbers.count;
FOR i in 1..arr_size loop
total :=total+numbers(i);
end loop;
dbms_output.put_line('total-' || total);
end;
here I want to count the number_array elements, but I can't get the correct answers. what is the problem with this?
A solution using VARRAYs could look like that:
DECLARE
type chararray IS VARRAY(6) OF CHAR(1);
type numarray IS VARRAY(6) OF INTEGER;
grades chararray;
cnt numarray;
BEGIN
select grade, count(*)
bulk collect into grades, cnt
from gradeReport1
group by grade
order by grade;
for i in 1..grades.count loop
DBMS_OUTPUT.PUT_LINE('There are total ' || cnt(i) || ' ' ||grades(i)||'s');
end loop;
END;
/
But honestly, it's pointless to use VARRAYs in that case. Just use a cursor loop:
BEGIN
for c in ( select grade, count(*) cnt
from gradeReport1
group by grade
order by grade ) loop
DBMS_OUTPUT.PUT_LINE('There are total ' || c.cnt || ' ' ||c.grade||'s');
end loop;
END;
/
Finding the missing marks (those with a count of 0) is a bit more difficult, though.