Tuesday, April 1, 2014

New New Index Bloat Query

Earlier this week Ioguix posted an excellent overhaul of the well-known Index Bloat Estimation from check_postgres.  However, I felt that we needed several additional changes before the query is ready for me to use in our internal monitoring utilities, and thought I'd post our version here.

Here's the New New Index Bloat Query.

Here's what I changed:
  1. Had it pull btree indexes only, because the calculations don't work for GIN/GiST indexes.
  2. Moved all of the nested subqueries up into WITH clauses for easier readability and maitainability.   Since all supported Postgres versions now do CTEs, I didn't see any reason to work around them.
  3. Removed the \set statements, because they don't work for embedding the query in a script.  And they only need setting dynamically if you've compiled PostgreSQL with nonstandard options, anyway.
  4. Removed a couple of CASE statements aimed at 7.X support; who cares?
  5. Added some extra informational columns for using this in interactive mode: table size, pretty index size, and index scans.  This helps folks figure out whether to rebuild an index, ignore the bloat or to drop it.
  6. In the example query, filtering down to indexes with bloat over 50% and 50MB, which is our threshold for "significant bloat"
Example output:

-[ RECORD 27 ]+-----------------------------
database_name | prod-web

schema_name   | partitions
table_name    | transactions_201308
index_name    | transactions_201308_uuid_idx
bloat_pct     | 52.9
bloat_bytes   | 351649792
bloat_size    | 335 MB
index_bytes   | 664788992
index_size    | 634 MB
table_bytes   | 4570447872
table_size    | 4359 MB
index_scans   | 263

So this index would be a good candidate for deletion, since it's 50% bloated and seldom scanned.

Now, there's still more to do on this.  We need a similar query for GIN/GiST/SPGiST.  The query could use some more cleanup; removing one-letter table aliases, and GROUP BY 1,2,3,4 stuff.  But it's vastly improved for checking which of your indexes need VACUUM/REINDEX.  Thanks, Ioguix!


  1. Very interesting, but one side note: the old query uses pg_stats and thus enables check_postgres to query the statistics without being a superuser for example.

    1. Which bits of my version require superuser perms? I don't see any.

    2. The JOIN on pg_statistics. I get this (sorry for the german) when I execute your query as a non-superuser:
      FEHLER: keine Berechtigung für Relation pg_statistic
      Bernd rewrote it here: http://pgsql.privatepaste.com/36c6206434
      Which works fine for me as non-superuser.

    3. Please, again, fix this piece of the query from:

      maxalign - CASE /*...*/
      WHEN 2%maxalign = 0 THEN maxalign
      ELSE 2%maxalign


      maxalign - CASE /*...*/
      WHEN index_tuple_hdr%maxalign = 0 THEN maxalign
      ELSE index_tuple_hdr%maxalign

      See my previous comment here and on my blog.


    4. This comment has been removed by the author.

    5. Bernd,

      Hmmm. Seems like we should be able to get what we need out of pg_stats. Lemme play around with it.

  2. Patches welcome, Josh, but plenty of users *do* care about 7.x and 8.x, so we cannot support WITH quite yet. :)

    1. I care about 7.x and 8.x as well :)

      Note that my query is 8.2+ only. It's because pg_index.indkey (int2vector) can not be cast to anything before 8.2. I'm afraid we'll be forced to deal with that on the app side using two queries. Hadn't time to work on that yet, but this is quite easy to do anyway.

    2. This comment has been removed by the author.

    3. Greg, I'm just solving the 90% case. I figure if a Postgres version is out-of-support, the user probably isn't updating their monitoring either.

    4. Nah, upgrading your monitoring is easy. Upgrading your large production database on the other hand,,,

  3. Thanks for improvements Josh !

    1. I hadn't time to work or test on GiST/GIN, and it probably doesn't work properly anyway. +1 for limiting explicitly to B-Tree
    2. and 4. as Greg explained, I care about 7.x and 8.x as well, so I'll keep the sub-query version
    3. \set was actually to make the query more "readable" and understandable. Obviously, it's easy to serialize them back in query for tools :-)

    WARNING: You directly use value "2" in CTE "index_aligned" for "nulldatahdrwidth" instead of "index_tuple_hdr" computed in CTE "index_item_sizes". So your stats are not good enough with index referencing null-able fields. I answered you about this on my blog.


  4. What about comparison with pgstattuple? How different the results are?

    1. This part is covered in my blog post about the original query and how it compare to reality. See: