Skip to content

Commit 124a2bd

Browse files
authored
Merge pull request #89 from NikolayS/rename-c1-to-m1-and-amcheck
Add amcheck corruption checks (c1/c2), rename buffercache to m1
2 parents ea23c3f + 3861421 commit 124a2bd

File tree

6 files changed

+314
-6
lines changed

6 files changed

+314
-6
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
psql -h localhost -U postgres -d test -c 'CREATE EXTENSION IF NOT EXISTS pgstattuple;'
5656
psql -h localhost -U postgres -d test -c 'CREATE EXTENSION IF NOT EXISTS intarray;'
5757
psql -h localhost -U postgres -d test -c 'CREATE EXTENSION IF NOT EXISTS pg_buffercache;'
58+
psql -h localhost -U postgres -d test -c 'CREATE EXTENSION IF NOT EXISTS amcheck;'
59+
# amcheck needs execute privileges for non-superusers
60+
psql -h localhost -U postgres -d test -c 'GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO PUBLIC;'
5861
5962
# Minimal privilege user
6063
psql -h localhost -U postgres -d test -c "CREATE USER dba_user;"

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ Then connect to any Postgres server via psql and type `:dba` to open the interac
4343
| b4 | B-tree index bloat via `pgstattuple` (expensive) |
4444
| b5 | Tables and columns without stats (bloat cannot be estimated) |
4545

46-
### Cache
46+
### Corruption checks
4747
| ID | Report |
4848
|----|--------|
49-
| c1 | Buffer cache contents (requires `pg_buffercache`, expensive) |
49+
| c1 | B-tree index integrity check (amcheck, non-blocking) |
50+
| c2 | Full B-tree + heap integrity check (amcheck, takes locks — use on standby!) |
51+
52+
### Memory
53+
| ID | Report |
54+
|----|--------|
55+
| m1 | Buffer cache contents (requires `pg_buffercache`, expensive) |
5056

5157
### Indexes
5258
| ID | Report |

