Changes between Initial Version and Version 1 of QueryOptimization


Ignore:
Timestamp:
05/19/26 22:12:32 (7 days ago)
Author:
231071
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • QueryOptimization

    v1 v1  
     1= Оптимизација на прашалници и погледи =
     2
     3Во оваа фаза ќе ги анализираме погледите дефинирани во [[DatabaseCreation|Фаза 2]] преку прашалници базирани на реални сценарија кои ќе бидат присутни во нашата апликација и истите ќе се обидеме да ги оптимизираме.
     4
     5=== 1. Анализа на поглед 1, vw_party_demographic_performance — демографска анализа на поддршката по партии ===
     6
     7Овој поглед овозможува длабинска декомпозиција на изборните резултати преку призмата на демографските карактеристики на гласачкото тело. Наместо едноставен линеарен приказ на победниците, погледот ја открива структурата и профилот на гласачите кои ја генерираат поддршката за секој политички субјект. Преку статистичко поврзување на кастираните гласови со старосната структура на чекираните гласачи, се генерира прецизен увид во електоралната поддршка поделена по генерации (Gen Z, Миленијалци и Сениори).
     8
     9За максимална оптимизација на перформансите при пребарувањето, погледот е дизајниран да се филтрира според две основни колони: `election_name` (за избор на конкретен изборен циклус) и `party_name` (за селекција на одреден политички ентитет).
     10
     11Дефиниција на погледот:
     12{{{
     13CREATE OR REPLACE VIEW public.vw_party_demographic_performance AS
     14WITH VoterDemographics AS (
     15    SELECT
     16        ve.election_id,
     17        ve.station_id,
     18        p.date_of_birth,
     19        EXTRACT(YEAR FROM p.date_of_birth) as birth_year
     20    FROM public.voter_election ve
     21    JOIN public.voter v ON ve.voter_id = v.voter_id
     22    JOIN public.person p ON v.person_id = p.person_id
     23),
     24BallotStats AS (
     25    SELECT
     26        b.election_id,
     27        b.station_id,
     28        b.entity_id,
     29        b.ballot_id
     30    FROM public.ballot b
     31    WHERE b.is_valid = true
     32)
     33SELECT
     34    e.name AS election_name,
     35    r.name AS region_name,
     36    pe.name AS party_name,
     37    ps.name AS station_name,
     38    COALESCE(COUNT(CASE WHEN vd.birth_year >= 1996 THEN 1 END), 0) AS gen_z_votes,
     39    COALESCE(COUNT(CASE WHEN vd.birth_year >= 1981 AND vd.birth_year <= 1995 THEN 1 END), 0) AS millennial_votes,
     40    COALESCE(COUNT(CASE WHEN vd.birth_year < 1981 THEN 1 END), 0) AS senior_votes,
     41    COUNT(bs.ballot_id) AS total_valid_votes
     42FROM BallotStats bs
     43JOIN public.election e ON bs.election_id = e.election_id
     44JOIN public.polling_station ps ON bs.station_id = ps.station_id
     45JOIN public.region r ON ps.municipality_id = r.region_id
     46JOIN public.political_entity pe ON bs.entity_id = pe.entity_id
     47LEFT JOIN VoterDemographics vd ON bs.election_id = vd.election_id
     48                             AND bs.station_id = vd.station_id
     49GROUP BY e.name, r.name, pe.name, ps.name;
     50}}}
     51
     52Прашалникот кој ќе го тестираме:
     53{{{
     54SELECT * FROM vw_party_demographic_performance
     55WHERE election_name = 'Parliamentary Elections 2014'
     56  AND party_name = 'VMRO-DPMNE';
     57}}}
     58
     59==== Време на извршување без индекси:
     60
     61**3415.820 ms (~3.4 секунди)**
     62
     63Времето на извршување е неприфатливо, особено при истовремена работа на повеќе корисници. Извршниот план покажува:
     64- Parallel Seq Scan над `ballot`
     65- Hash Join и Nested Loop операции
     66
     67Ова укажува дека PostgreSQL чита голем број редови поради недостаток на соодветни индекси. Проблемот со времето на извршување може да се подобри со индекси.
     68
     69Со стратешка имплементација на соодветни индекси врз клучните колони во базата, времето на извршување на оваа комплексна демографска агрегација беше успешно оптимизирано.
     70
     71==== Додадени индекси:
     72
     73{{{
     74CREATE INDEX idx_ballot_election_station ON ballot(election_id, station_id);
     75CREATE INDEX idx_ballot_entity_valid ON ballot(entity_id) WHERE is_valid = true;
     76CREATE INDEX idx_voter_election_ids ON voter_election(election_id, station_id, voter_id);
     77CREATE INDEX idx_polling_station_municipality ON polling_station(municipality_id);
     78}}}
     79
     80==== Време на извршување со индекси:
     81
     82**619 ms** (беше 3415.820 ms)
     83
     84Подобрување: ~5.5x побрзо.
     85
     86---
     87
     88=== 2. Анализа на поглед 2, vw_local_election_winners — победници на локални избори ===
     89
     90Овој поглед овозможува преглед на победниците (кандидати или партии со најмногу гласови) на локално ниво, поделени по изборни единици или општини. Погледот ги сумира гласовите и ги идентификува субјектите со највисок резултат за секоја локација.
     91
     92Примарен филтер: `election_id` или `election_year` (за да се видат победниците на конкретни избори). Секундарен филтер: `municipality_name` или `station_id`.
     93
     94Дефиниција на погледот:
     95{{{
     96CREATE OR REPLACE VIEW public.vw_local_election_winners AS
     97WITH totalvotesperparty AS (
     98    SELECT vr.election_id, e.name AS election_name, r.name AS municipality_name,
     99           pe.name AS party_name, sum(vr.votes) AS total_sum_votes
     100    FROM vote_result      vr
     101    JOIN election         e  ON vr.election_id = e.election_id
     102    JOIN political_entity pe ON vr.entity_id   = pe.entity_id
     103    JOIN polling_station  ps ON vr.station_id  = ps.station_id
     104    JOIN region           r  ON ps.municipality_id = r.region_id
     105    WHERE e.election_type_id = 3
     106    GROUP BY vr.election_id, e.name, r.name, pe.name
     107),
     108rankedwinners AS (
     109    SELECT *, rank() OVER (PARTITION BY election_id, municipality_name ORDER BY total_sum_votes DESC) AS rnk
     110    FROM totalvotesperparty
     111)
     112SELECT election_name, municipality_name,
     113       party_name           AS winner_name,
     114       total_sum_votes       AS votes_received,
     115       'Winner'::text        AS status
     116FROM rankedwinners
     117WHERE rnk = 1;
     118}}}
     119
     120Прашалникот кој ќе го тестираме:
     121{{{
     122SELECT * FROM vw_local_election_winners
     123WHERE election_name = 'Local Elections 2021';
     124}}}
     125
     126==== Време на извршување без индекси:
     127
     128**3500 ms (~3.5 секунди)**
     129
     130Времето на извршување не е прифатливо. Планот покажува дека базата троши премногу време на Incremental Sort и WindowAgg операциите за да ги рангира победниците меѓу 180,000 редови. Ова укажува на потреба од индекси за да се избегнат вгнездените циклуси (Nested Loops) и да се забрза пребарувањето низ табелите.
     131
     132==== Додадени индекси:
     133
     134{{{
     135CREATE INDEX idx_vote_result_election_entity ON vote_result(election_id, entity_id);
     136CREATE INDEX idx_vote_result_station ON vote_result(station_id);
     137CREATE INDEX idx_election_type ON election(election_type_id);
     138}}}
     139
     140==== Време на извршување со индекси:
     141
     142**290 ms** (беше ~3500 ms)
     143
     144Иако со дополнително индексирање времето се стабилизираше на околу 290 ms, ова се смета за оптимален резултат за овој тип на комплексен аналитички поглед. Ова време е резултат на балансот помеѓу ефикасното пребарување низ индексите и потребата на базата да изврши агрегација и рангирање врз голема количина податоци во реално време.
     145
     146---
     147
     148=== 3. Анализа на поглед 3, vw_parliamentary_municipality_winners — победници на парламентарни избори по општини ===
     149
     150Овој поглед служи за детална анализа на изборните резултати од парламентарните избори на ниво на општина. Тој ги агрегира гласовите за секој политички субјект во секоја општина и го идентификува победникот (партијата со најголем број гласови) за таа територијална единица.
     151
     152Примарен филтер: `election_name` (на пр. `'Parliamentary Elections 2014'`). Секундарен филтер: `municipality_name`.
     153
     154Дефиниција на погледот:
     155{{{
     156CREATE OR REPLACE VIEW public.vw_parliamentary_municipality_winners AS
     157WITH aggregatedvotes AS (
     158    SELECT e.election_id, e.name AS election_name, r.name AS municipality_name,
     159           pe.name AS party_name, sum(vr.votes) AS total_votes
     160    FROM vote_result      vr
     161    JOIN election         e  ON vr.election_id    = e.election_id
     162    JOIN political_entity pe ON vr.entity_id      = pe.entity_id
     163    JOIN polling_station  ps ON vr.station_id     = ps.station_id
     164    JOIN region           r  ON ps.municipality_id = r.region_id
     165    WHERE e.election_type_id = 2
     166    GROUP BY e.election_id, e.name, r.name, pe.name
     167),
     168rankedwinners AS (
     169    SELECT *, rank() OVER (PARTITION BY election_id, municipality_name ORDER BY total_votes DESC) AS pos
     170    FROM aggregatedvotes
     171)
     172SELECT election_name, municipality_name,
     173       party_name          AS dominant_party,
     174       total_votes         AS winning_votes,
     175       'Municipality Winner'::text AS status
     176FROM rankedwinners
     177WHERE pos = 1;
     178}}}
     179
     180Прашалникот кој ќе го тестираме:
     181{{{
     182SELECT * FROM vw_parliamentary_municipality_winners
     183WHERE election_name = 'Parliamentary Elections 2014';
     184}}}
     185
     186==== Време на извршување без индекси:
     187
     188**17900 ms (~17.9 секунди)**
     189
     190Времето на извршување воопшто не е прифатливо. Според приложениот извршен план, прашалникот трае 17.9 секунди главно поради користењето на дискот за сортирање податоци и големиот број на меѓу-резултати (скоро 1 милион редови) кои се префрлаат помеѓу паралелните процеси. Без соодветни индекси, базата не може да ги избегне овие скапи операции.
     191
     192==== Додадени индекси:
     193
     194{{{
     195CREATE INDEX idx_vote_result_election_entity ON vote_result(election_id, entity_id);
     196CREATE INDEX idx_vote_result_station ON vote_result(station_id);
     197CREATE INDEX idx_election_type ON election(election_type_id);
     198CREATE INDEX idx_polling_station_municipality ON polling_station(municipality_id);
     199}}}
     200
     201==== Време на извршување со индекси:
     202
     203**1500 ms** (беше 17900 ms)
     204
     205По извршената финална оптимизација и консолидација на индексите, времето на извршување е драстично намалено од 17.9 секунди на 1.5 секунди. Подобрување: ~12x побрзо.
     206
     207---
     208
     209=== 4. Анализа на поглед 4, vw_presidential_by_municipality — претседателски гласови по општини ===
     210
     211Погледот овозможува детален приказ на гласовите освоени од претседателските кандидати, групирани по општина. Тој ги агрегира податоците од табелата `vote_result` и ги поврзува со податоците за кандидатите и општините за да генерира извештај за поддршката на кандидатите на локално ниво.
     212
     213Овој извештај ќе се користи за визуелизација на географската распределба на гласовите, каде преку филтрирање по `election_name` и `municipality_name` корисникот може моментално да ги добие финалните резултати за специфичен регион.
     214
     215Дефиниција на погледот:
     216{{{
     217CREATE OR REPLACE VIEW public.vw_presidential_by_municipality AS
     218WITH cityvotes AS (
     219    SELECT e.name AS election_name, r.name AS municipality_name,
     220           pe.name AS candidate_name, sum(vr.votes) AS total_votes
     221    FROM vote_result      vr
     222    JOIN election         e  ON vr.election_id    = e.election_id
     223    JOIN political_entity pe ON vr.entity_id      = pe.entity_id
     224    JOIN polling_station  ps ON vr.station_id     = ps.station_id
     225    JOIN region           r  ON ps.municipality_id = r.region_id
     226    WHERE e.election_type_id = 1
     227    GROUP BY e.name, r.name, pe.name
     228),
     229rankedcities AS (
     230    SELECT *, rank() OVER (PARTITION BY election_name, municipality_name ORDER BY total_votes DESC) AS pos
     231    FROM cityvotes
     232)
     233SELECT election_name, municipality_name, candidate_name,
     234       total_votes, 'City Winner'::text AS level
     235FROM rankedcities
     236WHERE pos = 1;
     237}}}
     238
     239Прашалникот кој ќе го тестираме:
     240{{{
     241SELECT * FROM vw_presidential_by_municipality
     242WHERE election_name = 'Presidential Elections 2019'
     243  AND municipality_name = 'Skopje';
     244}}}
     245
     246==== Време на извршување:
     247
     248**15 ms**
     249
     250При извршување на погледот за претседателски избори по општини, постигнати се врвни перформанси со време на одговор од само 15 милисекунди. Овој резултат е директна последица на претходно имплементираната стратегија за индексирање (индексите додадени за претходните погледи). Ова време е прифатливо па затоа нема да имаме потреба од дополнително индексирање.
     251
     252---
     253
     254=== 5. Анализа на поглед 5, vw_polling_station_voter_count — број на регистрирани гласачи по гласачко место ===
     255
     256Погледот служи за пресметување на вкупниот број на регистрирани гласачи за секое поединечно гласачко место. Наместо да се чуваат статични податоци во табелата `polling_station` (што би довело до редундантност и потенцијални аномалии), овој поглед врши агрегација во реално време врз основа на поврзаноста помеѓу гласачите и нивните доделени избирачки места.
     257
     258Ќе се користи за проверка дали одредено гласачко место има преголем број гласачи (надминување на капацитетот). Најчесто ќе се користи со филтер по `station_id` или `municipality_name`.
     259
     260Дефиниција на погледот:
     261{{{
     262CREATE OR REPLACE VIEW public.vw_polling_station_voter_count AS
     263SELECT r.name  AS region_name,
     264       ps.name AS station_name,
     265       count(v.voter_id) AS actual_registered_voters
     266FROM polling_station  ps
     267JOIN region           r  ON ps.municipality_id = r.region_id
     268LEFT JOIN voter       v  ON ps.station_id      = v.station_id
     269GROUP BY r.name, ps.name;
     270}}}
     271
     272Прашалникот кој ќе го тестираме:
     273{{{
     274SELECT * FROM vw_polling_station_voter_count
     275WHERE region_name = 'Куманово';
     276}}}
     277
     278==== Време на извршување без индекси:
     279
     280**3500 ms (~3.5 секунди)**
     281
     282При тестирање на погледот со филтер за општина Куманово, постигнато е почетно време на извршување од 3.5 секунди. Оптимизаторот успешно користи Index Scan врз колоната за име на регион, со што веднаш го изолира бараниот ентитет. Главниот товар на прашалникот доаѓа од агрегацијата на 83,500 записи за гласачи поврзани со избирачките места во таа општина. Иако времето е во секунди, HashAggregate операцијата работи со минимален мемориски зафат, што укажува на тоа дека погледот е стабилен и точно ги пресметува капацитетите на гласачките места во реално време.
     283
     284==== Додаден индекс:
     285
     286{{{
     287CREATE INDEX idx_voter_station ON voter(station_id);
     288}}}
     289
     290==== Време на извршување со индекс:
     291
     292**53 ms** (беше ~3500 ms)
     293
     294Со додавање на соодветен индекс, извршниот план за погледот покажа значителна структурна промена. Подобрување: ~66x побрзо.
     295
     296---
     297
     298=== 6. Анализа на поглед 6, vw_election_results — општи резултати по избори ===
     299
     300Овој поглед прикажува збирни изборни резултати по политички ентитет за секои избори, вклучувајќи го процентот на гласови, вкупниот број важечки гласови и дали ентитетот го поминал прагот од 5%.
     301
     302Дефиниција на погледот:
     303{{{
     304CREATE OR REPLACE VIEW public.vw_election_results AS
     305WITH election_totals AS (
     306    SELECT vote_result.election_id,
     307           sum(vote_result.votes) AS total_valid_votes
     308    FROM vote_result
     309    GROUP BY vote_result.election_id
     310),
     311entity_votes AS (
     312    SELECT vote_result.election_id,
     313           vote_result.entity_id,
     314           sum(vote_result.votes) AS total_votes
     315    FROM vote_result
     316    WHERE vote_result.candidate_id IS NULL
     317    GROUP BY vote_result.election_id, vote_result.entity_id
     318),
     319calc AS (
     320    SELECT ev.election_id,
     321           ev.entity_id,
     322           ev.total_votes,
     323           el.total_valid_votes,
     324           ev.total_votes::numeric * 100.0 / NULLIF(el.total_valid_votes, 0)::numeric AS vote_pct
     325    FROM entity_votes ev
     326    JOIN election_totals el USING (election_id)
     327)
     328SELECT e.election_id,
     329       e.name                            AS election_name,
     330       e.election_date,
     331       et.type_name                      AS election_type,
     332       pe.entity_id,
     333       pe.name                           AS entity_name,
     334       pe.type                           AS entity_type,
     335       c.total_votes,
     336       c.total_valid_votes,
     337       round(c.vote_pct, 2)              AS vote_share_pct,
     338       c.vote_pct >= 5.0                 AS passed_threshold,
     339       wm.method_name                    AS winner_method
     340FROM calc c
     341JOIN election          e  USING (election_id)
     342JOIN election_type     et ON et.election_type_id = e.election_type_id
     343JOIN winner_method     wm ON wm.method_id        = e.winner_method_id
     344JOIN political_entity  pe USING (entity_id);
     345}}}
     346
     347Прашалникот кој ќе го тестираме:
     348{{{
     349SELECT * FROM vw_election_results
     350WHERE election_name = 'Parliamentary Elections 2014'
     351ORDER BY vote_share_pct DESC;
     352}}}
     353
     354Овој поглед се потпира на агрегации над `vote_result` и join-ови со речниски табели (`election_type`, `winner_method`, `political_entity`) кои се мали. Основниот индекс на `vote_result(election_id)` е доволен за задоволителни перформанси. Дополнителна оптимизација не е потребна доколку веќе постои индекс на примарниот клуч и `election_id`.
     355
     356---
     357
     358=== 7. Анализа на поглед 7, vw_dhondt_seat_allocation — распределба на мандати по Д'Онт ===
     359
     360Овој поглед ја пресметува распределбата на пратенички мандати во изборни единици според методот Д'Онт. Тој е наменет за парламентарни избори и ги пресметува квоциентите за секој политички ентитет по изборен округ.
     361
     362Дефиниција на погледот:
     363{{{
     364CREATE OR REPLACE VIEW public.vw_dhondt_seat_allocation AS
     365WITH district_votes AS (
     366    SELECT ed.district_id, ed.election_id, ed.region_id, ed.seats_available,
     367           vr.entity_id, sum(vr.votes) AS district_votes
     368    FROM electoral_district ed
     369    JOIN election         e_1 ON e_1.election_id = ed.election_id
     370    JOIN vote_result       vr  ON vr.election_id  = ed.election_id
     371    JOIN polling_station   ps  ON ps.station_id   = vr.station_id
     372    JOIN region            r2  ON r2.region_id    = ps.municipality_id
     373    WHERE e_1.winner_method_id = 8
     374      AND vr.candidate_id IS NULL
     375      AND (r2.region_id = ed.region_id OR r2.parent_region_id = ed.region_id)
     376    GROUP BY ed.district_id, ed.election_id, ed.region_id, ed.seats_available, vr.entity_id
     377),
     378district_totals AS (
     379    SELECT district_id, sum(district_votes) AS total_district_votes FROM district_votes GROUP BY district_id
     380),
     381eligible AS (
     382    SELECT dv.*, dt.total_district_votes,
     383           dv.district_votes::numeric * 100.0 / NULLIF(dt.total_district_votes, 0::numeric) AS vote_pct
     384    FROM district_votes dv JOIN district_totals dt USING (district_id)
     385    WHERE (dv.district_votes::numeric * 100.0 / NULLIF(dt.total_district_votes, 0::numeric)) >= 5.0
     386),
     387dhondt AS (
     388    SELECT e_1.*, d.divisor,
     389           e_1.district_votes::numeric / d.divisor::numeric AS quotient
     390    FROM eligible e_1 CROSS JOIN LATERAL generate_series(1, e_1.seats_available) d(divisor)
     391),
     392ranked AS (
     393    SELECT *, rank() OVER (PARTITION BY district_id ORDER BY quotient DESC) AS rn FROM dhondt
     394),
     395seats AS (
     396    SELECT district_id, election_id, region_id, seats_available, entity_id,
     397           district_votes, total_district_votes, vote_pct,
     398           count(*) FILTER (WHERE rn <= seats_available) AS seats_won
     399    FROM ranked
     400    GROUP BY district_id, election_id, region_id, seats_available, entity_id,
     401             district_votes, total_district_votes, vote_pct
     402)
     403SELECT s.election_id, e.name AS election_name, e.election_date,
     404       s.district_id, r.name AS region_name, s.seats_available,
     405       s.entity_id, pe.name AS entity_name, pe.type AS entity_type,
     406       s.district_votes AS total_votes, s.total_district_votes,
     407       round(s.vote_pct, 2) AS vote_share_pct, s.seats_won
     408FROM seats s
     409JOIN election        e  USING (election_id)
     410JOIN region          r  ON r.region_id  = s.region_id
     411JOIN political_entity pe USING (entity_id)
     412ORDER BY s.election_id, s.district_id, s.seats_won DESC;
     413}}}
     414
     415Прашалникот кој ќе го тестираме:
     416{{{
     417SELECT * FROM vw_dhondt_seat_allocation
     418WHERE election_name = 'Parliamentary Elections 2014';
     419}}}
     420
     421Поради комплексноста на пресметките (CROSS JOIN LATERAL со generate_series, повеќе CTE-и, window функции), овој поглед е по природа тежок за извршување. Клучните индекси кои помагаат се:
     422
     423{{{
     424CREATE INDEX idx_vote_result_election_candidate ON vote_result(election_id, candidate_id);
     425CREATE INDEX idx_electoral_district_election ON electoral_district(election_id);
     426CREATE INDEX idx_region_parent ON region(parent_region_id);
     427}}}
     428
     429---
     430
     431=== 8. Анализа на поглед 8, vw_invalid_ballot_analysis — анализа на неважечки ливчиња ===
     432
     433Погледот прикажува број и процент на неважечки ливчиња по регион и избори.
     434
     435Дефиниција на погледот:
     436{{{
     437CREATE OR REPLACE VIEW public.vw_invalid_ballot_analysis AS
     438SELECT e.election_id,
     439       e.name                                                           AS election_name,
     440       r.region_id,
     441       r.name                                                           AS region_name,
     442       count(*)                                                         AS total_ballots,
     443       count(*) FILTER (WHERE NOT b.is_valid)                          AS invalid_ballots,
     444       round(count(*) FILTER (WHERE NOT b.is_valid)::numeric * 100.0
     445             / NULLIF(count(*), 0)::numeric, 2)                        AS invalid_pct
     446FROM ballot           b
     447JOIN election         e  ON e.election_id    = b.election_id
     448JOIN polling_station  ps ON ps.station_id    = b.station_id
     449JOIN region           r  ON r.region_id      = ps.municipality_id
     450GROUP BY e.election_id, e.name, r.region_id, r.name;
     451}}}
     452
     453Прашалникот кој ќе го тестираме:
     454{{{
     455SELECT * FROM vw_invalid_ballot_analysis
     456WHERE election_name = 'Parliamentary Elections 2014'
     457ORDER BY invalid_pct DESC;
     458}}}
     459
     460Индексот на `ballot(election_id, station_id)` (додаден за поглед 1) и `polling_station(municipality_id)` покриваат го најголемиот дел од оптимизацијата. Дополнителен индекс за брзо филтрирање по `is_valid`:
     461
     462{{{
     463CREATE INDEX idx_ballot_is_valid ON ballot(is_valid);
     464}}}
     465
     466---
     467
     468=== 9. Анализа на поглед 9, vw_voter_turnout — излезност на гласачи ===
     469
     470Погледот пресметува излезност на гласачи (turnout) по регион и избори, споредувајќи го бројот на регистрирани гласачи со вистински кастираните ливчиња.
     471
     472Дефиниција на погледот:
     473{{{
     474CREATE OR REPLACE VIEW public.vw_voter_turnout AS
     475WITH registered AS (
     476    SELECT se.election_id, ps.municipality_id AS region_id,
     477           sum(ps.registered_voter) AS registered_voters
     478    FROM station_election se JOIN polling_station ps ON ps.station_id = se.station_id
     479    GROUP BY se.election_id, ps.municipality_id
     480),
     481ballots AS (
     482    SELECT b.election_id, ps.municipality_id AS region_id,
     483           count(*) AS ballots_cast,
     484           count(*) FILTER (WHERE b.is_valid)      AS valid_ballots,
     485           count(*) FILTER (WHERE NOT b.is_valid)  AS invalid_ballots
     486    FROM ballot b JOIN polling_station ps ON ps.station_id = b.station_id
     487    GROUP BY b.election_id, ps.municipality_id
     488),
     489combined AS (
     490    SELECT COALESCE(r.election_id, b.election_id) AS election_id,
     491           COALESCE(r.region_id,   b.region_id)   AS region_id,
     492           r.registered_voters,
     493           b.ballots_cast, b.valid_ballots, b.invalid_ballots,
     494           b.ballots_cast::numeric * 100.0 / NULLIF(r.registered_voters, 0)::numeric AS turnout_pct_raw,
     495           b.invalid_ballots::numeric * 100.0 / NULLIF(b.ballots_cast, 0)::numeric   AS invalid_pct_raw
     496    FROM registered r FULL JOIN ballots b ON r.election_id = b.election_id AND r.region_id = b.region_id
     497)
     498SELECT e.election_id, e.name AS election_name, e.election_date,
     499       et.type_name AS election_type,
     500       c.region_id, rg.name AS region_name,
     501       COALESCE(c.registered_voters, 0::bigint) AS registered_voters,
     502       COALESCE(c.ballots_cast,      0::bigint) AS ballots_cast,
     503       COALESCE(c.valid_ballots,     0::bigint) AS valid_ballots,
     504       COALESCE(c.invalid_ballots,   0::bigint) AS invalid_ballots,
     505       round(c.turnout_pct_raw, 2) AS turnout_pct,
     506       round(c.invalid_pct_raw, 2) AS invalid_pct
     507FROM combined c
     508JOIN election      e  USING (election_id)
     509JOIN election_type et ON et.election_type_id = e.election_type_id
     510JOIN region        rg ON rg.region_id        = c.region_id
     511WHERE c.registered_voters IS NOT NULL OR c.ballots_cast IS NOT NULL
     512ORDER BY e.election_date, round(c.turnout_pct_raw, 2) DESC;
     513}}}
     514
     515Прашалникот кој ќе го тестираме:
     516{{{
     517SELECT * FROM vw_voter_turnout
     518WHERE election_name = 'Parliamentary Elections 2014';
     519}}}
     520
     521Постоечките индекси на `ballot(election_id, station_id)` и `station_election(station_id)` се доволни за оптимално извршување. Доколку `station_election` нема индекс на `election_id`:
     522
     523{{{
     524CREATE INDEX idx_station_election_election ON station_election(election_id);
     525}}}
     526
     527---
     528
     529=== 10. Анализа на поглед 10, vw_polling_station_stats — статистики по гласачко место ===
     530
     531Погледот дава детален статистички преглед по гласачко место: регистрирани гласачи, кастирани ливчиња, важечки/неважечки и процент на излезност.
     532
     533Дефиниција на погледот:
     534{{{
     535CREATE OR REPLACE VIEW public.vw_polling_station_stats AS
     536SELECT e.election_id,
     537       e.name                                                      AS election_name,
     538       ps.station_id,
     539       ps.name                                                     AS station_name,
     540       count(DISTINCT v.voter_id)                                  AS registered_voters,
     541       count(b.ballot_id)                                          AS ballots_cast,
     542       count(b.ballot_id) FILTER (WHERE b.is_valid)               AS valid_ballots,
     543       count(b.ballot_id) FILTER (WHERE NOT b.is_valid)           AS invalid_ballots,
     544       round(count(b.ballot_id)::numeric * 100.0
     545             / NULLIF(count(DISTINCT v.voter_id), 0)::numeric, 2) AS turnout_pct
     546FROM polling_station  ps
     547JOIN station_election se ON se.station_id  = ps.station_id
     548JOIN election         e  ON e.election_id  = se.election_id
     549LEFT JOIN ballot      b  ON b.station_id   = ps.station_id AND b.election_id = e.election_id
     550LEFT JOIN voter       v  ON v.station_id   = ps.station_id
     551GROUP BY e.election_id, e.name, ps.station_id, ps.name;
     552}}}
     553
     554Прашалникот кој ќе го тестираме:
     555{{{
     556SELECT * FROM vw_polling_station_stats
     557WHERE election_name = 'Parliamentary Elections 2014'
     558ORDER BY turnout_pct DESC
     559LIMIT 20;
     560}}}
     561
     562Потребните индекси (дел веќе постојат):
     563
     564{{{
     565CREATE INDEX idx_voter_station ON voter(station_id);
     566CREATE INDEX idx_ballot_station_election ON ballot(station_id, election_id);
     567CREATE INDEX idx_station_election_ids ON station_election(station_id, election_id);
     568}}}
     569
     570---
     571
     572=== 11. Анализа на поглед 11, vw_regional_voting_patterns — регионални образци на гласање ===
     573
     574Погледот ги прикажува доминантните политички субјекти по регион за секои избори, земајќи ја предвид хиерархијата на региони (општина → подрегион → држава).
     575
     576Дефиниција на погледот:
     577{{{
     578CREATE OR REPLACE VIEW public.vw_regional_voting_patterns AS
     579WITH regionhierarchy AS (
     580    SELECT ps.station_id,
     581           r.name        AS municipality_name,
     582           parent.name   AS sub_region_name,
     583           grandparent.name AS country_name
     584    FROM polling_station ps
     585    JOIN region      r           ON ps.municipality_id     = r.region_id
     586    LEFT JOIN region parent      ON r.parent_region_id     = parent.region_id
     587    LEFT JOIN region grandparent ON parent.parent_region_id = grandparent.region_id
     588),
     589candidatevotes AS (
     590    SELECT e.election_id, e.name AS election_name, e.election_type_id,
     591           pe.name AS candidate_name,
     592           CASE WHEN e.election_type_id = 1 THEN rh.country_name ELSE rh.municipality_name END AS final_region,
     593           sum(vr.votes) AS total_votes_sum
     594    FROM vote_result      vr
     595    JOIN election         e  ON vr.election_id = e.election_id
     596    JOIN political_entity pe ON vr.entity_id   = pe.entity_id
     597    JOIN regionhierarchy  rh ON vr.station_id  = rh.station_id
     598    GROUP BY e.election_id, e.name, e.election_type_id, pe.name,
     599             CASE WHEN e.election_type_id = 1 THEN rh.country_name ELSE rh.municipality_name END
     600),
     601rankedwinners AS (
     602    SELECT *, rank() OVER (PARTITION BY election_id, final_region ORDER BY total_votes_sum DESC) AS rnk
     603    FROM candidatevotes
     604)
     605SELECT election_name, final_region AS region_name,
     606       candidate_name AS dominant_entity,
     607       total_votes_sum AS winning_votes,
     608       'Winner'::text AS status
     609FROM rankedwinners
     610WHERE rnk = 1 AND final_region IS NOT NULL;
     611}}}
     612
     613Прашалникот кој ќе го тестираме:
     614{{{
     615SELECT * FROM vw_regional_voting_patterns
     616WHERE election_name = 'Parliamentary Elections 2014';
     617}}}
     618
     619Индексот на `region(parent_region_id)` (додаден за поглед 7) и постоечките индекси на `vote_result` и `polling_station` се доволни за оптимизација на овој поглед.
     620
     621---
     622
     623=== 12. Анализа на поглед 12, vw_party_performance_over_time — перформанси на партии низ изборите ===
     624
     625Погледот го прати уделот на гласови на секој политички субјект низ повеќе изборни циклуси, овозможувајќи трендовска анализа.
     626
     627Дефиниција на погледот:
     628{{{
     629CREATE OR REPLACE VIEW public.vw_party_performance_over_time AS
     630WITH party_votes AS (
     631    SELECT e.election_id, e.name AS election_name, e.election_date,
     632           pe.entity_id, pe.name AS entity_name, sum(vr.votes) AS total_votes
     633    FROM vote_result       vr
     634    JOIN election          e  ON e.election_id  = vr.election_id
     635    JOIN political_entity  pe ON pe.entity_id   = vr.entity_id
     636    WHERE vr.candidate_id IS NULL
     637    GROUP BY e.election_id, e.name, e.election_date, pe.entity_id, pe.name
     638),
     639totals AS (
     640    SELECT election_id, sum(total_votes) AS total_election_votes FROM party_votes GROUP BY election_id
     641)
     642SELECT pv.election_id, pv.election_name, pv.election_date,
     643       pv.entity_id, pv.entity_name, pv.total_votes, t.total_election_votes,
     644       round(pv.total_votes::numeric * 100.0 / NULLIF(t.total_election_votes, 0::numeric), 2) AS vote_share_pct
     645FROM party_votes pv JOIN totals t USING (election_id)
     646ORDER BY pv.entity_name, pv.election_date;
     647}}}
     648
     649Прашалникот кој ќе го тестираме:
     650{{{
     651SELECT * FROM vw_party_performance_over_time
     652WHERE entity_name = 'VMRO-DPMNE'
     653ORDER BY election_date;
     654}}}
     655
     656Прашалникот пребарува по `entity_name`, па додаваме индекс:
     657
     658{{{
     659CREATE INDEX idx_political_entity_name ON political_entity(name);
     660CREATE INDEX idx_vote_result_candidate_null ON vote_result(election_id, entity_id) WHERE candidate_id IS NULL;
     661}}}
     662
     663Со овие индекси времето на извршување значително се намалува, бидејќи PostgreSQL може директно да ги пронајде гласовите без кандидат и да го изолира ентитетот по ime.