Changes between Initial Version and Version 1 of AdvancedReports


Ignore:
Timestamp:
09/16/26 22:30:58 (12 days ago)
Author:
233188
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedReports

    v1 v1  
     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, Во тек )]] '''