Skip to content

Commit 745062b

Browse files
committed
Split amcheck into 4 reports: c1 (quick), c2 (heap), c3 (parent), c4 (full)
- c1: btree + GIN index check only. Actually fast. AccessShareLock. - c2: c1 + verify_heapam. Still AccessShareLock but reads all data. - c3: bt_index_parent_check — glibc/collation corruption. ShareLock. - c4: heapallindexed + parent + heap. The full paranoia. ShareLock. Tested on PG13, PG17, PG18 (GIN). 34 reports total.
1 parent c3f73ec commit 745062b

File tree

7 files changed

+146
-25
lines changed

7 files changed

+146
-25
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ Then connect to any Postgres server via psql and type `:dba` to open the interac
4646
### Corruption checks
4747
| ID | Report |
4848
|----|--------|
49-
| c1 | Quick: btree + GIN (PG18) + heap (PG14) check — safe for production (AccessShareLock) |
50-
| c2 | Parent: btree parent-child check — detects glibc/collation corruption (⚠️ ShareLock — use on clones) |
51-
| c3 | Full: heapallindexed + parent + heap — proves every tuple is indexed (⚠️⚠️ slow + ShareLock — use on clones) |
49+
| c1 | Quick index check: btree + GIN (PG18+). Fast, safe for production (AccessShareLock) |
50+
| c2 | Indexes + heap/TOAST (PG14+). Safe for production but reads all data (AccessShareLock) |
51+
| c3 | B-tree parent check — detects glibc/collation corruption (⚠️ ShareLock — use on clones) |
52+
| c4 | Full: heapallindexed + parent + heap — proves every tuple is indexed (⚠️⚠️ slow + ShareLock — use on clones) |
5253

5354
### Memory
5455
| ID | Report |

RELEASE_NOTES.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# postgres_dba 7.0
22

3-
**33 reports** | Tested on **PostgreSQL 13–18** | Works with `pg_monitor` role
3+
**34 reports** | Tested on **PostgreSQL 13–18** | Works with `pg_monitor` role
44

55
## New Reports
66

@@ -10,9 +10,10 @@ Three levels of integrity checking, all requiring `CREATE EXTENSION amcheck`:
1010

1111
| Report | Lock | What it checks | When to use |
1212
|--------|------|----------------|-------------|
13-
| **c1** | AccessShareLock | B-tree pages, GIN indexes (PG18+), heap+TOAST (PG14+) | **Production primary** — safe, non-blocking |
14-
| **c2** | ShareLock ⚠️ | B-tree parent-child ordering, sibling pointers, rootdescend, checkunique (PG14+) | **Clones or standbys** — detects glibc/collation corruption |
15-
| **c3** | ShareLock ⚠️⚠️ | Everything in c2 + heapallindexed + verify_heapam with full TOAST | **Clones only** — proves every heap tuple is indexed, slow on large DBs |
13+
| **c1** | AccessShareLock | B-tree pages, GIN indexes (PG18+) | **Production** — fast, safe, non-blocking |
14+
| **c2** | AccessShareLock | c1 + heap/TOAST integrity (PG14+) | **Production** — safe but reads all data |
15+
| **c3** | ShareLock ⚠️ | B-tree parent-child ordering, sibling pointers, rootdescend, checkunique (PG14+) | **Clones or standbys** — detects glibc/collation corruption |
16+
| **c4** | ShareLock ⚠️⚠️ | Everything in c3 + heapallindexed + verify_heapam with full TOAST | **Clones only** — proves every heap tuple is indexed, slow on large DBs |
1617

1718
All three check system catalog indexes (`pg_catalog`, `pg_toast`) — because catalog corruption is the scariest kind.
1819

@@ -75,4 +76,4 @@ Categories reorganized for consistency:
7576

7677
## Compatibility
7778

78-
Tested on PostgreSQL 13, 14, 15, 16, 17, and 18 — all 33 reports pass with both superuser and `pg_monitor` roles.
79+
Tested on PostgreSQL 13, 14, 15, 16, 17, and 18 — all 34 reports pass with both superuser and `pg_monitor` roles.

