From 8d7af9eb854652709a2a9b00977634bb92a98e7c Mon Sep 17 00:00:00 2001 From: kqr Date: Mon, 7 Sep 2026 09:28:20 +0200 Subject: [PATCH] fix(trigger): exclude constraint indexes from critical severity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, any large (> 100 MB) index with 0 scans was considered unused with a diagnostic severity of CRITICAL. Some indexes are never intended to be read, but instead only used for enforcing uniqueness or foreign key constraints. It is misleading to label the lack of scans of these indexes as a CRITICAL error. This commit - keeps the CRITICAL severity diagnostic for un-scanned indexes that are not used in constraints, but - demotes the severity of un-scanned unique/FK/exclusion constraint indexes to MEDIUM, and clarifies that the recommended action for them might be different than for access-serving indexes. (Note that exclusion indexes do actually trigger a scan and increment idx_scan, but this seems more like an implementation detail and not a reliable requirement on exclusion indexes, so they are still covered by this change.) The reason for the low severity level for these constraint-backing indexes is that it seems highly probable they are there for a reason. If a constraint-backing index is incorrect, then that is very likely to have been discovered by the time it has grown large, because then many expected-to-succeed inserts would have failed due to violation of the constraint. There's also no way from within the database to tell whether the constraint is still relevant or not. The combination of high risk of false positive with low risk of false negative means a low severity level is appropriate. This also makes the name "Unused Large Index" of the pre-existing check more accurate, because previously it said "unused" even though it may have been used to enforce constraints (it was merely "un-scanned"). Now that name is correct – an access-serving index that is unread truly appears unused as far as can be told from within the database. --- pgFirstAid.sql | 35 +++++++++++++++++++++++++++++++---- view_pgFirstAid.sql | 33 +++++++++++++++++++++++++++++---- view_pgFirstAid_managed.sql | 33 +++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/pgFirstAid.sql b/pgFirstAid.sql index 931e1c0..1d741a0 100644 --- a/pgFirstAid.sql +++ b/pgFirstAid.sql @@ -383,7 +383,7 @@ where and n.nspname = pt.schemaname and c.relname = pt.tablename ); --- CRITICAL: Unused indexes consuming significant space +-- CRITICAL: Large indexes never used for reads consuming significant space insert into health_results @@ -392,7 +392,7 @@ where 'Table Health' as category, 'Unused Large Index' as check_name, quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, - 'Large unused index consuming disk space and potentially impacting write performance' as issue_description, + 'Large index never used for reads, consuming disk space and potentially impacting write performance' as issue_description, pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, 'Consider dropping this index if truly unused after monitoring usage patterns. Never drop an index without validating usage!' as recommended_action, 'https://www.postgresql.org/docs/current/sql-dropindex.html' as documentation_link, @@ -401,10 +401,37 @@ from pg_stat_user_indexes psi join pg_statio_user_indexes psio on psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid where idx_scan = 0 - and pg_relation_size(psi.indexrelid) > 104857600; --- 100MB + and not pgi.indisunique + and not pgi.indisexclusion + and pg_relation_size(psi.indexrelid) > 104857600; -- 100MB +-- MEDIUM: Large unique/constraint indexes never used for reads + insert + into + health_results + select + 'MEDIUM' as severity, + 'Table Health' as category, + 'Unread Large Constraint-Backing Index' as check_name, + quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, + 'Large unique or constraint index never used for reads. It is enforced on every write, so review whether the constraint is needed.' as issue_description, + pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, + 'Confirm the underlying unique or primary-key constraint is still required. Drop the constraint if it is not necessary.' as recommended_action, + 'https://www.postgresql.org/docs/current/ddl-constraints.html' as documentation_link, + 3 as severity_order +from + pg_stat_user_indexes psi +join pg_statio_user_indexes psio on + psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid +where + idx_scan = 0 + and (pgi.indisunique or pgi.indisexclusion) + and pg_relation_size(psi.indexrelid) > 104857600; -- 100MB -- HIGH: Inactive Replication slots insert into diff --git a/view_pgFirstAid.sql b/view_pgFirstAid.sql index 3791d2d..9b0e2b5 100644 --- a/view_pgFirstAid.sql +++ b/view_pgFirstAid.sql @@ -375,13 +375,13 @@ where and c.relname = pt.tablename ) union all --- CRITICAL: Unused indexes consuming significant space +-- CRITICAL: Large indexes never used for reads consuming significant space select 'CRITICAL' as severity, 'Table Health' as category, 'Unused Large Index' as check_name, quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, - 'Large unused index consuming disk space and potentially impacting write performance' as issue_description, + 'Large index never used for reads, consuming disk space and potentially impacting write performance' as issue_description, pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, 'Consider dropping this index if truly unused after monitoring usage patterns. Never drop an index without validating usage!' as recommended_action, 'https://www.postgresql.org/docs/current/sql-dropindex.html' as documentation_link, @@ -390,10 +390,35 @@ from pg_stat_user_indexes psi join pg_statio_user_indexes psio on psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid where idx_scan = 0 - and pg_relation_size(psi.indexrelid) > 104857600 - -- 100MB + and not pgi.indisunique + and not pgi.indisexclusion + and pg_relation_size(psi.indexrelid) > 104857600 -- 100MB +union all +-- MEDIUM: Large unique/constraint indexes never used for reads + select + 'MEDIUM' as severity, + 'Table Health' as category, + 'Unread Large Constraint-Backing Index' as check_name, + quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, + 'Large unique or constraint index never used for reads. It is enforced on every write, so review whether the constraint is needed.' as issue_description, + pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, + 'Confirm the underlying unique or primary-key constraint is still required. Drop the constraint if it is not necessary.' as recommended_action, + 'https://www.postgresql.org/docs/current/ddl-constraints.html' as documentation_link, + 3 as severity_order +from + pg_stat_user_indexes psi +join pg_statio_user_indexes psio on + psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid +where + idx_scan = 0 + and (pgi.indisunique or pgi.indisexclusion) + and pg_relation_size(psi.indexrelid) > 104857600 -- 100MB union all -- HIGH: Inactive Replication slots (with q as ( diff --git a/view_pgFirstAid_managed.sql b/view_pgFirstAid_managed.sql index 7a6373f..5d46afe 100644 --- a/view_pgFirstAid_managed.sql +++ b/view_pgFirstAid_managed.sql @@ -370,13 +370,13 @@ where and c.relname = pt.tablename ) union all --- CRITICAL: Unused indexes consuming significant space +-- CRITICAL: Large indexes never used for reads consuming significant space select 'CRITICAL' as severity, 'Table Health' as category, 'Unused Large Index' as check_name, quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, - 'Large unused index consuming disk space and potentially impacting write performance' as issue_description, + 'Large index never used for reads, consuming disk space and potentially impacting write performance' as issue_description, pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, 'Consider dropping this index if truly unused after monitoring usage patterns. Never drop an index without validating usage!' as recommended_action, 'https://www.postgresql.org/docs/current/sql-dropindex.html' as documentation_link, @@ -385,10 +385,35 @@ from pg_stat_user_indexes psi join pg_statio_user_indexes psio on psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid where idx_scan = 0 - and pg_relation_size(psi.indexrelid) > 104857600 - -- 100MB + and not pgi.indisunique + and not pgi.indisexclusion + and pg_relation_size(psi.indexrelid) > 104857600 -- 100MB +union all +-- MEDIUM: Large unique/constraint indexes never used for reads + select + 'MEDIUM' as severity, + 'Table Health' as category, + 'Unread Large Constraint-Backing Index' as check_name, + quote_ident(psi.schemaname) || '.' || quote_ident(psio.indexrelname) as object_name, + 'Large unique or constraint index never used for reads. It is enforced on every write, so review whether the constraint is needed.' as issue_description, + pg_size_pretty(pg_relation_size(psi.indexrelid)) || ' (0 scans)' as current_value, + 'Confirm the underlying unique or primary-key constraint is still required. Drop the constraint if it is not necessary.' as recommended_action, + 'https://www.postgresql.org/docs/current/ddl-constraints.html' as documentation_link, + 3 as severity_order +from + pg_stat_user_indexes psi +join pg_statio_user_indexes psio on + psi.indexrelid = psio.indexrelid +join pg_index pgi on + pgi.indexrelid = psi.indexrelid +where + idx_scan = 0 + and (pgi.indisunique or pgi.indisexclusion) + and pg_relation_size(psi.indexrelid) > 104857600 -- 100MB union all -- HIGH: Inactive Replication slots (with q as (