sql/c1_amcheck_btree.sql

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
-- Corruption: B-tree index integrity check (amcheck, non-blocking)
2+
-- Requires: CREATE EXTENSION amcheck
3+
-- Uses bt_index_check() — lightweight, safe for production primaries.
4+
-- Does NOT lock tables (only AccessShareLock on indexes).
5+
-- Checks internal page consistency of all btree indexes.
6+
-- On PG14+, also runs verify_heapam() to detect heap corruption.
7+
8+
do $$
9+
declare
10+
rec record;
11+
corruption record;
12+
idx_count int := 0;
13+
err_count int := 0;
14+
skip_count int := 0;
15+
tbl_count int := 0;
16+
tbl_err_count int := 0;
17+
tbl_skip_count int := 0;
18+
has_errors boolean;
19+
pg_version int;
20+
begin
21+
-- Check extension
22+
if not exists (select 1 from pg_extension where extname = 'amcheck') then
23+
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
24+
return;
25+
end if;
26+
27+
select current_setting('server_version_num')::int into pg_version;
28+
29+
raise notice '';
30+
raise notice '=== B-tree index integrity (bt_index_check) ===';
31+
raise notice 'Checking all btree indexes in the current database...';
32+
raise notice '';
33+
34+
for rec in
35+
select
36+
n.nspname as schema_name,
37+
c.relname as index_name,
38+
t.relname as table_name,
39+
c.oid as index_oid
40+
from pg_index i
41+
join pg_class c on c.oid = i.indexrelid
42+
join pg_class t on t.oid = i.indrelid
43+
join pg_namespace n on n.oid = c.relnamespace
44+
join pg_am a on a.oid = c.relam
45+
where a.amname = 'btree'
46+
and n.nspname not in ('pg_catalog', 'information_schema')
47+
and n.nspname !~ '^pg_toast'
48+
and c.relpersistence != 't'
49+
and i.indisvalid
50+
order by n.nspname, t.relname, c.relname
51+
loop
52+
begin
53+
perform bt_index_check(rec.index_oid);
54+
idx_count := idx_count + 1;
55+
exception
56+
when insufficient_privilege then
57+
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.index_name;
58+
skip_count := skip_count + 1;
59+
when others then
60+
raise warning '❌ CORRUPTION in %.% (table %.%): %',
61+
rec.schema_name, rec.index_name,
62+
rec.schema_name, rec.table_name,
63+
sqlerrm;
64+
err_count := err_count + 1;
65+
end;
66+
end loop;
67+
68+
if err_count = 0 and skip_count = 0 then
69+
raise notice '✅ All % btree indexes passed integrity check.', idx_count;
70+
elsif err_count = 0 then
71+
raise notice '✅ % btree indexes passed, % skipped (insufficient privileges).', idx_count, skip_count;
72+
else
73+
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
74+
end if;
75+
76+
-- Heap verification (PG14+ only)
77+
if pg_version >= 140000 then
78+
raise notice '';
79+
raise notice '=== Heap integrity (verify_heapam) ===';
80+
raise notice 'Checking all user tables for heap corruption...';
81+
raise notice '';
82+
83+
for rec in
84+
select
85+
n.nspname as schema_name,
86+
c.relname as table_name,
87+
c.oid as table_oid
88+
from pg_class c
89+
join pg_namespace n on n.oid = c.relnamespace
90+
where c.relkind = 'r'
91+
and n.nspname not in ('pg_catalog', 'information_schema')
92+
and c.relpersistence != 't'
93+
order by n.nspname, c.relname
94+
loop
95+
has_errors := false;
96+
begin
97+
for corruption in
98+
select * from verify_heapam(rec.table_oid, check_toast := true)
99+
loop
100+
if not has_errors then
101+
raise warning '❌ CORRUPTION in %.%:', rec.schema_name, rec.table_name;
102+
has_errors := true;
103+
tbl_err_count := tbl_err_count + 1;
104+
end if;
105+
raise warning ' block %, offset %, attnum %: %',
106+
corruption.blkno, corruption.offnum, corruption.attnum, corruption.msg;
107+
end loop;
108+
exception
109+
when insufficient_privilege then
110+
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.table_name;
111+
tbl_skip_count := tbl_skip_count + 1;
112+
when others then
113+
raise warning 'ERROR checking %.%: %', rec.schema_name, rec.table_name, sqlerrm;
114+
tbl_err_count := tbl_err_count + 1;
115+
end;
116+
tbl_count := tbl_count + 1;
117+
end loop;
118+
119+
if tbl_err_count = 0 and tbl_skip_count = 0 then
120+
raise notice '✅ All % tables passed heap integrity check.', tbl_count;
121+
elsif tbl_err_count = 0 then
122+
raise notice '✅ % tables passed, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
123+
else
124+
raise warning '❌ % of % tables have corruption!', tbl_err_count, tbl_count;
125+
end if;
126+
else
127+
raise notice '';
128+
raise notice 'ℹ️ Heap verification (verify_heapam) requires PostgreSQL 14+. Skipped.';
129+
end if;
130+
end;
131+
$$;