sql/c1_amcheck_indexes.sql

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
-- Corruption: quick index check — btree + GIN (PG18+). Safe for production, fast.
2+
-- Requires: CREATE EXTENSION amcheck
3+
-- All checks use AccessShareLock only — no write blocking.
4+
-- Checks page-level consistency of btree indexes (bt_index_check).
5+
-- On PG18+, also checks GIN indexes (gin_index_check).
6+
7+
do $$
8+
declare
9+
rec record;
10+
idx_count int := 0;
11+
err_count int := 0;
12+
skip_count int := 0;
13+
gin_count int := 0;
14+
gin_err_count int := 0;
15+
gin_skip_count int := 0;
16+
pg_version int;
17+
begin
18+
if not exists (select 1 from pg_extension where extname = 'amcheck') then
19+
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
20+
return;
21+
end if;
22+
23+
select current_setting('server_version_num')::int into pg_version;
24+
25+
-- === B-tree indexes ===
26+
raise notice '';
27+
raise notice '=== B-tree index check (bt_index_check, AccessShareLock) ===';
28+
raise notice '';
29+
30+
for rec in
31+
select
32+
n.nspname as schema_name,
33+
c.relname as index_name,
34+
t.relname as table_name,
35+
c.oid as index_oid
36+
from pg_index i
37+
join pg_class c on c.oid = i.indexrelid
38+
join pg_class t on t.oid = i.indrelid
39+
join pg_namespace n on n.oid = c.relnamespace
40+
join pg_am a on a.oid = c.relam
41+
where a.amname = 'btree'
42+
and c.relpersistence != 't'
43+
and i.indisvalid
44+
order by n.nspname, t.relname, c.relname
45+
loop
46+
begin
47+
perform bt_index_check(rec.index_oid);
48+
idx_count := idx_count + 1;
49+
exception
50+
when insufficient_privilege then
51+
skip_count := skip_count + 1;
52+
when others then
53+
raise warning '❌ CORRUPTION in %.%: %',
54+
rec.schema_name, rec.index_name, sqlerrm;
55+
err_count := err_count + 1;
56+
end;
57+
end loop;
58+
59+
if err_count = 0 and skip_count = 0 then
60+
raise notice '✅ All % btree indexes OK.', idx_count;
61+
elsif err_count = 0 then
62+
raise notice '✅ % btree indexes OK, % skipped (insufficient privileges).', idx_count, skip_count;
63+
else
64+
raise warning '❌ % of % btree indexes have corruption!', err_count, idx_count + err_count + skip_count;
65+
end if;
66+
67+
-- === GIN indexes (PG18+) ===
68+
if pg_version >= 180000 then
69+
raise notice '';
70+
raise notice '=== GIN index check (gin_index_check, AccessShareLock) ===';
71+
raise notice '';
72+
73+
for rec in
74+
select
75+
n.nspname as schema_name,
76+
c.relname as index_name,
77+
t.relname as table_name,
78+
c.oid as index_oid
79+
from pg_index i
80+
join pg_class c on c.oid = i.indexrelid
81+
join pg_class t on t.oid = i.indrelid
82+
join pg_namespace n on n.oid = c.relnamespace
83+
join pg_am a on a.oid = c.relam
84+
where a.amname = 'gin'
85+
and c.relpersistence != 't'
86+
and i.indisvalid
87+
order by n.nspname, t.relname, c.relname
88+
loop
89+
begin
90+
perform gin_index_check(rec.index_oid);
91+
gin_count := gin_count + 1;
92+
exception
93+
when insufficient_privilege then
94+
gin_skip_count := gin_skip_count + 1;
95+
when others then
96+
raise warning '❌ CORRUPTION in %.%: %',
97+
rec.schema_name, rec.index_name, sqlerrm;
98+
gin_err_count := gin_err_count + 1;
99+
end;
100+
end loop;
101+
102+
if gin_count + gin_err_count + gin_skip_count = 0 then
103+
raise notice 'No GIN indexes found.';
104+
elsif gin_err_count = 0 and gin_skip_count = 0 then
105+
raise notice '✅ All % GIN indexes OK.', gin_count;
106+
elsif gin_err_count = 0 then
107+
raise notice '✅ % GIN indexes OK, % skipped (insufficient privileges).', gin_count, gin_skip_count;
108+
else
109+
raise warning '❌ % of % GIN indexes have corruption!', gin_err_count, gin_count + gin_err_count + gin_skip_count;
110+
end if;
111+
else
112+
raise notice '';
113+
raise notice 'ℹ️ GIN index checking requires PostgreSQL 18+. Skipped.';
114+
end if;
115+
end;
116+
$$;
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
-- Corruption: quick check — btree, GIN (PG18+), heap (PG14+). Safe for production.
1+
-- Corruption: indexes + heap/TOAST check. Safe for production but reads all data.
22
-- Requires: CREATE EXTENSION amcheck
3-
-- All checks use AccessShareLock only (same as SELECT) — no write blocking.
4-
-- Checks page-level consistency of btree indexes (bt_index_check).
5-
-- On PG18+, also checks GIN indexes (gin_index_check).
6-
-- On PG14+, also checks heap and TOAST integrity (verify_heapam).
3+
-- All checks use AccessShareLock only — no write blocking.
4+
-- Same index checks as c1, plus verify_heapam (PG14+) which reads every heap page.
5+
-- ℹ️ On large databases this can take a while — it's a full sequential read.
76

