rails_db_inspector 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -1
- data/app/controllers/rails_db_inspector/console_controller.rb +48 -0
- data/app/controllers/rails_db_inspector/queries_controller.rb +1 -0
- data/app/controllers/rails_db_inspector/schema_controller.rb +26 -0
- data/app/helpers/rails_db_inspector/plan_renderer.rb +128 -2
- data/app/views/layouts/rails_db_inspector/application.html.erb +1 -0
- data/app/views/rails_db_inspector/console/index.html.erb +444 -0
- data/app/views/rails_db_inspector/queries/explain.html.erb +428 -28
- data/app/views/rails_db_inspector/queries/index.html.erb +259 -2
- data/app/views/rails_db_inspector/schema/index.html.erb +71 -3
- data/config/routes.rb +4 -0
- data/lib/rails_db_inspector/configuration.rb +2 -0
- data/lib/rails_db_inspector/explain/sqlite.rb +36 -1
- data/lib/rails_db_inspector/schema_inspector.rb +74 -0
- data/lib/rails_db_inspector/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08c6511ec0aa29b649a9939c4bf17ea042346cc99a5bdb645382764621589da6'
|
|
4
|
+
data.tar.gz: 9a171cdcd171cfd8a64488bb2b5f570175d9e26979866743c5f8571c3fd1ed5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a1df1506a50af86702c516e9994b54a1ec488c3dfa23115c27836c6699a3eae10f961239bc361da14771d26a995ef3c80864d0e11cdd7629e381a71d4d73db7
|
|
7
|
+
data.tar.gz: bc17122e5f23aadfb6edd76911f3d26b6fac1d88c13b8ac048f3134525a22ae5add60d188702055297f848cf18d7fffd1de2e8e4fd74c9f63eccea184ebbfcfb
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Supports **PostgreSQL**, **MySQL**, and **SQLite**.
|
|
|
22
22
|
- **EXPLAIN ANALYZE** — optionally run `EXPLAIN ANALYZE` to get real execution statistics, buffer usage, and timing (opt-in, SELECT only)
|
|
23
23
|
- **Plan Analysis** — rich visual rendering of PostgreSQL plans including cost breakdown, row estimate accuracy, index usage analysis, performance hotspots, buffer statistics, and actionable recommendations
|
|
24
24
|
- **Interactive Schema / ERD Visualization** — drag-and-drop entity relationship diagram with pan, zoom, search, column expansion, heat-map by row count, missing index warnings, polymorphic detection, and SVG export
|
|
25
|
+
- **SQL Console** — interactive query editor with syntax-aware snippets, query history, table browser, EXPLAIN support, and read-only safety by default
|
|
25
26
|
- **Dev Widget** — floating button injected into your app's pages in development for quick access to the dashboard
|
|
26
27
|
- **Zero Dependencies** — no JavaScript build step, no external CSS frameworks, everything is self-contained
|
|
27
28
|
|
|
@@ -84,6 +85,11 @@ RailsDbInspector.configure do |config|
|
|
|
84
85
|
# Default: false
|
|
85
86
|
config.allow_explain_analyze = true
|
|
86
87
|
|
|
88
|
+
# Allow write queries (INSERT, UPDATE, DELETE) in the SQL Console.
|
|
89
|
+
# Destructive DDL (DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE) is always blocked.
|
|
90
|
+
# Default: false
|
|
91
|
+
config.allow_console_writes = false
|
|
92
|
+
|
|
87
93
|
# Show the floating dev widget on your app's pages in development.
|
|
88
94
|
# The widget provides quick links to the query monitor and schema viewer.
|
|
89
95
|
# Default: true
|
|
@@ -98,6 +104,7 @@ end
|
|
|
98
104
|
| `enabled` | Boolean | `true` | Master switch — disables SQL subscription and widget when `false` |
|
|
99
105
|
| `max_queries` | Integer | `2000` | Max queries stored in memory (FIFO eviction) |
|
|
100
106
|
| `allow_explain_analyze` | Boolean | `false` | Permit EXPLAIN ANALYZE (executes the query — SELECT only) |
|
|
107
|
+
| `allow_console_writes` | Boolean | `false` | Allow write queries (`INSERT`, `UPDATE`, `DELETE`) in the SQL Console |
|
|
101
108
|
| `show_widget` | Boolean | `true` | Inject floating widget into HTML pages in development |
|
|
102
109
|
|
|
103
110
|
---
|
|
@@ -168,6 +175,27 @@ Navigate to the **Schema** page to see an interactive entity relationship diagra
|
|
|
168
175
|
- **Health summary** — table count, column count, index count, total rows, missing indexes, tables without timestamps, tables without primary keys, polymorphic columns
|
|
169
176
|
- **Export SVG** — download the diagram as an SVG file
|
|
170
177
|
|
|
178
|
+
### SQL Console
|
|
179
|
+
|
|
180
|
+
Navigate to the **Console** page for an interactive SQL editor:
|
|
181
|
+
|
|
182
|
+
- **Write and run** raw SQL queries against your development database
|
|
183
|
+
- **Adapter-aware snippets** — pre-built queries organized by category (Overview, Explore, Performance, Schema) that adapt to your database adapter
|
|
184
|
+
- **Table browser** — click any table in the sidebar to insert a `SELECT * FROM <table> LIMIT 20` query
|
|
185
|
+
- **Query history** — the last 50 queries are kept in-session with duration and status
|
|
186
|
+
- **EXPLAIN** — run an EXPLAIN plan on any query directly from the editor
|
|
187
|
+
- **Keyboard shortcuts** — `⌘/Ctrl + Enter` to run, `⌘/Ctrl + E` to explain
|
|
188
|
+
|
|
189
|
+
#### Safety
|
|
190
|
+
|
|
191
|
+
The console is **read-only by default** — only `SELECT`, `EXPLAIN`, `ANALYZE`, `PRAGMA`, `SHOW`, `DESCRIBE`, and `WITH` statements are permitted. Enable write access with:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
config.allow_console_writes = true
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Destructive DDL (`DROP`, `TRUNCATE`, `ALTER`, `CREATE`, `GRANT`, `REVOKE`) is **always blocked** regardless of settings.
|
|
198
|
+
|
|
171
199
|
### Dev Widget
|
|
172
200
|
|
|
173
201
|
In development, a floating blue button (🛢️) appears in the bottom-right corner of every page. Click it to reveal quick links to:
|
|
@@ -197,7 +225,8 @@ EXPLAIN uses `FORMAT JSON` for PostgreSQL, standard `EXPLAIN` for MySQL, and `EX
|
|
|
197
225
|
2. **Query Store** — an in-memory singleton (`QueryStore`) stores captured queries with thread-safe access. Oldest queries are evicted when `max_queries` is exceeded.
|
|
198
226
|
3. **Explain** — wraps the captured SQL in an `EXPLAIN` statement appropriate for the database adapter and parses the result.
|
|
199
227
|
4. **Schema Inspector** — introspects `ActiveRecord::Base.connection` for tables, columns, indexes, foreign keys, primary keys, row counts, associations, polymorphic columns, and missing indexes.
|
|
200
|
-
5. **
|
|
228
|
+
5. **SQL Console** — runs user-provided SQL through `ActiveRecord::Base.connection.exec_query` with statement-level allow/deny lists to enforce read-only safety.
|
|
229
|
+
6. **Dev Widget Middleware** — a Rack middleware that injects a small HTML snippet before `</body>` on HTML responses in development.
|
|
201
230
|
|
|
202
231
|
---
|
|
203
232
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDbInspector
|
|
4
|
+
class ConsoleController < ApplicationController
|
|
5
|
+
DANGEROUS_KEYWORDS = /\b(DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE)\b/i
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
connection = ActiveRecord::Base.connection
|
|
9
|
+
@tables = connection.tables.reject { |t| t.match?(/^(schema_migrations|ar_internal_metadata)$/) }.sort
|
|
10
|
+
@adapter = connection.adapter_name.downcase
|
|
11
|
+
@allow_writes = RailsDbInspector.configuration.allow_console_writes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
sql = params[:sql].to_s.strip
|
|
16
|
+
return render json: { error: "SQL query is required" }, status: :bad_request if sql.blank?
|
|
17
|
+
|
|
18
|
+
# Block destructive DDL regardless of config
|
|
19
|
+
if sql.match?(DANGEROUS_KEYWORDS)
|
|
20
|
+
return render json: { error: "Destructive DDL statements (DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE) are not allowed." }, status: :forbidden
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Block writes unless explicitly enabled
|
|
24
|
+
unless RailsDbInspector.configuration.allow_console_writes
|
|
25
|
+
unless sql.match?(/\A\s*(SELECT|EXPLAIN|ANALYZE|PRAGMA|SHOW|DESCRIBE|DESC|\\.d|WITH)\b/i)
|
|
26
|
+
return render json: { error: "Write queries are disabled. Enable with: RailsDbInspector.configuration.allow_console_writes = true" }, status: :forbidden
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
connection = ActiveRecord::Base.connection
|
|
31
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
result = connection.exec_query(sql)
|
|
35
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
36
|
+
|
|
37
|
+
render json: {
|
|
38
|
+
columns: result.columns,
|
|
39
|
+
rows: result.rows,
|
|
40
|
+
row_count: result.rows.length,
|
|
41
|
+
duration_ms: elapsed
|
|
42
|
+
}
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -28,6 +28,7 @@ module RailsDbInspector
|
|
|
28
28
|
|
|
29
29
|
explainer = RailsDbInspector::Explain.for_connection(ActiveRecord::Base.connection)
|
|
30
30
|
@explain = explainer.explain(@query.sql, analyze: analyze)
|
|
31
|
+
@tables = ActiveRecord::Base.connection.tables.reject { |t| t.match?(/^(schema_migrations|ar_internal_metadata)$/) }.sort
|
|
31
32
|
rescue RailsDbInspector::Explain::DangerousQuery => e
|
|
32
33
|
render plain: e.message, status: :unprocessable_entity
|
|
33
34
|
rescue RailsDbInspector::Explain::UnsupportedAdapter => e
|
|
@@ -8,6 +8,32 @@ module RailsDbInspector
|
|
|
8
8
|
inspector = RailsDbInspector::SchemaInspector.new
|
|
9
9
|
@schema = inspector.introspect
|
|
10
10
|
@relationships = inspector.relationships
|
|
11
|
+
@table_sizes = inspector.table_sizes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze_table
|
|
15
|
+
table = params[:table].to_s.gsub(/[^a-zA-Z0-9_]/, "")
|
|
16
|
+
return render json: { error: "Table name required" }, status: :bad_request if table.blank?
|
|
17
|
+
|
|
18
|
+
connection = ActiveRecord::Base.connection
|
|
19
|
+
adapter = connection.adapter_name.downcase
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
case adapter
|
|
23
|
+
when /postgres/
|
|
24
|
+
connection.execute("ANALYZE #{connection.quote_table_name(table)}")
|
|
25
|
+
when /mysql/
|
|
26
|
+
connection.execute("ANALYZE TABLE #{connection.quote_table_name(table)}")
|
|
27
|
+
when /sqlite/
|
|
28
|
+
connection.execute("ANALYZE #{connection.quote_table_name(table)}")
|
|
29
|
+
else
|
|
30
|
+
return render json: { error: "ANALYZE not supported for #{adapter}" }, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
render json: { success: true, table: table, adapter: adapter, message: "Statistics refreshed for '#{table}'" }
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
36
|
+
end
|
|
11
37
|
end
|
|
12
38
|
end
|
|
13
39
|
end
|
|
@@ -26,9 +26,12 @@ module RailsDbInspector
|
|
|
26
26
|
buffer_stats = collect_buffer_stats(root_plan["Plan"]) if @analyze
|
|
27
27
|
recommendations = generate_recommendations(root_plan, index_analysis, buffer_stats)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# Build verdict card
|
|
30
|
+
summary_html = render_verdict(execution_time, total_cost, index_analysis, recommendations)
|
|
31
|
+
|
|
32
|
+
summary_html += <<~HTML
|
|
30
33
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 mb-6">
|
|
31
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
|
34
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Performance Metrics</h3>
|
|
32
35
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
33
36
|
HTML
|
|
34
37
|
|
|
@@ -81,6 +84,22 @@ module RailsDbInspector
|
|
|
81
84
|
</div>
|
|
82
85
|
HTML
|
|
83
86
|
|
|
87
|
+
# Add cardinality accuracy card (ANALYZE only)
|
|
88
|
+
if @analyze
|
|
89
|
+
accuracy = cardinality_accuracy(root_plan["Plan"])
|
|
90
|
+
if accuracy
|
|
91
|
+
accuracy_color = accuracy[:worst_ratio] <= 2.0 ? "border-green-400" : (accuracy[:worst_ratio] <= 10.0 ? "border-yellow-400" : "border-red-400")
|
|
92
|
+
accuracy_label = accuracy[:worst_ratio] <= 2.0 ? "Good" : (accuracy[:worst_ratio] <= 10.0 ? "Fair" : "Poor")
|
|
93
|
+
summary_html += <<~HTML
|
|
94
|
+
<div class="bg-white p-4 rounded-md border-l-4 #{accuracy_color}">
|
|
95
|
+
<div class="text-xs font-medium text-gray-500 uppercase tracking-wide">Cardinality Accuracy</div>
|
|
96
|
+
<div class="text-lg font-semibold text-gray-900 font-mono">#{accuracy_label}</div>
|
|
97
|
+
<div class="text-xs text-gray-400 mt-1">Worst estimate was #{accuracy[:worst_ratio]}x off#{accuracy[:worst_table] ? " on #{accuracy[:worst_table]}" : ""}. Run ANALYZE on tables with poor estimates.</div>
|
|
98
|
+
</div>
|
|
99
|
+
HTML
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
84
103
|
# Add cache hit ratio if we have buffer stats
|
|
85
104
|
if buffer_stats && buffer_stats[:total_blocks] > 0
|
|
86
105
|
hit_ratio = ((buffer_stats[:hit_blocks].to_f / buffer_stats[:total_blocks]) * 100).round(1)
|
|
@@ -234,6 +253,70 @@ module RailsDbInspector
|
|
|
234
253
|
|
|
235
254
|
private
|
|
236
255
|
|
|
256
|
+
def render_verdict(execution_time, total_cost, index_analysis, recommendations)
|
|
257
|
+
critical_count = recommendations.count { |r| r[:severity] == :critical }
|
|
258
|
+
warning_count = recommendations.count { |r| r[:severity] == :warning }
|
|
259
|
+
all_indexed = index_analysis[:total_scans] > 0 && index_analysis[:index_scans] == index_analysis[:total_scans]
|
|
260
|
+
|
|
261
|
+
if @analyze && execution_time
|
|
262
|
+
if execution_time > 1000 || critical_count > 0
|
|
263
|
+
verdict = :bad
|
|
264
|
+
icon = "🔴"
|
|
265
|
+
title = "This query needs attention"
|
|
266
|
+
desc = []
|
|
267
|
+
desc << "Took #{execution_time}ms to execute" if execution_time > 1000
|
|
268
|
+
desc << "#{critical_count} critical issue#{"s" if critical_count != 1}" if critical_count > 0
|
|
269
|
+
desc << "No indexes used" unless all_indexed || index_analysis[:total_scans] == 0
|
|
270
|
+
description = desc.join(" · ") + ". See the recommendations below for specific fixes."
|
|
271
|
+
elsif execution_time > 100 || warning_count > 0
|
|
272
|
+
verdict = :ok
|
|
273
|
+
icon = "🟡"
|
|
274
|
+
title = "Room for improvement"
|
|
275
|
+
desc = []
|
|
276
|
+
desc << "#{execution_time}ms execution time (aim for <50ms in web requests)" if execution_time > 100
|
|
277
|
+
desc << "#{warning_count} suggestion#{"s" if warning_count != 1}" if warning_count > 0
|
|
278
|
+
description = desc.join(" · ") + ". Review the recommendations below."
|
|
279
|
+
else
|
|
280
|
+
verdict = :good
|
|
281
|
+
icon = "🟢"
|
|
282
|
+
title = "This query looks good"
|
|
283
|
+
description = "Executed in #{execution_time}ms"
|
|
284
|
+
description += ", all scans use indexes" if all_indexed && index_analysis[:total_scans] > 0
|
|
285
|
+
description += ". No critical issues found."
|
|
286
|
+
end
|
|
287
|
+
else
|
|
288
|
+
# EXPLAIN without ANALYZE — evaluate based on cost and plan shape
|
|
289
|
+
if critical_count > 0
|
|
290
|
+
verdict = :bad
|
|
291
|
+
icon = "🔴"
|
|
292
|
+
title = "Potential problems detected"
|
|
293
|
+
description = "#{critical_count + warning_count} issue#{"s" if critical_count + warning_count != 1} found in the estimated plan. Run with ANALYZE to confirm with real metrics."
|
|
294
|
+
elsif warning_count > 0 || !all_indexed
|
|
295
|
+
verdict = :ok
|
|
296
|
+
icon = "🟡"
|
|
297
|
+
title = "Some concerns in the estimated plan"
|
|
298
|
+
description = "The planner's strategy has #{warning_count} suggestion#{"s" if warning_count != 1}. Run with ANALYZE to see actual performance."
|
|
299
|
+
else
|
|
300
|
+
verdict = :good
|
|
301
|
+
icon = "🟢"
|
|
302
|
+
title = "Estimated plan looks efficient"
|
|
303
|
+
description = "The planner chose index-based access"
|
|
304
|
+
description += " with an estimated cost of #{total_cost}" if total_cost
|
|
305
|
+
description += ". Run with ANALYZE to verify with real numbers."
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
<<~HTML
|
|
310
|
+
<div class="verdict-card verdict-#{verdict}" style="margin-bottom: 24px;">
|
|
311
|
+
<span class="verdict-icon">#{icon}</span>
|
|
312
|
+
<div class="verdict-body">
|
|
313
|
+
<h3>#{ERB::Util.html_escape(title)}</h3>
|
|
314
|
+
<p>#{ERB::Util.html_escape(description)}</p>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
HTML
|
|
318
|
+
end
|
|
319
|
+
|
|
237
320
|
def render_node(node, depth)
|
|
238
321
|
node_id = "node_#{SecureRandom.hex(6)}"
|
|
239
322
|
warnings = detect_warnings(node)
|
|
@@ -371,7 +454,30 @@ module RailsDbInspector
|
|
|
371
454
|
details << [ "Cost", "#{node["Startup Cost"]}..#{node["Total Cost"]}", "Startup cost (before first row) to total cost (all rows). Arbitrary units — compare relative to other nodes." ]
|
|
372
455
|
end
|
|
373
456
|
|
|
457
|
+
# Cardinality metrics — estimated rows and row width
|
|
458
|
+
if node["Plan Rows"]
|
|
459
|
+
details << [ "Estimated Rows (Cardinality)", number_with_delimiter(node["Plan Rows"]), "The planner's estimate of how many rows this node will produce. This is the estimated cardinality — a key factor in plan selection." ]
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if node["Plan Width"]
|
|
463
|
+
details << [ "Estimated Row Width", "#{node["Plan Width"]} bytes", "Average width of each output row in bytes. Combined with cardinality (row count), this determines memory and I/O cost estimates." ]
|
|
464
|
+
end
|
|
465
|
+
|
|
374
466
|
if @analyze
|
|
467
|
+
if node["Actual Rows"]
|
|
468
|
+
details << [ "Actual Rows", number_with_delimiter(node["Actual Rows"]), "The real number of rows returned by this node. Compare with Estimated Rows to judge planner accuracy." ]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
if node["Actual Rows"] && node["Plan Rows"] && node["Plan Rows"] > 0
|
|
472
|
+
ratio = (node["Actual Rows"].to_f / node["Plan Rows"]).round(2)
|
|
473
|
+
accuracy = if ratio == 1.0 then "exact match"
|
|
474
|
+
elsif ratio > 0.5 && ratio < 2.0 then "good (within 2x)"
|
|
475
|
+
elsif ratio > 0.1 && ratio < 10 then "off (#{ratio}x)"
|
|
476
|
+
else "poor (#{ratio}x) — consider ANALYZE on this table"
|
|
477
|
+
end
|
|
478
|
+
details << [ "Estimate Accuracy", accuracy, "How close the planner's cardinality estimate was to reality. If poor, run ANALYZE to update table statistics." ]
|
|
479
|
+
end
|
|
480
|
+
|
|
375
481
|
if node["Actual Startup Time"] && node["Actual Total Time"]
|
|
376
482
|
details << [ "Actual Time", "#{node["Actual Startup Time"]}..#{node["Actual Total Time"]} ms", "Real time: from start until all rows returned for this node." ]
|
|
377
483
|
end
|
|
@@ -520,6 +626,26 @@ module RailsDbInspector
|
|
|
520
626
|
node["Plans"] && node["Plans"].any?
|
|
521
627
|
end
|
|
522
628
|
|
|
629
|
+
def cardinality_accuracy(plan_node, result = nil)
|
|
630
|
+
result ||= { worst_ratio: 1.0, worst_table: nil }
|
|
631
|
+
|
|
632
|
+
if plan_node["Actual Rows"] && plan_node["Plan Rows"] && plan_node["Plan Rows"] > 0
|
|
633
|
+
ratio = plan_node["Actual Rows"].to_f / plan_node["Plan Rows"]
|
|
634
|
+
ratio = 1.0 / ratio if ratio > 0 && ratio < 1.0 # normalize so ratio >= 1
|
|
635
|
+
ratio = ratio.round(1)
|
|
636
|
+
if ratio > result[:worst_ratio]
|
|
637
|
+
result[:worst_ratio] = ratio
|
|
638
|
+
result[:worst_table] = plan_node["Relation Name"]
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
if plan_node["Plans"]
|
|
643
|
+
plan_node["Plans"].each { |child| cardinality_accuracy(child, result) }
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
result[:worst_ratio] > 1.0 ? result : nil
|
|
647
|
+
end
|
|
648
|
+
|
|
523
649
|
def find_hotspots(plan_node, hotspots = [])
|
|
524
650
|
return hotspots unless @analyze
|
|
525
651
|
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<nav class="flex items-center space-x-3 ml-6">
|
|
36
36
|
<%= link_to "Queries", queries_path, class: "text-sm font-medium #{'text-blue-600' if controller_name == 'queries'} #{'text-gray-500 hover:text-gray-700' unless controller_name == 'queries'} transition-colors" %>
|
|
37
37
|
<%= link_to "Schema", schema_index_path, class: "text-sm font-medium #{'text-blue-600' if controller_name == 'schema'} #{'text-gray-500 hover:text-gray-700' unless controller_name == 'schema'} transition-colors" %>
|
|
38
|
+
<%= link_to "Console", console_index_path, class: "text-sm font-medium #{'text-blue-600' if controller_name == 'console'} #{'text-gray-500 hover:text-gray-700' unless controller_name == 'console'} transition-colors" %>
|
|
38
39
|
</nav>
|
|
39
40
|
</div>
|
|
40
41
|
<div class="flex items-center space-x-4">
|