summaryrefslogtreecommitdiff
path: root/postgres/postgres.go
blob: c38bb31bcb5a0e425571e29e0e7114ec2046a630 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package postgres

import (
	"encoding/json"
	"fmt"
	"math"
	"time"

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

type DB struct {
	*sqlx.DB
}

func Open(conn string) (*DB, error) {
	db, err := sqlx.Open("postgres", conn)
	if err != nil {
		return nil, err
	}
	return &DB{db}, nil
}

type Schema struct {
	SchemaName string `db:"schema_name"`
}

func (db *DB) GetSchemata() ([]*Schema, error) {
	var schemata []*Schema
	if err := db.Select(&schemata, "SELECT schema_name FROM information_schema.schemata"); err != nil {
		return nil, err
	}
	return schemata, nil
}

type PgStatStatement struct {
	UserID            int     `db:"userid"`
	DBID              int     `db:"dbid"`
	QueryID           int64   `db:"queryid"`
	Query             string  `db:"query"`
	Calls             int     `db:"calls"`
	TotalTime         float32 `db:"total_time"`
	MinTime           float32 `db:"min_time"`
	MaxTime           float32 `db:"max_time"`
	MeanTime          float32 `db:"mean_time"`
	StddevTime        float32 `db:"stddev_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       float32 `db:"blk_read_time"`
	BlkWriteTime      float32 `db:"blk_write_time"`
}

func (db *DB) GetPgStatStatements(schema string) ([]*PgStatStatement, error) {
	var stmts []*PgStatStatement
	if err := db.Select(&stmts, fmt.Sprintf("SELECT * FROM %s.pg_stat_statements ORDER BY total_time DESC", schema)); err != nil {
		return nil, err
	}
	return stmts, nil
}

type Explain struct {
	Plan         *Plan   `json:"Plan"`
	PlanningTime float32 `json:"Planning Time"`
	// Triggers      []string `json:"Triggers"`
	ExecutionTime float32 `json:"Execution Time"`
}

type Plan struct {
	NodeType                  string   `json:"Node Type"`
	StartupCost               float32  `json:"Startup Cost"`
	TotalCost                 float32  `json:"Total Cost"`
	PlanRows                  int      `json:"Plan Rows"`
	PlanWidth                 int      `json:"Plan Width"`
	ActualStartupTime         float32  `json:"Actual Startup Time"`
	ActualTotalTime           float32  `json:"Actual Total Time"`
	ActualRows                int      `json:"Actual Rows"`
	ActualLoops               int      `json:"Actual Loops"`
	Output                    []string `json:"Output"`
	SharedHitBlocks           int      `json:"Shared Hit Blocks"`
	SharedReadBlocks          int      `json:"Shared Read Blocks"`
	SharedDirtiedBlocks       int      `json:"Shared Dirtied Blocks"`
	SharedWrittenBlocks       int      `json:"Shared Written Blocks"`
	LocalHitBlocks            int      `json:"Local Hit Blocks"`
	LocalReadBlocks           int      `json:"Local Read Blocks"`
	LocalDirtiedBlocks        int      `json:"Local Dirtied Blocks"`
	LocalWrittenBlocks        int      `json:"Local Written Blocks"`
	TempReadBlocks            int      `json:"Temp Read Blocks"`
	TempWrittenBlocks         int      `json:"Temp Written Blocks"`
	IOReadTime                float32  `json:"I/O Read Time"`
	IOWriteTime               float32  `json:"I/O Write Time"`
	ParentRelationship        *string  `json:"Parent Relationship"`
	SortKey                   []string `json:"Sort Key"`
	SortMethod                *string  `json:"Sort Method"`
	SortSpaceUsed             *int     `json:"Sort Space Used"`
	SortSpaceType             *string  `json:"Sort Space Type"`
	JoinType                  *string  `json:"Join Type"`
	Strategy                  *string  `json:"Strategy"`
	RelationName              *string  `json:"Relation Name"`
	Schema                    *string  `json:"Schema"`
	Alias                     *string  `json:"Alias"`
	RecheckCond               *string  `json:"Recheck Cond"`
	RowsRemovedByIndexRecheck *int     `json:"Rows Removed by Index Recheck"`
	RowsRemovedByFilter       *int     `json:"Rows Removed by Filter"`
	ExactHeapBlocks           *int     `json:"Exact Heap Blocks"`
	LossyHeapBlocks           *int     `json:"Lossy Heap Blocks"`
	IndexName                 *string  `json:"Index Name"`
	IndexCond                 *string  `json:"Index Cond"`
	ScanDirection             *string  `json:"Scan Direction"`
	ParallelAware             *bool    `json:"Parallel Aware"`
	FunctionName              *string  `json:"Function Name"`
	TableFunctionName         *string  `json:"Table Function Name"`
	CTEName                   *string  `json:"CTE Name"`
	TuplestoreName            *string  `json:"Tuplestore Name"`
	PartialMode               *string  `json:"Partial Mode"`
	Operation                 *string  `json:"Operation"`
	SubplanName               *string  `json:"Subplan Name"`
	CustomPlanProvider        *string  `json:"Custom Plan Provider"`
	Command                   *string  `json:"Command"`
	InnerUnique               *bool    `json:"Inner Unique"`
	OrderBy                   *string  `json:"Order By"`
	Filter                    *string  `json:"Filter"`
	HeapFetches               *string  `json:"HeapFetches"`
	WorkersPlanned            *int     `json:"Workers Planned"`
	WorkersLaunched           *int     `json:"Workers Launched"`
	SingleCopy                *bool    `json:"Single Copy"`
	FunctionCall              []string `json:"Function Call"`
	TableFunctionCall         []string `json:"Table Function Call"`
	TIDCond                   *string  `json:"TID Cond"`
	JoinFilter                *string  `json:"Join Filter"`
	MergeCond                 *string  `json:"Merge Cond"`
	HashCond                  *string  `json:"Hash Cond"`
	PresortedKey              []string `json:"Presorted Key"`
	Plans                     []*Plan  `json:"Plans"`
}

func (db *DB) Analyze(query string) ([]*Explain, error) {
	tx, err := db.Beginx()
	if err != nil {
		return nil, fmt.Errorf("could not begin transaction: %v", err)
	}
	defer tx.Rollback()

	type qp struct {
		Plan string `db:"QUERY PLAN"`
	}

	var plan qp
	if err := tx.Get(&plan, fmt.Sprintf("EXPLAIN (ANALYZE, COSTS, BUFFERS, VERBOSE, FORMAT JSON) %s", query)); err != nil {
		return nil, fmt.Errorf("could not analyze query: %v", err)
	}

	if err := tx.Rollback(); err != nil {
		return nil, fmt.Errorf("could not roll back transaction: %v", err)
	}

	var exp []*Explain
	// if err := json.Unmarshal([]byte(plan.Plan), &exp); err != nil {
	if err := json.Unmarshal([]byte(testPlan), &exp); err != nil {
		return nil, fmt.Errorf("could not unmarshal plan: %v", err)
	}

	return exp, nil
}

func (p *Plan) EffectiveTotalTime() time.Duration {
	var childTime time.Duration
	for _, p := range p.Plans {
		childTime = childTime + f2d(p.ActualTotalTime)
	}
	return f2d(p.ActualTotalTime) - childTime
}

func (p *Plan) EstimateWrongness() (string, int) {
	plan, actual := float64(p.PlanRows), float64(p.ActualRows)
	bigger, smaller := math.Max(plan, actual), math.Min(plan, actual)
	ratio := int(bigger / smaller)
	if plan > actual {
		return "over", ratio
	}
	return "under", ratio
}

func (p *Plan) IsBadEstimate() bool {
	_, ratio := p.EstimateWrongness()
	return ratio >= 100
}

func f2d(f float32) time.Duration {
	// f is in milliseconds, multiply by 1k to get microseconds
	// (thousandths of milliseconds)
	return time.Duration(int(f)*1000) * time.Microsecond
}