/* * Author: DBRE - Joe Smith * Date: October 2022 * Package: index_maintenance * Purpose: sets up tables, functions and extensions for index maintenance when deleting on a regular basis. */ -- pre-requisite extensions. create extension pg_cron; create extension pgstattuple; CREATE TABLE index_maintenance_configuration ( id SERIAL primary key, index_name varchar(63) UNIQUE not null, maintenance_threshold_percent float not null, man_window_start time not null, man_window_end time not null, weekday_override boolean default false, weekdays integer[] default array[1, 2, 3, 4, 5, 6, 7] not null ); CREATE TABLE index_maintenance ( index_name varchar(63), index_iteration bigint default 0, maintenance_started timestamp default null, maintenance_done timestamp default null, scheduled boolean default false, threshold_breach_value float, PRIMARY KEY(index_name, index_iteration, scheduled) ); CREATE TABLE index_maintenance_lock ( is_locked boolean, lock_change_time timestamp default now(), index_holding_lock text ); ALTER TABLE index_maintenance ADD FOREIGN KEY (index_name) REFERENCES index_maintenance_configuration(index_name); /* * Author: DBRE - Joe Smith * Date: October 2022 * Function: idx_maintenance * Purpose: function called by pg_cron at a regular interval. */ Create or replace function idx_maintenance() returns void as $$ declare rec record; current_iteration record; lock_rec record; done boolean; today int; day_allowed boolean; Begin today = EXTRACT(isodow FROM now()); SELECT * INTO lock_rec FROM index_maintenance_lock; -- if the lock is held already, we do nothing. if lock_rec.is_locked == true then raise notice 'index_man_lock_held(lock held already, skipping maintenance until lock is released)', exit; end if; -- if a lock is not held, we check the indexes we know about. for rec in select * from index_maintenance_configuration loop day_allowed = true; if rec.weekday_override == true then day_allowed = today=ANY(rec.weekdays); end if; -- check for existing maintenance records. SELECT * INTO current_iteration FROM index_maintenance WHERE index_name = rec.index_name; -- set current iteration if no records are found for this index -- insert record into index_maintenance table with scheduled set to false if current_iteration.index_iteration is null then insert into index_maintenance(index_name, scheduled) VALUES (rec.index_name, false) on conflict do nothing; -- return record back into cursor so we can be lazy and reusing the current_iteration pointer. SELECT * INTO current_iteration FROM index_maintenance WHERE index_name = rec.index_name; end if; end if; done = do_idx_maintenance(current_iteration); raise notice 'idx_maintenance(index_name: %, maintenance_done: %)', current_iteration.index_name, done; end loop; End; $$ Language 'plpgsql'; /* * Author: DBRE - Joe Smith * Date: October 2022 * Function: do_idx_maintenance * Purpose: calls lock_and_reindex function if we are within the maintenance window of a given index */ Create or replace function do_idx_maintenance(r record, maintenance_percent float) returns boolean as $$ declare lf float; is_between_man_window boolean; Begin -- we assume we are not allowed to do maintenance is_between_man_window = false; -- this avoids calculating this multiple times. if now()::time between r.man_window_start::time and r.man_window_end::time then is_between_man_window = true end if; -- scheduled, no re-index running, fragmentation determined to be greater than threshold in prior run if r.scheduled == true then if is_between_man_window == true then lock_and_reindex(r, r.threshold_breach_value) return true; else return false; end if; else -- not scheduled, so we check fragmentation on the index. select leaf_fragmentation into lf from pgstatindex(r.index_name); -- not scheduled, no re-index running, fragmentation is greater than our maintenance threshold. -- lf is passed to track the amount of fragmentation that triggered the run. if lf > maintenance_percent then if is_between_man_window == true then lock_and_reindex(r, lf); return true; else -- leaf fragmentation is above threshold update index_maintenance set scheduled = true where id = r.id; return false; end if; else -- leaf fragmentation is under threshold, nothing to do return false; end if; end if; End; $$ Language 'plpgsql'; /* * Author: DBRE - Joe Smith * Date: October 2022 * Function: lock_and_reindex * Purpose: sets up tables, functions and extensions for index maintenance when deleting on a regular basis. */ Create or replace function lock_and_reindex(r record, lf float) returns void as $$ declare is_between_man_window boolean; Begin raise notice 'lock_and_reindex(index_name: %, time: %)', r.index_name, now()::text; -- lock the lock table to prevent duplicate operations update index_maintenance_lock set is_locked = true, index_holding_lock = rec.index_name where index_holding_lock is null; -- set the maintenance started value in the record we have update index_maintenance set maintenance_started = now(), threshold_breach_value = lf where id = r.id; -- reindex REINDEX INDEX CONCURRENTLY rec.index_name; -- set the maintenance done value in the record we have. update index_maintenance set maintenance_done = now() where id = r.id; -- add next record to table, incrementing the iteration value by 1. insert into index_maintenance (index_name, index_iteration) VALUES (r.index_name, index_iteration + 1); End; $$ Language 'plpgsql'; /* * Author: DBRE - Joe Smith * Date: October 2022 * Function: add_index_configuration * Purpose: inserts a configuration into the configuration table, can be called by internal users. */ Create or replace function add_index_configuration(index_name varchar(63), man_start time, man_end time, threshold float) returns void as $$ Begin insert into index_maintenance_configuration(index_name, maintenance_threshold_percent, man_window_start, man_window_end) VALUES (index_name, threshold, man_start, man_end) on conflict do nothing; raise notice 'attempted to add config for %, manually set weekday override if needed.', index_name; End; $$ Language 'plpgsql'; /* * Author: DBRE - Joe Smith * Date: October 2022 * Function: kill_reindex * Purpose: end current reindex operation safely */ Create or replace function kill_reindex() returns void as $$ declare is_locked boolean; idx_name varchar(63); proc_id int; Begin -- not scheduled, so we check fragmentation on the index. select index_name into idx_name from index_maintenance_lock; raise notice 'killing pid running reindex operation for %', index_name; -- this should only ever return 1 row. select pid into proc_id from pg_stat_activity where query like 'REINDEX INDEX %' LIMIT 1; raise notice 'found pid % proc_id running reindex operation for %', proc_id::text, index_name; SELECT pg_cancel_backend(proc_id); raise notice 'killed pid % proc_id running reindex operation for %',proc_id::text, index_name; End; $$ Language 'plpgsql';