| | 1 | == Напредни извештаи од базата (SQL, складирани процедури и релациона алгебра) |
| | 2 | |
| | 3 | Сите извештаи подолу се напишани и тестирани врз шемата '''project''' на доделената |
| | 4 | проектна база, врз податоците од [attachment:data_load.sql]. |
| | 5 | |
| | 6 | Секој извештај е даден во три форми: |
| | 7 | |
| | 8 | * '''SQL''' - барањето како што се извршува врз базата |
| | 9 | * '''Релациона алгебра''' - истото барање изразено со операторите на релациона алгебра |
| | 10 | * '''Складирана процедура''' - барањето спакувано како рутина во базата, за да апликацијата го повикува со едно име наместо да го носи барањето во кодот |
| | 11 | |
| | 12 | Сите рутини се во скриптата [attachment:reports.sql], која се стартува по |
| | 13 | schema_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 |
| | 32 | SET search_path TO project; |
| | 33 | |
| | 34 | WITH 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 | ), |
| | 45 | signed_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 | ), |
| | 54 | subject_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 | ) |
| | 61 | SELECT 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 |
| | 71 | FROM enrolled_per_semester eps |
| | 72 | JOIN subjects s ON s.id = eps.subject_id |
| | 73 | JOIN active_semesters a ON a.id = eps.semester_id |
| | 74 | LEFT JOIN signed_per_semester sps ON sps.subject_id = eps.subject_id |
| | 75 | AND sps.semester_id = eps.semester_id |
| | 76 | LEFT JOIN subject_average sa ON sa.subject_id = eps.subject_id |
| | 77 | ORDER BY a.year, a.type, pass_rate DESC, s.code; |
| | 78 | }}} |
| | 79 | |
| | 80 | === Релациона алгебра |
| | 81 | |
| | 82 | {{{ |
| | 83 | EnrolledPerSemester <- |
| | 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 | |
| | 97 | SignedPerSemester <- |
| | 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 | |
| | 109 | SubjectAverage <- |
| | 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 | |
| | 116 | Result <- |
| | 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 |
| | 148 | CREATE 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 |
| | 164 | AS $$ |
| | 165 | -- барањето прикажано погоре |
| | 166 | $$; |
| | 167 | |
| | 168 | SELECT * 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 |
| | 199 | SET search_path TO project; |
| | 200 | |
| | 201 | WITH 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 | ), |
| | 212 | semester_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 | ), |
| | 219 | pending_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 | ), |
| | 228 | payment_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 | ), |
| | 235 | document_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 | ) |
| | 243 | SELECT 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 |
| | 254 | FROM users u |
| | 255 | LEFT JOIN passed_stats ps ON ps.user_id = u.id |
| | 256 | LEFT JOIN semester_stats sem ON sem.user_id = u.id |
| | 257 | LEFT JOIN pending_stats pen ON pen.user_id = u.id |
| | 258 | LEFT JOIN payment_stats pay ON pay.user_id = u.id |
| | 259 | LEFT JOIN document_stats doc ON doc.user_id = u.id |
| | 260 | WHERE u.role = 'student' |
| | 261 | ORDER 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, а не преку |
| | 270 | payment.user_id, согласно нормализираниот модел од Фаза 5 во кој таа колона е |
| | 271 | отстранета. |
| | 272 | |
| | 273 | === Релациона алгебра |
| | 274 | |
| | 275 | {{{ |
| | 276 | PassedStats <- |
| | 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 | |
| | 289 | SemesterStats <- |
| | 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 | |
| | 297 | PendingStats <- |
| | 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 | |
| | 308 | PaymentStats <- |
| | 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 | |
| | 315 | DocumentStats <- |
| | 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 | |
| | 323 | Result <- |
| | 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 |
| | 364 | CREATE 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 |
| | 381 | AS $$ |
| | 382 | -- барањето прикажано погоре, со услов: |
| | 383 | -- AND (p_user_id IS NULL OR u.id = p_user_id) |
| | 384 | $$; |
| | 385 | |
| | 386 | SELECT * FROM rep_student_dossier(); -- сите студенти |
| | 387 | SELECT * FROM rep_student_dossier(1); -- едно досие |
| | 388 | }}} |
| | 389 | |
| | 390 | Истиот извештај е спакуван и како вистинска процедура која отвора курсор, за |
| | 391 | повикувач кој ги чита редовите еден по еден наместо одеднаш: |
| | 392 | |
| | 393 | {{{#!sql |
| | 394 | CREATE 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 |
| | 399 | AS $$ |
| | 400 | BEGIN |
| | 401 | OPEN p_cursor FOR SELECT * FROM rep_student_dossier(p_user_id); |
| | 402 | END; |
| | 403 | $$; |
| | 404 | }}} |
| | 405 | |
| | 406 | Повик: |
| | 407 | |
| | 408 | {{{#!sql |
| | 409 | BEGIN; |
| | 410 | CALL rep_student_dossier_cursor(1); |
| | 411 | FETCH ALL FROM dossier; |
| | 412 | COMMIT; |
| | 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 |
| | 444 | SET search_path TO project; |
| | 445 | |
| | 446 | WITH 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 | ) |
| | 458 | SELECT 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 |
| | 464 | FROM standing st |
| | 465 | JOIN users u ON u.id = st.user_id |
| | 466 | JOIN major m ON m.id = st.major_id |
| | 467 | WHERE 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 | ) |
| | 476 | ORDER BY m.name; |
| | 477 | }}} |
| | 478 | |
| | 479 | Празните вредности се претворени во нула уште во подизразот standing. Ако тоа не |
| | 480 | се направи, споредбите со NULL даваат NULL наместо точно или неточно, па студент |
| | 481 | без ниту една оценка не би можел да биде ниту задржан ниту отфрлен. |
| | 482 | |
| | 483 | === Релациона алгебра |
| | 484 | |
| | 485 | {{{ |
| | 486 | Standing <- |
| | 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 | |
| | 500 | Dominated <- |
| | 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 | |
| | 515 | TopStudents <- Standing − Dominated |
| | 516 | |
| | 517 | Result <- |
| | 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 |
| | 541 | CREATE 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 |
| | 553 | AS $$ |
| | 554 | -- барањето прикажано погоре |
| | 555 | $$; |
| | 556 | |
| | 557 | SELECT * 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 | студенти, а при ист број - оној кој оценил повеќе. Извештајот тргнува од табелата |
| | 575 | active_semesters, па во него се појавуваат и семестрите во кои сè уште никој не се |
| | 576 | запишал; за нив се прикажува 'n/a' и нули. Тоа е список кој администраторот го |
| | 577 | користи за да види каде распоредот сè уште не е пополнет. |
| | 578 | |
| | 579 | === SQL |
| | 580 | |
| | 581 | {{{#!sql |
| | 582 | SET search_path TO project; |
| | 583 | |
| | 584 | WITH 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 | ), |
| | 595 | busiest 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 | ) |
| | 610 | SELECT 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 |
| | 616 | FROM active_semesters a |
| | 617 | LEFT JOIN busiest b ON b.semester_id = a.id |
| | 618 | LEFT JOIN users u ON u.id = b.professor_id |
| | 619 | ORDER BY a.year, CASE a.type WHEN 'summer' THEN 1 ELSE 2 END; |
| | 620 | }}} |
| | 621 | |
| | 622 | Подредувањето не оди по името на семестарот, туку по година и по редоследот во |
| | 623 | академската година - летниот семестар доаѓа пред зимскиот од истата година. |
| | 624 | |
| | 625 | === Релациона алгебра |
| | 626 | |
| | 627 | {{{ |
| | 628 | ProfessorLoad <- |
| | 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 | |
| | 642 | Dominated <- |
| | 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 | |
| | 659 | Busiest <- ProfessorLoad − Dominated |
| | 660 | |
| | 661 | Result <- |
| | 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 |
| | 682 | CREATE 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 |
| | 694 | AS $$ |
| | 695 | -- барањето прикажано погоре |
| | 696 | $$; |
| | 697 | |
| | 698 | SELECT * 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 |
| | 729 | SET search_path TO project; |
| | 730 | |
| | 731 | WITH 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 | ), |
| | 738 | semester_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 | ), |
| | 752 | consecutive 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 | ) |
| | 766 | SELECT 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 |
| | 775 | FROM consecutive c |
| | 776 | ORDER BY c.chrono; |
| | 777 | }}} |
| | 778 | |
| | 779 | Процентот се спојува со знакот '%' преку операторот {{{||}}}, а не преку CONCAT. |
| | 780 | CONCAT ги игнорира празните вредности и за семестар без претходник би вратил само |
| | 781 | '%', па COALESCE никогаш не би се активирал. Операторот {{{||}}} враќа NULL ако |
| | 782 | некој од операндите е NULL, што е токму она што му треба на COALESCE за да испише |
| | 783 | 'n/a'. |
| | 784 | |
| | 785 | Делењето е заштитено со NULLIF, за семестар во кој претходно немало ниту еден |
| | 786 | запишан предмет да не предизвика делење со нула. |
| | 787 | |
| | 788 | === Релациона алгебра |
| | 789 | |
| | 790 | {{{ |
| | 791 | OrderedSemesters <- |
| | 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 | |
| | 800 | SemesterStats <- |
| | 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 | |
| | 818 | Earlier <- |
| | 819 | σ prev.chrono < cur.chrono |
| | 820 | ( |
| | 821 | ρ cur(SemesterStats) × ρ prev(SemesterStats) |
| | 822 | ) |
| | 823 | |
| | 824 | NotImmediate <- |
| | 825 | π attributes(Earlier) |
| | 826 | ( |
| | 827 | σ mid.chrono < cur.chrono ∧ mid.chrono > prev.chrono |
| | 828 | ( |
| | 829 | Earlier × ρ mid(SemesterStats) |
| | 830 | ) |
| | 831 | ) |
| | 832 | |
| | 833 | Immediate <- Earlier − NotImmediate |
| | 834 | |
| | 835 | Consecutive <- |
| | 836 | SemesterStats cur ⟕ (cur.chrono = imm.cur_chrono) Immediate imm |
| | 837 | |
| | 838 | Result <- |
| | 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 |
| | 862 | CREATE 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 |
| | 875 | AS $$ |
| | 876 | -- барањето прикажано погоре |
| | 877 | $$; |
| | 878 | |
| | 879 | SELECT * 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, Во тек )]] ''' |