Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5b251fe

Browse files
authored
Create db_validate_v2.sql
1 parent c693be3 commit 5b251fe

File tree

1 file changed

+266
-0
lines changed

1 file changed

+266
-0
lines changed

‎functions/db_validate_v2.sql‎

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /