source: sql/reports.sql@ 2853f2e

Last change on this file since 2853f2e was bcacc91, checked in by Boris Gjorgjievski <boris@…>, 7 days ago

Phase 6: advanced reports as SQL, relational algebra and stored routines

sql/reports.sql adds five reporting routines over the project schema:
pass rate per subject and semester, student dossier, top student per
major, busiest professor per semester and semester-over-semester growth.
Top-1-per-group is done with NOT EXISTS so it maps directly onto a
relational-algebra difference, and payments are joined through
enrolled_semesters per the normalized model from phase 5.

docs/Ph6.md is the wiki page: every report with its SQL, relational
algebra, stored routine and the output it produces on data_load.sql.

  • Property mode set to 100644
File size: 12.4 KB
RevLine 
[bcacc91]1-- Phase 6 - advanced reports.
2-- Runs after schema_creation.sql and data_load.sql; every routine is created
3-- with OR REPLACE, so the script can be run repeatedly.
4
5SET search_path TO project;
6
7-- 1. Pass rate per subject and semester, compared to the subject's own average.
8CREATE OR REPLACE FUNCTION rep_subject_pass_rate()
9 RETURNS TABLE (
10 subject_code VARCHAR,
11 subject VARCHAR,
12 year INTEGER,
13 semester_type semester_type,
14 enrolled BIGINT,
15 signed_count BIGINT,
16 passed BIGINT,
17 pass_rate NUMERIC,
18 average_grade VARCHAR,
19 deviation VARCHAR
20 )
21 LANGUAGE sql
22 STABLE
23 SET search_path = project
24AS $$
25 WITH enrolled_per_semester AS (
26 SELECT ss.subjects_id AS subject_id,
27 es.semester_id AS semester_id,
28 COUNT(ss.id) AS enrolled,
29 COUNT(ps.id) AS passed,
30 ROUND(AVG(ps.grade::TEXT::INT), 2) AS average_grade
31 FROM semesters_subjects ss
32 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
33 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
34 GROUP BY ss.subjects_id, es.semester_id
35 ),
36 signed_per_semester AS (
37 SELECT ss.subjects_id AS subject_id,
38 es.semester_id AS semester_id,
39 COUNT(ss.id) AS signed_count
40 FROM semesters_subjects ss
41 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
42 WHERE ss.signature = TRUE
43 GROUP BY ss.subjects_id, es.semester_id
44 ),
45 subject_average AS (
46 SELECT ss.subjects_id AS subject_id,
47 ROUND(AVG(ps.grade::TEXT::INT), 2) AS subject_average
48 FROM semesters_subjects ss
49 JOIN passed_subjects ps ON ps.enrolled_id = ss.id
50 GROUP BY ss.subjects_id
51 )
52 SELECT s.code,
53 s.name,
54 a.year,
55 a.type,
56 eps.enrolled,
57 COALESCE(sps.signed_count, 0),
58 eps.passed,
59 ROUND(100.0 * eps.passed / eps.enrolled, 1),
60 COALESCE(CAST(eps.average_grade AS VARCHAR), 'нема оценки'),
61 COALESCE(CAST(eps.average_grade - sa.subject_average AS VARCHAR), 'n/a')
62 FROM enrolled_per_semester eps
63 JOIN subjects s ON s.id = eps.subject_id
64 JOIN active_semesters a ON a.id = eps.semester_id
65 LEFT JOIN signed_per_semester sps ON sps.subject_id = eps.subject_id
66 AND sps.semester_id = eps.semester_id
67 LEFT JOIN subject_average sa ON sa.subject_id = eps.subject_id
68 ORDER BY a.year, a.type, ROUND(100.0 * eps.passed / eps.enrolled, 1) DESC, s.code;
69$$;
70
71-- 2. Full dossier of a student: studies, money and documents in one row.
72-- p_user_id IS NULL returns every student.
73CREATE OR REPLACE FUNCTION rep_student_dossier(p_user_id INTEGER DEFAULT NULL)
74 RETURNS TABLE (
75 student_index VARCHAR,
76 student TEXT,
77 enrolled_semesters BIGINT,
78 completed_semesters BIGINT,
79 passed_subjects BIGINT,
80 pending_subjects BIGINT,
81 credits BIGINT,
82 average_grade VARCHAR,
83 total_paid BIGINT,
84 num_documents BIGINT,
85 documents_cost BIGINT
86 )
87 LANGUAGE sql
88 STABLE
89 SET search_path = project
90AS $$
91 WITH passed_stats AS (
92 SELECT es.user_id,
93 COUNT(ps.id) AS passed_subjects,
94 SUM(s.awarded_credits) AS credits,
95 ROUND(AVG(ps.grade::TEXT::INT), 2) AS average_grade
96 FROM enrolled_semesters es
97 JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
98 JOIN passed_subjects ps ON ps.enrolled_id = ss.id
99 JOIN subjects s ON s.id = ss.subjects_id
100 GROUP BY es.user_id
101 ),
102 semester_stats AS (
103 SELECT es.user_id,
104 COUNT(es.id) AS enrolled_semesters,
105 COUNT(es.completed) AS completed_semesters
106 FROM enrolled_semesters es
107 GROUP BY es.user_id
108 ),
109 pending_stats AS (
110 SELECT es.user_id,
111 COUNT(ss.id) AS pending_subjects
112 FROM enrolled_semesters es
113 JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
114 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
115 WHERE ps.id IS NULL
116 GROUP BY es.user_id
117 ),
118 payment_stats AS (
119 SELECT es.user_id,
120 SUM(p.amount) AS total_paid
121 FROM payment p
122 JOIN enrolled_semesters es ON es.id = p.enrollment_id
123 GROUP BY es.user_id
124 ),
125 document_stats AS (
126 SELECT ud.user_id,
127 COUNT(ud.document_id) AS num_documents,
128 SUM(d.cost) AS documents_cost
129 FROM user_documents ud
130 JOIN documents d ON d.id = ud.document_id
131 GROUP BY ud.user_id
132 )
133 SELECT u."index",
134 u.name || ' ' || u.surname,
135 COALESCE(sem.enrolled_semesters, 0),
136 COALESCE(sem.completed_semesters, 0),
137 COALESCE(ps.passed_subjects, 0),
138 COALESCE(pen.pending_subjects, 0),
139 COALESCE(ps.credits, 0),
140 COALESCE(CAST(ps.average_grade AS VARCHAR), 'нема оценки'),
141 COALESCE(pay.total_paid, 0),
142 COALESCE(doc.num_documents, 0),
143 COALESCE(doc.documents_cost, 0)
144 FROM users u
145 LEFT JOIN passed_stats ps ON ps.user_id = u.id
146 LEFT JOIN semester_stats sem ON sem.user_id = u.id
147 LEFT JOIN pending_stats pen ON pen.user_id = u.id
148 LEFT JOIN payment_stats pay ON pay.user_id = u.id
149 LEFT JOIN document_stats doc ON doc.user_id = u.id
150 WHERE u.role = 'student'
151 AND (p_user_id IS NULL OR u.id = p_user_id)
152 ORDER BY COALESCE(ps.credits, 0) DESC, COALESCE(ps.average_grade, 0) DESC, u."index";
153$$;
154
155-- 3. Best student of every study programme; ties broken deterministically.
156CREATE OR REPLACE FUNCTION rep_top_student_per_major()
157 RETURNS TABLE (
158 major VARCHAR,
159 student_index VARCHAR,
160 student TEXT,
161 passed_subjects BIGINT,
162 credits BIGINT,
163 average_grade NUMERIC
164 )
165 LANGUAGE sql
166 STABLE
167 SET search_path = project
168AS $$
169 WITH standing AS (
170 SELECT es.user_id,
171 es.major_id,
172 COUNT(ps.id) AS passed_subjects,
173 COALESCE(SUM(s.awarded_credits), 0) AS credits,
174 COALESCE(ROUND(AVG(ps.grade::TEXT::INT), 2), 0) AS average_grade
175 FROM enrolled_semesters es
176 LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
177 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
178 LEFT JOIN subjects s ON s.id = ss.subjects_id AND ps.id IS NOT NULL
179 GROUP BY es.user_id, es.major_id
180 )
181 SELECT m.name,
182 u."index",
183 u.name || ' ' || u.surname,
184 st.passed_subjects,
185 st.credits,
186 st.average_grade
187 FROM standing st
188 JOIN users u ON u.id = st.user_id
189 JOIN major m ON m.id = st.major_id
190 WHERE NOT EXISTS (
191 SELECT 1
192 FROM standing st1
193 WHERE st1.major_id = st.major_id
194 AND (st.credits < st1.credits
195 OR (st.credits = st1.credits AND st.average_grade < st1.average_grade)
196 OR (st.credits = st1.credits AND st.average_grade = st1.average_grade
197 AND st.user_id > st1.user_id))
198 )
199 ORDER BY m.name;
200$$;
201
202-- 4. Busiest professor of every active semester, including semesters nobody
203-- has enrolled in yet.
204CREATE OR REPLACE FUNCTION rep_busiest_professor()
205 RETURNS TABLE (
206 year INTEGER,
207 semester_type semester_type,
208 professor TEXT,
209 subjects_taught BIGINT,
210 enrolled_students BIGINT,
211 graded_students BIGINT
212 )
213 LANGUAGE sql
214 STABLE
215 SET search_path = project
216AS $$
217 WITH professor_load AS (
218 SELECT es.semester_id,
219 ss.professor_id,
220 COUNT(ss.id) AS enrolled_students,
221 COUNT(ps.id) AS graded_students,
222 COUNT(DISTINCT ss.subjects_id) AS subjects_taught
223 FROM semesters_subjects ss
224 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
225 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
226 GROUP BY es.semester_id, ss.professor_id
227 ),
228 busiest AS (
229 SELECT pl.*
230 FROM professor_load pl
231 WHERE NOT EXISTS (
232 SELECT 1
233 FROM professor_load pl1
234 WHERE pl1.semester_id = pl.semester_id
235 AND (pl.enrolled_students < pl1.enrolled_students
236 OR (pl.enrolled_students = pl1.enrolled_students
237 AND pl.graded_students < pl1.graded_students)
238 OR (pl.enrolled_students = pl1.enrolled_students
239 AND pl.graded_students = pl1.graded_students
240 AND pl.professor_id > pl1.professor_id))
241 )
242 )
243 SELECT a.year,
244 a.type,
245 COALESCE(u.name || ' ' || u.surname, 'n/a'),
246 COALESCE(b.subjects_taught, 0),
247 COALESCE(b.enrolled_students, 0),
248 COALESCE(b.graded_students, 0)
249 FROM active_semesters a
250 LEFT JOIN busiest b ON b.semester_id = a.id
251 LEFT JOIN users u ON u.id = b.professor_id
252 ORDER BY a.year, CASE a.type WHEN 'summer' THEN 1 ELSE 2 END;
253$$;
254
255-- 5. Change of activity between two consecutive semesters.
256CREATE OR REPLACE FUNCTION rep_semester_growth()
257 RETURNS TABLE (
258 semester TEXT,
259 previous_semester TEXT,
260 enrolments BIGINT,
261 subject_enrolments BIGINT,
262 passed BIGINT,
263 prev_subject_enrolments VARCHAR,
264 pct_change VARCHAR
265 )
266 LANGUAGE sql
267 STABLE
268 SET search_path = project
269AS $$
270 WITH ordered_semesters AS (
271 SELECT a.id,
272 a.year,
273 a.type,
274 a.year * 10 + CASE a.type WHEN 'summer' THEN 1 ELSE 2 END AS chrono
275 FROM active_semesters a
276 ),
277 semester_stats AS (
278 SELECT o.id AS semester_id,
279 o.year,
280 o.type,
281 o.chrono,
282 COUNT(DISTINCT es.id) AS enrolments,
283 COUNT(ss.id) AS subject_enrolments,
284 COUNT(ps.id) AS passed
285 FROM ordered_semesters o
286 LEFT JOIN enrolled_semesters es ON es.semester_id = o.id
287 LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
288 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
289 GROUP BY o.id, o.year, o.type, o.chrono
290 ),
291 consecutive AS (
292 SELECT cur.year, cur.type, cur.chrono,
293 cur.enrolments, cur.subject_enrolments, cur.passed,
294 prev.year AS prev_year,
295 prev.type AS prev_type,
296 prev.subject_enrolments AS prev_subject_enrolments
297 FROM semester_stats cur
298 LEFT JOIN semester_stats prev
299 ON prev.chrono < cur.chrono
300 AND NOT EXISTS (SELECT 1
301 FROM semester_stats mid
302 WHERE mid.chrono < cur.chrono
303 AND mid.chrono > prev.chrono)
304 )
305 SELECT c.year || '-' || c.type,
306 COALESCE(c.prev_year || '-' || c.prev_type, 'нема претходен'),
307 c.enrolments,
308 c.subject_enrolments,
309 c.passed,
310 COALESCE(CAST(c.prev_subject_enrolments AS VARCHAR), 'n/a'),
311 COALESCE(CAST(ROUND(((c.subject_enrolments - c.prev_subject_enrolments) * 100.0)
312 / NULLIF(c.prev_subject_enrolments, 0)) AS VARCHAR) || '%', 'n/a')
313 FROM consecutive c
314 ORDER BY c.chrono;
315$$;
316
317-- The same dossier as a stored procedure returning an open cursor, for callers
318-- that read the report row by row instead of as a result set.
319CREATE OR REPLACE PROCEDURE rep_student_dossier_cursor(
320 IN p_user_id INTEGER,
321 INOUT p_cursor REFCURSOR DEFAULT 'dossier')
322 LANGUAGE plpgsql
323 SET search_path = project
324AS $$
325BEGIN
326 OPEN p_cursor FOR SELECT * FROM rep_student_dossier(p_user_id);
327END;
328$$;
Note: See TracBrowser for help on using the repository browser.