diff --git a/internal/inspect/index_stats/index_stats.go b/internal/inspect/index_stats/index_stats.go index 6db47c4bfe..bdec377dca 100644 --- a/internal/inspect/index_stats/index_stats.go +++ b/internal/inspect/index_stats/index_stats.go @@ -19,6 +19,8 @@ var IndexStatsQuery string type Result struct { Name string + Table string + Columns string Size string Percent_used string Index_scans int64 @@ -41,9 +43,9 @@ func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...fu return err } - table := "|Name|Size|Percent used|Index scans|Seq scans|Unused|\n|-|-|-|-|-|-|\n" + table := "|Name|Table|Columns|Size|Percent used|Index scans|Seq scans|Unused|\n|-|-|-|-|-|-|-|-|\n" for _, r := range result { - table += fmt.Sprintf("|`%s`|`%s`|`%s`|`%d`|`%d`|`%t`|\n", r.Name, r.Size, r.Percent_used, r.Index_scans, r.Seq_scans, r.Unused) + table += fmt.Sprintf("|`%s`|`%s`|`%s`|`%s`|`%s`|`%d`|`%d`|`%t`|\n", r.Name, r.Table, r.Columns, r.Size, r.Percent_used, r.Index_scans, r.Seq_scans, r.Unused) } return utils.RenderTable(table) } diff --git a/internal/inspect/index_stats/index_stats.sql b/internal/inspect/index_stats/index_stats.sql index c8d33e95dd..7547fc205b 100644 --- a/internal/inspect/index_stats/index_stats.sql +++ b/internal/inspect/index_stats/index_stats.sql @@ -1,13 +1,20 @@ --- Combined index statistics: size, usage percent, seq scans, and mark unused +-- Combined index statistics: size, usage percent, seq scans, mark unused, expose table + columns WITH idx_sizes AS ( SELECT i.indexrelid AS oid, FORMAT('%I.%I', n.nspname, c.relname) AS name, + FORMAT('%I.%I', tn.nspname, tc.relname) AS table_name, + ( + SELECT STRING_AGG(pg_get_indexdef(i.indexrelid, ord::int, false), ',' ORDER BY ord) + FROM unnest(i.indkey::int[]) WITH ORDINALITY AS k(attnum, ord) + ) AS columns, pg_relation_size(i.indexrelid) AS index_size_bytes FROM pg_stat_user_indexes ui JOIN pg_index i ON ui.indexrelid = i.indexrelid JOIN pg_class c ON ui.indexrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid + JOIN pg_class tc ON tc.oid = i.indrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace WHERE NOT n.nspname LIKE ANY($1) ), idx_usage AS ( @@ -37,6 +44,8 @@ usage_pct AS ( ) SELECT s.name, + s.table_name AS "table", + s.columns, pg_size_pretty(s.index_size_bytes) AS size, COALESCE(up.percent_used, 0)::text || '%' AS percent_used, COALESCE(u.idx_scans, 0) AS index_scans, diff --git a/internal/inspect/index_stats/index_stats_test.go b/internal/inspect/index_stats/index_stats_test.go index abc3cd6543..c9edc49581 100644 --- a/internal/inspect/index_stats/index_stats_test.go +++ b/internal/inspect/index_stats/index_stats_test.go @@ -30,6 +30,8 @@ func TestIndexStatsCommand(t *testing.T) { conn.Query(IndexStatsQuery, reset.LikeEscapeSchema(utils.InternalSchemas)). Reply("SELECT 1", Result{ Name: "public.test_idx", + Table: "public.test", + Columns: "id", Size: "1GB", Percent_used: "50%", Index_scans: 5, diff --git a/internal/inspect/report.go b/internal/inspect/report.go index 162d0bb08f..1f038cdc52 100644 --- a/internal/inspect/report.go +++ b/internal/inspect/report.go @@ -117,6 +117,8 @@ func printSummary(ctx context.Context, outDir string) error { } if !match.Valid { match.String = "-" + } else if len(match.String) > 20 { + match.String = fmt.Sprintf("%d matches", strings.Count(match.String, ",")+1) } table += fmt.Sprintf("|`%s`|`%s`|`%s`|\n", r.Name, status, match.String) } diff --git a/internal/inspect/templates/rules.toml b/internal/inspect/templates/rules.toml index 2597ee720d..7d69efda63 100644 --- a/internal/inspect/templates/rules.toml +++ b/internal/inspect/templates/rules.toml @@ -19,7 +19,13 @@ pass = "✔" fail = "There is at least one unused index" [[rules]] -query = "SELECT LISTAGG(name, ',') AS match FROM `db_stats.csv` WHERE index_hit_rate < 0.94 OR table_hit_rate < 0.94" +query = "SELECT LISTAGG(i.name, ',') AS match FROM `index_stats.csv` AS i JOIN (SELECT `table`, columns FROM `index_stats.csv` GROUP BY `table`, columns HAVING COUNT(*) > 1) AS d ON i.`table` = d.`table` AND i.columns = d.columns" +name = "No duplicate indexes" +pass = "✔" +fail = "There is at least one duplicate index (same columns on the same table)" + +[[rules]] +query = "SELECT 'index: ' || index_hit_rate || ', table: ' || table_hit_rate AS match FROM `db_stats.csv` WHERE index_hit_rate < 0.94 OR table_hit_rate < 0.94" name = "Check cache hit is within acceptable bounds" pass = "✔" fail = "There is a cache hit ratio (table or index) below 94%" @@ -31,7 +37,7 @@ pass = "✔" fail = "At least one table is showing sequential scans more than 10% of total row count" [[rules]] -query = "SELECT LISTAGG(s.tbl, ',') AS match FROM `vacuum_stats.csv` s WHERE s.expect_autovacuum = 'yes' and s.rowcount > 1000;" +query = "SELECT LISTAGG(s.name, ',') AS match FROM `vacuum_stats.csv` s WHERE s.expect_autovacuum = 'yes' and s.rowcount > 1000;" name = "No large tables waiting on autovacuum" pass = "✔" fail = "At least one table is waiting on autovacuum" @@ -41,3 +47,33 @@ query = "SELECT LISTAGG(s.name, ',') AS match FROM `vacuum_stats.csv` s WHERE s. name = "No tables yet to be vacuumed" pass = "✔" fail = "At least one table has never had autovacuum or vacuum run on it" + +[[rules]] +query = "SELECT LISTAGG(s.name, ',') AS match FROM `vacuum_stats.csv` s WHERE FLOAT(REPLACE(s.rowcount, ',', '')) > 1000 AND FLOAT(REPLACE(s.dead_rowcount, ',', '')) > 0.2 * FLOAT(REPLACE(s.rowcount, ',', ''))" +name = "No tables with more than 20% dead rows" +pass = "✔" +fail = "At least one table has more than 20% dead rows" + +[[rules]] +query = "SELECT LISTAGG(slot_name, ',') AS match FROM `replication_slots.csv` WHERE active = 'f'" +name = "No inactive replication slots" +pass = "✔" +fail = "There is at least one inactive replication slot" + +[[rules]] +query = "SELECT LISTAGG(blocked_pid, ',') AS match FROM `blocking.csv`" +name = "No blocked queries" +pass = "✔" +fail = "There is at least one query blocked on another" + +[[rules]] +query = "SELECT LISTAGG(pid, ',') AS match FROM `long_running_queries.csv`" +name = "No queries running longer than 5 minutes" +pass = "✔" +fail = "At least one query has been running for more than 5 minutes" + +[[rules]] +query = "SELECT LISTAGG(name, ',') AS match FROM `bloat.csv` WHERE bloat > 4" +name = "No tables or indexes with bloat ratio above 4x" +pass = "✔" +fail = "At least one table or index is more than 4x its expected size" diff --git a/pkg/config/db.go b/pkg/config/db.go index 7b0c76a163..963cfba9c7 100644 --- a/pkg/config/db.go +++ b/pkg/config/db.go @@ -92,7 +92,7 @@ type ( Seed seed `toml:"seed" json:"seed"` Settings settings `toml:"settings" json:"settings"` NetworkRestrictions networkRestrictions `toml:"network_restrictions" json:"network_restrictions"` - SslEnforcement *sslEnforcement `toml:"ssl_enforcement" json:"ssl_enforcement"` + SslEnforcement *sslEnforcement `toml:"ssl_enforcement" json:"ssl_enforcement"` Vault map[string]Secret `toml:"vault" json:"vault"` }