pg_insights 0.3.2 → 0.4.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/app/assets/javascripts/pg_insights/application.js +91 -21
- data/app/assets/javascripts/pg_insights/plan_performance.js +53 -0
- data/app/assets/javascripts/pg_insights/query_comparison.js +1129 -0
- data/app/assets/javascripts/pg_insights/results/view_toggles.js +26 -5
- data/app/assets/javascripts/pg_insights/results.js +231 -1
- data/app/assets/stylesheets/pg_insights/analysis.css +2628 -0
- data/app/assets/stylesheets/pg_insights/application.css +51 -1
- data/app/assets/stylesheets/pg_insights/results.css +12 -1
- data/app/controllers/pg_insights/insights_controller.rb +486 -9
- data/app/helpers/pg_insights/application_helper.rb +339 -0
- data/app/helpers/pg_insights/insights_helper.rb +567 -0
- data/app/jobs/pg_insights/query_analysis_job.rb +142 -0
- data/app/models/pg_insights/query_execution.rb +198 -0
- data/app/services/pg_insights/query_analysis_service.rb +269 -0
- data/app/views/layouts/pg_insights/application.html.erb +2 -0
- data/app/views/pg_insights/insights/_compare_view.html.erb +264 -0
- data/app/views/pg_insights/insights/_empty_state.html.erb +9 -0
- data/app/views/pg_insights/insights/_execution_table_view.html.erb +86 -0
- data/app/views/pg_insights/insights/_history_bar.html.erb +33 -0
- data/app/views/pg_insights/insights/_perf_view.html.erb +244 -0
- data/app/views/pg_insights/insights/_plan_nodes.html.erb +12 -0
- data/app/views/pg_insights/insights/_plan_tree.html.erb +30 -0
- data/app/views/pg_insights/insights/_plan_tree_modern.html.erb +12 -0
- data/app/views/pg_insights/insights/_plan_view.html.erb +159 -0
- data/app/views/pg_insights/insights/_query_panel.html.erb +3 -2
- data/app/views/pg_insights/insights/_result.html.erb +19 -4
- data/app/views/pg_insights/insights/_results_info.html.erb +33 -9
- data/app/views/pg_insights/insights/_results_info_empty.html.erb +10 -0
- data/app/views/pg_insights/insights/_results_panel.html.erb +7 -9
- data/app/views/pg_insights/insights/_results_table.html.erb +0 -5
- data/app/views/pg_insights/insights/_visual_view.html.erb +212 -0
- data/app/views/pg_insights/insights/index.html.erb +4 -1
- data/app/views/pg_insights/timeline/compare.html.erb +3 -3
- data/config/routes.rb +6 -0
- data/lib/generators/pg_insights/install_generator.rb +20 -14
- data/lib/generators/pg_insights/templates/db/migrate/create_pg_insights_query_executions.rb +45 -0
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +30 -2
- metadata +20 -2
@@ -111,8 +111,19 @@ module PgInsights
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
def query_info_text
|
115
|
+
execute_timeout = format_timeout(PgInsights.query_execution_timeout)
|
116
|
+
analyze_timeout = format_timeout(PgInsights.query_analysis_timeout)
|
117
|
+
"SELECT only • #{execute_timeout} exec • #{analyze_timeout} analyze • 1k row limit"
|
118
|
+
end
|
119
|
+
|
114
120
|
private
|
115
121
|
|
122
|
+
def format_timeout(timeout)
|
123
|
+
seconds = timeout.to_f
|
124
|
+
seconds >= 1 ? "#{seconds.to_i}s" : "#{(seconds * 1000).to_i}ms"
|
125
|
+
end
|
126
|
+
|
116
127
|
def prepare_chart_data(result)
|
117
128
|
return { labels: [], chartData: [] } unless result&.rows&.any?
|
118
129
|
|
@@ -186,5 +197,561 @@ module PgInsights
|
|
186
197
|
|
187
198
|
nil
|
188
199
|
end
|
200
|
+
|
201
|
+
def extract_rich_plan_metrics(execution)
|
202
|
+
return {} unless execution.execution_plan.present?
|
203
|
+
|
204
|
+
plan_data = execution.execution_plan.is_a?(Array) ? execution.execution_plan[0] : execution.execution_plan
|
205
|
+
return {} unless plan_data && plan_data["Plan"]
|
206
|
+
|
207
|
+
root_plan = plan_data["Plan"]
|
208
|
+
|
209
|
+
{
|
210
|
+
rows_returned: root_plan["Actual Rows"],
|
211
|
+
rows_scanned: extract_total_rows_scanned_helper(root_plan),
|
212
|
+
workers_planned: extract_workers_info_helper(root_plan)[:planned],
|
213
|
+
workers_launched: extract_workers_info_helper(root_plan)[:launched],
|
214
|
+
memory_usage_kb: extract_peak_memory_usage_helper(root_plan),
|
215
|
+
sort_methods: extract_sort_methods_helper(root_plan),
|
216
|
+
index_usage: extract_index_usage_helper(root_plan),
|
217
|
+
node_count: count_plan_nodes_helper(root_plan),
|
218
|
+
join_types: extract_join_types_helper(root_plan),
|
219
|
+
scan_types: extract_scan_types_helper(root_plan)
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def extract_workers_info_helper(node, workers_info = { planned: 0, launched: 0 })
|
226
|
+
return workers_info unless node
|
227
|
+
|
228
|
+
if node["Workers Planned"]
|
229
|
+
workers_info[:planned] = [ workers_info[:planned], node["Workers Planned"] ].max
|
230
|
+
end
|
231
|
+
|
232
|
+
if node["Workers Launched"]
|
233
|
+
workers_info[:launched] = [ workers_info[:launched], node["Workers Launched"] ].max
|
234
|
+
end
|
235
|
+
|
236
|
+
if node["Plans"]&.any?
|
237
|
+
node["Plans"].each { |child| extract_workers_info_helper(child, workers_info) }
|
238
|
+
end
|
239
|
+
|
240
|
+
workers_info
|
241
|
+
end
|
242
|
+
|
243
|
+
def extract_peak_memory_usage_helper(node, max_memory = 0)
|
244
|
+
return max_memory unless node
|
245
|
+
|
246
|
+
if node["Peak Memory Usage"]
|
247
|
+
max_memory = [ max_memory, node["Peak Memory Usage"] ].max
|
248
|
+
end
|
249
|
+
|
250
|
+
if node["Plans"]&.any?
|
251
|
+
node["Plans"].each { |child| max_memory = extract_peak_memory_usage_helper(child, max_memory) }
|
252
|
+
end
|
253
|
+
|
254
|
+
max_memory
|
255
|
+
end
|
256
|
+
|
257
|
+
def extract_sort_methods_helper(node, methods = Set.new)
|
258
|
+
return methods.to_a unless node
|
259
|
+
|
260
|
+
if node["Sort Method"]
|
261
|
+
methods.add(node["Sort Method"])
|
262
|
+
end
|
263
|
+
|
264
|
+
if node["Plans"]&.any?
|
265
|
+
node["Plans"].each { |child| extract_sort_methods_helper(child, methods) }
|
266
|
+
end
|
267
|
+
|
268
|
+
methods.to_a
|
269
|
+
end
|
270
|
+
|
271
|
+
def extract_index_usage_helper(node, indexes = Set.new)
|
272
|
+
return indexes.to_a unless node
|
273
|
+
|
274
|
+
if node["Index Name"]
|
275
|
+
indexes.add(node["Index Name"])
|
276
|
+
end
|
277
|
+
|
278
|
+
if node["Plans"]&.any?
|
279
|
+
node["Plans"].each { |child| extract_index_usage_helper(child, indexes) }
|
280
|
+
end
|
281
|
+
|
282
|
+
indexes.to_a
|
283
|
+
end
|
284
|
+
|
285
|
+
def count_plan_nodes_helper(node)
|
286
|
+
return 0 unless node
|
287
|
+
|
288
|
+
count = 1
|
289
|
+
if node["Plans"]&.any?
|
290
|
+
count += node["Plans"].sum { |child| count_plan_nodes_helper(child) }
|
291
|
+
end
|
292
|
+
count
|
293
|
+
end
|
294
|
+
|
295
|
+
def extract_join_types_helper(node, types = Set.new)
|
296
|
+
return types.to_a unless node
|
297
|
+
|
298
|
+
if node["Node Type"]&.include?("Join")
|
299
|
+
join_type = node["Join Type"] ? "#{node['Join Type']} #{node['Node Type']}" : node["Node Type"]
|
300
|
+
types.add(join_type)
|
301
|
+
end
|
302
|
+
|
303
|
+
if node["Plans"]&.any?
|
304
|
+
node["Plans"].each { |child| extract_join_types_helper(child, types) }
|
305
|
+
end
|
306
|
+
|
307
|
+
types.to_a
|
308
|
+
end
|
309
|
+
|
310
|
+
def extract_scan_types_helper(node, types = Set.new)
|
311
|
+
return types.to_a unless node
|
312
|
+
|
313
|
+
if node["Node Type"]&.include?("Scan")
|
314
|
+
types.add(node["Node Type"])
|
315
|
+
end
|
316
|
+
|
317
|
+
if node["Plans"]&.any?
|
318
|
+
node["Plans"].each { |child| extract_scan_types_helper(child, types) }
|
319
|
+
end
|
320
|
+
|
321
|
+
types.to_a
|
322
|
+
end
|
323
|
+
|
324
|
+
def extract_total_rows_scanned_helper(node)
|
325
|
+
return 0 unless node
|
326
|
+
|
327
|
+
scanned_rows = 0
|
328
|
+
|
329
|
+
# Count rows from scan operations
|
330
|
+
if node["Node Type"]&.include?("Scan") && node["Actual Rows"]
|
331
|
+
scanned_rows += node["Actual Rows"] || 0
|
332
|
+
end
|
333
|
+
|
334
|
+
if node["Plans"]&.any?
|
335
|
+
scanned_rows += node["Plans"].sum { |child| extract_total_rows_scanned_helper(child) }
|
336
|
+
end
|
337
|
+
|
338
|
+
scanned_rows
|
339
|
+
end
|
340
|
+
|
341
|
+
def calculate_plan_efficiency_score(plan_metrics)
|
342
|
+
return "N/A" unless plan_metrics[:rows_returned] && plan_metrics[:rows_scanned]
|
343
|
+
|
344
|
+
score = 100
|
345
|
+
|
346
|
+
# Penalize based on I/O efficiency
|
347
|
+
if plan_metrics[:rows_scanned] > 0
|
348
|
+
io_efficiency = (plan_metrics[:rows_returned].to_f / plan_metrics[:rows_scanned]) * 100
|
349
|
+
if io_efficiency < 20
|
350
|
+
score -= 40
|
351
|
+
elsif io_efficiency < 50
|
352
|
+
score -= 20
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Bonus for index usage
|
357
|
+
score += 10 if plan_metrics[:index_usage]&.any?
|
358
|
+
|
359
|
+
# Penalty for sequential scans on large datasets
|
360
|
+
if plan_metrics[:scan_types]&.include?("Seq Scan") && plan_metrics[:rows_scanned] > 10000
|
361
|
+
score -= 20
|
362
|
+
end
|
363
|
+
|
364
|
+
# Penalty for external sorting
|
365
|
+
score -= 15 if plan_metrics[:sort_methods]&.include?("external merge")
|
366
|
+
|
367
|
+
# Bonus for parallel execution
|
368
|
+
score += 5 if plan_metrics[:workers_launched] && plan_metrics[:workers_launched] > 0
|
369
|
+
|
370
|
+
score = [ 0, [ 100, score ].min ].max
|
371
|
+
|
372
|
+
case score
|
373
|
+
when 80..100 then "Excellent"
|
374
|
+
when 60..79 then "Good"
|
375
|
+
when 40..59 then "Fair"
|
376
|
+
else "Poor"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def render_plan_performance_issues(plan_metrics, execution)
|
381
|
+
issues = []
|
382
|
+
|
383
|
+
# Check for performance issues
|
384
|
+
if execution.total_time_ms && execution.total_time_ms > 5000
|
385
|
+
issues << "<li>Very slow execution time (>5 seconds)</li>"
|
386
|
+
elsif execution.total_time_ms && execution.total_time_ms > 1000
|
387
|
+
issues << "<li>Slow execution time (>1 second)</li>"
|
388
|
+
end
|
389
|
+
|
390
|
+
if plan_metrics[:memory_usage_kb] && plan_metrics[:memory_usage_kb] > 100000
|
391
|
+
issues << "<li>High memory usage (>100MB)</li>"
|
392
|
+
end
|
393
|
+
|
394
|
+
if plan_metrics[:sort_methods]&.include?("external merge")
|
395
|
+
issues << "<li>Sorting spilled to disk</li>"
|
396
|
+
end
|
397
|
+
|
398
|
+
if plan_metrics[:scan_types]&.include?("Seq Scan") && plan_metrics[:rows_scanned] && plan_metrics[:rows_scanned] > 100000
|
399
|
+
issues << "<li>Large sequential scan detected</li>"
|
400
|
+
end
|
401
|
+
|
402
|
+
if plan_metrics[:workers_planned] && plan_metrics[:workers_launched] &&
|
403
|
+
plan_metrics[:workers_planned] > plan_metrics[:workers_launched]
|
404
|
+
issues << "<li>Worker shortage detected</li>"
|
405
|
+
end
|
406
|
+
|
407
|
+
if issues.empty?
|
408
|
+
"<li>No major performance issues detected</li>"
|
409
|
+
else
|
410
|
+
issues.join.html_safe
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def render_plan_optimizations(plan_metrics, execution)
|
415
|
+
optimizations = []
|
416
|
+
|
417
|
+
if plan_metrics[:scan_types]&.include?("Seq Scan")
|
418
|
+
optimizations << "<li>Consider adding indexes to eliminate sequential scans</li>"
|
419
|
+
end
|
420
|
+
|
421
|
+
if plan_metrics[:sort_methods]&.include?("external merge")
|
422
|
+
optimizations << "<li>Increase work_mem to avoid disk-based sorting</li>"
|
423
|
+
end
|
424
|
+
|
425
|
+
if (!plan_metrics[:workers_launched] || plan_metrics[:workers_launched] == 0) &&
|
426
|
+
execution.total_time_ms && execution.total_time_ms > 1000
|
427
|
+
optimizations << "<li>Query could benefit from parallel execution</li>"
|
428
|
+
end
|
429
|
+
|
430
|
+
if (!plan_metrics[:index_usage] || plan_metrics[:index_usage].empty?) &&
|
431
|
+
plan_metrics[:rows_scanned] && plan_metrics[:rows_scanned] > 10000
|
432
|
+
optimizations << "<li>Add indexes to improve query performance</li>"
|
433
|
+
end
|
434
|
+
|
435
|
+
if plan_metrics[:join_types]&.any? && execution.total_time_ms && execution.total_time_ms > 500
|
436
|
+
optimizations << "<li>Review join strategy and column statistics</li>"
|
437
|
+
end
|
438
|
+
|
439
|
+
if optimizations.empty?
|
440
|
+
"<li>Query is well-optimized</li>"
|
441
|
+
else
|
442
|
+
optimizations.join.html_safe
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def calculate_advanced_metrics(plan_metrics)
|
447
|
+
{
|
448
|
+
io_efficiency: calculate_io_efficiency(plan_metrics),
|
449
|
+
memory_efficiency: calculate_memory_efficiency(plan_metrics),
|
450
|
+
parallelization: calculate_parallelization_score(plan_metrics),
|
451
|
+
index_utilization: calculate_index_utilization(plan_metrics)
|
452
|
+
}
|
453
|
+
end
|
454
|
+
|
455
|
+
def calculate_io_efficiency(plan_metrics)
|
456
|
+
return "N/A" unless plan_metrics[:rows_returned] && plan_metrics[:rows_scanned] &&
|
457
|
+
plan_metrics[:rows_scanned] > 0
|
458
|
+
|
459
|
+
efficiency = (plan_metrics[:rows_returned].to_f / plan_metrics[:rows_scanned]) * 100
|
460
|
+
case efficiency
|
461
|
+
when 80..Float::INFINITY then "Excellent"
|
462
|
+
when 50..79 then "Good"
|
463
|
+
when 20..49 then "Fair"
|
464
|
+
else "Poor"
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
def calculate_memory_efficiency(plan_metrics)
|
469
|
+
return "N/A" unless plan_metrics[:memory_usage_kb] && plan_metrics[:rows_returned] &&
|
470
|
+
plan_metrics[:rows_returned] > 0
|
471
|
+
|
472
|
+
memory_per_row = plan_metrics[:memory_usage_kb].to_f / plan_metrics[:rows_returned]
|
473
|
+
case memory_per_row
|
474
|
+
when 0..1 then "Excellent"
|
475
|
+
when 1..5 then "Good"
|
476
|
+
when 5..20 then "Fair"
|
477
|
+
else "Poor"
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def calculate_parallelization_score(plan_metrics)
|
482
|
+
return "N/A" unless plan_metrics[:workers_planned] && plan_metrics[:workers_planned] > 0
|
483
|
+
|
484
|
+
if plan_metrics[:workers_launched].nil? || plan_metrics[:workers_launched] == 0
|
485
|
+
return "None"
|
486
|
+
end
|
487
|
+
|
488
|
+
utilization = (plan_metrics[:workers_launched].to_f / plan_metrics[:workers_planned]) * 100
|
489
|
+
case utilization
|
490
|
+
when 90..Float::INFINITY then "Excellent"
|
491
|
+
when 70..89 then "Good"
|
492
|
+
when 50..69 then "Fair"
|
493
|
+
else "Poor"
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def calculate_index_utilization(plan_metrics)
|
498
|
+
return "N/A" unless plan_metrics[:scan_types]&.any?
|
499
|
+
|
500
|
+
has_seq_scan = plan_metrics[:scan_types].include?("Seq Scan")
|
501
|
+
index_scans = plan_metrics[:scan_types].count { |type| type.include?("Index") }
|
502
|
+
|
503
|
+
if !has_seq_scan && index_scans > 0
|
504
|
+
"Excellent"
|
505
|
+
elsif index_scans > 0
|
506
|
+
"Good"
|
507
|
+
elsif has_seq_scan
|
508
|
+
"Poor"
|
509
|
+
else
|
510
|
+
"N/A"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def score_css_class(score)
|
515
|
+
case score.to_s.downcase
|
516
|
+
when "excellent"
|
517
|
+
"score-excellent"
|
518
|
+
when "good"
|
519
|
+
"score-good"
|
520
|
+
when "fair"
|
521
|
+
"score-fair"
|
522
|
+
when "poor"
|
523
|
+
"score-poor"
|
524
|
+
when "none"
|
525
|
+
"score-none"
|
526
|
+
else
|
527
|
+
""
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Enhanced performance analysis helper methods
|
532
|
+
def calculate_overall_performance_score(execution)
|
533
|
+
return 75 unless execution.total_time_ms && execution.query_cost
|
534
|
+
|
535
|
+
score = 100
|
536
|
+
|
537
|
+
# Time penalty
|
538
|
+
if execution.total_time_ms > 10000
|
539
|
+
score -= 40
|
540
|
+
elsif execution.total_time_ms > 5000
|
541
|
+
score -= 25
|
542
|
+
elsif execution.total_time_ms > 1000
|
543
|
+
score -= 15
|
544
|
+
elsif execution.total_time_ms > 500
|
545
|
+
score -= 8
|
546
|
+
end
|
547
|
+
|
548
|
+
# Cost penalty
|
549
|
+
if execution.query_cost > 1000000
|
550
|
+
score -= 20
|
551
|
+
elsif execution.query_cost > 100000
|
552
|
+
score -= 10
|
553
|
+
end
|
554
|
+
|
555
|
+
# Plan metrics bonus/penalty
|
556
|
+
if execution.execution_plan.present?
|
557
|
+
plan_metrics = extract_rich_plan_metrics(execution)
|
558
|
+
score += 5 if plan_metrics[:index_usage]&.any?
|
559
|
+
score += 5 if plan_metrics[:workers_launched] && plan_metrics[:workers_launched] > 0
|
560
|
+
score -= 10 if plan_metrics[:sort_methods]&.include?("external merge")
|
561
|
+
end
|
562
|
+
|
563
|
+
[ 0, [ 100, score ].min ].max
|
564
|
+
end
|
565
|
+
|
566
|
+
def planning_vs_execution_insight(planning_ms, execution_ms)
|
567
|
+
return "" unless planning_ms && execution_ms && execution_ms > 0
|
568
|
+
|
569
|
+
ratio = planning_ms / execution_ms.to_f
|
570
|
+
|
571
|
+
if ratio > 0.5
|
572
|
+
"High planning overhead"
|
573
|
+
elsif ratio > 0.2
|
574
|
+
"Moderate planning cost"
|
575
|
+
elsif ratio < 0.05
|
576
|
+
"Efficient planning"
|
577
|
+
else
|
578
|
+
"Balanced timing"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def performance_benchmark_text(total_time_ms)
|
583
|
+
return "" unless total_time_ms
|
584
|
+
|
585
|
+
case total_time_ms
|
586
|
+
when 0..100
|
587
|
+
"Excellent"
|
588
|
+
when 101..500
|
589
|
+
"Good"
|
590
|
+
when 501..2000
|
591
|
+
"Acceptable"
|
592
|
+
when 2001..10000
|
593
|
+
"Slow"
|
594
|
+
else
|
595
|
+
"Very Slow"
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def cost_threshold_indicator(cost)
|
600
|
+
return "" unless cost
|
601
|
+
|
602
|
+
case cost
|
603
|
+
when 0..1000
|
604
|
+
"\u2713 Low"
|
605
|
+
when 1001..10000
|
606
|
+
"\u26A0 Medium"
|
607
|
+
when 10001..100000
|
608
|
+
"\u26A0 High"
|
609
|
+
else
|
610
|
+
"\u26A0 Very High"
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def memory_threshold_indicator(memory_kb)
|
615
|
+
return "" unless memory_kb
|
616
|
+
|
617
|
+
case memory_kb
|
618
|
+
when 0..10000
|
619
|
+
"\u2713 Low"
|
620
|
+
when 10001..50000
|
621
|
+
"\u26A0 Medium"
|
622
|
+
when 50001..200000
|
623
|
+
"\u26A0 High"
|
624
|
+
else
|
625
|
+
"\u26A0 Very High"
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
def format_memory_size(kb)
|
630
|
+
return "N/A" unless kb
|
631
|
+
|
632
|
+
if kb >= 1024 * 1024
|
633
|
+
"#{(kb / (1024.0 * 1024)).round(1)} GB"
|
634
|
+
elsif kb >= 1024
|
635
|
+
"#{(kb / 1024.0).round(1)} MB"
|
636
|
+
else
|
637
|
+
"#{number_with_delimiter(kb)} KB"
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def io_efficiency_impact(plan_metrics)
|
642
|
+
return "N/A" unless plan_metrics[:rows_returned] && plan_metrics[:rows_scanned]
|
643
|
+
|
644
|
+
ratio = plan_metrics[:rows_returned].to_f / plan_metrics[:rows_scanned]
|
645
|
+
|
646
|
+
case ratio
|
647
|
+
when 0.8..1.0
|
648
|
+
"Excellent selectivity"
|
649
|
+
when 0.5..0.79
|
650
|
+
"Good filtering"
|
651
|
+
when 0.1..0.49
|
652
|
+
"Moderate waste"
|
653
|
+
else
|
654
|
+
"Poor selectivity"
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
def memory_efficiency_impact(plan_metrics)
|
659
|
+
return "N/A" unless plan_metrics[:memory_usage_kb] && plan_metrics[:rows_returned]
|
660
|
+
|
661
|
+
memory_per_row = plan_metrics[:memory_usage_kb].to_f / plan_metrics[:rows_returned]
|
662
|
+
|
663
|
+
case memory_per_row
|
664
|
+
when 0..1
|
665
|
+
"Very efficient"
|
666
|
+
when 1..10
|
667
|
+
"Reasonable usage"
|
668
|
+
when 10..100
|
669
|
+
"High consumption"
|
670
|
+
else
|
671
|
+
"Memory intensive"
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
def parallelization_impact(plan_metrics)
|
676
|
+
return "N/A" unless plan_metrics[:workers_planned]
|
677
|
+
|
678
|
+
if plan_metrics[:workers_launched] == 0
|
679
|
+
"No parallelization"
|
680
|
+
elsif plan_metrics[:workers_launched] == plan_metrics[:workers_planned]
|
681
|
+
"Full utilization"
|
682
|
+
else
|
683
|
+
"Partial utilization"
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
def index_usage_impact(plan_metrics)
|
688
|
+
return "N/A" unless plan_metrics[:scan_types]&.any?
|
689
|
+
|
690
|
+
has_seq_scan = plan_metrics[:scan_types].include?("Seq Scan")
|
691
|
+
has_index_scan = plan_metrics[:scan_types].any? { |type| type.include?("Index") }
|
692
|
+
|
693
|
+
if has_index_scan && !has_seq_scan
|
694
|
+
"Optimal access"
|
695
|
+
elsif has_index_scan && has_seq_scan
|
696
|
+
"Mixed access pattern"
|
697
|
+
elsif has_seq_scan
|
698
|
+
"Sequential scans"
|
699
|
+
else
|
700
|
+
"Unknown pattern"
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
def recommendation_priority_class(suggestion, index)
|
705
|
+
priority = recommendation_priority_level(suggestion, index)
|
706
|
+
"priority-#{priority.downcase}"
|
707
|
+
end
|
708
|
+
|
709
|
+
def recommendation_priority_text(suggestion, index)
|
710
|
+
recommendation_priority_level(suggestion, index)
|
711
|
+
end
|
712
|
+
|
713
|
+
def recommendation_priority_level(suggestion, index)
|
714
|
+
text = suggestion.downcase
|
715
|
+
|
716
|
+
if text.include?("index") && (text.include?("avoid") || text.include?("eliminate"))
|
717
|
+
"HIGH"
|
718
|
+
elsif text.include?("work_mem") || text.include?("memory")
|
719
|
+
"MEDIUM"
|
720
|
+
elsif index == 0
|
721
|
+
"HIGH"
|
722
|
+
elsif index <= 2
|
723
|
+
"MEDIUM"
|
724
|
+
else
|
725
|
+
"LOW"
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
def recommendation_impact_estimate(suggestion)
|
730
|
+
text = suggestion.downcase
|
731
|
+
|
732
|
+
if text.include?("index") && text.include?("avoid")
|
733
|
+
"~50-80% faster"
|
734
|
+
elsif text.include?("work_mem")
|
735
|
+
"~20-40% improvement"
|
736
|
+
elsif text.include?("parallel")
|
737
|
+
"~30-60% faster"
|
738
|
+
else
|
739
|
+
"Performance gain"
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
def recommendation_hint(suggestion)
|
744
|
+
text = suggestion.downcase
|
745
|
+
|
746
|
+
if text.include?("index") && text.include?("order_items")
|
747
|
+
"CREATE INDEX ON order_items(...)"
|
748
|
+
elsif text.include?("work_mem")
|
749
|
+
"SET work_mem = '256MB'"
|
750
|
+
elsif text.include?("parallel")
|
751
|
+
"Adjust max_parallel_workers"
|
752
|
+
else
|
753
|
+
"See PostgreSQL docs"
|
754
|
+
end
|
755
|
+
end
|
189
756
|
end
|
190
757
|
end
|