| | 1 | = Фаза 5: Напредна тема - Партиционирање = |
| | 2 | |
| | 3 | |
| | 4 | |
| | 5 | |
| | 6 | [[BR]] |
| | 7 | == 1. Опис == |
| | 8 | Партиционирана е централната табела '''Zapisnik''' (~10.000.000 записи) по '''RANGE''' стратегија врз колоната `datum`, со гранулација по '''година''' (партиции 2021–2026 + `DEFAULT`). Партиционирањето повлече измени и врз двете зависни табели што референцираат `Zapisnik` - '''Stavka_Zapisnik''' и '''Uplata''' - за да се зачува интегритетот, како и дополнување на процедурите што вметнуваат во нив. |
| | 9 | |
| | 10 | |
| | 11 | [[BR]] |
| | 12 | == 2. Избор на табела и стратегија == |
| | 13 | `Zapisnik` е најголемата и најфреквентно пребарувана табела во системот. Записите се додаваат постојано, се групираат природно по `datum`, а голем дел од погледите и прегледите филтрираат по временски период. Со партиционирање по `datum`: |
| | 14 | * при филтрирање по период, планерот пристапува само до релевантната година (партиција), наместо до сите ~10М редови; |
| | 15 | * архивирање на стари записници станува отстранување на цела партиција, наместо `DELETE` врз милиони редови. |
| | 16 | |
| | 17 | |
| | 18 | [[BR]] |
| | 19 | == 3. Измени во шемата == |
| | 20 | |
| | 21 | === 3.1 Примарен клуч === |
| | 22 | Оригиналниот PK беше само `id_na_zapisnik`. Бидејќи PostgreSQL бара партициониониот клуч да биде дел од PK, тој е направен '''композитен''': `(id_na_zapisnik, datum)`. |
| | 23 | |
| | 24 | === 3.2 Надворешни клучеви === |
| | 25 | Поради композитниот PK, веќе нема уникатност само на `id_na_zapisnik`, па старите FK од `Stavka_Zapisnik` и `Uplata` не важат. За да се зачува интегритетот: |
| | 26 | * во `Stavka_Zapisnik` и во `Uplata` е додадена нова колона `datum_zapisnik`; |
| | 27 | * Foreign keys се преправени во '''композитни''': `(id, datum_zapisnik) → Zapisnik(id_na_zapisnik, datum)`. |
| | 28 | |
| | 29 | === 3.3 Погледи === |
| | 30 | Сите погледи (`v_site_zapisnici`, `v_zapisnici_detalno`, `v_neplateni_kazni`, `v_povtoreni_prekrsoci`, `v_policajci_prosek_zapisnici`, `v_istorija_gragjanin`) '''не бараат никаква измена''' — продолжуваат да работат, бидејќи пристапот до партиционираната табела е ист како до обична. |
| | 31 | |
| | 32 | === 3.4 Процедури === |
| | 33 | Процедурите `kreiraj_zapisnik_so_prekrsok` и `dodadi_stavka_zapisnik` се '''дополнети''' да ја пополнуваат `datum_zapisnik` при вметнување во `Uplata`/`Stavka_Zapisnik`, инаку композитниот FK би бил прекршен. |
| | 34 | |
| | 35 | === 3.5 Тригери === |
| | 36 | Тригерот `azuriraj_iznos_uplata` врз `Stavka_Zapisnik` е исклучен само за време на масовното пополнување на `datum_zapisnik`, за да не се активира 20М пати. |
| | 37 | |
| | 38 | |
| | 39 | [[BR]] |
| | 40 | == 4. Имплементација == |
| | 41 | |
| | 42 | === Чекор 1 - Додавање и пополнување на datum_zapisnik во децата === |
| | 43 | Се прави '''додека старата `Zapisnik` сè уште постои''' (за да се извлече датумот). Тригерите се исклучуваат за време на пополнувањето. |
| | 44 | {{{ |
| | 45 | #!sql |
| | 46 | ALTER TABLE Stavka_Zapisnik DISABLE TRIGGER USER; |
| | 47 | ALTER TABLE Stavka_Zapisnik ADD COLUMN datum_zapisnik date; |
| | 48 | UPDATE Stavka_Zapisnik sz |
| | 49 | SET datum_zapisnik = z.datum |
| | 50 | FROM Zapisnik z |
| | 51 | WHERE sz.id_na_zapisnik = z.id_na_zapisnik; |
| | 52 | ALTER TABLE Stavka_Zapisnik ENABLE TRIGGER USER; |
| | 53 | |
| | 54 | ALTER TABLE Uplata ADD COLUMN datum_zapisnik date; |
| | 55 | UPDATE Uplata u |
| | 56 | SET datum_zapisnik = z.datum |
| | 57 | FROM Zapisnik z |
| | 58 | WHERE u.id_zapisnik = z.id_na_zapisnik; |
| | 59 | }}} |
| | 60 | |
| | 61 | === Чекор 2 - Отстранување на старите FK === |
| | 62 | Не можат да опстанат покрај новиот композитен PK. |
| | 63 | {{{ |
| | 64 | #!sql |
| | 65 | ALTER TABLE Stavka_Zapisnik DROP CONSTRAINT IF EXISTS stavka_zapisnik_id_na_zapisnik_fkey; |
| | 66 | ALTER TABLE Uplata DROP CONSTRAINT IF EXISTS uplata_id_zapisnik_fkey; |
| | 67 | }}} |
| | 68 | |
| | 69 | === Чекор 3 - Преименување и креирање на партиционираната табела === |
| | 70 | Старата `Zapisnik_old` се задржува како baseline копија. Новата се создава со композитен PK и `PARTITION BY RANGE (datum)`. |
| | 71 | {{{ |
| | 72 | #!sql |
| | 73 | ALTER TABLE Zapisnik RENAME TO Zapisnik_old; |
| | 74 | |
| | 75 | CREATE TABLE Zapisnik ( |
| | 76 | id_na_zapisnik int, |
| | 77 | vreme time, |
| | 78 | datum date NOT NULL, |
| | 79 | lokacija varchar(100), |
| | 80 | Potpis boolean DEFAULT false, |
| | 81 | id_slucaj int, |
| | 82 | EMBG_Prekrsuvach char(13), |
| | 83 | Vozilo_Broj_Sasija varchar(17), |
| | 84 | EMBG_Policaec char(13), |
| | 85 | PRIMARY KEY (id_na_zapisnik, datum), |
| | 86 | FOREIGN KEY (id_slucaj) REFERENCES Slucaj(id_slucaj) ON UPDATE CASCADE ON DELETE RESTRICT, |
| | 87 | FOREIGN KEY (EMBG_Prekrsuvach) REFERENCES Gragjanin(EMBG) ON UPDATE CASCADE ON DELETE RESTRICT, |
| | 88 | FOREIGN KEY (Vozilo_Broj_Sasija) REFERENCES Vozilo(broj_na_sasija) ON UPDATE CASCADE ON DELETE RESTRICT, |
| | 89 | FOREIGN KEY (EMBG_Policaec) REFERENCES Policaec(EMBG_P) ON UPDATE CASCADE ON DELETE RESTRICT |
| | 90 | ) PARTITION BY RANGE (datum); |
| | 91 | }}} |
| | 92 | |
| | 93 | === Чекор 4 - Креирање на партициите по година === |
| | 94 | {{{ |
| | 95 | #!sql |
| | 96 | CREATE TABLE Zapisnik_2021 PARTITION OF Zapisnik FOR VALUES FROM ('2021-01-01') TO ('2022-01-01'); |
| | 97 | CREATE TABLE Zapisnik_2022 PARTITION OF Zapisnik FOR VALUES FROM ('2022-01-01') TO ('2023-01-01'); |
| | 98 | CREATE TABLE Zapisnik_2023 PARTITION OF Zapisnik FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); |
| | 99 | CREATE TABLE Zapisnik_2024 PARTITION OF Zapisnik FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); |
| | 100 | CREATE TABLE Zapisnik_2025 PARTITION OF Zapisnik FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); |
| | 101 | CREATE TABLE Zapisnik_2026 PARTITION OF Zapisnik FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); |
| | 102 | CREATE TABLE Zapisnik_default PARTITION OF Zapisnik DEFAULT; |
| | 103 | }}} |
| | 104 | |
| | 105 | === Чекор 5 - Миграција на податоците === |
| | 106 | PostgreSQL автоматски го распоредува секој од ~10М редови во соодветната партиција. `ANALYZE` за свежа статистика. |
| | 107 | {{{ |
| | 108 | #!sql |
| | 109 | INSERT INTO Zapisnik (id_na_zapisnik, vreme, datum, lokacija, Potpis, |
| | 110 | id_slucaj, EMBG_Prekrsuvach, Vozilo_Broj_Sasija, EMBG_Policaec) |
| | 111 | SELECT id_na_zapisnik, vreme, datum, lokacija, Potpis, |
| | 112 | id_slucaj, EMBG_Prekrsuvach, Vozilo_Broj_Sasija, EMBG_Policaec |
| | 113 | FROM Zapisnik_old; |
| | 114 | |
| | 115 | |
| | 116 | }}} |
| | 117 | |
| | 118 | === Чекор 6 - Враќање на интегритетот (композитни FK) === |
| | 119 | {{{ |
| | 120 | #!sql |
| | 121 | ALTER TABLE Stavka_Zapisnik |
| | 122 | ADD CONSTRAINT stavka_zapisnik_zapisnik_fk |
| | 123 | FOREIGN KEY (id_na_zapisnik, datum_zapisnik) |
| | 124 | REFERENCES Zapisnik (id_na_zapisnik, datum) |
| | 125 | ON UPDATE CASCADE ON DELETE CASCADE; |
| | 126 | |
| | 127 | ALTER TABLE Uplata |
| | 128 | ADD CONSTRAINT uplata_zapisnik_fk |
| | 129 | FOREIGN KEY (id_zapisnik, datum_zapisnik) |
| | 130 | REFERENCES Zapisnik (id_na_zapisnik, datum) |
| | 131 | ON UPDATE CASCADE ON DELETE RESTRICT; |
| | 132 | }}} |
| | 133 | |
| | 134 | === Чекор 7 - Дополнување на процедурите === |
| | 135 | За процедурите да продолжат да работат со композитниот FK, при вметнување се предава и `datum_zapisnik`. Пример од `kreiraj_zapisnik_so_prekrsok`: |
| | 136 | {{{ |
| | 137 | #!sql |
| | 138 | INSERT INTO Uplata (iznos, status, Uplatil_Gragjanin, id_zapisnik, datum_zapisnik) |
| | 139 | VALUES (0, 'Neplateno', p_embg_prekrsuvac, v_id_zapisnik, p_datum); |
| | 140 | |
| | 141 | INSERT INTO Stavka_Zapisnik (reden_broj, id_na_zapisnik, id_na_prekrsok, datum_zapisnik) |
| | 142 | VALUES (1, v_id_zapisnik, p_id_prekrsok, p_datum); |
| | 143 | }}} |
| | 144 | Истото важи и за `dodadi_stavka_zapisnik` — прво се чита `datum` на записникот, па се запишува во `datum_zapisnik`. |
| | 145 | |
| | 146 | |
| | 147 | [[BR]] |
| | 148 | == 5. Резултати и споредба == |
| | 149 | |
| | 150 | === 5.1 План на извршување === |
| | 151 | Истото барање е извршено врз непартиционираната (`Zapisnik_old`) и врз партиционираната табела, со `EXPLAIN (ANALYZE, BUFFERS)`: |
| | 152 | {{{ |
| | 153 | #!sql |
| | 154 | EXPLAIN (ANALYZE, BUFFERS) |
| | 155 | SELECT * FROM Zapisnik |
| | 156 | WHERE datum BETWEEN '2024-03-01' AND '2024-03-31'; |
| | 157 | }}} |
| | 158 | '''Пред партиционирањето''' — планерот изведе `Parallel Seq Scan` врз целата табела `Zapisnik_old` (време ~2325 ms): |
| | 159 | |
| | 160 | [[Image(before.png, width=1000)]] |
| | 161 | |
| | 162 | '''По партиционирањето''' — благодарение на '''partition pruning''', планерот пристапи '''само''' до партицијата `Zapisnik_2024` (`Seq Scan`, време ~449 ms), отфрлајќи ги сите останати партиции: |
| | 163 | |
| | 164 | [[Image(after.png, width=1000)]] |
| | 165 | |
| | 166 | === 5.3 Споредба на перформанси === |
| | 167 | ||='''Аспект'''=||='''Пред (Zapisnik_old)'''=||='''Потоа (партиционирана)'''=|| |
| | 168 | || Тип на пристап || Parallel Seq Scan врз цела табела || Seq Scan само на Zapisnik_2024 || |
| | 169 | || Скенирани партиции || цела табела || само Zapisnik_2024 (pruning) || |
| | 170 | || Цена (cost) || 1000.00 – 362222.83 || 0.00 – 60456.30 || |
| | 171 | || Време || 2325.078 ms || 449.092 ms || |
| | 172 | || Вратени редови || ~168.330 || ~168.330 || |
| | 173 | |
| | 174 | Резултатот е ист (~168.330 редови за месецот), но '''количината на работа е драстично помала''': времето падна од ~2325 ms на ~449 ms (≈ '''5 пати побрзо'''), а проценетата цена од ~362.000 на ~60.000 (≈ '''6 пати помала'''). Дополнително, baseline-от мораше да вклучи паралелизам за да ја совлада целата табела, додека партиционираната верзија беше побрза и со едноставен `Seq Scan` врз една партиција. |
| | 175 | |
| | 176 | |
| | 177 | [[BR]] |
| | 178 | == 6. Одржување == |
| | 179 | Архивирање на цела година и додавање нова партиција, без `DELETE`: |
| | 180 | {{{ |
| | 181 | #!sql |
| | 182 | ALTER TABLE Zapisnik DETACH PARTITION Zapisnik_2021; |
| | 183 | |
| | 184 | CREATE TABLE Zapisnik_2027 PARTITION OF Zapisnik |
| | 185 | FOR VALUES FROM ('2027-01-01') TO ('2028-01-01'); |
| | 186 | }}} |
| | 187 | |
| | 188 | |
| | 189 | [[BR]] |
| | 190 | == 7. Прилози == |
| | 191 | * [attachment:particioniranje.sql Скрипта за партиционирање] — целосна имплементација. |
| | 192 | * before.png, after.png — слики од плановите на извршување (вметнати во секција 5.2). |