Class: QueryGuard::Explain::PlanNode
- Inherits:
-
Object
- Object
- QueryGuard::Explain::PlanNode
- Defined in:
- lib/query_guard/explain/plan_signals.rb
Overview
Represents a single node in the query plan tree
Instance Attribute Summary collapse
-
#actual_duration_ms ⇒ Object
readonly
Returns the value of attribute actual_duration_ms.
-
#actual_rows ⇒ Object
readonly
Returns the value of attribute actual_rows.
-
#children ⇒ Object
readonly
Returns the value of attribute children.
-
#depth ⇒ Object
readonly
Returns the value of attribute depth.
-
#estimated_cost ⇒ Object
readonly
Returns the value of attribute estimated_cost.
-
#estimated_rows ⇒ Object
readonly
Returns the value of attribute estimated_rows.
-
#filter ⇒ Object
readonly
Returns the value of attribute filter.
-
#index_name ⇒ Object
readonly
Returns the value of attribute index_name.
-
#node_type ⇒ Object
readonly
Returns the value of attribute node_type.
-
#relation_name ⇒ Object
readonly
Returns the value of attribute relation_name.
-
#sort_key ⇒ Object
readonly
Returns the value of attribute sort_key.
Instance Method Summary collapse
-
#estimate_accuracy_ratio ⇒ Float?
Estimate quality: ratio of actual vs estimated rows Returns nil if no actual execution data.
-
#estimate_inaccurate?(threshold = 10) ⇒ Boolean
Check if estimates were wildly inaccurate.
-
#index_scan? ⇒ Boolean
Check if this node uses an index.
-
#initialize(data, depth = 0) ⇒ PlanNode
constructor
A new instance of PlanNode.
-
#nodes_of_type(type) ⇒ Array<PlanNode>
Find all nodes of given type.
-
#sequential_scan? ⇒ Boolean
Check if this node performs a sequential scan.
-
#sequential_scans ⇒ Array<PlanNode>
Find all sequential scans in plan tree.
-
#to_h ⇒ Hash
Convert to hash for serialization.
-
#to_s ⇒ String
Human-readable node description.
Constructor Details
#initialize(data, depth = 0) ⇒ PlanNode
Returns a new instance of PlanNode.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/query_guard/explain/plan_signals.rb', line 14 def initialize(data, depth = 0) @raw_data = data @node_type = data.dig("Node Type") || "Unknown" # Seq Scan, Index Scan, etc. @relation_name = data.dig("Relation Name") # users, orders, etc. @index_name = data.dig("Index Name") # idx_users_email, etc. @depth = depth # Estimated metrics (from planner) @estimated_rows = data.dig("Estimated Rows") || 0 @estimated_cost = data.dig("Total Cost") || 0.0 # Actual metrics (only with ANALYZE) @actual_rows = data.dig("Actual Rows") @actual_duration_ms = data.dig("Actual Total Time") # Plan details @filter = data.dig("Filter") # WHERE condition @sort_key = data.dig("Sort Key") # ORDER BY clause # Recursively process child plans child_plans = data.dig("Plans") || [] @children = child_plans.map { |child| PlanNode.new(child, depth + 1) } end |
Instance Attribute Details
#actual_duration_ms ⇒ Object (readonly)
Returns the value of attribute actual_duration_ms.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def actual_duration_ms @actual_duration_ms end |
#actual_rows ⇒ Object (readonly)
Returns the value of attribute actual_rows.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def actual_rows @actual_rows end |
#children ⇒ Object (readonly)
Returns the value of attribute children.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def children @children end |
#depth ⇒ Object (readonly)
Returns the value of attribute depth.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def depth @depth end |
#estimated_cost ⇒ Object (readonly)
Returns the value of attribute estimated_cost.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def estimated_cost @estimated_cost end |
#estimated_rows ⇒ Object (readonly)
Returns the value of attribute estimated_rows.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def estimated_rows @estimated_rows end |
#filter ⇒ Object (readonly)
Returns the value of attribute filter.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def filter @filter end |
#index_name ⇒ Object (readonly)
Returns the value of attribute index_name.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def index_name @index_name end |
#node_type ⇒ Object (readonly)
Returns the value of attribute node_type.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def node_type @node_type end |
#relation_name ⇒ Object (readonly)
Returns the value of attribute relation_name.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def relation_name @relation_name end |
#sort_key ⇒ Object (readonly)
Returns the value of attribute sort_key.
10 11 12 |
# File 'lib/query_guard/explain/plan_signals.rb', line 10 def sort_key @sort_key end |
Instance Method Details
#estimate_accuracy_ratio ⇒ Float?
Estimate quality: ratio of actual vs estimated rows Returns nil if no actual execution data
75 76 77 78 79 |
# File 'lib/query_guard/explain/plan_signals.rb', line 75 def estimate_accuracy_ratio return nil if actual_rows.nil? || estimated_rows.zero? (actual_rows.to_f / estimated_rows).round(2) end |
#estimate_inaccurate?(threshold = 10) ⇒ Boolean
Check if estimates were wildly inaccurate
85 86 87 88 89 90 |
# File 'lib/query_guard/explain/plan_signals.rb', line 85 def estimate_inaccurate?(threshold = 10) ratio = estimate_accuracy_ratio return false if ratio.nil? ratio > threshold || ratio < (1.0 / threshold) end |
#index_scan? ⇒ Boolean
Check if this node uses an index
48 49 50 |
# File 'lib/query_guard/explain/plan_signals.rb', line 48 def index_scan? node_type.include?("Index") end |
#nodes_of_type(type) ⇒ Array<PlanNode>
Find all nodes of given type
65 66 67 68 69 |
# File 'lib/query_guard/explain/plan_signals.rb', line 65 def nodes_of_type(type) nodes = node_type == type ? [self] : [] children.each { |child| nodes.concat(child.nodes_of_type(type)) } nodes end |
#sequential_scan? ⇒ Boolean
Check if this node performs a sequential scan
41 42 43 |
# File 'lib/query_guard/explain/plan_signals.rb', line 41 def sequential_scan? node_type == "Seq Scan" end |
#sequential_scans ⇒ Array<PlanNode>
Find all sequential scans in plan tree
55 56 57 58 59 |
# File 'lib/query_guard/explain/plan_signals.rb', line 55 def sequential_scans scans = sequential_scan? ? [self] : [] children.each { |child| scans.concat(child.sequential_scans) } scans end |
#to_h ⇒ Hash
Convert to hash for serialization
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/query_guard/explain/plan_signals.rb', line 105 def to_h { node_type: node_type, relation_name: relation_name, index_name: index_name, depth: depth, estimated_rows: estimated_rows, estimated_cost: estimated_cost, actual_rows: actual_rows, actual_duration_ms: actual_duration_ms, is_sequential_scan: sequential_scan?, is_index_scan: index_scan?, estimate_friendly: estimate_accuracy_ratio, children: children.map(&:to_h) } end |
#to_s ⇒ String
Human-readable node description
95 96 97 98 99 100 |
# File 'lib/query_guard/explain/plan_signals.rb', line 95 def to_s parts = [node_type] parts << "on #{relation_name}" if relation_name parts << "using #{index_name}" if index_name parts.join(" ") end |