sql/c2_amcheck_full.sql

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
-- Corruption: Full B-tree + heap check (amcheck, takes locks – use on standby!)
2+
-- Requires: CREATE EXTENSION amcheck
3+
-- Uses bt_index_parent_check(heapallindexed := true) — thorough, takes locks.
4+
-- ⚠️ Takes ShareLock on each index. Run on standbys or during maintenance windows.
5+
-- Checks parent-child consistency, sibling pointers, root descent, unique constraints.
6+
-- Verifies all heap tuples have corresponding index entries.
7+
-- On PG14+, also runs verify_heapam() with full TOAST checking.
8+
9+
do $$
10+
declare
11+
rec record;
12+
corruption record;
13+
idx_count int := 0;
14+
err_count int := 0;
15+
skip_count int := 0;
16+
tbl_count int := 0;
17+
tbl_err_count int := 0;
18+
tbl_skip_count int := 0;
19+
has_errors boolean;
20+
pg_version int;
21+
begin
22+
-- Check extension
23+
if not exists (select 1 from pg_extension where extname = 'amcheck') then
24+
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
25+
return;
26+
end if;
27+
28+
select current_setting('server_version_num')::int into pg_version;
29+
30+
raise warning '⚠️ This check takes locks! Use on standbys or during maintenance windows.';
31+
raise notice '';
32+
raise notice '=== Full B-tree index integrity (bt_index_parent_check + heapallindexed) ===';
33+
raise notice 'Checking all btree indexes with parent-child + heap verification...';
34+
raise notice '';
35+
36+
for rec in
37+
select
38+
n.nspname as schema_name,
39+
c.relname as index_name,
40+
t.relname as table_name,
41+
c.oid as index_oid,
42+
pg_relation_size(c.oid) as index_size
43+
from pg_index i
44+
join pg_class c on c.oid = i.indexrelid
45+
join pg_class t on t.oid = i.indrelid
46+
join pg_namespace n on n.oid = c.relnamespace
47+
join pg_am a on a.oid = c.relam
48+
where a.amname = 'btree'
49+
and n.nspname not in ('pg_catalog', 'information_schema')
50+
and n.nspname !~ '^pg_toast'
51+
and c.relpersistence != 't'
52+
and i.indisvalid
53+
order by pg_relation_size(c.oid) asc -- smallest first
54+
loop
55+
begin
56+
if pg_version >= 140000 then
57+
perform bt_index_parent_check(
58+
rec.index_oid,
59+
heapallindexed := true,
60+
rootdescend := true,
61+
checkunique := true
62+
);
63+
elsif pg_version >= 110000 then
64+
perform bt_index_parent_check(
65+
rec.index_oid,
66+
heapallindexed := true,
67+
rootdescend := true
68+
);
69+
else
70+
perform bt_index_parent_check(rec.index_oid, heapallindexed := true);
71+
end if;
72+
idx_count := idx_count + 1;
73+
exception
74+
when insufficient_privilege then
75+
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.index_name;
76+
skip_count := skip_count + 1;
77+
when others then
78+
raise warning '❌ CORRUPTION in %.% (table %.%, size %): %',
79+
rec.schema_name, rec.index_name,
80+
rec.schema_name, rec.table_name,
81+
pg_size_pretty(rec.index_size),
82+
sqlerrm;
83+
err_count := err_count + 1;
84+
end;
85+
end loop;
86+
87+
if err_count = 0 and skip_count = 0 then
88+
raise notice '✅ All % btree indexes passed full integrity check.', idx_count;
89+
elsif err_count = 0 then
90+
raise notice '✅ % btree indexes passed, % skipped (insufficient privileges).', idx_count, skip_count;
91+
else
92+
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
93+
end if;
94+
95+
-- Full heap verification (PG14+ only)
96+
if pg_version >= 140000 then
97+
raise notice '';
98+
raise notice '=== Full heap integrity (verify_heapam + TOAST) ===';
99+
raise notice 'Checking all user tables for heap and TOAST corruption...';
100+
raise notice '';
101+
102+
for rec in
103+
select
104+
n.nspname as schema_name,
105+
c.relname as table_name,
106+
c.oid as table_oid,
107+
pg_relation_size(c.oid) as table_size
108+
from pg_class c
109+
join pg_namespace n on n.oid = c.relnamespace
110+
where c.relkind = 'r'
111+
and n.nspname not in ('pg_catalog', 'information_schema')
112+
and c.relpersistence != 't'
113+
order by n.nspname, c.relname
114+
loop
115+
has_errors := false;
116+
begin
117+
for corruption in
118+
select * from verify_heapam(
119+
rec.table_oid,
120+
on_error_stop := false,
121+
check_toast := true,
122+
skip := 'none'
123+
)
124+
loop
125+
if not has_errors then
126+
raise warning '❌ CORRUPTION in %.% (size %):', rec.schema_name, rec.table_name, pg_size_pretty(rec.table_size);
127+
has_errors := true;
128+
tbl_err_count := tbl_err_count + 1;
129+
end if;
130+
raise warning ' block %, offset %, attnum %: %',
131+
corruption.blkno, corruption.offnum, corruption.attnum, corruption.msg;
132+
end loop;
133+
exception
134+
when insufficient_privilege then
135+
raise notice '⚠️ Permission denied for %.% — need superuser or amcheck privileges', rec.schema_name, rec.table_name;
136+
tbl_skip_count := tbl_skip_count + 1;
137+
when others then
138+
raise warning 'ERROR checking %.%: %', rec.schema_name, rec.table_name, sqlerrm;
139+
tbl_err_count := tbl_err_count + 1;
140+
end;
141+
tbl_count := tbl_count + 1;
142+
end loop;
143+
144+
if tbl_err_count = 0 and tbl_skip_count = 0 then
145+
raise notice '✅ All % tables passed full heap integrity check.', tbl_count;
146+
elsif tbl_err_count = 0 then
147+
raise notice '✅ % tables passed, % skipped (insufficient privileges).', tbl_count - tbl_skip_count, tbl_skip_count;
148+
else
149+
raise warning '❌ % of % tables have corruption!', tbl_err_count, tbl_count;
150+
end if;
151+
else
152+
raise notice '';
153+
raise notice 'ℹ️ Heap verification (verify_heapam) requires PostgreSQL 14+. Skipped.';
154+
end if;
155+
end;
156+
$$;

