summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2020-06-02 03:30:49 -0400
committerBen Burwell <ben@benburwell.com>2020-06-02 03:30:49 -0400
commitb005938c00c62465dac06693703ab3f592d15369 (patch)
treeabdb9a61cc2513ad3e87eb5408bebdeb467161a9
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--Makefile13
-rw-r--r--go.mod10
-rw-r--r--go.sum65
-rw-r--r--main.go574
-rw-r--r--plan.go338
6 files changed, 1001 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a00410a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+pgqt
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e12d79d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+SOURCES=$(wildcard *.go) $(wildcard */*.go)
+BINARY=pgqt
+
+$(BINARY): $(SOURCES)
+ go build -o $@ .
+
+.PHONY: run
+run: $(BINARY)
+ ./$(BINARY) 2>>/tmp/pgqt.log
+
+.PHONY: clean
+clean:
+ rm -f $(BINARY)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c3a7b22
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module bnbl.io/pgqt
+
+go 1.14
+
+require (
+ github.com/gdamore/tcell v1.3.0
+ github.com/jmoiron/sqlx v1.2.0
+ github.com/lib/pq v1.6.0
+ github.com/rivo/tview v0.0.0-20200528200248-fe953220389f
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..2beec68
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,65 @@
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
+github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
+github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
+github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
+github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.2.0 h1:lzPl/30ZLkTveYsYZPKMcgXc8MbnE6RsTd4F9KgiLtk=
+github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM=
+github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
+github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc=
+github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g=
+github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
+github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
+github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
+github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/tview v0.0.0-20200528200248-fe953220389f h1:tRx/LLIP2PSA7johw9xhf+6NUCLC4BbMhpGdm110MGI=
+github.com/rivo/tview v0.0.0-20200528200248-fe953220389f/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
+github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
+golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
+gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
+gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
+gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
+gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..3509cd2
--- /dev/null
+++ b/main.go
@@ -0,0 +1,574 @@
+package main
+
+import (
+ // "database/sql"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gdamore/tcell"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+ "github.com/rivo/tview"
+)
+
+var app = tview.NewApplication()
+
+func main() {
+ db, err := sqlx.Open("postgres", "postgres://postgres:password@localhost:5432/postgres?sslmode=disable")
+ if err != nil {
+ log.Printf("could not connect to db: %v", err)
+ os.Exit(1)
+ }
+
+ schemata := newSchemaSelector(db)
+ queries := newQuerySelector(db)
+ pv := newPlanView()
+ flex := tview.NewFlex().
+ AddItem(schemata, 30, 0, true).
+ AddItem(tview.NewFlex().
+ SetDirection(tview.FlexRow).
+ AddItem(queries, 0, 25, false).
+ AddItem(pv, 0, 75, false), 0, 100, false)
+
+ schemata.SetSelectedFunc(func(_ int, t, _ string, _ rune) {
+ go func() {
+ app.QueueUpdateDraw(func() {
+ if queries.SelectSchema(t) {
+ app.SetFocus(queries)
+ }
+ })
+ }()
+ })
+
+ app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ switch event.Key() {
+ case tcell.KeyF1:
+ app.SetFocus(schemata)
+ case tcell.KeyF2:
+ app.SetFocus(queries)
+ case tcell.KeyF3:
+ app.SetFocus(pv)
+ case tcell.KeyEnter:
+ if app.GetFocus() == queries {
+ p, err := queries.Analyze()
+ if err != nil {
+ pv.SetError(err)
+ } else {
+ pv.SetPlan(p)
+ app.SetFocus(pv)
+ }
+ }
+ case tcell.KeyTAB:
+ if app.GetFocus() == pv.tree {
+ app.SetFocus(pv.detail)
+ } else if app.GetFocus() == pv.detail {
+ app.SetFocus(pv.tree)
+ }
+ }
+ return event
+ })
+
+ if err := app.SetRoot(flex, true).Run(); err != nil {
+ log.Printf("could not create application: %v", err)
+ os.Exit(1)
+ }
+}
+
+func newSchemaSelector(db *sqlx.DB) *tview.List {
+ v := tview.NewList()
+ v.SetTitle("Schemata")
+ v.SetBorder(true)
+ v.ShowSecondaryText(false)
+ v.SetHighlightFullLine(true)
+
+ // v.SetMainTextColor(tcell.ColorDefault)
+ // v.SetBackgroundColor(tcell.ColorDefault)
+ // v.SetSelectedTextColor(tcell.ColorRed)
+ // v.SetSelectedBackgroundColor(tcell.ColorDefault)
+
+ type schema struct {
+ SchemaName string `db:"schema_name"`
+ }
+
+ var schemata []*schema
+ if err := db.Select(&schemata, "SELECT schema_name FROM information_schema.schemata"); err != nil {
+ v.AddItem(fmt.Sprintf("could not find schemata: %v", err), "", 'a', nil)
+ return v
+ }
+
+ for _, s := range schemata {
+ v.AddItem(s.SchemaName, "", 0, nil)
+ }
+
+ return v
+}
+
+type pg_stat_statement 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"`
+}
+
+type querySelector struct {
+ *tview.Table
+ db *sqlx.DB
+ stmts []*pg_stat_statement
+}
+
+func newQuerySelector(db *sqlx.DB) *querySelector {
+ v := tview.NewTable()
+ v.SetTitle("Queries")
+ v.SetBorder(true)
+ v.SetSelectable(true, false)
+ // v.SetBackgroundColor(tcell.ColorDefault)
+ // v.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrReverse)
+
+ // v.SetMainTextColor(tcell.ColorDefault)
+ // v.SetBackgroundColor(tcell.ColorDefault)
+ // v.SetSelectedTextColor(tcell.ColorRed)
+ // v.SetSelectedBackgroundColor(tcell.ColorDefault)
+
+ qs := &querySelector{
+ Table: v,
+ db: db,
+ }
+
+ return qs
+}
+
+func (q *querySelector) SelectSchema(schema string) bool {
+ q.Clear()
+
+ q.stmts = make([]*pg_stat_statement, 0)
+
+ if err := q.db.Select(&q.stmts, fmt.Sprintf("SELECT * FROM %s.pg_stat_statements ORDER BY total_time DESC", schema)); err != nil {
+ log.Printf("could not select schema: %v", err)
+ cell := tview.NewTableCell(fmt.Sprintf("could not find queries: %v", err))
+ // cell.SetTextColor(tcell.ColorYellow)
+ cell.SetAlign(tview.AlignCenter)
+ cell.SetSelectable(false)
+ q.SetCell(0, 0, cell)
+ return false
+ }
+
+ q.SetFixed(1, 0)
+
+ newHeaderCell := func(t string) *tview.TableCell {
+ cell := tview.NewTableCell(t)
+ cell.SetSelectable(false)
+ // cell.SetTextColor(tcell.ColorGreen)
+ return cell
+ }
+
+ q.SetCell(0, 0, newHeaderCell("User ID"))
+ q.SetCell(0, 1, newHeaderCell("Query"))
+ q.SetCell(0, 2, newHeaderCell("Calls"))
+ q.SetCell(0, 3, newHeaderCell("Total Time"))
+
+ newTableCell := func(t string) *tview.TableCell {
+ cell := tview.NewTableCell(t)
+ // cell.SetStyle(tcell.StyleDefault)
+ return cell
+ }
+
+ for idx, stmt := range q.stmts {
+ q.SetCell(idx+1, 0, newTableCell(strconv.Itoa(stmt.UserID)))
+ q.SetCell(idx+1, 1, newTableCell(stmt.Query))
+ q.SetCell(idx+1, 2, newTableCell(strconv.Itoa(stmt.Calls)))
+ q.SetCell(idx+1, 3, newTableCell(strconv.Itoa(int(stmt.TotalTime))))
+ }
+
+ return true
+}
+
+func (q *querySelector) Analyze() ([]*analysis, error) {
+ row, _ := q.GetSelection()
+ if len(q.stmts) < row {
+ return nil, fmt.Errorf("no queries to analyze")
+ }
+ stmt := q.stmts[row-1]
+ tx, err := q.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", stmt.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)
+ }
+
+ a := make([]*analysis, 0)
+ // if err := json.Unmarshal([]byte(plan.Plan), &a); err != nil {
+ if err := json.Unmarshal([]byte(testPlan), &a); err != nil {
+ return nil, fmt.Errorf("could not unmarshal plan: %v", err)
+ }
+
+ return a, nil
+}
+
+type analysis 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"`
+ 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"`
+ Plans []*plan `json:"Plans"`
+}
+
+type planView struct {
+ *tview.Flex
+
+ tree *tview.TreeView
+ detail *tview.Table
+}
+
+func newPlanView() *planView {
+ f := tview.NewFlex()
+ f.SetBorder(true).SetTitle("Plan")
+ f.SetDirection(tview.FlexRow)
+
+ t := tview.NewTreeView()
+ d := tview.NewTable()
+
+ f.AddItem(t, 0, 50, true)
+ f.AddItem(d, 0, 50, false)
+
+ pv := &planView{
+ Flex: f,
+ tree: t,
+ detail: d,
+ }
+
+ t.SetSelectedFunc(func(node *tview.TreeNode) {
+ go func() {
+ app.QueueUpdateDraw(func() {
+ ref := node.GetReference()
+ if ref == nil {
+ pv.ShowDetail(nil)
+ return
+ }
+ p, ok := ref.(*plan)
+ if !ok {
+ pv.ShowDetail(nil)
+ return
+ }
+ log.Printf("selected node named %s", p.NodeType)
+ pv.ShowDetail(p)
+ })
+ }()
+ })
+ return pv
+}
+
+func (p *planView) SetError(err error) {
+ p.tree.SetRoot(tview.NewTreeNode(err.Error()))
+}
+
+func (p *planView) SetPlan(as []*analysis) {
+ if len(as) == 0 {
+ p.tree.SetRoot(tview.NewTreeNode("No plan results to display"))
+ return
+ }
+
+ if len(as) > 1 {
+ log.Printf("skipping display of more than one analysis result")
+ }
+
+ a := as[0]
+ root := tview.NewTreeNode(fmt.Sprintf("Query Plan (planning=%s execution=%s)", f2d(a.PlanningTime), f2d(a.ExecutionTime)))
+ root.AddChild(buildNode(a.Plan))
+ p.tree.SetRoot(root)
+ p.tree.SetCurrentNode(root)
+}
+
+func buildNode(p *plan) *tview.TreeNode {
+ dur := f2d(p.ActualTotalTime)
+ n := tview.NewTreeNode(fmt.Sprintf("%s (%s)", p.NodeType, dur.String()))
+ n.SetSelectable(true)
+ n.SetReference(p)
+ for _, subplan := range p.Plans {
+ n.AddChild(buildNode(subplan))
+ }
+ return n
+}
+
+func (pv *planView) ShowDetail(p *plan) {
+ pv.detail.Clear()
+
+ if p == nil {
+ return
+ }
+
+ pv.detail.SetCellSimple(0, 0, "Node Type")
+ pv.detail.SetCellSimple(0, 1, p.NodeType)
+
+ pv.detail.SetCellSimple(1, 0, "Startup Cost")
+ pv.detail.SetCellSimple(1, 1, f2a(p.StartupCost))
+
+ pv.detail.SetCellSimple(2, 0, "Total Cost")
+ pv.detail.SetCellSimple(2, 1, f2a(p.TotalCost))
+
+ pv.detail.SetCellSimple(3, 0, "Plan Rows")
+ pv.detail.SetCellSimple(3, 1, strconv.Itoa(p.PlanRows))
+
+ pv.detail.SetCellSimple(4, 0, "Actual Rows")
+ pv.detail.SetCellSimple(4, 1, strconv.Itoa(p.ActualRows))
+
+ pv.detail.SetCellSimple(5, 0, "Plan Width")
+ pv.detail.SetCellSimple(5, 1, strconv.Itoa(p.PlanWidth))
+
+ pv.detail.SetCellSimple(6, 0, "Actual Startup Time")
+ pv.detail.SetCellSimple(6, 1, f2d(p.ActualStartupTime).String())
+
+ pv.detail.SetCellSimple(7, 0, "Actual Total Time")
+ pv.detail.SetCellSimple(7, 1, f2d(p.ActualTotalTime).String())
+
+ pv.detail.SetCellSimple(8, 0, "Actual Loops")
+ pv.detail.SetCellSimple(8, 1, strconv.Itoa(p.ActualLoops))
+
+ pv.detail.SetCellSimple(9, 0, "Shared Hit Blocks")
+ pv.detail.SetCellSimple(9, 1, strconv.Itoa(p.SharedHitBlocks))
+
+ pv.detail.SetCellSimple(10, 0, "Shared Read Blocks")
+ pv.detail.SetCellSimple(10, 1, strconv.Itoa(p.SharedReadBlocks))
+
+ pv.detail.SetCellSimple(11, 0, "Shared Dirtied Blocks")
+ pv.detail.SetCellSimple(11, 1, strconv.Itoa(p.SharedDirtiedBlocks))
+
+ pv.detail.SetCellSimple(12, 0, "Shared Written Blocks")
+ pv.detail.SetCellSimple(12, 1, strconv.Itoa(p.SharedWrittenBlocks))
+
+ pv.detail.SetCellSimple(13, 0, "Local Hit Blocks")
+ pv.detail.SetCellSimple(13, 1, strconv.Itoa(p.LocalHitBlocks))
+
+ pv.detail.SetCellSimple(14, 0, "Local Read Blocks")
+ pv.detail.SetCellSimple(14, 1, strconv.Itoa(p.LocalReadBlocks))
+
+ pv.detail.SetCellSimple(15, 0, "Local Dirtied Blocks")
+ pv.detail.SetCellSimple(15, 1, strconv.Itoa(p.LocalDirtiedBlocks))
+
+ pv.detail.SetCellSimple(16, 0, "Local Written Blocks")
+ pv.detail.SetCellSimple(16, 1, strconv.Itoa(p.LocalWrittenBlocks))
+
+ pv.detail.SetCellSimple(17, 0, "Temp Read Blocks")
+ pv.detail.SetCellSimple(17, 1, strconv.Itoa(p.TempReadBlocks))
+
+ pv.detail.SetCellSimple(18, 0, "Temp Written Blocks")
+ pv.detail.SetCellSimple(18, 1, strconv.Itoa(p.TempWrittenBlocks))
+
+ pv.detail.SetCellSimple(19, 0, "I/O Read Time")
+ pv.detail.SetCellSimple(19, 1, f2d(p.IOReadTime).String())
+
+ pv.detail.SetCellSimple(20, 0, "I/O Write Time")
+ pv.detail.SetCellSimple(20, 1, f2d(p.IOWriteTime).String())
+
+ idx := 21
+
+ if p.ParentRelationship != nil {
+ pv.detail.SetCellSimple(idx, 0, "Parent Relationship")
+ pv.detail.SetCellSimple(idx, 1, *p.ParentRelationship)
+ idx++
+ }
+
+ if len(p.Output) > 0 {
+ pv.detail.SetCellSimple(idx, 0, "Output")
+ pv.detail.SetCellSimple(idx, 1, strings.Join(p.Output, ", "))
+ idx++
+ }
+
+ if len(p.SortKey) > 0 {
+ pv.detail.SetCellSimple(idx, 0, "Sort Key")
+ pv.detail.SetCellSimple(idx, 1, strings.Join(p.SortKey, ", "))
+ idx++
+ }
+
+ if p.SortMethod != nil {
+ pv.detail.SetCellSimple(idx, 0, "Sort Method")
+ pv.detail.SetCellSimple(idx, 1, *p.SortMethod)
+ idx++
+ }
+
+ if p.SortSpaceUsed != nil {
+ pv.detail.SetCellSimple(idx, 0, "Sort Space Used")
+ pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.SortSpaceUsed))
+ idx++
+ }
+
+ if p.SortSpaceType != nil {
+ pv.detail.SetCellSimple(idx, 0, "Sort Space Type")
+ pv.detail.SetCellSimple(idx, 1, *p.SortSpaceType)
+ idx++
+ }
+
+ if p.JoinType != nil {
+ pv.detail.SetCellSimple(idx, 0, "Join Type")
+ pv.detail.SetCellSimple(idx, 1, *p.JoinType)
+ idx++
+ }
+
+ if p.Strategy != nil {
+ pv.detail.SetCellSimple(idx, 0, "Strategy")
+ pv.detail.SetCellSimple(idx, 1, *p.Strategy)
+ idx++
+ }
+
+ if p.RelationName != nil {
+ pv.detail.SetCellSimple(idx, 0, "Relation Name")
+ pv.detail.SetCellSimple(idx, 1, *p.RelationName)
+ idx++
+ }
+
+ if p.Schema != nil {
+ pv.detail.SetCellSimple(idx, 0, "Schema")
+ pv.detail.SetCellSimple(idx, 1, *p.Schema)
+ idx++
+ }
+
+ if p.Alias != nil {
+ pv.detail.SetCellSimple(idx, 0, "Alias")
+ pv.detail.SetCellSimple(idx, 1, *p.Alias)
+ idx++
+ }
+
+ if p.RecheckCond != nil {
+ pv.detail.SetCellSimple(idx, 0, "Recheck Cond")
+ pv.detail.SetCellSimple(idx, 1, *p.RecheckCond)
+ idx++
+ }
+
+ if p.RowsRemovedByIndexRecheck != nil {
+ pv.detail.SetCellSimple(idx, 0, "Rows Removed by Index Recheck")
+ pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.RowsRemovedByIndexRecheck))
+ idx++
+ }
+
+ if p.ExactHeapBlocks != nil {
+ pv.detail.SetCellSimple(idx, 0, "Exact Heap Blocks")
+ pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.ExactHeapBlocks))
+ idx++
+ }
+
+ if p.LossyHeapBlocks != nil {
+ pv.detail.SetCellSimple(idx, 0, "Lossy Heap Blocks")
+ pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.LossyHeapBlocks))
+ idx++
+ }
+
+ if p.IndexName != nil {
+ pv.detail.SetCellSimple(idx, 0, "Index Name")
+ pv.detail.SetCellSimple(idx, 1, *p.IndexName)
+ idx++
+ }
+
+ if p.IndexCond != nil {
+ pv.detail.SetCellSimple(idx, 0, "Index Cond")
+ pv.detail.SetCellSimple(idx, 1, *p.IndexCond)
+ idx++
+ }
+
+ if p.ScanDirection != nil {
+ pv.detail.SetCellSimple(idx, 0, "Scan Direction")
+ pv.detail.SetCellSimple(idx, 1, *p.ScanDirection)
+ idx++
+ }
+
+ if p.ParallelAware != nil {
+ pv.detail.SetCellSimple(idx, 0, "Parallel Aware")
+ pv.detail.SetCellSimple(idx, 1, fmt.Sprintf("%t", *p.ParallelAware))
+ idx++
+ }
+
+ if p.FunctionName != nil {
+ pv.detail.SetCellSimple(idx, 0, "Function Name")
+ pv.detail.SetCellSimple(idx, 1, *p.FunctionName)
+ idx++
+ }
+}
+
+func f2a(f float32) string {
+ return fmt.Sprintf("%f", f)
+}
+
+func f2d(f float32) time.Duration {
+ return time.Duration(int(f)) * time.Millisecond
+ // ms := time.Duration(int(f)) * time.Millisecond
+ // ns := time.Duration(int((f-float32(ms))*1000)) * time.Nanosecond
+ // return time.Duration(ms + ns)
+}
diff --git a/plan.go b/plan.go
new file mode 100644
index 0000000..89606d6
--- /dev/null
+++ b/plan.go
@@ -0,0 +1,338 @@
+package main
+
+const testPlan = `
+[
+ {
+ "Plan": {
+ "Node Type": "Limit",
+ "Startup Cost": 60970.08,
+ "Total Cost": 60970.11,
+ "Plan Rows": 10,
+ "Plan Width": 468,
+ "Actual Startup Time": 1941.751,
+ "Actual Total Time": 1941.780,
+ "Actual Rows": 10,
+ "Actual Loops": 1,
+ "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"],
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Sort",
+ "Parent Relationship": "Outer",
+ "Startup Cost": 60970.08,
+ "Total Cost": 60977.40,
+ "Plan Rows": 2927,
+ "Plan Width": 468,
+ "Actual Startup Time": 1941.747,
+ "Actual Total Time": 1941.757,
+ "Actual Rows": 10,
+ "Actual Loops": 1,
+ "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"],
+ "Sort Key": ["(sum(s.total_alerts))"],
+ "Sort Method": "top-N heapsort",
+ "Sort Space Used": 27,
+ "Sort Space Type": "Memory",
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Left",
+ "Startup Cost": 37552.36,
+ "Total Cost": 60906.83,
+ "Plan Rows": 2927,
+ "Plan Width": 468,
+ "Actual Startup Time": 522.250,
+ "Actual Total Time": 1865.979,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["a.id", "COALESCE(a.display_name, a.alert_name)", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "percenttotal((sum(s.total_followed)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_overridden)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_ignored)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_unknown)), (sum(s.total_alerts)))", "((sum(s.total_alerts)) * a.estimated_cost)"],
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Left",
+ "Startup Cost": 37552.22,
+ "Total Cost": 56750.41,
+ "Plan Rows": 2927,
+ "Plan Width": 461,
+ "Actual Startup Time": 522.141,
+ "Actual Total Time": 1161.187,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.subtype_id", "altt.code", "altt.name"],
+ "Shared Hit Blocks": 197115,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Inner",
+ "Startup Cost": 37552.07,
+ "Total Cost": 56230.56,
+ "Plan Rows": 2927,
+ "Plan Width": 405,
+ "Actual Startup Time": 522.131,
+ "Actual Total Time": 918.895,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.type_id", "a.subtype_id"],
+ "Shared Hit Blocks": 132411,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Aggregate",
+ "Strategy": "Hashed",
+ "Parent Relationship": "Outer",
+ "Startup Cost": 37551.64,
+ "Total Cost": 37624.82,
+ "Plan Rows": 2927,
+ "Plan Width": 56,
+ "Actual Startup Time": 522.107,
+ "Actual Total Time": 587.536,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["s.alert_id", "sum(s.total_alerts)", "sum(s.total_followed)", "sum(s.total_overridden)", "sum(s.total_ignored)", "sum(s.total_not_seen)", "sum(s.total_unknown)"],
+ "Group Key": ["s.alert_id"],
+ "Shared Hit Blocks": 2784,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Bitmap Heap Scan",
+ "Parent Relationship": "Outer",
+ "Relation Name": "alert_daily_summaries",
+ "Schema": "analytics",
+ "Alias": "s",
+ "Startup Cost": 3265.35,
+ "Total Cost": 35524.50,
+ "Plan Rows": 115837,
+ "Plan Width": 56,
+ "Actual Startup Time": 10.745,
+ "Actual Total Time": 165.318,
+ "Actual Rows": 140451,
+ "Actual Loops": 1,
+ "Output": ["s.id", "s.org_id", "s.alert_id", "s.alert_day", "s.total_alerts", "s.total_followed", "s.total_overridden", "s.total_ignored", "s.total_not_seen", "s.total_unknown", "s.override_comments"],
+ "Recheck Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))",
+ "Rows Removed by Index Recheck": 0,
+ "Exact Heap Blocks": 2243,
+ "Lossy Heap Blocks": 0,
+ "Shared Hit Blocks": 2784,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Bitmap Index Scan",
+ "Parent Relationship": "Outer",
+ "Index Name": "alert_daily_summaries_org_id_alert_day_idx",
+ "Startup Cost": 0.00,
+ "Total Cost": 3236.39,
+ "Plan Rows": 115837,
+ "Plan Width": 0,
+ "Actual Startup Time": 10.445,
+ "Actual Total Time": 10.445,
+ "Actual Rows": 140451,
+ "Actual Loops": 1,
+ "Index Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))",
+ "Shared Hit Blocks": 541,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alerts_pkey1",
+ "Relation Name": "alerts",
+ "Schema": "analytics",
+ "Alias": "a",
+ "Startup Cost": 0.42,
+ "Total Cost": 6.34,
+ "Plan Rows": 1,
+ "Plan Width": 213,
+ "Actual Startup Time": 0.004,
+ "Actual Total Time": 0.005,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["a.id", "a.org_id", "a.external_alert_id", "a.alert_name", "a.display_name", "a.estimated_cost", "a.description", "a.source", "a.venue_code", "a.action_expected", "a.cancel_expected", "a.responsible_provider_type", "a.status", "a.version", "a.created_by", "a.created_date", "a.modified_by", "a.modified_date", "a.tracking_id", "a.record_date", "a.released_date", "a.released", "a.comments", "a.default_lockout_hours", "a.deploy_mode", "a.stop_type", "a.importance_level", "a.type_id", "a.subtype_id"],
+ "Index Cond": "(a.id = s.alert_id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 129627,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alert_types_id_org_id_key",
+ "Relation Name": "alert_types",
+ "Schema": "analytics",
+ "Alias": "altt",
+ "Startup Cost": 0.15,
+ "Total Cost": 0.17,
+ "Plan Rows": 1,
+ "Plan Width": 72,
+ "Actual Startup Time": 0.002,
+ "Actual Total Time": 0.003,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["altt.id", "altt.version", "altt.code", "altt.name", "altt.org_id"],
+ "Index Cond": "(a.type_id = altt.id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 64704,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alert_subtypes_id_org_id_key",
+ "Relation Name": "alert_subtypes",
+ "Schema": "analytics",
+ "Alias": "altst",
+ "Startup Cost": 0.14,
+ "Total Cost": 0.16,
+ "Plan Rows": 1,
+ "Plan Width": 23,
+ "Actual Startup Time": 0.002,
+ "Actual Total Time": 0.003,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["altst.id", "altst.version", "altst.code", "altst.name", "altst.org_id"],
+ "Index Cond": "(a.subtype_id = altst.id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 63472,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "Planning Time": 0.507,
+ "Triggers": [
+ ],
+ "Execution Time": 1953.684
+ }
+]
+`