source: docs/Ph6.md

Last change on this file 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: 35.8 KB
Line 
1== Напредни извештаи од базата (SQL, складирани процедури и релациона алгебра)
2
3Сите извештаи подолу се напишани и тестирани врз шемата '''project''' на доделената
4проектна база, врз податоците од [attachment:data_load.sql].
5
6Секој извештај е даден во три форми:
7
8 * '''SQL''' - барањето како што се извршува врз базата
9 * '''Релациона алгебра''' - истото барање изразено со операторите на релациона алгебра
10 * '''Складирана процедура''' - барањето спакувано како рутина во базата, за да апликацијата го повикува со едно име наместо да го носи барањето во кодот
11
12Сите рутини се во скриптата [attachment:reports.sql], која се стартува по
13schema_creation.sql и data_load.sql. Скриптата е повторлива - секоја рутина се
14креира со OR REPLACE.
15
16Заедничка забелешка за сите извештаи: оценката е од набројувачки тип (grade_type),
17па за пресметка на просек мора двојно да се претвори - {{{ps.grade::TEXT::INT}}}.
18
19== 1. Проодност по предмет и по семестар со отстапување од просекот на предметот
20
21=== Опис на извештајот
22
23За секој предмет и секој семестар во кој тој бил слушан се прикажува колку студенти
24го запишале, колку добиле потпис, колку го положиле, процентот на проодност и
25просечната оценка. Последната колона покажува колку просекот во тој семестар
26отстапува од вкупниот просек на предметот, што покажува дали предметот во определен
27семестар бил полесен или потежок од вообичаеното.
28
29=== SQL
30
31{{{#!sql
32SET search_path TO project;
33
34WITH enrolled_per_semester AS (
35 SELECT ss.subjects_id AS subject_id,
36 es.semester_id AS semester_id,
37 COUNT(ss.id) AS enrolled,
38 COUNT(ps.id) AS passed,
39 ROUND(AVG(ps.grade::TEXT::INT), 2) AS average_grade
40 FROM semesters_subjects ss
41 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
42 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
43 GROUP BY ss.subjects_id, es.semester_id
44),
45signed_per_semester AS (
46 SELECT ss.subjects_id AS subject_id,
47 es.semester_id AS semester_id,
48 COUNT(ss.id) AS signed_count
49 FROM semesters_subjects ss
50 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
51 WHERE ss.signature = TRUE
52 GROUP BY ss.subjects_id, es.semester_id
53),
54subject_average AS (
55 SELECT ss.subjects_id AS subject_id,
56 ROUND(AVG(ps.grade::TEXT::INT), 2) AS subject_average
57 FROM semesters_subjects ss
58 JOIN passed_subjects ps ON ps.enrolled_id = ss.id
59 GROUP BY ss.subjects_id
60)
61SELECT s.code AS subject_code,
62 s.name AS subject,
63 a.year,
64 a.type AS semester_type,
65 eps.enrolled,
66 COALESCE(sps.signed_count, 0) AS signed_count,
67 eps.passed,
68 ROUND(100.0 * eps.passed / eps.enrolled, 1) AS pass_rate,
69 COALESCE(CAST(eps.average_grade AS VARCHAR), 'нема оценки') AS average_grade,
70 COALESCE(CAST(eps.average_grade - sa.subject_average AS VARCHAR), 'n/a') AS deviation
71FROM enrolled_per_semester eps
72JOIN subjects s ON s.id = eps.subject_id
73JOIN active_semesters a ON a.id = eps.semester_id
74LEFT JOIN signed_per_semester sps ON sps.subject_id = eps.subject_id
75 AND sps.semester_id = eps.semester_id
76LEFT JOIN subject_average sa ON sa.subject_id = eps.subject_id
77ORDER BY a.year, a.type, pass_rate DESC, s.code;
78}}}
79
80=== Релациона алгебра
81
82{{{
83EnrolledPerSemester <-
84γ subject_id := ss.subjects_id,
85 semester_id := es.semester_id;
86 enrolled := COUNT(ss.id),
87 passed := COUNT(ps.id),
88 average_grade := AVG(ps.grade)
89(
90 (
91 semesters_subjects ss
92 ⨝ (ss.enrolled_semesters_id = es.id) enrolled_semesters es
93 )
94 ⟕ (ss.id = ps.enrolled_id) passed_subjects ps
95)
96
97SignedPerSemester <-
98γ subject_id := ss.subjects_id,
99 semester_id := es.semester_id;
100 signed_count := COUNT(ss.id)
101(
102 σ ss.signature = TRUE
103 (
104 semesters_subjects ss
105 ⨝ (ss.enrolled_semesters_id = es.id) enrolled_semesters es
106 )
107)
108
109SubjectAverage <-
110γ subject_id := ss.subjects_id;
111 subject_average := AVG(ps.grade)
112(
113 semesters_subjects ss ⨝ (ss.id = ps.enrolled_id) passed_subjects ps
114)
115
116Result <-
117τ a.year, a.type, pass_rate DESC, s.code
118(
119 π s.code,
120 s.name,
121 a.year,
122 a.type,
123 eps.enrolled,
124 sps.signed_count,
125 eps.passed,
126 pass_rate := (eps.passed * 100) / eps.enrolled,
127 eps.average_grade,
128 deviation := eps.average_grade − sa.subject_average
129 (
130 (
131 (
132 (
133 EnrolledPerSemester eps ⨝ (eps.subject_id = s.id) subjects s
134 )
135 ⨝ (eps.semester_id = a.id) active_semesters a
136 )
137 ⟕ (eps.subject_id = sps.subject_id ∧
138 eps.semester_id = sps.semester_id) SignedPerSemester sps
139 )
140 ⟕ (eps.subject_id = sa.subject_id) SubjectAverage sa
141 )
142)
143}}}
144
145=== Складирана процедура
146
147{{{#!sql
148CREATE OR REPLACE FUNCTION rep_subject_pass_rate()
149 RETURNS TABLE (
150 subject_code VARCHAR,
151 subject VARCHAR,
152 year INTEGER,
153 semester_type semester_type,
154 enrolled BIGINT,
155 signed_count BIGINT,
156 passed BIGINT,
157 pass_rate NUMERIC,
158 average_grade VARCHAR,
159 deviation VARCHAR
160 )
161 LANGUAGE sql
162 STABLE
163 SET search_path = project
164AS $$
165 -- барањето прикажано погоре
166$$;
167
168SELECT * FROM rep_subject_pass_rate();
169}}}
170
171Функцијата е означена како STABLE бидејќи само чита, и има сопствена патека
172{{{SET search_path = project}}}, па повикувачот не мора однапред да ја постави.
173
174=== Резултат врз тест податоците
175
176{{{
177 subject_code | subject | year | semester_type | enrolled | signed_count | passed | pass_rate | average_grade | deviation
178--------------+-----------------------------+------+---------------+----------+--------------+--------+-----------+---------------+-----------
179 F18L1S001 | Structured Programming | 2024 | winter | 3 | 3 | 3 | 100.0 | 8.67 | 0.00
180 F18L2S011 | Operating Systems | 2024 | winter | 1 | 1 | 1 | 100.0 | 8.00 | 0.00
181 F18L2S010 | Databases | 2024 | winter | 1 | 0 | 0 | 0.0 | нема оценки | n/a
182 F18L1S002 | Object Oriented Programming | 2025 | summer | 1 | 0 | 0 | 0.0 | нема оценки | n/a
183}}}
184
185== 2. Досие на студент - студии, финансии и документи во еден ред
186
187=== Опис на извештајот
188
189За секој студент се собираат податоци кои инаку се расфрлани низ пет различни
190табели: колку семестри запишал и колку завршил, колку предмети положил, колку му
191остануваат неположени, колку кредити собрал, каков просек има, колку вкупно платил
192и колку документи подигнал. Секој од овие броеви се пресметува во посебен
193подизраз, а потоа сите се спојуваат со надворешно спојување, за студент кој нема
194ниту еден запис во некоја од табелите сепак да се појави во извештајот.
195
196=== SQL
197
198{{{#!sql
199SET search_path TO project;
200
201WITH passed_stats AS (
202 SELECT es.user_id,
203 COUNT(ps.id) AS passed_subjects,
204 SUM(s.awarded_credits) AS credits,
205 ROUND(AVG(ps.grade::TEXT::INT), 2) AS average_grade
206 FROM enrolled_semesters es
207 JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
208 JOIN passed_subjects ps ON ps.enrolled_id = ss.id
209 JOIN subjects s ON s.id = ss.subjects_id
210 GROUP BY es.user_id
211),
212semester_stats AS (
213 SELECT es.user_id,
214 COUNT(es.id) AS enrolled_semesters,
215 COUNT(es.completed) AS completed_semesters
216 FROM enrolled_semesters es
217 GROUP BY es.user_id
218),
219pending_stats AS (
220 SELECT es.user_id,
221 COUNT(ss.id) AS pending_subjects
222 FROM enrolled_semesters es
223 JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
224 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
225 WHERE ps.id IS NULL
226 GROUP BY es.user_id
227),
228payment_stats AS (
229 SELECT es.user_id,
230 SUM(p.amount) AS total_paid
231 FROM payment p
232 JOIN enrolled_semesters es ON es.id = p.enrollment_id
233 GROUP BY es.user_id
234),
235document_stats AS (
236 SELECT ud.user_id,
237 COUNT(ud.document_id) AS num_documents,
238 SUM(d.cost) AS documents_cost
239 FROM user_documents ud
240 JOIN documents d ON d.id = ud.document_id
241 GROUP BY ud.user_id
242)
243SELECT u."index" AS student_index,
244 u.name || ' ' || u.surname AS student,
245 COALESCE(sem.enrolled_semesters, 0) AS enrolled_semesters,
246 COALESCE(sem.completed_semesters, 0) AS completed_semesters,
247 COALESCE(ps.passed_subjects, 0) AS passed_subjects,
248 COALESCE(pen.pending_subjects, 0) AS pending_subjects,
249 COALESCE(ps.credits, 0) AS credits,
250 COALESCE(CAST(ps.average_grade AS VARCHAR), 'нема оценки') AS average_grade,
251 COALESCE(pay.total_paid, 0) AS total_paid,
252 COALESCE(doc.num_documents, 0) AS num_documents,
253 COALESCE(doc.documents_cost, 0) AS documents_cost
254FROM users u
255LEFT JOIN passed_stats ps ON ps.user_id = u.id
256LEFT JOIN semester_stats sem ON sem.user_id = u.id
257LEFT JOIN pending_stats pen ON pen.user_id = u.id
258LEFT JOIN payment_stats pay ON pay.user_id = u.id
259LEFT JOIN document_stats doc ON doc.user_id = u.id
260WHERE u.role = 'student'
261ORDER BY COALESCE(ps.credits, 0) DESC, COALESCE(ps.average_grade, 0) DESC, u."index";
262}}}
263
264Подредувањето намерно оди по нумеричките колони од подизразите, а не по излезните
265колони. Излезната колона average_grade е претворена во текст заради вредноста
266'нема оценки', па подредувањето по неа би било азбучно и оценката 9.00 би излегла
267пред 10.00.
268
269Плаќањата се врзуваат за студентот преку enrolled_semesters, а не преку
270payment.user_id, согласно нормализираниот модел од Фаза 5 во кој таа колона е
271отстранета.
272
273=== Релациона алгебра
274
275{{{
276PassedStats <-
277γ user_id := es.user_id;
278 passed_subjects := COUNT(ps.id),
279 credits := SUM(s.awarded_credits),
280 average_grade := AVG(ps.grade)
281(
282 (
283 (enrolled_semesters es ⨝ (es.id = ss.enrolled_semesters_id) semesters_subjects ss)
284 ⨝ (ss.id = ps.enrolled_id) passed_subjects ps
285 )
286 ⨝ (ss.subjects_id = s.id) subjects s
287)
288
289SemesterStats <-
290γ user_id := es.user_id;
291 enrolled_semesters := COUNT(es.id),
292 completed_semesters := COUNT(es.completed)
293(
294 enrolled_semesters es
295)
296
297PendingStats <-
298γ user_id := es.user_id;
299 pending_subjects := COUNT(ss.id)
300(
301 σ ps.id IS NULL
302 (
303 (enrolled_semesters es ⨝ (es.id = ss.enrolled_semesters_id) semesters_subjects ss)
304 ⟕ (ss.id = ps.enrolled_id) passed_subjects ps
305 )
306)
307
308PaymentStats <-
309γ user_id := es.user_id;
310 total_paid := SUM(p.amount)
311(
312 payment p ⨝ (p.enrollment_id = es.id) enrolled_semesters es
313)
314
315DocumentStats <-
316γ user_id := ud.user_id;
317 num_documents := COUNT(ud.document_id),
318 documents_cost := SUM(d.cost)
319(
320 user_documents ud ⨝ (ud.document_id = d.id) documents d
321)
322
323Result <-
324τ credits DESC, average_grade DESC, u.index
325(
326 π u.index,
327 student := u.name || ' ' || u.surname,
328 sem.enrolled_semesters,
329 sem.completed_semesters,
330 ps.passed_subjects,
331 pen.pending_subjects,
332 ps.credits,
333 ps.average_grade,
334 pay.total_paid,
335 doc.num_documents,
336 doc.documents_cost
337 (
338 σ u.role = 'student'
339 (
340 (
341 (
342 (
343 (
344 users u ⟕ (u.id = ps.user_id) PassedStats ps
345 )
346 ⟕ (u.id = sem.user_id) SemesterStats sem
347 )
348 ⟕ (u.id = pen.user_id) PendingStats pen
349 )
350 ⟕ (u.id = pay.user_id) PaymentStats pay
351 )
352 ⟕ (u.id = doc.user_id) DocumentStats doc
353 )
354 )
355)
356}}}
357
358=== Складирана процедура
359
360Извештајот е даден и како функција со параметар, за апликацијата со истата рутина
361да добие и едно досие и списокот на сите студенти:
362
363{{{#!sql
364CREATE OR REPLACE FUNCTION rep_student_dossier(p_user_id INTEGER DEFAULT NULL)
365 RETURNS TABLE (
366 student_index VARCHAR,
367 student TEXT,
368 enrolled_semesters BIGINT,
369 completed_semesters BIGINT,
370 passed_subjects BIGINT,
371 pending_subjects BIGINT,
372 credits BIGINT,
373 average_grade VARCHAR,
374 total_paid BIGINT,
375 num_documents BIGINT,
376 documents_cost BIGINT
377 )
378 LANGUAGE sql
379 STABLE
380 SET search_path = project
381AS $$
382 -- барањето прикажано погоре, со услов:
383 -- AND (p_user_id IS NULL OR u.id = p_user_id)
384$$;
385
386SELECT * FROM rep_student_dossier(); -- сите студенти
387SELECT * FROM rep_student_dossier(1); -- едно досие
388}}}
389
390Истиот извештај е спакуван и како вистинска процедура која отвора курсор, за
391повикувач кој ги чита редовите еден по еден наместо одеднаш:
392
393{{{#!sql
394CREATE OR REPLACE PROCEDURE rep_student_dossier_cursor(
395 IN p_user_id INTEGER,
396 INOUT p_cursor REFCURSOR DEFAULT 'dossier')
397 LANGUAGE plpgsql
398 SET search_path = project
399AS $$
400BEGIN
401 OPEN p_cursor FOR SELECT * FROM rep_student_dossier(p_user_id);
402END;
403$$;
404}}}
405
406Повик:
407
408{{{#!sql
409BEGIN;
410CALL rep_student_dossier_cursor(1);
411FETCH ALL FROM dossier;
412COMMIT;
413}}}
414
415Курсорот постои само во рамки на трансакцијата во која е отворен, затоа повикот е
416опкружен со BEGIN и COMMIT.
417
418=== Резултат врз тест податоците
419
420{{{
421 student_index | student | enrolled_semesters | completed_semesters | passed_subjects | pending_subjects | credits | average_grade | total_paid | num_documents | documents_cost
422---------------+--------------------+--------------------+---------------------+-----------------+------------------+---------+---------------+------------+---------------+----------------
423 233149 | Stefan Saveski | 2 | 1 | 2 | 1 | 12 | 9.00 | 400 | 2 | 150
424 233188 | Boris Gjorgjievski | 1 | 1 | 1 | 0 | 6 | 9.00 | 400 | 1 | 50
425 233200 | Ana Petrova | 1 | 0 | 1 | 1 | 6 | 7.00 | 200 | 1 | 100
426}}}
427
428== 3. Најуспешен студент по студиска програма
429
430=== Опис на извештајот
431
432За секоја студиска програма се бара студентот со најмногу собрани кредити, а при
433ист број кредити - оној со повисок просек. Ако и просекот е ист, победува
434студентот со помал идентификатор, за извештајот да враќа точно еден ред по
435програма и да дава ист резултат при секое стартување.
436
437Барањето е решено без прозорски функции, со NOT EXISTS: се задржува само оној ред
438за кој '''не постои''' подобар ред во истата програма. Истата техника е употребена
439и во извештаите 4 и 5.
440
441=== SQL
442
443{{{#!sql
444SET search_path TO project;
445
446WITH standing AS (
447 SELECT es.user_id,
448 es.major_id,
449 COUNT(ps.id) AS passed_subjects,
450 COALESCE(SUM(s.awarded_credits), 0) AS credits,
451 COALESCE(ROUND(AVG(ps.grade::TEXT::INT), 2), 0) AS average_grade
452 FROM enrolled_semesters es
453 LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
454 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
455 LEFT JOIN subjects s ON s.id = ss.subjects_id AND ps.id IS NOT NULL
456 GROUP BY es.user_id, es.major_id
457)
458SELECT m.name AS major,
459 u."index" AS student_index,
460 u.name || ' ' || u.surname AS student,
461 st.passed_subjects,
462 st.credits,
463 st.average_grade
464FROM standing st
465JOIN users u ON u.id = st.user_id
466JOIN major m ON m.id = st.major_id
467WHERE NOT EXISTS (
468 SELECT 1
469 FROM standing st1
470 WHERE st1.major_id = st.major_id
471 AND (st.credits < st1.credits
472 OR (st.credits = st1.credits AND st.average_grade < st1.average_grade)
473 OR (st.credits = st1.credits AND st.average_grade = st1.average_grade
474 AND st.user_id > st1.user_id))
475)
476ORDER BY m.name;
477}}}
478
479Празните вредности се претворени во нула уште во подизразот standing. Ако тоа не
480се направи, споредбите со NULL даваат NULL наместо точно или неточно, па студент
481без ниту една оценка не би можел да биде ниту задржан ниту отфрлен.
482
483=== Релациона алгебра
484
485{{{
486Standing <-
487γ user_id := es.user_id,
488 major_id := es.major_id;
489 passed_subjects := COUNT(ps.id),
490 credits := SUM(s.awarded_credits),
491 average_grade := AVG(ps.grade)
492(
493 (
494 (enrolled_semesters es ⟕ (es.id = ss.enrolled_semesters_id) semesters_subjects ss)
495 ⟕ (ss.id = ps.enrolled_id) passed_subjects ps
496 )
497 ⟕ (ss.subjects_id = s.id ∧ ps.id IS NOT NULL) subjects s
498)
499
500Dominated <-
501π attributes(st)
502(
503 σ st.major_id = st1.major_id ∧
504 (
505 st.credits < st1.credits
506 ∨ (st.credits = st1.credits ∧ st.average_grade < st1.average_grade)
507 ∨ (st.credits = st1.credits ∧ st.average_grade = st1.average_grade ∧
508 st.user_id > st1.user_id)
509 )
510 (
511 ρ st(Standing) × ρ st1(Standing)
512 )
513)
514
515TopStudents <- Standing − Dominated
516
517Result <-
518τ m.name
519(
520 π m.name,
521 u.index,
522 student := u.name || ' ' || u.surname,
523 st.passed_subjects,
524 st.credits,
525 st.average_grade
526 (
527 (
528 TopStudents st ⨝ (st.user_id = u.id) users u
529 )
530 ⨝ (st.major_id = m.id) major m
531 )
532)
533}}}
534
535Условот NOT EXISTS во релациона алгебра се изразува со разлика: од сите редови се
536одземаат оние за кои постои подобар ред во истата група.
537
538=== Складирана процедура
539
540{{{#!sql
541CREATE OR REPLACE FUNCTION rep_top_student_per_major()
542 RETURNS TABLE (
543 major VARCHAR,
544 student_index VARCHAR,
545 student TEXT,
546 passed_subjects BIGINT,
547 credits BIGINT,
548 average_grade NUMERIC
549 )
550 LANGUAGE sql
551 STABLE
552 SET search_path = project
553AS $$
554 -- барањето прикажано погоре
555$$;
556
557SELECT * FROM rep_top_student_per_major();
558}}}
559
560=== Резултат врз тест податоците
561
562{{{
563 major | student_index | student | passed_subjects | credits | average_grade
564----------------------------------------------+---------------+----------------+-----------------+---------+---------------
565 Computer Science and Engineering | 233200 | Ana Petrova | 1 | 6 | 7.00
566 Software Engineering and Information Systems | 233149 | Stefan Saveski | 2 | 12 | 9.00
567}}}
568
569== 4. Најоптоварен професор по активен семестар
570
571=== Опис на извештајот
572
573За секој активен семестар се бара професорот кај кого се запишани најмногу
574студенти, а при ист број - оној кој оценил повеќе. Извештајот тргнува од табелата
575active_semesters, па во него се појавуваат и семестрите во кои сè уште никој не се
576запишал; за нив се прикажува 'n/a' и нули. Тоа е список кој администраторот го
577користи за да види каде распоредот сè уште не е пополнет.
578
579=== SQL
580
581{{{#!sql
582SET search_path TO project;
583
584WITH professor_load AS (
585 SELECT es.semester_id,
586 ss.professor_id,
587 COUNT(ss.id) AS enrolled_students,
588 COUNT(ps.id) AS graded_students,
589 COUNT(DISTINCT ss.subjects_id) AS subjects_taught
590 FROM semesters_subjects ss
591 JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
592 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
593 GROUP BY es.semester_id, ss.professor_id
594),
595busiest AS (
596 SELECT pl.*
597 FROM professor_load pl
598 WHERE NOT EXISTS (
599 SELECT 1
600 FROM professor_load pl1
601 WHERE pl1.semester_id = pl.semester_id
602 AND (pl.enrolled_students < pl1.enrolled_students
603 OR (pl.enrolled_students = pl1.enrolled_students
604 AND pl.graded_students < pl1.graded_students)
605 OR (pl.enrolled_students = pl1.enrolled_students
606 AND pl.graded_students = pl1.graded_students
607 AND pl.professor_id > pl1.professor_id))
608 )
609)
610SELECT a.year,
611 a.type AS semester_type,
612 COALESCE(u.name || ' ' || u.surname, 'n/a') AS professor,
613 COALESCE(b.subjects_taught, 0) AS subjects_taught,
614 COALESCE(b.enrolled_students, 0) AS enrolled_students,
615 COALESCE(b.graded_students, 0) AS graded_students
616FROM active_semesters a
617LEFT JOIN busiest b ON b.semester_id = a.id
618LEFT JOIN users u ON u.id = b.professor_id
619ORDER BY a.year, CASE a.type WHEN 'summer' THEN 1 ELSE 2 END;
620}}}
621
622Подредувањето не оди по името на семестарот, туку по година и по редоследот во
623академската година - летниот семестар доаѓа пред зимскиот од истата година.
624
625=== Релациона алгебра
626
627{{{
628ProfessorLoad <-
629γ semester_id := es.semester_id,
630 professor_id := ss.professor_id;
631 enrolled_students := COUNT(ss.id),
632 graded_students := COUNT(ps.id),
633 subjects_taught := COUNT(DISTINCT ss.subjects_id)
634(
635 (
636 semesters_subjects ss
637 ⨝ (ss.enrolled_semesters_id = es.id) enrolled_semesters es
638 )
639 ⟕ (ss.id = ps.enrolled_id) passed_subjects ps
640)
641
642Dominated <-
643π attributes(pl)
644(
645 σ pl.semester_id = pl1.semester_id ∧
646 (
647 pl.enrolled_students < pl1.enrolled_students
648 ∨ (pl.enrolled_students = pl1.enrolled_students ∧
649 pl.graded_students < pl1.graded_students)
650 ∨ (pl.enrolled_students = pl1.enrolled_students ∧
651 pl.graded_students = pl1.graded_students ∧
652 pl.professor_id > pl1.professor_id)
653 )
654 (
655 ρ pl(ProfessorLoad) × ρ pl1(ProfessorLoad)
656 )
657)
658
659Busiest <- ProfessorLoad − Dominated
660
661Result <-
662τ a.year, (a.type = 'summer' ? 1 : 2)
663(
664 π a.year,
665 a.type,
666 professor := u.name || ' ' || u.surname,
667 b.subjects_taught,
668 b.enrolled_students,
669 b.graded_students
670 (
671 (
672 active_semesters a ⟕ (a.id = b.semester_id) Busiest b
673 )
674 ⟕ (b.professor_id = u.id) users u
675 )
676)
677}}}
678
679=== Складирана процедура
680
681{{{#!sql
682CREATE OR REPLACE FUNCTION rep_busiest_professor()
683 RETURNS TABLE (
684 year INTEGER,
685 semester_type semester_type,
686 professor TEXT,
687 subjects_taught BIGINT,
688 enrolled_students BIGINT,
689 graded_students BIGINT
690 )
691 LANGUAGE sql
692 STABLE
693 SET search_path = project
694AS $$
695 -- барањето прикажано погоре
696$$;
697
698SELECT * FROM rep_busiest_professor();
699}}}
700
701=== Резултат врз тест податоците
702
703{{{
704 year | semester_type | professor | subjects_taught | enrolled_students | graded_students
705------+---------------+------------------+-----------------+-------------------+-----------------
706 2024 | winter | Vangel Ajanovski | 1 | 3 | 3
707 2025 | summer | Vangel Ajanovski | 1 | 1 | 0
708 2025 | winter | n/a | 0 | 0 | 0
709 2026 | summer | n/a | 0 | 0 | 0
710 2026 | winter | n/a | 0 | 0 | 0
711}}}
712
713== 5. Процентуална промена на активноста меѓу два последователни семестри
714
715=== Опис на извештајот
716
717За секој активен семестар се прикажува колку запишувања, колку запишани предмети и
718колку положени предмети имало, и колку тоа отстапува од претходниот семестар,
719изразено во проценти. Ова е извештајот со кој се следи дали бројот на запишувања
720расте или опаѓа.
721
722Семестрите немаат колона со редослед - тие се пар од година и тип. Затоа прво се
723пресметува хронолошки клуч, а потоа за секој семестар се бара претходниот како оној
724пред него за кој '''не постои''' семестар помеѓу нив.
725
726=== SQL
727
728{{{#!sql
729SET search_path TO project;
730
731WITH ordered_semesters AS (
732 SELECT a.id,
733 a.year,
734 a.type,
735 a.year * 10 + CASE a.type WHEN 'summer' THEN 1 ELSE 2 END AS chrono
736 FROM active_semesters a
737),
738semester_stats AS (
739 SELECT o.id AS semester_id,
740 o.year,
741 o.type,
742 o.chrono,
743 COUNT(DISTINCT es.id) AS enrolments,
744 COUNT(ss.id) AS subject_enrolments,
745 COUNT(ps.id) AS passed
746 FROM ordered_semesters o
747 LEFT JOIN enrolled_semesters es ON es.semester_id = o.id
748 LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
749 LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
750 GROUP BY o.id, o.year, o.type, o.chrono
751),
752consecutive AS (
753 SELECT cur.year, cur.type, cur.chrono,
754 cur.enrolments, cur.subject_enrolments, cur.passed,
755 prev.year AS prev_year,
756 prev.type AS prev_type,
757 prev.subject_enrolments AS prev_subject_enrolments
758 FROM semester_stats cur
759 LEFT JOIN semester_stats prev
760 ON prev.chrono < cur.chrono
761 AND NOT EXISTS (SELECT 1
762 FROM semester_stats mid
763 WHERE mid.chrono < cur.chrono
764 AND mid.chrono > prev.chrono)
765)
766SELECT c.year || '-' || c.type AS semester,
767 COALESCE(c.prev_year || '-' || c.prev_type, 'нема претходен') AS previous_semester,
768 c.enrolments,
769 c.subject_enrolments,
770 c.passed,
771 COALESCE(CAST(c.prev_subject_enrolments AS VARCHAR), 'n/a') AS prev_subject_enrolments,
772 COALESCE(CAST(ROUND(((c.subject_enrolments - c.prev_subject_enrolments) * 100.0)
773 / NULLIF(c.prev_subject_enrolments, 0)) AS VARCHAR) || '%',
774 'n/a') AS pct_change
775FROM consecutive c
776ORDER BY c.chrono;
777}}}
778
779Процентот се спојува со знакот '%' преку операторот {{{||}}}, а не преку CONCAT.
780CONCAT ги игнорира празните вредности и за семестар без претходник би вратил само
781'%', па COALESCE никогаш не би се активирал. Операторот {{{||}}} враќа NULL ако
782некој од операндите е NULL, што е токму она што му треба на COALESCE за да испише
783'n/a'.
784
785Делењето е заштитено со NULLIF, за семестар во кој претходно немало ниту еден
786запишан предмет да не предизвика делење со нула.
787
788=== Релациона алгебра
789
790{{{
791OrderedSemesters <-
792π a.id,
793 a.year,
794 a.type,
795 chrono := a.year * 10 + (a.type = 'summer' ? 1 : 2)
796(
797 active_semesters a
798)
799
800SemesterStats <-
801γ semester_id := o.id,
802 year := o.year,
803 type := o.type,
804 chrono := o.chrono;
805 enrolments := COUNT(DISTINCT es.id),
806 subject_enrolments := COUNT(ss.id),
807 passed := COUNT(ps.id)
808(
809 (
810 (
811 OrderedSemesters o ⟕ (o.id = es.semester_id) enrolled_semesters es
812 )
813 ⟕ (es.id = ss.enrolled_semesters_id) semesters_subjects ss
814 )
815 ⟕ (ss.id = ps.enrolled_id) passed_subjects ps
816)
817
818Earlier <-
819σ prev.chrono < cur.chrono
820(
821 ρ cur(SemesterStats) × ρ prev(SemesterStats)
822)
823
824NotImmediate <-
825π attributes(Earlier)
826(
827 σ mid.chrono < cur.chrono ∧ mid.chrono > prev.chrono
828 (
829 Earlier × ρ mid(SemesterStats)
830 )
831)
832
833Immediate <- Earlier − NotImmediate
834
835Consecutive <-
836SemesterStats cur ⟕ (cur.chrono = imm.cur_chrono) Immediate imm
837
838Result <-
839τ c.chrono
840(
841 π semester := c.year || '-' || c.type,
842 previous_semester := c.prev_year || '-' || c.prev_type,
843 c.enrolments,
844 c.subject_enrolments,
845 c.passed,
846 c.prev_subject_enrolments,
847 pct_change := ((c.subject_enrolments − c.prev_subject_enrolments) * 100)
848 / c.prev_subject_enrolments
849 (
850 Consecutive c
851 )
852)
853}}}
854
855Релацијата Immediate ги содржи само паровите (семестар, претходен семестар) меѓу
856кои нема трет семестар. Тоа е истата разлика како во извештаите 3 и 4: од сите
857порани парови се одземаат оние за кои постои семестар помеѓу.
858
859=== Складирана процедура
860
861{{{#!sql
862CREATE OR REPLACE FUNCTION rep_semester_growth()
863 RETURNS TABLE (
864 semester TEXT,
865 previous_semester TEXT,
866 enrolments BIGINT,
867 subject_enrolments BIGINT,
868 passed BIGINT,
869 prev_subject_enrolments VARCHAR,
870 pct_change VARCHAR
871 )
872 LANGUAGE sql
873 STABLE
874 SET search_path = project
875AS $$
876 -- барањето прикажано погоре
877$$;
878
879SELECT * FROM rep_semester_growth();
880}}}
881
882=== Резултат врз тест податоците
883
884{{{
885 semester | previous_semester | enrolments | subject_enrolments | passed | prev_subject_enrolments | pct_change
886-------------+-------------------+------------+--------------------+--------+-------------------------+------------
887 2024-winter | нема претходен | 3 | 5 | 4 | n/a | n/a
888 2025-summer | 2024-winter | 1 | 1 | 0 | 5 | -80%
889 2025-winter | 2025-summer | 0 | 0 | 0 | 1 | -100%
890 2026-summer | 2025-winter | 0 | 0 | 0 | 0 | n/a
891 2026-winter | 2026-summer | 0 | 0 | 0 | 0 | n/a
892}}}
893
894== Заеднички забелешки за имплементацијата
895
896 * '''Еден ред по група без прозорски функции.''' Извештаите 3, 4 и 5 бараат „најдобриот во групата“ односно „претходниот по ред“. Наместо ROW_NUMBER, употребен е NOT EXISTS, кој во релациона алгебра директно се пресликува во разлика на две релации. Условот за израмнување е секогаш строг и завршува со споредба на идентификатор, па извештајот враќа точно еден ред по група и е повторлив.
897 * '''Надворешни спојувања наместо внатрешни.''' Предмет кој никој не го положил, семестар во кој никој не се запишал и студент кој нема платено сепак се појавуваат во извештаите. Внатрешно спојување би ги сокрило токму редовите кои се најинтересни за администраторот.
898 * '''Претворање на оценката.''' grade е од набројувачки тип, па {{{AVG(grade)}}} не е дозволено. Секаде е употребено {{{ps.grade::TEXT::INT}}}.
899 * '''Празни вредности.''' Сите бројачи излегуваат преку COALESCE како нула, а просеците како текстот 'нема оценки'. Делењата се заштитени со NULLIF.
900 * '''Патека на шемата.''' Секоја рутина носи {{{SET search_path = project}}}, па работи исто без оглед на тоа што повикувачот поставил.
901
902== Историјат
903
904 '''Верзија 1''' - Прва верзија: пет извештаи, секој со SQL, релациона алгебра и
905 складирана рутина. Сите барања се тестирани врз шемата project со податоците од
906 data_load.sql.
907
908== Статус
909
910 ''' [[span(style=color: #FF8000, Во тек )]] '''
Note: See TracBrowser for help on using the repository browser.