start.psql

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
\echo ' b3 – Table bloat (requires pgstattuple; expensive)'
1111
\echo ' b4 – B-tree indexes bloat (requires pgstattuple; expensive)'
1212
\echo ' b5 – Tables and columns without stats (so bloat cannot be estimated)'
13-
\echo ' c1 – Buffer cache contents (requires pg_buffercache; expensive)'
13+
\echo ' c1 – Corruption: B-tree index integrity check (amcheck, non-blocking)'
14+
\echo ' c2 – Corruption: Full B-tree + heap check (amcheck, takes locks – use on standby!)'
1415
\echo ' e1 – Extensions installed in current database'
1516
\echo ' i1 – Unused and rarely used indexes'
1617
\echo ' i2 – Redundant indexes'
@@ -19,14 +20,15 @@
1920
\echo ' i5 – Cleanup unused and redundant indexes – DO & UNDO migration DDL'
2021
\echo ' l1 – Lock trees (lightweight)'
2122
\echo ' l2 – Lock trees, detailed (based on pg_blocking_pids())'
23+
\echo ' m1 – Buffer cache contents (requires pg_buffercache; expensive on large shared_buffers)'
2224
\echo ' p1 – Index (re)creation progress (CREATE INDEX / REINDEX)'
2325
\echo ' r1 – Create user with random password (interactive)'
2426
\echo ' r2 – Alter user with random password (interactive)'
2527
\echo ' s1 – Slowest queries, by total time (requires pg_stat_statements)'
2628
\echo ' s2 – Slowest queries report (requires pg_stat_statements)'
27-
\echo ' s3 – Workload profile by query type (requires pg_stat_statements)'
29+
\echo ' s3 – Workload profile by query type (requires pg_stat_statements)'
2830
\echo ' t1 – Postgres parameters tuning'
29-
\echo ' t2 – Objects with custom storage parameters'
31+
\echo ' t2 – Objects with custom storage parameters'
3032
\echo ' v1 – Vacuum: current activity'
3133
\echo ' v2 – VACUUM progress and autovacuum queue'
3234
\echo ' x1 – [EXP] Alignment padding: how many bytes can be saved if columns are reordered?'
@@ -47,6 +49,7 @@ select
4749
:d_stp::text = 'b4' as d_step_is_b4,
4850
:d_stp::text = 'b5' as d_step_is_b5,
4951
:d_stp::text = 'c1' as d_step_is_c1,
52+
:d_stp::text = 'c2' as d_step_is_c2,
5053
:d_stp::text = 'e1' as d_step_is_e1,
5154
:d_stp::text = 'i1' as d_step_is_i1,
5255
:d_stp::text = 'i2' as d_step_is_i2,
@@ -55,6 +58,7 @@ select
5558
:d_stp::text = 'i5' as d_step_is_i5,
5659
:d_stp::text = 'l1' as d_step_is_l1,
5760
:d_stp::text = 'l2' as d_step_is_l2,
61+
:d_stp::text = 'm1' as d_step_is_m1,
5862
:d_stp::text = 'p1' as d_step_is_p1,
5963
:d_stp::text = 'r1' as d_step_is_r1,
6064
:d_stp::text = 'r2' as d_step_is_r2,
@@ -111,7 +115,11 @@ select
111115
\prompt 'Press <Enter> to continue…' d_dummy
112116
\ir ./start.psql
113117
\elif :d_step_is_c1
114-
\ir ./sql/c1_buffercache.sql
118+
\ir ./sql/c1_amcheck_btree.sql
119+
\prompt 'Press <Enter> to continue…' d_dummy
120+
\ir ./start.psql
121+
\elif :d_step_is_c2
122+
\ir ./sql/c2_amcheck_full.sql
115123
\prompt 'Press <Enter> to continue…' d_dummy
116124
\ir ./start.psql
117125
\elif :d_step_is_e1
@@ -146,6 +154,10 @@ select
146154
\ir ./sql/l2_lock_trees.sql
147155
\prompt 'Press <Enter> to continue…' d_dummy
148156
\ir ./start.psql
157+
\elif :d_step_is_m1
158+
\ir ./sql/m1_buffercache.sql
159+
\prompt 'Press <Enter> to continue…' d_dummy
160+
\ir ./start.psql
149161
\elif :d_step_is_p1
150162
\ir ./sql/p1_index_create_progress.sql
151163
\prompt 'Press <Enter> to continue…' d_dummy

0 commit comments

Comments
 (0)