|
| 1 | +--Валидатор схемы БД v2 |
| 2 | + |
| 3 | +create or replace function db_validate_v2( |
| 4 | + checks text[] default null, -- Коды необходимых проверок |
| 5 | + -- Если передан null - то все возможные проверки |
| 6 | + -- Если передан пустой массив - то ни одной проверки |
| 7 | + |
| 8 | + schemas_ignore_regexp text default null, -- Регулярное выражение со схемами, которые нужно проигнорировать |
| 9 | + schemas_ignore regnamespace[] default null, -- Список схем, которые нужно проигнорировать |
| 10 | + -- В список схем автоматически добавляются служебные схемы "information_schema" и "pg_catalog", указывать их явно не нужно |
| 11 | + |
| 12 | + tables_ignore_regexp text default null, -- Регулярное выражение с таблицами (с указанием схемы), которые нужно проигнорировать |
| 13 | + tables_ignore regclass[] default null -- Список таблиц в формате {scheme}.{table}, которые нужно проигнорировать |
| 14 | +) |
| 15 | + returns void |
| 16 | + stable |
| 17 | + --returns null on null input |
| 18 | + parallel safe |
| 19 | + language plpgsql |
| 20 | +AS $$ |
| 21 | +DECLARE |
| 22 | + rec record; |
| 23 | +BEGIN |
| 24 | + |
| 25 | + schemas_ignore := coalesce(schemas_ignore, '{}') || '{information_schema,pg_catalog}'; |
| 26 | + |
| 27 | + -- Наличие первичного или уникального индекса в таблице |
| 28 | + if checks is null or 'has_pk_uk' = any(checks) then |
| 29 | + raise notice 'check has_pk_uk'; |
| 30 | + |
| 31 | + SELECT t.* |
| 32 | + INTO rec |
| 33 | + FROM information_schema.tables AS t |
| 34 | + cross join lateral (select concat_ws('.', quote_ident(t.table_schema), quote_ident(t.table_name))) as p(table_full_name) |
| 35 | + |
| 36 | + WHERE t.table_type = 'BASE TABLE' |
| 37 | + |
| 38 | + -- исключаем схемы |
| 39 | + AND (schemas_ignore_regexp is null OR t.table_schema !~ schemas_ignore_regexp) |
| 40 | + AND NOT t.table_schema = ANY (schemas_ignore::text[]) |
| 41 | + |
| 42 | + -- исключаем таблицы |
| 43 | + AND (tables_ignore_regexp is null OR p.table_full_name !~ tables_ignore_regexp) |
| 44 | + AND (tables_ignore is null OR NOT p.table_full_name = ANY (tables_ignore::text[])) |
| 45 | + |
| 46 | + AND NOT EXISTS(SELECT |
| 47 | + FROM information_schema.key_column_usage AS kcu |
| 48 | + WHERE kcu.table_schema = t.table_schema AND |
| 49 | + kcu.table_name = t.table_name |
| 50 | + ) |
| 51 | + |
| 52 | + -- исключаем таблицы, которые имеют секционирование (partitions) |
| 53 | + AND NOT EXISTS (SELECT --i.inhrelid::regclass AS child -- optionally cast to text |
| 54 | + FROM pg_catalog.pg_inherits AS i |
| 55 | + WHERE i.inhparent = (t.table_schema || '.' || t.table_name)::regclass |
| 56 | + ) |
| 57 | + --ORDER BY c.table_schema, c.table_name |
| 58 | + LIMIT 1; |
| 59 | + |
| 60 | + IF FOUND THEN |
| 61 | + RAISE EXCEPTION 'Таблица %.% должна иметь первичный или уникальный индекс!', rec.table_schema, rec.table_name; |
| 62 | + END IF; |
| 63 | + |
| 64 | + end if; |
| 65 | + |
| 66 | + -- Отсутствие избыточных индексов в таблице |
| 67 | + if checks is null or 'has_not_redundant_index' = any(checks) then |
| 68 | + raise notice 'check has_not_redundant_index'; |
| 69 | + |
| 70 | + WITH index_data AS ( |
| 71 | + SELECT idx.*, |
| 72 | + string_to_array(idx.indkey::text, ' ') as key_array, |
| 73 | + array_length(string_to_array(idx.indkey::text, ' '), 1) as nkeys, |
| 74 | + am.amname |
| 75 | + FROM pg_index AS idx |
| 76 | + JOIN pg_class AS cls ON cls.oid = idx.indexrelid |
| 77 | + -- исключаем схемы |
| 78 | + AND (schemas_ignore_regexp is null OR cls.relnamespace::regnamespace::text !~ schemas_ignore_regexp) |
| 79 | + AND NOT cls.relnamespace::regnamespace = ANY (schemas_ignore) |
| 80 | + |
| 81 | + JOIN pg_am am ON am.oid = cls.relam |
| 82 | + ), |
| 83 | + t AS ( |
| 84 | + SELECT |
| 85 | + i1.indrelid::regclass::text as table_name, |
| 86 | + pg_get_indexdef(i1.indexrelid) main_index, |
| 87 | + pg_get_indexdef(i2.indexrelid) redundant_index, |
| 88 | + pg_size_pretty(pg_relation_size(i2.indexrelid)) redundant_index_size |
| 89 | + FROM index_data as i1 |
| 90 | + JOIN index_data as i2 ON i1.indrelid = i2.indrelid |
| 91 | + AND i1.indexrelid <> i2.indexrelid |
| 92 | + AND i1.amname = i2.amname |
| 93 | + WHERE (regexp_replace(i1.indpred, 'location \d+', 'location', 'g') IS NOT DISTINCT FROM |
| 94 | + regexp_replace(i2.indpred, 'location \d+', 'location', 'g')) |
| 95 | + AND (regexp_replace(i1.indexprs, 'location \d+', 'location', 'g') IS NOT DISTINCT FROM |
| 96 | + regexp_replace(i2.indexprs, 'location \d+', 'location', 'g')) |
| 97 | + AND ((i1.nkeys > i2.nkeys and not i2.indisunique) |
| 98 | + OR (i1.nkeys = i2.nkeys and |
| 99 | + ((i1.indisunique and i2.indisunique and (i1.indexrelid > i2.indexrelid)) or |
| 100 | + (not i1.indisunique and not i2.indisunique and |
| 101 | + (i1.indexrelid > i2.indexrelid)) or |
| 102 | + (i1.indisunique and not i2.indisunique))) |
| 103 | + ) |
| 104 | + AND i1.key_array[1:i2.nkeys] = i2.key_array |
| 105 | + ORDER BY pg_relation_size(i2.indexrelid) desc, |
| 106 | + i1.indexrelid::regclass::text, |
| 107 | + i2.indexrelid::regclass::text |
| 108 | + ) |
| 109 | + SELECT DISTINCT ON (redundant_index) t.* INTO rec FROM t LIMIT 1; |
| 110 | + |
| 111 | + end if; |
| 112 | + |
| 113 | + IF FOUND THEN |
| 114 | + RAISE EXCEPTION E'Таблица % уже имеет индекс %\nУдалите избыточный индекс %', rec.table_name, rec.main_index, rec.redundant_index; |
| 115 | + END IF; |
| 116 | + |
| 117 | + -- Наличие индексов для ограничений внешних ключей в таблице |
| 118 | + if checks is null or 'has_index_for_fk' = any(checks) then |
| 119 | + raise notice 'check has_index_for_fk'; |
| 120 | + |
| 121 | + -- запрос для получения FK без индексов, взял по ссылке ниже и модифицировал |
| 122 | + -- https://github.com/NikolayS/postgres_dba/blob/master/sql/i3_non_indexed_fks.sql |
| 123 | + with fk_actions ( code, action ) as ( |
| 124 | + values ('a', 'error'), |
| 125 | + ('r', 'restrict'), |
| 126 | + ('c', 'cascade'), |
| 127 | + ('n', 'set null'), |
| 128 | + ('d', 'set default') |
| 129 | + ), fk_list as ( |
| 130 | + select |
| 131 | + pg_constraint.oid as fkoid, conrelid, confrelid as parentid, |
| 132 | + conname, |
| 133 | + relname, |
| 134 | + nspname, |
| 135 | + fk_actions_update.action as update_action, |
| 136 | + fk_actions_delete.action as delete_action, |
| 137 | + conkey as key_cols |
| 138 | + from pg_constraint |
| 139 | + join pg_class on conrelid = pg_class.oid |
| 140 | + join pg_namespace on pg_class.relnamespace = pg_namespace.oid |
| 141 | + join fk_actions as fk_actions_update on confupdtype = fk_actions_update.code |
| 142 | + join fk_actions as fk_actions_delete on confdeltype = fk_actions_delete.code |
| 143 | + where contype = 'f' |
| 144 | + ), fk_attributes as ( |
| 145 | + select fkoid, conrelid, attname, attnum |
| 146 | + from fk_list |
| 147 | + join pg_attribute on conrelid = attrelid and attnum = any(key_cols) |
| 148 | + order by fkoid, attnum |
| 149 | + ), fk_cols_list as ( |
| 150 | + select fkoid, array_agg(attname) as cols_list |
| 151 | + from fk_attributes |
| 152 | + group by fkoid |
| 153 | + ), index_list as ( |
| 154 | + select |
| 155 | + indexrelid as indexid, |
| 156 | + pg_class.relname as indexname, |
| 157 | + indrelid, |
| 158 | + indkey, |
| 159 | + indpred is not null as has_predicate, |
| 160 | + pg_get_indexdef(indexrelid) as indexdef |
| 161 | + from pg_index |
| 162 | + join pg_class on indexrelid = pg_class.oid |
| 163 | + where indisvalid |
| 164 | + ), fk_index_match as ( |
| 165 | + select |
| 166 | + fk_list.*, |
| 167 | + indexid, |
| 168 | + indexname, |
| 169 | + indkey::int[] as indexatts, |
| 170 | + has_predicate, |
| 171 | + indexdef, |
| 172 | + array_length(key_cols, 1) as fk_colcount, |
| 173 | + array_length(indkey,1) as index_colcount, |
| 174 | + round(pg_relation_size(conrelid)/(1024^2)::numeric) as table_mb, |
| 175 | + cols_list |
| 176 | + from fk_list |
| 177 | + join fk_cols_list using (fkoid) |
| 178 | + left join index_list on |
| 179 | + conrelid = indrelid |
| 180 | + and (indkey::int2[])[0:(array_length(key_cols,1) -1)] operator(pg_catalog.@>) key_cols |
| 181 | + |
| 182 | + ), fk_perfect_match as ( |
| 183 | + select fkoid |
| 184 | + from fk_index_match |
| 185 | + where |
| 186 | + (index_colcount - 1) <= fk_colcount |
| 187 | + and not has_predicate |
| 188 | + and indexdef like '%USING btree%' |
| 189 | + ), fk_index_check as ( |
| 190 | + select 'no index' as issue, *, 1 as issue_sort |
| 191 | + from fk_index_match |
| 192 | + where indexid is null |
| 193 | + /*union all |
| 194 | + select 'questionable index' as issue, *, 2 |
| 195 | + from fk_index_match |
| 196 | + where |
| 197 | + indexid is not null |
| 198 | + and fkoid not in (select fkoid from fk_perfect_match)*/ |
| 199 | + ), parent_table_stats as ( |
| 200 | + select |
| 201 | + fkoid, |
| 202 | + tabstats.relname as parent_name, |
| 203 | + (n_tup_ins + n_tup_upd + n_tup_del + n_tup_hot_upd) as parent_writes, |
| 204 | + round(pg_relation_size(parentid)/(1024^2)::numeric) as parent_mb |
| 205 | + from pg_stat_user_tables as tabstats |
| 206 | + join fk_list on relid = parentid |
| 207 | + ), fk_table_stats as ( |
| 208 | + select |
| 209 | + fkoid, |
| 210 | + (n_tup_ins + n_tup_upd + n_tup_del + n_tup_hot_upd) as writes, |
| 211 | + seq_scan as table_scans |
| 212 | + from pg_stat_user_tables as tabstats |
| 213 | + join fk_list on relid = conrelid |
| 214 | + ), result as ( |
| 215 | + select |
| 216 | + nspname as schema_name, |
| 217 | + relname as table_name, |
| 218 | + conname as fk_name, |
| 219 | + issue, |
| 220 | + table_mb, |
| 221 | + writes, |
| 222 | + table_scans, |
| 223 | + parent_name, |
| 224 | + parent_mb, |
| 225 | + parent_writes, |
| 226 | + cols_list, |
| 227 | + coalesce(indexdef, 'CREATE INDEX /*CONCURRENTLY*/ ' || relname || '_' || cols_list[1] || ' ON ' || |
| 228 | + quote_ident(nspname) || '.' || quote_ident(relname) || ' (' || quote_ident(cols_list[1]) || ')') as indexdef |
| 229 | + from fk_index_check |
| 230 | + join parent_table_stats using (fkoid) |
| 231 | + join fk_table_stats using (fkoid) |
| 232 | + where |
| 233 | + true /*table_mb > 9*/ |
| 234 | + and ( |
| 235 | + /* writes > 1000 |
| 236 | + or parent_writes > 1000 |
| 237 | + or parent_mb > 10*/ |
| 238 | + true |
| 239 | + ) |
| 240 | + and issue = 'no index' |
| 241 | + order by issue_sort, table_mb asc, table_name, fk_name |
| 242 | + limit 1 |
| 243 | + ) |
| 244 | + select * INTO rec from result; |
| 245 | + |
| 246 | + end if; |
| 247 | + |
| 248 | + IF FOUND THEN |
| 249 | + RAISE EXCEPTION E'Отсутствует индекс для внешнего ключа\nДобавьте индекс %', rec.indexdef; |
| 250 | + END IF; |
| 251 | + |
| 252 | +END |
| 253 | +$$; |
| 254 | + |
| 255 | +-- запускаем валидатор БД |
| 256 | +select db_validate_v2( |
| 257 | + '{has_pk_uk,has_not_redundant_index,has_index_for_fk}', --checks |
| 258 | + |
| 259 | + null, --schemas_ignore_regexp |
| 260 | + '{unused,migration,test}', --schemas_ignore |
| 261 | + |
| 262 | + '(?<![a-z\d])(te?mp|test|unused|backups?|deleted)(?![a-z\d])', --tables_ignore_regexp |
| 263 | + null --tables_ignore |
| 264 | +); |
| 265 | + |
| 266 | +--SELECT EXISTS(SELECT * FROM pg_proc WHERE proname = 'db_validate_v2'); -- проверяем наличие валидатора |
0 commit comments