Unused indexes take lot of space and slow down data modifications. How can I check if my indexes are really useful and delete unused ones?
Make sure your applications have run long enough since the last stat reset. Think especially of periodic jobs that runs once every week and may require an index.
This is the code to make a human readable view with usefull indices information. The indices on partitionned table are aggregated. If you have templates tables for your partitions, make sure the filters for template tables names are accurate.
CREATE VIEW stats_index_aggregate as
-- Index for partitionned tables
select
'partitioned index' as indextype,
nsp.nspname as schemaname,
table_class.relname as tablename,
parent_class.relname as indexname,
index_columns.idx_columns as idx_columns,
seek_childs.nb_child_index,
seek_childs.nb_scans,
seek_childs.index_size
from pg_class parent_class
join pg_index parent_index on parent_index.indexrelid = parent_class.oid
join pg_namespace nsp on nsp.oid = parent_class.relnamespace -- to get schemaname
join pg_class table_class on table_class.oid = parent_index.indrelid
, lateral (
select count(stats_child.idx_scan) as nb_child_index,
sum(stats_child.idx_scan) as nb_scans,
sum(pg_relation_size(stats_child.indexrelid)) as index_size
from pg_catalog.pg_stat_user_indexes stats_child
join pg_inherits pi on pi.inhrelid = stats_child.indexrelid
where pi.inhparent = parent_class.oid
) seek_childs
, LATERAL (
SELECT string_agg(attname, ', ' order by attnum) AS idx_columns
FROM pg_attribute
WHERE attrelid = parent_class.oid
) index_columns
where parent_class.relkind = 'I'
AND 0 <>ALL (parent_index.indkey) -- no index column is an expression
AND NOT parent_index.indisunique -- is not a UNIQUE index
AND NOT EXISTS -- does not enforce a constraint
(SELECT 1 FROM pg_catalog.pg_constraint cc WHERE cc.conindid = parent_index.indexrelid)
and table_class.relname not like '%template' -- filter for template tables
union
-- Index for regular tables
select
'regular index' as indextype,
stats_child.schemaname,
stats_child.relname AS tablename,
c.relname as indexname,
index_columns.idx_columns as idx_columns,
null as nb_child_index,
stats_child.idx_scan as id_scan_count,
pg_relation_size(stats_child.indexrelid) as index_size
from pg_class c
join pg_index idx_parent on idx_parent.indexrelid = c.oid
join pg_catalog.pg_stat_user_indexes stats_child on c.oid = stats_child.indexrelid
, LATERAL (
SELECT string_agg(attname, ', ' order by attnum) AS idx_columns
FROM pg_attribute
WHERE attrelid = c.oid
) index_columns
where c.relkind = 'i'
AND 0 <>ALL (idx_parent.indkey) -- no index column is an expression
AND NOT idx_parent.indisunique -- is not a UNIQUE index
AND NOT EXISTS -- does not enforce a constraint
(SELECT 1 FROM pg_catalog.pg_constraint cc
WHERE cc.conindid = idx_parent.indexrelid)
AND NOT EXISTS -- is not a child index
(SELECT 1 FROM pg_inherits pi
where pi.inhrelid = c.oid)
and stats_child.relname not like '%template'; -- filter for template tables
Then you can check how much space you can save with this summary :
select count(*) || ' indices are not used, you can drop them and save ' || pg_size_pretty(sum(index_size)) as pretty_size from stats_index_aggregate where nb_scans = 0;
Execute this query output to clean your database :
select
case when nb_scans = 0 then 'DROP INDEX ' || indexname || ';' else '' end as drop_command, -- make sure we don't print drop command for used index
*, pg_size_pretty(index_size) as pretty_size
from stats_index_aggregate;