|
| 1 | +package regresql |
| 2 | + |
| 3 | +import "math" |
| 4 | + |
| 5 | +type ( |
| 6 | + // ExplainOutput is the top-level structure from EXPLAIN (FORMAT JSON) |
| 7 | + ExplainOutput struct { |
| 8 | + Plan PlanNode `json:"Plan"` |
| 9 | + Planning BufferStats `json:"Planning,omitempty"` |
| 10 | + PlanningTime float64 `json:"Planning Time,omitempty"` |
| 11 | + ExecutionTime float64 `json:"Execution Time,omitempty"` |
| 12 | + } |
| 13 | + |
| 14 | + // PlanNode represents a node in the query execution plan |
| 15 | + PlanNode struct { |
| 16 | + // Core fields (always present) |
| 17 | + NodeType string `json:"Node Type"` |
| 18 | + StartupCost float64 `json:"Startup Cost"` |
| 19 | + TotalCost float64 `json:"Total Cost"` |
| 20 | + PlanRows float64 `json:"Plan Rows"` |
| 21 | + PlanWidth int `json:"Plan Width"` |
| 22 | + ParallelAware bool `json:"Parallel Aware,omitempty"` |
| 23 | + AsyncCapable bool `json:"Async Capable,omitempty"` |
| 24 | + |
| 25 | + // Relationship to parent |
| 26 | + ParentRelationship string `json:"Parent Relationship,omitempty"` |
| 27 | + |
| 28 | + // Scan node fields |
| 29 | + RelationName string `json:"Relation Name,omitempty"` |
| 30 | + Alias string `json:"Alias,omitempty"` |
| 31 | + ScanDirection string `json:"Scan Direction,omitempty"` |
| 32 | + IndexName string `json:"Index Name,omitempty"` |
| 33 | + |
| 34 | + // Conditions |
| 35 | + IndexCond string `json:"Index Cond,omitempty"` |
| 36 | + Filter string `json:"Filter,omitempty"` |
| 37 | + RecheckCond string `json:"Recheck Cond,omitempty"` |
| 38 | + MergeCond string `json:"Merge Cond,omitempty"` |
| 39 | + HashCond string `json:"Hash Cond,omitempty"` |
| 40 | + JoinFilter string `json:"Join Filter,omitempty"` |
| 41 | + |
| 42 | + // Join fields |
| 43 | + JoinType string `json:"Join Type,omitempty"` |
| 44 | + InnerUnique bool `json:"Inner Unique,omitempty"` |
| 45 | + |
| 46 | + // Sort fields |
| 47 | + SortKey []string `json:"Sort Key,omitempty"` |
| 48 | + SortMethod string `json:"Sort Method,omitempty"` |
| 49 | + SortSpaceUsed int64 `json:"Sort Space Used,omitempty"` |
| 50 | + SortSpaceType string `json:"Sort Space Type,omitempty"` |
| 51 | + |
| 52 | + // Parallel/Gather fields |
| 53 | + WorkersPlanned int `json:"Workers Planned,omitempty"` |
| 54 | + WorkersLaunched int `json:"Workers Launched,omitempty"` |
| 55 | + SingleCopy bool `json:"Single Copy,omitempty"` |
| 56 | + |
| 57 | + // Bitmap scan fields |
| 58 | + ExactHeapBlocks int64 `json:"Exact Heap Blocks,omitempty"` |
| 59 | + LossyHeapBlocks int64 `json:"Lossy Heap Blocks,omitempty"` |
| 60 | + HeapFetches int64 `json:"Heap Fetches,omitempty"` |
| 61 | + |
| 62 | + // Append fields |
| 63 | + SubplansRemoved int `json:"Subplans Removed,omitempty"` |
| 64 | + |
| 65 | + // Row removal stats |
| 66 | + RowsRemovedByFilter int64 `json:"Rows Removed by Filter,omitempty"` |
| 67 | + RowsRemovedByIndexRecheck int64 `json:"Rows Removed by Index Recheck,omitempty"` |
| 68 | + |
| 69 | + // ANALYZE fields (only present with ANALYZE true) |
| 70 | + ActualStartupTime float64 `json:"Actual Startup Time,omitempty"` |
| 71 | + ActualTotalTime float64 `json:"Actual Total Time,omitempty"` |
| 72 | + ActualRows int64 `json:"Actual Rows,omitempty"` |
| 73 | + ActualLoops int64 `json:"Actual Loops,omitempty"` |
| 74 | + |
| 75 | + // BUFFERS fields (only present with BUFFERS true) |
| 76 | + SharedHitBlocks int64 `json:"Shared Hit Blocks,omitempty"` |
| 77 | + SharedReadBlocks int64 `json:"Shared Read Blocks,omitempty"` |
| 78 | + SharedDirtiedBlocks int64 `json:"Shared Dirtied Blocks,omitempty"` |
| 79 | + SharedWrittenBlocks int64 `json:"Shared Written Blocks,omitempty"` |
| 80 | + LocalHitBlocks int64 `json:"Local Hit Blocks,omitempty"` |
| 81 | + LocalReadBlocks int64 `json:"Local Read Blocks,omitempty"` |
| 82 | + LocalDirtiedBlocks int64 `json:"Local Dirtied Blocks,omitempty"` |
| 83 | + LocalWrittenBlocks int64 `json:"Local Written Blocks,omitempty"` |
| 84 | + TempReadBlocks int64 `json:"Temp Read Blocks,omitempty"` |
| 85 | + TempWrittenBlocks int64 `json:"Temp Written Blocks,omitempty"` |
| 86 | + IOReadTime float64 `json:"I/O Read Time,omitempty"` |
| 87 | + IOWriteTime float64 `json:"I/O Write Time,omitempty"` |
| 88 | + TempIOReadTime float64 `json:"Temp I/O Read Time,omitempty"` |
| 89 | + TempIOWriteTime float64 `json:"Temp I/O Write Time,omitempty"` |
| 90 | + |
| 91 | + // Child plans |
| 92 | + Plans []PlanNode `json:"Plans,omitempty"` |
| 93 | + } |
| 94 | + |
| 95 | + // BufferStats holds buffer I/O statistics from EXPLAIN BUFFERS |
| 96 | + BufferStats struct { |
| 97 | + SharedHitBlocks int64 `json:"Shared Hit Blocks,omitempty"` |
| 98 | + SharedReadBlocks int64 `json:"Shared Read Blocks,omitempty"` |
| 99 | + SharedDirtiedBlocks int64 `json:"Shared Dirtied Blocks,omitempty"` |
| 100 | + SharedWrittenBlocks int64 `json:"Shared Written Blocks,omitempty"` |
| 101 | + LocalHitBlocks int64 `json:"Local Hit Blocks,omitempty"` |
| 102 | + LocalReadBlocks int64 `json:"Local Read Blocks,omitempty"` |
| 103 | + LocalDirtiedBlocks int64 `json:"Local Dirtied Blocks,omitempty"` |
| 104 | + LocalWrittenBlocks int64 `json:"Local Written Blocks,omitempty"` |
| 105 | + TempReadBlocks int64 `json:"Temp Read Blocks,omitempty"` |
| 106 | + TempWrittenBlocks int64 `json:"Temp Written Blocks,omitempty"` |
| 107 | + IOReadTime float64 `json:"I/O Read Time,omitempty"` |
| 108 | + IOWriteTime float64 `json:"I/O Write Time,omitempty"` |
| 109 | + TempIOReadTime float64 `json:"Temp I/O Read Time,omitempty"` |
| 110 | + TempIOWriteTime float64 `json:"Temp I/O Write Time,omitempty"` |
| 111 | + } |
| 112 | + |
| 113 | + // RowEstimate compares planner estimate vs actual rows |
| 114 | + RowEstimate struct { |
| 115 | + NodeType string `json:"node_type"` |
| 116 | + RelationName string `json:"relation_name,omitempty"` |
| 117 | + PlanRows float64 `json:"plan_rows"` |
| 118 | + ActualRows int64 `json:"actual_rows"` |
| 119 | + ActualLoops int64 `json:"actual_loops"` |
| 120 | + Ratio float64 `json:"ratio"` |
| 121 | + } |
| 122 | + |
| 123 | + // RowEstimateAnalysis holds all row estimate comparisons |
| 124 | + RowEstimateAnalysis struct { |
| 125 | + Estimates []RowEstimate `json:"estimates"` |
| 126 | + WorstOver *RowEstimate `json:"worst_overestimate,omitempty"` |
| 127 | + WorstUnder *RowEstimate `json:"worst_underestimate,omitempty"` |
| 128 | + } |
| 129 | +) |
| 130 | + |
| 131 | +// GetBufferStats returns buffer statistics for a plan node |
| 132 | +func (n *PlanNode) GetBufferStats() BufferStats { |
| 133 | + return BufferStats{ |
| 134 | + SharedHitBlocks: n.SharedHitBlocks, |
| 135 | + SharedReadBlocks: n.SharedReadBlocks, |
| 136 | + SharedDirtiedBlocks: n.SharedDirtiedBlocks, |
| 137 | + SharedWrittenBlocks: n.SharedWrittenBlocks, |
| 138 | + LocalHitBlocks: n.LocalHitBlocks, |
| 139 | + LocalReadBlocks: n.LocalReadBlocks, |
| 140 | + LocalDirtiedBlocks: n.LocalDirtiedBlocks, |
| 141 | + LocalWrittenBlocks: n.LocalWrittenBlocks, |
| 142 | + TempReadBlocks: n.TempReadBlocks, |
| 143 | + TempWrittenBlocks: n.TempWrittenBlocks, |
| 144 | + IOReadTime: n.IOReadTime, |
| 145 | + IOWriteTime: n.IOWriteTime, |
| 146 | + TempIOReadTime: n.TempIOReadTime, |
| 147 | + TempIOWriteTime: n.TempIOWriteTime, |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// CompareRowEstimates walks the plan tree and compares Plan Rows vs Actual Rows |
| 152 | +func (e *ExplainOutput) CompareRowEstimates() *RowEstimateAnalysis { |
| 153 | + analysis := &RowEstimateAnalysis{} |
| 154 | + collectNodeRowEstimates(&e.Plan, analysis) |
| 155 | + |
| 156 | + for i := range analysis.Estimates { |
| 157 | + est := &analysis.Estimates[i] |
| 158 | + if est.Ratio > 1 { |
| 159 | + if analysis.WorstUnder == nil || est.Ratio > analysis.WorstUnder.Ratio { |
| 160 | + analysis.WorstUnder = est |
| 161 | + } |
| 162 | + } else if est.Ratio < 1 && est.Ratio > 0 { |
| 163 | + if analysis.WorstOver == nil || est.Ratio < analysis.WorstOver.Ratio { |
| 164 | + analysis.WorstOver = est |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + return analysis |
| 170 | +} |
| 171 | + |
| 172 | +func collectNodeRowEstimates(node *PlanNode, analysis *RowEstimateAnalysis) { |
| 173 | + if node.ActualLoops > 0 { |
| 174 | + ratio := 0.0 |
| 175 | + if node.PlanRows > 0 { |
| 176 | + ratio = float64(node.ActualRows) / node.PlanRows |
| 177 | + } else if node.ActualRows > 0 { |
| 178 | + ratio = math.Inf(1) |
| 179 | + } |
| 180 | + |
| 181 | + est := RowEstimate{ |
| 182 | + NodeType: node.NodeType, |
| 183 | + RelationName: node.RelationName, |
| 184 | + PlanRows: node.PlanRows, |
| 185 | + ActualRows: node.ActualRows, |
| 186 | + ActualLoops: node.ActualLoops, |
| 187 | + Ratio: ratio, |
| 188 | + } |
| 189 | + analysis.Estimates = append(analysis.Estimates, est) |
| 190 | + } |
| 191 | + |
| 192 | + for i := range node.Plans { |
| 193 | + collectNodeRowEstimates(&node.Plans[i], analysis) |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +func toInt64(v any) int64 { |
| 198 | + switch val := v.(type) { |
| 199 | + case float64: |
| 200 | + return int64(val) |
| 201 | + case int64: |
| 202 | + return val |
| 203 | + case int: |
| 204 | + return int64(val) |
| 205 | + default: |
| 206 | + return 0 |
| 207 | + } |
| 208 | +} |
0 commit comments