| 1 | -- напредна тема 1: партиционирање на song_streams по streamed_at
|
|---|
| 2 |
|
|---|
| 3 | -- 1. pravime nova shema za arhiviranite particii
|
|---|
| 4 |
|
|---|
| 5 | -- idealno ova bi bilo cuvano vo nekoj poevtin storage bidejki sakame da gi cuvame podatocite,
|
|---|
| 6 | -- no nema cesto da gi pristapuvame
|
|---|
| 7 | CREATE SCHEMA IF NOT EXISTS archive;
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 | -- 2. transakcija za migriranje na song_streams vo particionirana tabela
|
|---|
| 11 | BEGIN;
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 | -- 2.1 prvo pravime detach na sekvencata za posle da mozeme da ja prikacime kon novata tabela,
|
|---|
| 15 | -- inaku drop table bi ja izbrisala
|
|---|
| 16 | ALTER SEQUENCE song_streams_id_seq OWNED BY NONE;
|
|---|
| 17 |
|
|---|
| 18 | -- 2.2a se uste ne ja drop-nuvame tabelata bidejki ke ni treba za proverka na kraj. samo ja preimenuvame
|
|---|
| 19 | ALTER TABLE song_streams RENAME TO song_streams_old;
|
|---|
| 20 |
|
|---|
| 21 | -- 2.2b gi preimenuvame indeksite bidejki ovie iminja za indeksite ke ni trebaat za indeksite na novata relacija
|
|---|
| 22 | ALTER INDEX song_streams_pkey RENAME TO song_streams_old_pkey;
|
|---|
| 23 | ALTER INDEX idx_song_streams_streamed_at_song_id RENAME TO song_streams_old_streamed_at_song_id;
|
|---|
| 24 | ALTER INDEX idx_song_streams_user_id RENAME TO song_streams_old_user_id;
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 | -- 2.3 pravime nova particionirana parent tabela. istite koloni no so slozen PK koj sto go sodrzi i
|
|---|
| 29 | -- particioniot kluc (streamed_at) - ova e requirement od samiot postgres
|
|---|
| 30 | CREATE TABLE song_streams (
|
|---|
| 31 | id bigint NOT NULL DEFAULT nextval('song_streams_id_seq'),
|
|---|
| 32 | playback_session_id bigint NOT NULL,
|
|---|
| 33 | song_id bigint NOT NULL,
|
|---|
| 34 | streamed_at timestamp without time zone NOT NULL,
|
|---|
| 35 | user_id bigint NOT NULL,
|
|---|
| 36 |
|
|---|
| 37 | PRIMARY KEY (id, streamed_at),
|
|---|
| 38 |
|
|---|
| 39 | FOREIGN KEY (playback_session_id) REFERENCES playback_sessions(id) ON DELETE CASCADE,
|
|---|
| 40 | FOREIGN KEY (song_id) REFERENCES songs(id)
|
|---|
| 41 | ) PARTITION BY RANGE (streamed_at);
|
|---|
| 42 |
|
|---|
| 43 | -- 2.4 ja prikacuvame sekvencata od prethodno
|
|---|
| 44 | ALTER SEQUENCE song_streams_id_seq OWNED BY song_streams.id;
|
|---|
| 45 |
|
|---|
| 46 | -- 2.5 gi rekreirame indeksite na parent-ot
|
|---|
| 47 | -- postgres avtomatski ke gi kreira i menadzira indeksite za site particii
|
|---|
| 48 | CREATE INDEX idx_song_streams_streamed_at_song_id ON song_streams (streamed_at, song_id);
|
|---|
| 49 | CREATE INDEX idx_song_streams_user_id ON song_streams (user_id);
|
|---|
| 50 |
|
|---|
| 51 | -- 2.6 gi kreirame inicijalnite particii za postoeckite podatoci i nekolku meseci unapred.
|
|---|
| 52 | -- vo idnina particiite ke bidat kreirani avtomatski, ova se koristi samo za inicijalniot bootstrap
|
|---|
| 53 | -- kreirame particii od 2025-11 do 2026-07
|
|---|
| 54 | CREATE TABLE song_streams_y2025m11 PARTITION OF song_streams FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
|
|---|
| 55 | CREATE TABLE song_streams_y2025m12 PARTITION OF song_streams FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
|
|---|
| 56 | CREATE TABLE song_streams_y2026m01 PARTITION OF song_streams FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
|---|
| 57 | CREATE TABLE song_streams_y2026m02 PARTITION OF song_streams FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
|
|---|
| 58 | CREATE TABLE song_streams_y2026m03 PARTITION OF song_streams FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
|
|---|
| 59 | CREATE TABLE song_streams_y2026m04 PARTITION OF song_streams FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
|---|
| 60 | CREATE TABLE song_streams_y2026m05 PARTITION OF song_streams FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
|---|
| 61 | CREATE TABLE song_streams_y2026m06 PARTITION OF song_streams FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
|---|
| 62 | CREATE TABLE song_streams_y2026m07 PARTITION OF song_streams FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
|
|---|
| 63 |
|
|---|
| 64 | -- default particija za zapisi koj nemaat soodvetna particija
|
|---|
| 65 | CREATE TABLE song_streams_default PARTITION OF song_streams DEFAULT;
|
|---|
| 66 |
|
|---|
| 67 | -- 2.7 migracija na podatocite. insertot implicitno gi rutira site zapisi kon tocnata particija
|
|---|
| 68 | INSERT INTO song_streams (id, playback_session_id, song_id, streamed_at, user_id)
|
|---|
| 69 | SELECT id, playback_session_id, song_id, streamed_at, user_id
|
|---|
| 70 | FROM song_streams_old;
|
|---|
| 71 |
|
|---|
| 72 | -- 2.8 update na sekvencata
|
|---|
| 73 | SELECT setval('song_streams_id_seq', (SELECT max(id) FROM song_streams));
|
|---|
| 74 |
|
|---|
| 75 | -- 2.9 proverki za da vidime deka site podatoci se preneseni
|
|---|
| 76 | -- SELECT count(*) FROM song_streams;
|
|---|
| 77 | -- SELECT count(*) FROM song_streams_old;
|
|---|
| 78 |
|
|---|
| 79 | COMMIT;
|
|---|
| 80 |
|
|---|
| 81 | -- 2.10 drop na starata tabela (samo otkako sme utvrdile deka zavisnite views/matviews se postaveni kon novata tabela)
|
|---|
| 82 | -- DROP TABLE song_streams_old;
|
|---|
| 83 |
|
|---|
| 84 | -- 2.11 refresh na statistikite na planner-ot
|
|---|
| 85 | ANALYZE song_streams;
|
|---|
| 86 |
|
|---|
| 87 |
|
|---|
| 88 |
|
|---|
| 89 | -- 3. funkcija za kreiranje segasna + idni particii
|
|---|
| 90 | -- funkcijata e idempotentna - moze da se izvrsuva poveke pati vo mesecot bez neposakuvani side effects
|
|---|
| 91 | CREATE OR REPLACE FUNCTION create_song_streams_partitions(months_ahead integer DEFAULT 2)
|
|---|
| 92 | RETURNS void
|
|---|
| 93 | LANGUAGE plpgsql
|
|---|
| 94 | AS $$
|
|---|
| 95 | DECLARE
|
|---|
| 96 | first_month date := date_trunc('month', current_date)::date;
|
|---|
| 97 | m date;
|
|---|
| 98 | part_name text;
|
|---|
| 99 | from_ts timestamp;
|
|---|
| 100 | to_ts timestamp;
|
|---|
| 101 | BEGIN
|
|---|
| 102 | FOR i IN 0..months_ahead LOOP
|
|---|
| 103 | m := (first_month + (i || ' months')::interval)::date;
|
|---|
| 104 | part_name := format('song_streams_y%sm%s', to_char(m, 'YYYY'), to_char(m, 'MM'));
|
|---|
| 105 | from_ts := m;
|
|---|
| 106 | to_ts := (m + interval '1 month');
|
|---|
| 107 |
|
|---|
| 108 | IF NOT EXISTS (
|
|---|
| 109 | SELECT 1 FROM pg_class WHERE relname = part_name AND relkind = 'r'
|
|---|
| 110 | ) THEN
|
|---|
| 111 | EXECUTE format(
|
|---|
| 112 | 'CREATE TABLE %I PARTITION OF song_streams FOR VALUES FROM (%L) TO (%L)',
|
|---|
| 113 | part_name, from_ts, to_ts
|
|---|
| 114 | );
|
|---|
| 115 | RAISE NOTICE 'Created partition % (% .. %)', part_name, from_ts, to_ts;
|
|---|
| 116 | END IF;
|
|---|
| 117 | END LOOP;
|
|---|
| 118 | END;
|
|---|
| 119 | $$;
|
|---|
| 120 |
|
|---|
| 121 |
|
|---|
| 122 | -- 4. funkcija za arhiviranje stari particii
|
|---|
| 123 | -- retention_months = kolku meseci sakame da cuvame. default vrednost e 12 (1 godina)
|
|---|
| 124 |
|
|---|
| 125 | CREATE OR REPLACE FUNCTION archive_song_streams_partitions(retention_months integer DEFAULT 12)
|
|---|
| 126 | RETURNS void
|
|---|
| 127 | LANGUAGE plpgsql
|
|---|
| 128 | AS $$
|
|---|
| 129 | DECLARE
|
|---|
| 130 | cutoff_month date := (date_trunc('month', current_date)
|
|---|
| 131 | - (retention_months || ' months')::interval)::date;
|
|---|
| 132 | r record;
|
|---|
| 133 | yr int;
|
|---|
| 134 | mo int;
|
|---|
| 135 | part_month date;
|
|---|
| 136 | archived_name text;
|
|---|
| 137 | BEGIN
|
|---|
| 138 | -- zamrznuvame brojaci za sekoj zatvoren mesec pred da pocneme
|
|---|
| 139 | PERFORM seal_closed_song_streams_months();
|
|---|
| 140 |
|
|---|
| 141 | FOR r IN
|
|---|
| 142 | SELECT c.relname
|
|---|
| 143 | FROM pg_inherits i
|
|---|
| 144 | JOIN pg_class c ON c.oid = i.inhrelid
|
|---|
| 145 | JOIN pg_class p ON p.oid = i.inhparent
|
|---|
| 146 | WHERE p.relname = 'song_streams'
|
|---|
| 147 | AND c.relname ~ '^song_streams_y\d{4}m\d{2}$' -- isklucuva default particija
|
|---|
| 148 | LOOP
|
|---|
| 149 | yr := substring(r.relname from 'y(\d{4})m')::int;
|
|---|
| 150 | mo := substring(r.relname from 'm(\d{2})$')::int;
|
|---|
| 151 | part_month := make_date(yr, mo, 1);
|
|---|
| 152 |
|
|---|
| 153 | IF part_month < cutoff_month THEN
|
|---|
| 154 | archived_name := r.relname || '_archived';
|
|---|
| 155 |
|
|---|
| 156 | -- 1. otstranuvame particija od parent tabelata -> stanuva standalone tabela.
|
|---|
| 157 | EXECUTE format('ALTER TABLE song_streams DETACH PARTITION %I', r.relname);
|
|---|
| 158 |
|
|---|
| 159 | -- 2. ja pomestuvame vo archive shemata i ja preimenuvame soodvetno
|
|---|
| 160 | EXECUTE format('ALTER TABLE %I SET SCHEMA archive', r.relname);
|
|---|
| 161 | EXECUTE format('ALTER TABLE archive.%I RENAME TO %I', r.relname, archived_name);
|
|---|
| 162 |
|
|---|
| 163 | RAISE NOTICE 'archived % -> archive.%', r.relname, archived_name;
|
|---|
| 164 | END IF;
|
|---|
| 165 | END LOOP;
|
|---|
| 166 |
|
|---|
| 167 | -- sinhronizacija na full-history view so novite arhivirani particii (sekcija 6)
|
|---|
| 168 | PERFORM rebuild_song_streams_all_view();
|
|---|
| 169 | END;
|
|---|
| 170 | $$;
|
|---|
| 171 |
|
|---|
| 172 |
|
|---|
| 173 | -- 5. job scheduling so pg_cron
|
|---|
| 174 |
|
|---|
| 175 | CREATE EXTENSION IF NOT EXISTS pg_cron;
|
|---|
| 176 |
|
|---|
| 177 |
|
|---|
| 178 | -- sozdava novi particii za mesecite sto sledat: se izvrsuva sekoj den vo 02:00.
|
|---|
| 179 | -- dnevno, namesto mesecno za propusten den da ne znaci deka nema da se sozdade novata particija
|
|---|
| 180 | SELECT cron.schedule(
|
|---|
| 181 | 'song_streams_create_partitions',
|
|---|
| 182 | '0 2 * * *',
|
|---|
| 183 | $$SELECT create_song_streams_partitions(2)$$
|
|---|
| 184 | );
|
|---|
| 185 |
|
|---|
| 186 | -- arhiviranje stari particii: se izvrsuva na vtoriot den od sekoj mesec vo 03:00.
|
|---|
| 187 | SELECT cron.schedule(
|
|---|
| 188 | 'song_streams_archive_partitions',
|
|---|
| 189 | '0 3 2 * *',
|
|---|
| 190 | $$SELECT archive_song_streams_partitions(12)$$
|
|---|
| 191 | );
|
|---|
| 192 |
|
|---|
| 193 |
|
|---|
| 194 | -- 6. funkcija za dinamicko kreiranje na view sto gi spojuva postoeckite song streams
|
|---|
| 195 | -- so site arhivirani particii
|
|---|
| 196 | CREATE OR REPLACE FUNCTION rebuild_song_streams_all_view()
|
|---|
| 197 | RETURNS void
|
|---|
| 198 | LANGUAGE plpgsql
|
|---|
| 199 | AS $$
|
|---|
| 200 | DECLARE
|
|---|
| 201 | cols constant text := 'id, playback_session_id, song_id, streamed_at, user_id';
|
|---|
| 202 | sql text;
|
|---|
| 203 | r record;
|
|---|
| 204 | BEGIN
|
|---|
| 205 | -- zapocni od postoeckata ("ziva") tabela
|
|---|
| 206 | sql := format('SELECT %s FROM song_streams', cols);
|
|---|
| 207 |
|
|---|
| 208 | -- dodavaj po edno UNION ALL za sekoja arhivirana particija, pocnuvajki od najstarata
|
|---|
| 209 | FOR r IN
|
|---|
| 210 | SELECT n.nspname, c.relname
|
|---|
| 211 | FROM pg_class c
|
|---|
| 212 | JOIN pg_namespace n ON n.oid = c.relnamespace
|
|---|
| 213 | WHERE n.nspname = 'archive'
|
|---|
| 214 | AND c.relkind = 'r'
|
|---|
| 215 | AND c.relname ~ '^song_streams_y\d{4}m\d{2}_archived$'
|
|---|
| 216 | ORDER BY c.relname
|
|---|
| 217 | LOOP
|
|---|
| 218 | sql := sql || format(
|
|---|
| 219 | E'\n UNION ALL\n SELECT %s FROM %I.%I',
|
|---|
| 220 | cols, r.nspname, r.relname
|
|---|
| 221 | );
|
|---|
| 222 | END LOOP;
|
|---|
| 223 |
|
|---|
| 224 | EXECUTE format('CREATE OR REPLACE VIEW song_streams_all AS %s', sql);
|
|---|
| 225 | RAISE NOTICE 'Rebuilt song_streams_all view';
|
|---|
| 226 | END;
|
|---|
| 227 | $$;
|
|---|
| 228 |
|
|---|
| 229 | -- SELECT rebuild_song_streams_all_view(); |
|---|