87
do $$
98
declare
@@ -21,7 +20,6 @@ declare
2120
has_errors boolean;
2221
pg_version int;
2322
begin
24-
-- Check extension
2523
if not exists (select 1 from pg_extension where extname = 'amcheck') then
2624
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
2725
return;
@@ -123,7 +121,8 @@ begin
123121
-- === Heap verification (PG14+) ===
124122
if pg_version >= 140000 then
125123
raise notice '';
126-
raise notice '=== Heap check (verify_heapam, AccessShareLock) ===';
124+
raise notice '=== Heap + TOAST check (verify_heapam, AccessShareLock) ===';
125+
raise notice 'ℹ️ Reads every heap page — may take a while on large databases.';
127126
raise notice '';
128127

129128
for rec in
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ declare
1717
skip_count int := 0;
1818
pg_version int;
1919
begin
20-
-- Check extension
2120
if not exists (select 1 from pg_extension where extname = 'amcheck') then
2221
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
2322
return;
@@ -48,7 +47,7 @@ begin
4847
where a.amname = 'btree'
4948
and c.relpersistence != 't'
5049
and i.indisvalid
51-
order by pg_relation_size(c.oid) asc -- smallest first
50+
order by pg_relation_size(c.oid) asc
5251
loop
5352
begin
5453
if pg_version >= 140000 then
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ declare
2121
has_errors boolean;
2222
pg_version int;
2323
begin
24-
-- Check extension
2524
if not exists (select 1 from pg_extension where extname = 'amcheck') then
2625
raise notice '❌ amcheck extension is not installed. Run: CREATE EXTENSION amcheck;';
2726
return;
@@ -55,7 +54,7 @@ begin
5554
where a.amname = 'btree'
5655
and c.relpersistence != 't'
5756
and i.indisvalid
58-
order by pg_relation_size(c.oid) asc -- smallest first
57+
order by pg_relation_size(c.oid) asc
5958
loop
6059
begin
6160
if pg_version >= 140000 then

start.psql

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
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 – Corruption: quick check — btree, GIN (PG18+), heap (PG14+). Safe for production.'
14-
\echo ' c2 – Corruption: B-tree parent check — detects glibc/collation corruption (⚠️ ShareLock, use on clones)'
15-
\echo ' c3 – Corruption: FULL check — heapallindexed + parent + heap (⚠️⚠️ SLOW + ShareLock, use on clones)'
13+
\echo ' c1 – Corruption: quick index check — btree + GIN (PG18+). Safe for production, fast.'
14+
\echo ' c2 – Corruption: indexes + heap/TOAST check. Safe for production but reads all data.'
15+
\echo ' c3 – Corruption: B-tree parent check — detects glibc/collation corruption (⚠️ ShareLock, use on clones)'
16+
\echo ' c4 – Corruption: FULL check — heapallindexed + parent + heap (⚠️⚠️ SLOW + ShareLock, use on clones)'
1617
\echo ' e1 – Extensions installed in current database'
1718
\echo ' i1 – Unused and rarely used indexes'
1819
\echo ' i2 – Redundant indexes'
@@ -52,6 +53,7 @@ select
5253
:d_stp::text = 'c1' as d_step_is_c1,
5354
:d_stp::text = 'c2' as d_step_is_c2,
5455
:d_stp::text = 'c3' as d_step_is_c3,
56+
:d_stp::text = 'c4' as d_step_is_c4,
5557
:d_stp::text = 'e1' as d_step_is_e1,
5658
:d_stp::text = 'i1' as d_step_is_i1,
5759
:d_stp::text = 'i2' as d_step_is_i2,
@@ -117,15 +119,19 @@ select
117119
\prompt 'Press <Enter> to continue…' d_dummy
118120
\ir ./start.psql
119121
\elif :d_step_is_c1
120-
\ir ./sql/c1_amcheck_quick.sql
122+
\ir ./sql/c1_amcheck_indexes.sql
121123
\prompt 'Press <Enter> to continue…' d_dummy
122124
\ir ./start.psql
123125
\elif :d_step_is_c2
124-
\ir ./sql/c2_amcheck_parent.sql
126+
\ir ./sql/c2_amcheck_heap.sql
125127
\prompt 'Press <Enter> to continue…' d_dummy
126128
\ir ./start.psql
127129
\elif :d_step_is_c3
128-
\ir ./sql/c3_amcheck_full.sql
130+
\ir ./sql/c3_amcheck_parent.sql
131+
\prompt 'Press <Enter> to continue…' d_dummy
132+
\ir ./start.psql
133+
\elif :d_step_is_c4
134+
\ir ./sql/c4_amcheck_full.sql
129135
\prompt 'Press <Enter> to continue…' d_dummy
130136
\ir ./start.psql
131137
\elif :d_step_is_e1

0 commit comments

Comments
 (0)