Skip to content

Commit 8be7949

Browse files
committed
feat: add PlanMetrics to extract EXPLAIN ANALYZE performance data
1 parent 590953f commit 8be7949

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

regresql/analyze_types.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,47 @@ type (
126126
WorstOver *RowEstimate `json:"worst_overestimate,omitempty"`
127127
WorstUnder *RowEstimate `json:"worst_underestimate,omitempty"`
128128
}
129+
130+
// PlanMetrics contains performance metrics from EXPLAIN ANALYZE.
131+
// Timing fields require ANALYZE, buffer fields require BUFFERS option.
132+
PlanMetrics struct {
133+
TotalCost float64 `json:"total_cost"`
134+
ExecutionTimeMs float64 `json:"execution_time_ms"`
135+
PlanningTimeMs float64 `json:"planning_time_ms"`
136+
ActualRows int64 `json:"actual_rows"`
137+
138+
// Buffer stats (root node only)
139+
SharedHitBlocks int64 `json:"shared_hit_blocks"`
140+
SharedReadBlocks int64 `json:"shared_read_blocks"`
141+
LocalHitBlocks int64 `json:"local_hit_blocks"`
142+
LocalReadBlocks int64 `json:"local_read_blocks"`
143+
TempReadBlocks int64 `json:"temp_read_blocks"`
144+
TempWrittenBlocks int64 `json:"temp_written_blocks"`
145+
TotalBuffers int64 `json:"total_buffers"`
146+
IOReadTimeMs float64 `json:"io_read_time_ms"`
147+
IOWriteTimeMs float64 `json:"io_write_time_ms"`
148+
}
129149
)
130150

151+
// ExtractMetrics extracts performance metrics from the root plan node.
152+
func (e *ExplainOutput) ExtractMetrics() PlanMetrics {
153+
return PlanMetrics{
154+
TotalCost: e.Plan.TotalCost,
155+
ExecutionTimeMs: e.ExecutionTime,
156+
PlanningTimeMs: e.PlanningTime,
157+
ActualRows: e.Plan.ActualRows,
158+
SharedHitBlocks: e.Plan.SharedHitBlocks,
159+
SharedReadBlocks: e.Plan.SharedReadBlocks,
160+
LocalHitBlocks: e.Plan.LocalHitBlocks,
161+
LocalReadBlocks: e.Plan.LocalReadBlocks,
162+
TempReadBlocks: e.Plan.TempReadBlocks,
163+
TempWrittenBlocks: e.Plan.TempWrittenBlocks,
164+
TotalBuffers: e.Plan.SharedHitBlocks + e.Plan.SharedReadBlocks,
165+
IOReadTimeMs: e.Plan.IOReadTime,
166+
IOWriteTimeMs: e.Plan.IOWriteTime,
167+
}
168+
}
169+
131170
// GetBufferStats returns buffer statistics for a plan node
132171
func (n *PlanNode) GetBufferStats() BufferStats {
133172
return BufferStats{

regresql/library.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type (
2929
PlanChanged bool
3030
PlanRegressions []PlanRegression
3131
PlanWarnings []PlanWarning
32+
33+
// Metrics from EXPLAIN ANALYZE (nil if ANALYZE not used or on error)
34+
Metrics *PlanMetrics
3235
}
3336
)
3437

@@ -122,6 +125,12 @@ func (p *Plan) CompareCostsData(db *sql.DB, baselines []Baseline, thresholdPerce
122125
PercentIncrease: percentIncrease,
123126
}
124127

128+
// Only populate metrics when ANALYZE was used
129+
if explainPlan.ExecutionTime > 0 {
130+
metrics := explainPlan.ExtractMetrics()
131+
result.Metrics = &metrics
132+
}
133+
125134
if baselines[i].PlanSignature != nil {
126135
currentSig := ExtractPlanSignatureFromNode(&explainPlan.Plan)
127136
result.PlanChanged = HasPlanChanged(baselines[i].PlanSignature, currentSig)

0 commit comments

Comments
 (0)