1
+ -- Copyright 2024 shaneborden
2
+ --
3
+ -- Licensed under the Apache License, Version 2.0 (the "License");
4
+ -- you may not use this file except in compliance with the License.
5
+ -- You may obtain a copy of the License at
6
+ --
7
+ -- https://www.apache.org/licenses/LICENSE-2.0
8
+ --
9
+ -- Unless required by applicable law or agreed to in writing, software
10
+ -- distributed under the License is distributed on an "AS IS" BASIS,
11
+ -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ -- See the License for the specific language governing permissions and
13
+ -- limitations under the License.
14
+
15
+ WITH btree_index_atts AS (
16
+ SELECT nspname,
17
+ indexclass .relname as index_name,
18
+ indexclass .reltuples ,
19
+ indexclass .relpages ,
20
+ indrelid,
21
+ indexclass .relam ,
22
+ tableclass .relname as tablename,
23
+ indexrelid as index_oid,
24
+ pg_attribute .attname ,
25
+ pg_attribute .attnum
26
+ FROM pg_index
27
+ JOIN pg_class AS indexclass ON pg_index .indexrelid = indexclass .oid
28
+ JOIN pg_class AS tableclass ON pg_index .indrelid = tableclass .oid
29
+ JOIN pg_namespace ON pg_namespace .oid = indexclass .relnamespace
30
+ JOIN pg_am ON indexclass .relam = pg_am .oid
31
+ JOIN pg_attribute ON pg_attribute .attrelid = tableclass .oid
32
+ AND pg_attribute .attnum = ANY(indkey)
33
+ WHERE pg_am .amname = ' btree' and indexclass .relpages > 0
34
+ AND nspname NOT IN (' pg_catalog' ,' information_schema' )
35
+ AND tableclass .relname like ' %'
36
+ ),
37
+ index_item_sizes AS (
38
+ SELECT
39
+ ind_atts .nspname , ind_atts .index_name ,
40
+ ind_atts .reltuples , ind_atts .relpages , ind_atts .relam ,
41
+ indrelid AS table_oid, index_oid,
42
+ current_setting(' block_size' )::numeric AS bs,
43
+ 8 AS maxalign,
44
+ 24 AS pagehdr,
45
+ CASE WHEN max (coalesce(pg_stats .null_frac ,0 )) = 0
46
+ THEN 2
47
+ ELSE 6
48
+ END AS index_tuple_hdr,
49
+ sum ( (1 - coalesce(pg_stats .null_frac , 0 )) * coalesce(pg_stats .avg_width , 1024 ) ) AS nulldatawidth
50
+ FROM btree_index_atts AS ind_atts
51
+ left JOIN pg_stats ON pg_stats .schemaname = ind_atts .nspname
52
+ -- stats for regular index columns
53
+ AND ( (pg_stats .tablename = ind_atts .tablename AND pg_stats .attname = ind_atts .attname )
54
+ -- stats for functional indexes
55
+ OR (pg_stats .tablename = ind_atts .index_name AND pg_stats .attname = ind_atts .attname ))
56
+ WHERE ind_atts .attnum > 0
57
+ GROUP BY 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
58
+ ),
59
+ index_aligned_est AS (
60
+ SELECT maxalign, bs, nspname, index_name, reltuples,
61
+ relpages, relam, table_oid, index_oid,
62
+ coalesce (
63
+ ceil (
64
+ reltuples * ( 6
65
+ + maxalign
66
+ - CASE
67
+ WHEN index_tuple_hdr%maxalign = 0 THEN maxalign
68
+ ELSE index_tuple_hdr%maxalign
69
+ END
70
+ + nulldatawidth
71
+ + maxalign
72
+ - CASE /* Add padding to the data to align on MAXALIGN */
73
+ WHEN nulldatawidth::integer %maxalign = 0 THEN maxalign
74
+ ELSE nulldatawidth::integer %maxalign
75
+ END
76
+ )::numeric
77
+ / ( bs - pagehdr::NUMERIC )
78
+ + 1 )
79
+ , 0 )
80
+ as expected
81
+ FROM index_item_sizes
82
+ ),
83
+ raw_bloat AS (
84
+ SELECT current_database() as dbname, nspname, pg_class .relname AS table_name, index_name,
85
+ bs* (index_aligned_est .relpages )::bigint AS totalbytes, expected,
86
+ CASE
87
+ WHEN index_aligned_est .relpages <= expected
88
+ THEN 0
89
+ ELSE bs* (index_aligned_est .relpages - expected)::bigint
90
+ END AS wastedbytes,
91
+ CASE
92
+ WHEN index_aligned_est .relpages <= expected
93
+ THEN 0
94
+ ELSE bs* (index_aligned_est .relpages - expected)::bigint * 100 / (bs* (index_aligned_est .relpages )::bigint )
95
+ END AS realbloat,
96
+ pg_relation_size(index_aligned_est .table_oid ) as table_bytes,
97
+ stat .idx_scan as index_scans
98
+ FROM index_aligned_est
99
+ JOIN pg_class ON pg_class .oid = index_aligned_est .table_oid
100
+ JOIN pg_stat_user_indexes AS stat ON index_aligned_est .index_oid = stat .indexrelid
101
+ ),
102
+ format_bloat AS (
103
+ SELECT dbname as database_name, nspname as schema_name, table_name, index_name,
104
+ round(realbloat) as bloat_pct, round(wastedbytes/ (1024 ^2 )::NUMERIC ) as bloat_mb,
105
+ round(totalbytes/ (1024 ^2 )::NUMERIC ,3 ) as index_mb,
106
+ round(table_bytes/ (1024 ^2 )::NUMERIC ,3 ) as table_mb,
107
+ index_scans
108
+ FROM raw_bloat
109
+ )
110
+ -- final query outputting the bloated indexes
111
+ -- change the where and order by to change
112
+ -- what shows up as bloated
113
+ SELECT *
114
+ FROM format_bloat
115
+ -- WHERE ( bloat_pct > 50 and bloat_mb > 10 )
116
+ -- WHERE index_name like '%_user_id_idx'
117
+ ORDER BY bloat_pct DESC NULLS LAST;
0 commit comments