Skip to content

Commit

Permalink
add pg_stats optimization suggestions, sort output of queries
Browse files Browse the repository at this point in the history
  • Loading branch information
patinthehat committed Oct 19, 2024
1 parent c2da180 commit 03a9bcc
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 6 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require github.com/blang/semver v3.5.1+incompatible

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
Expand Down
42 changes: 36 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"regexp"
"sort"
"strings"
"time"
)
Expand Down Expand Up @@ -57,6 +58,7 @@ func main() {
}

groupBySourceCodeLocation(entries)
// displayQueryOptimizationSuggestions(entries)
}

// parseLine parses a single line from the log file into a LogEntry.
Expand Down Expand Up @@ -105,13 +107,41 @@ func groupBySourceCodeLocation(entries []LogEntry) {
times[entry.SourceCodeLocation] += float64(time.Abs().Milliseconds())
}

fmt.Println("SourceCodeLocation counts:")
for location, count := range counts {
average := times[location]
ms := average / float64(count)
// sort by location name (alpha sort):
sortedLocations := make([]string, 0, len(counts))

for location, _ := range counts {
sortedLocations = append(sortedLocations, location)
}

if (count >= 200 || ms >= 10.5) && !strings.HasPrefix(location, "unknown:") {
fmt.Printf(" %s (count: %d, mean time: %0.4f ms, total time: %0.0f ms)\n", location, count, ms, average)
sort.Strings(sortedLocations)

fmt.Println("SourceCodeLocation counts:")
// for location, count := range counts {
for _, location := range sortedLocations {
count := counts[location]
ms := times[location]
average := ms / float64(count)

if (count >= 750 || average >= 30.0) && !strings.HasPrefix(location, "unknown:") {
fmt.Printf(" %s (count: %d, mean time: %0.4f ms, total time: %0.0f ms)\n", location, count, average, ms)
}
}
}

// func displayQueryOptimizationSuggestions(entries []LogEntry) {
// queries, err := pgstat.ConnectAndFetchPgStatStatements(pgstat.BuildPostgresDsn("localhost", 5432, "root", "password", "database"))
// if err != nil {
// fmt.Printf("Error fetching pg_stat_statements: %v\n", err)
// return
// }

// suggestions := pgstat.AnalyzeQueries(queries)

// fmt.Println("\nOptimization suggestions:")
// for _, suggestion := range suggestions {
// fmt.Printf(" QueryID: %d\n", suggestion.QueryID)
// fmt.Printf(" Query: %s\n", suggestion.Query)
// fmt.Printf(" Suggestions: %v\n", suggestion.Suggestions)
// }
// }
143 changes: 143 additions & 0 deletions pkg/pgstat/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package pgstat

import (
"fmt"
"strings"
)

// OptimizationSuggestion represents a suggestion for a query optimization.
type OptimizationSuggestion struct {
QueryID int64
Query string
Suggestions []string
}

// AnalyzeQueries analyzes an array of PgStatStatementEntry and returns optimization suggestions.
func AnalyzeQueries(entries []PgStatStatementEntry) []OptimizationSuggestion {
var suggestions []OptimizationSuggestion
for _, entry := range entries {
var entrySuggestions []string

if strings.Contains(entry.Query, " pg_") || strings.Contains(entry.Query, " information_schema") {
continue
}

if strings.Contains(entry.Query, "alter table") || strings.Contains(entry.Query, "create index") {
continue
}

// Check for high total execution time.
// if suggestion := checkHighTotalExecTime(entry); suggestion != "" {
// entrySuggestions = append(entrySuggestions, suggestion)
// }

// Check for high average execution time.
if suggestion := checkHighMeanExecTime(entry); suggestion != "" {
entrySuggestions = append(entrySuggestions, suggestion)
}

// Check for high standard deviation in execution time.
if suggestion := checkHighStdDevExecTime(entry); suggestion != "" {
entrySuggestions = append(entrySuggestions, suggestion)
}

// Check for high number of temporary blocks read or written.
if suggestion := checkTempBlksUsage(entry); suggestion != "" {
entrySuggestions = append(entrySuggestions, suggestion)
}

// Check for high number of shared blocks read vs. hit.
if suggestion := checkSharedBlksReadVsHit(entry); suggestion != "" {
entrySuggestions = append(entrySuggestions, suggestion)
}

// Check for low rows returned per call.
// if suggestion := checkRowsPerCall(entry); suggestion != "" {
// entrySuggestions = append(entrySuggestions, suggestion)
// }

// Check for high WAL usage.
if suggestion := checkHighWalUsage(entry); suggestion != "" {
entrySuggestions = append(entrySuggestions, suggestion)
}

// Add suggestions for this query if any exist.
if len(entrySuggestions) > 0 {
suggestions = append(suggestions, OptimizationSuggestion{
QueryID: entry.QueryID,
Query: entry.Query,
Suggestions: entrySuggestions,
})
}
}
return suggestions
}

// checkHighTotalExecTime checks if the total execution time is high.
func checkHighTotalExecTime(entry PgStatStatementEntry) string {
const totalExecTimeThreshold = 1000.0 // in milliseconds
if entry.TotalExecTime > totalExecTimeThreshold {
return fmt.Sprintf("Total execution time is high (%.2f ms). Consider optimizing the query or adding indexes.", entry.TotalExecTime)
}
return ""
}

// checkHighMeanExecTime checks if the mean execution time per call is high.
func checkHighMeanExecTime(entry PgStatStatementEntry) string {
const meanExecTimeThreshold = 100.0 // in milliseconds
if entry.MeanExecTime > meanExecTimeThreshold {
return fmt.Sprintf("Mean execution time per call is high (%.2f ms). Consider optimizing the query.", entry.MeanExecTime)
}
return ""
}

// checkHighStdDevExecTime checks if the standard deviation of execution time is high.
func checkHighStdDevExecTime(entry PgStatStatementEntry) string {
const stdDevThreshold = 50.0 // in milliseconds
if entry.StddevExecTime > stdDevThreshold {
return fmt.Sprintf("Execution time varies widely (stddev %.2f ms). Investigate possible causes for inconsistent performance.", entry.StddevExecTime)
}
return ""
}

// checkTempBlksUsage checks if the query uses a high number of temporary blocks.
func checkTempBlksUsage(entry PgStatStatementEntry) string {
if entry.TempBlksRead > 0 || entry.TempBlksWritten > 0 {
return "Query uses temporary disk space. Consider optimizing to reduce disk I/O, such as adding indexes or rewriting the query."
}
return ""
}

// checkSharedBlksReadVsHit checks if the query reads many shared blocks compared to hits.
func checkSharedBlksReadVsHit(entry PgStatStatementEntry) string {
totalSharedBlks := entry.SharedBlksHit + entry.SharedBlksRead
if totalSharedBlks == 0 {
return ""
}
readRatio := float64(entry.SharedBlksRead) / float64(totalSharedBlks)
if readRatio > 25.0 {
return fmt.Sprintf("High shared block read ratio (%.2f%%). Consider adding indexes to reduce I/O.", readRatio*100)
}
return ""
}

// checkRowsPerCall checks if the query returns a low number of rows per call.
func checkRowsPerCall(entry PgStatStatementEntry) string {
if entry.Calls == 0 {
return ""
}
rowsPerCall := float64(entry.Rows) / float64(entry.Calls)
if rowsPerCall < 10.0 {
return fmt.Sprintf("Low rows returned per call (%.2f). Verify if the query returns the expected results.", rowsPerCall)
}
return ""
}

// checkHighWalUsage checks if the query generates a high amount of WAL records or bytes.
func checkHighWalUsage(entry PgStatStatementEntry) string {
const walBytesThreshold = 1024 * 1024 * 50 // 50 MB
if entry.WalBytes > walBytesThreshold {
return fmt.Sprintf("High WAL usage (%.2f MB). Consider batching writes or optimizing the query.", float64(entry.WalBytes)/(1024*1024))
}
return ""
}
83 changes: 83 additions & 0 deletions pkg/pgstat/pgstat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package pgstat

import (
//"database/sql"
"fmt"

"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)

// PgStatStatementEntry represents a single entry from pg_stat_statements.
type PgStatStatementEntry struct {
UserID int64 `db:"userid"`
DbID int64 `db:"dbid"`
TopLevel bool `db:"toplevel"`
QueryID int64 `db:"queryid"`
Query string `db:"query"`
Plans int64 `db:"plans"`
TotalPlanTime float64 `db:"total_plan_time"`
MinPlanTime float64 `db:"min_plan_time"`
MaxPlanTime float64 `db:"max_plan_time"`
MeanPlanTime float64 `db:"mean_plan_time"`
StddevPlanTime float64 `db:"stddev_plan_time"`
Calls int64 `db:"calls"`
TotalExecTime float64 `db:"total_exec_time"`
MinExecTime float64 `db:"min_exec_time"`
MaxExecTime float64 `db:"max_exec_time"`
MeanExecTime float64 `db:"mean_exec_time"`
StddevExecTime float64 `db:"stddev_exec_time"`
Rows int64 `db:"rows"`
SharedBlksHit int64 `db:"shared_blks_hit"`
SharedBlksRead int64 `db:"shared_blks_read"`
SharedBlksDirtied int64 `db:"shared_blks_dirtied"`
SharedBlksWritten int64 `db:"shared_blks_written"`
LocalBlksHit int64 `db:"local_blks_hit"`
LocalBlksRead int64 `db:"local_blks_read"`
LocalBlksDirtied int64 `db:"local_blks_dirtied"`
LocalBlksWritten int64 `db:"local_blks_written"`
TempBlksRead int64 `db:"temp_blks_read"`
TempBlksWritten int64 `db:"temp_blks_written"`
BlkReadTime float64 `db:"blk_read_time"`
BlkWriteTime float64 `db:"blk_write_time"`
TempBlkReadTime float64 `db:"temp_blk_read_time"`
TempBlkWriteTime float64 `db:"temp_blk_write_time"`
WalRecords int64 `db:"wal_records"`
WalFPI int64 `db:"wal_fpi"`
WalBytes int64 `db:"wal_bytes"`
JitFunctions int64 `db:"jit_functions"`
JitGenerationTime float64 `db:"jit_generation_time"`
JitInliningCount int64 `db:"jit_inlining_count"`
JitInliningTime float64 `db:"jit_inlining_time"`
JitOptimizationCount int64 `db:"jit_optimization_count"`
JitOptimizationTime float64 `db:"jit_optimization_time"`
JitEmissionCount int64 `db:"jit_emission_count"`
JitEmissionTime float64 `db:"jit_emission_time"`
}

// GetPgStatStatements fetches the pg_stat_statements from the database.
func GetPgStatStatements(db *sqlx.DB) ([]PgStatStatementEntry, error) {
var entries []PgStatStatementEntry
query := `SELECT * FROM pg_stat_statements ORDER BY calls DESC;`
err := db.Select(&entries, query)
if err != nil {
return nil, err
}
return entries, nil
}

func BuildPostgresDsn(host string, port int, user string, password string, dbname string) string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
}

func ConnectAndFetchPgStatStatements(dsn string) ([]PgStatStatementEntry, error) {
db, err := sqlx.Connect("postgres", dsn)

if err != nil {
return nil, err
}

defer db.Close()

return GetPgStatStatements(db)
}

0 comments on commit 03a9bcc

Please sign in to comment.