pg_insights 0.3.2 → 0.4.1
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/engine.rb +8 -0
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +30 -2
- metadata +20 -2
@@ -0,0 +1,1129 @@
|
|
1
|
+
// Query History and Comparison JavaScript
|
2
|
+
document.addEventListener('DOMContentLoaded', function() {
|
3
|
+
// Only initialize if we're on the insights page
|
4
|
+
if (!document.querySelector('.insights-container')) return;
|
5
|
+
|
6
|
+
const QueryComparison = {
|
7
|
+
selectedQueries: [],
|
8
|
+
queryHistory: [],
|
9
|
+
|
10
|
+
init() {
|
11
|
+
this.bindEvents();
|
12
|
+
this.loadQueryHistory();
|
13
|
+
this.initializeCompareTab();
|
14
|
+
},
|
15
|
+
|
16
|
+
bindEvents() {
|
17
|
+
// Make functions globally available for onclick handlers
|
18
|
+
window.toggleHistoryBar = this.toggleHistoryBar.bind(this);
|
19
|
+
window.selectQuery = this.selectQuery.bind(this);
|
20
|
+
window.triggerCompare = this.triggerCompare.bind(this);
|
21
|
+
window.swapQueries = this.swapQueries.bind(this);
|
22
|
+
window.performComparison = this.performComparison.bind(this);
|
23
|
+
},
|
24
|
+
|
25
|
+
loadQueryHistory() {
|
26
|
+
const historyBar = document.getElementById('query-history-bar');
|
27
|
+
if (!historyBar) return;
|
28
|
+
|
29
|
+
fetch('/pg_insights/query_history.json')
|
30
|
+
.then(response => response.json())
|
31
|
+
.then(data => {
|
32
|
+
this.queryHistory = data;
|
33
|
+
this.renderHistoryItems(data);
|
34
|
+
this.updateHistoryCount(data.length);
|
35
|
+
})
|
36
|
+
.catch(error => {
|
37
|
+
console.error('Failed to load query history:', error);
|
38
|
+
this.showHistoryError();
|
39
|
+
});
|
40
|
+
},
|
41
|
+
|
42
|
+
renderHistoryItems(queries) {
|
43
|
+
const loadingEl = document.querySelector('.history-loading');
|
44
|
+
const itemsEl = document.querySelector('.history-items');
|
45
|
+
const emptyEl = document.querySelector('.history-empty');
|
46
|
+
|
47
|
+
loadingEl.style.display = 'none';
|
48
|
+
|
49
|
+
if (queries.length === 0) {
|
50
|
+
emptyEl.style.display = 'block';
|
51
|
+
itemsEl.style.display = 'none';
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
|
55
|
+
emptyEl.style.display = 'none';
|
56
|
+
itemsEl.style.display = 'grid';
|
57
|
+
itemsEl.innerHTML = '';
|
58
|
+
|
59
|
+
queries.forEach(query => {
|
60
|
+
const item = this.createHistoryItem(query);
|
61
|
+
itemsEl.appendChild(item);
|
62
|
+
});
|
63
|
+
},
|
64
|
+
|
65
|
+
createHistoryItem(query) {
|
66
|
+
const item = document.createElement('div');
|
67
|
+
item.className = 'history-item';
|
68
|
+
item.dataset.queryId = query.id;
|
69
|
+
|
70
|
+
item.innerHTML = `
|
71
|
+
<div class="history-checkbox">
|
72
|
+
<input type="checkbox" id="query-${query.id}" onchange="selectQuery(${query.id}, this.checked)">
|
73
|
+
</div>
|
74
|
+
<div class="history-details">
|
75
|
+
<div class="history-title-text">${query.title}</div>
|
76
|
+
<div class="history-meta">
|
77
|
+
<span>${query.created_at}</span>
|
78
|
+
<span>${query.summary}</span>
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
<div class="history-performance ${query.performance_class}"></div>
|
82
|
+
`;
|
83
|
+
|
84
|
+
return item;
|
85
|
+
},
|
86
|
+
|
87
|
+
selectQuery(queryId, selected) {
|
88
|
+
const query = this.queryHistory.find(q => q.id === queryId);
|
89
|
+
if (!query) return;
|
90
|
+
|
91
|
+
if (selected) {
|
92
|
+
if (this.selectedQueries.length < 2) {
|
93
|
+
this.selectedQueries.push(query);
|
94
|
+
document.querySelector(`[data-query-id="${queryId}"]`)?.classList.add('selected');
|
95
|
+
} else {
|
96
|
+
// Deselect the checkbox if we already have 2 selected
|
97
|
+
document.getElementById(`query-${queryId}`).checked = false;
|
98
|
+
alert('You can only select up to 2 queries for comparison.');
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
} else {
|
102
|
+
this.selectedQueries = this.selectedQueries.filter(q => q.id !== queryId);
|
103
|
+
document.querySelector(`[data-query-id="${queryId}"]`)?.classList.remove('selected');
|
104
|
+
}
|
105
|
+
|
106
|
+
this.updateSelectionUI();
|
107
|
+
},
|
108
|
+
|
109
|
+
updateSelectionUI() {
|
110
|
+
const selectedCount = this.selectedQueries.length;
|
111
|
+
|
112
|
+
// Update history bar elements
|
113
|
+
const countEl = document.getElementById('selected-count');
|
114
|
+
const selectedCountEl = document.querySelector('.selected-count');
|
115
|
+
const compareBtnEl = document.getElementById('compare-btn');
|
116
|
+
|
117
|
+
if (countEl) countEl.textContent = selectedCount;
|
118
|
+
if (selectedCountEl) selectedCountEl.style.display = selectedCount > 0 ? 'inline' : 'none';
|
119
|
+
if (compareBtnEl) compareBtnEl.style.display = selectedCount === 2 ? 'inline-block' : 'none';
|
120
|
+
|
121
|
+
// Update compare tab state
|
122
|
+
this.updateCompareTabState(selectedCount);
|
123
|
+
},
|
124
|
+
|
125
|
+
updateCompareTabState(selectedCount) {
|
126
|
+
const compareTabEl = document.getElementById('compare-tab');
|
127
|
+
if (!compareTabEl) return;
|
128
|
+
|
129
|
+
if (selectedCount === 2) {
|
130
|
+
compareTabEl.classList.remove('disabled');
|
131
|
+
compareTabEl.title = 'Compare selected queries';
|
132
|
+
} else {
|
133
|
+
compareTabEl.classList.add('disabled');
|
134
|
+
compareTabEl.title = selectedCount === 0
|
135
|
+
? 'Select 2 queries from history to compare'
|
136
|
+
: 'Select exactly 2 queries to compare';
|
137
|
+
}
|
138
|
+
},
|
139
|
+
|
140
|
+
toggleHistoryBar() {
|
141
|
+
const historyBar = document.getElementById('query-history-bar');
|
142
|
+
const isExpanded = historyBar.classList.contains('expanded');
|
143
|
+
|
144
|
+
if (isExpanded) {
|
145
|
+
historyBar.classList.remove('expanded');
|
146
|
+
historyBar.classList.add('collapsed');
|
147
|
+
} else {
|
148
|
+
historyBar.classList.remove('collapsed');
|
149
|
+
historyBar.classList.add('expanded');
|
150
|
+
}
|
151
|
+
},
|
152
|
+
|
153
|
+
triggerCompare(event) {
|
154
|
+
event.stopPropagation();
|
155
|
+
|
156
|
+
if (this.selectedQueries.length !== 2) {
|
157
|
+
alert('Please select exactly 2 queries to compare.');
|
158
|
+
return;
|
159
|
+
}
|
160
|
+
|
161
|
+
this.activateCompareTab();
|
162
|
+
this.loadComparisonInterface();
|
163
|
+
},
|
164
|
+
|
165
|
+
activateCompareTab() {
|
166
|
+
// Deactivate all tabs
|
167
|
+
document.querySelectorAll('.toggle-btn').forEach(btn => btn.classList.remove('active'));
|
168
|
+
|
169
|
+
// Activate compare tab
|
170
|
+
const compareTab = document.getElementById('compare-tab');
|
171
|
+
if (compareTab) {
|
172
|
+
compareTab.classList.add('active');
|
173
|
+
}
|
174
|
+
|
175
|
+
// Hide all views, show compare view
|
176
|
+
document.querySelectorAll('.view-content').forEach(view => view.style.display = 'none');
|
177
|
+
const compareView = document.getElementById('compare-view');
|
178
|
+
if (compareView) {
|
179
|
+
compareView.style.display = 'block';
|
180
|
+
}
|
181
|
+
},
|
182
|
+
|
183
|
+
loadComparisonInterface() {
|
184
|
+
if (this.selectedQueries.length !== 2) return;
|
185
|
+
|
186
|
+
const queryA = this.selectedQueries[0];
|
187
|
+
const queryB = this.selectedQueries[1];
|
188
|
+
|
189
|
+
// Update query selector cards
|
190
|
+
this.updateQueryCard('a', queryA);
|
191
|
+
this.updateQueryCard('b', queryB);
|
192
|
+
|
193
|
+
// Hide empty state, show header
|
194
|
+
document.getElementById('comparison-empty').style.display = 'none';
|
195
|
+
document.querySelector('.compare-header').classList.add('active');
|
196
|
+
},
|
197
|
+
|
198
|
+
updateQueryCard(position, query) {
|
199
|
+
const titleEl = document.getElementById(`compare-title-${position}`);
|
200
|
+
const summaryEl = document.getElementById(`compare-summary-${position}`);
|
201
|
+
const cardEl = document.querySelector(`.query-card.query-${position}`);
|
202
|
+
|
203
|
+
titleEl.textContent = query.title;
|
204
|
+
summaryEl.textContent = query.summary;
|
205
|
+
cardEl.classList.add('selected');
|
206
|
+
},
|
207
|
+
|
208
|
+
swapQueries() {
|
209
|
+
if (this.selectedQueries.length === 2) {
|
210
|
+
[this.selectedQueries[0], this.selectedQueries[1]] = [this.selectedQueries[1], this.selectedQueries[0]];
|
211
|
+
this.loadComparisonInterface();
|
212
|
+
}
|
213
|
+
},
|
214
|
+
|
215
|
+
performComparison() {
|
216
|
+
if (this.selectedQueries.length !== 2) return;
|
217
|
+
|
218
|
+
// Show loading state
|
219
|
+
document.getElementById('comparison-empty').style.display = 'none';
|
220
|
+
document.getElementById('comparison-results').style.display = 'none';
|
221
|
+
document.getElementById('comparison-loading').style.display = 'block';
|
222
|
+
|
223
|
+
const executionIds = this.selectedQueries.map(q => q.id);
|
224
|
+
|
225
|
+
fetch('/pg_insights/compare.json', {
|
226
|
+
method: 'POST',
|
227
|
+
headers: {
|
228
|
+
'Content-Type': 'application/json',
|
229
|
+
'Accept': 'application/json',
|
230
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
231
|
+
},
|
232
|
+
body: JSON.stringify({
|
233
|
+
execution_ids: executionIds,
|
234
|
+
authenticity_token: document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
235
|
+
})
|
236
|
+
})
|
237
|
+
.then(response => response.json())
|
238
|
+
.then(data => {
|
239
|
+
if (data.error) {
|
240
|
+
throw new Error(data.error);
|
241
|
+
}
|
242
|
+
this.renderComparisonResults(data);
|
243
|
+
})
|
244
|
+
.catch(error => {
|
245
|
+
console.error('Comparison failed:', error);
|
246
|
+
this.showComparisonError(error.message);
|
247
|
+
});
|
248
|
+
},
|
249
|
+
|
250
|
+
renderComparisonResults(data) {
|
251
|
+
document.getElementById('comparison-loading').style.display = 'none';
|
252
|
+
document.getElementById('comparison-results').style.display = 'block';
|
253
|
+
|
254
|
+
// Phase 2: Enhanced rendering
|
255
|
+
this.renderMetricsOverview(data);
|
256
|
+
this.renderMetricsTable(data);
|
257
|
+
this.renderWinnerSummary(data);
|
258
|
+
this.renderPlanAnalysis(data);
|
259
|
+
this.renderExecutionPlans(data);
|
260
|
+
this.renderOptimizationAnalysis(data);
|
261
|
+
this.renderInsights(data);
|
262
|
+
|
263
|
+
// Initialize plan view toggles
|
264
|
+
this.initializePlanToggles();
|
265
|
+
},
|
266
|
+
|
267
|
+
renderMetricsOverview(data) {
|
268
|
+
const execA = data.executions.a;
|
269
|
+
const execB = data.executions.b;
|
270
|
+
const comparison = data.comparison;
|
271
|
+
|
272
|
+
// Time Metric
|
273
|
+
this.updateMetricCard('time-metric',
|
274
|
+
execA.metrics.total_time_ms ? `${execA.metrics.total_time_ms}ms` : 'N/A',
|
275
|
+
execB.metrics.total_time_ms ? `${execB.metrics.total_time_ms}ms` : 'N/A',
|
276
|
+
comparison.performance.time_difference_pct ?
|
277
|
+
`${Math.abs(comparison.performance.time_difference_pct)}% ${comparison.performance.time_faster === 'b' ? 'B faster' : 'A faster'}` : 'Same'
|
278
|
+
);
|
279
|
+
|
280
|
+
// Cost Metric
|
281
|
+
this.updateMetricCard('cost-metric',
|
282
|
+
execA.metrics.query_cost || 'N/A',
|
283
|
+
execB.metrics.query_cost || 'N/A',
|
284
|
+
comparison.performance.cost_difference_pct ?
|
285
|
+
`${Math.abs(comparison.performance.cost_difference_pct)}% ${comparison.performance.cost_cheaper === 'b' ? 'B cheaper' : 'A cheaper'}` : 'Same'
|
286
|
+
);
|
287
|
+
|
288
|
+
// Rows Metric
|
289
|
+
const rowsA = execA.metrics.rows_returned || execA.metrics.rows_scanned || 0;
|
290
|
+
const rowsB = execB.metrics.rows_returned || execB.metrics.rows_scanned || 0;
|
291
|
+
this.updateMetricCard('rows-metric',
|
292
|
+
rowsA.toLocaleString(),
|
293
|
+
rowsB.toLocaleString(),
|
294
|
+
rowsA === rowsB ? 'Same' : (rowsA > rowsB ? 'A processes more' : 'B processes more')
|
295
|
+
);
|
296
|
+
|
297
|
+
// Efficiency Metric (rows per ms)
|
298
|
+
const efficiencyA = execA.metrics.total_time_ms ? (rowsA / execA.metrics.total_time_ms).toFixed(2) : 0;
|
299
|
+
const efficiencyB = execB.metrics.total_time_ms ? (rowsB / execB.metrics.total_time_ms).toFixed(2) : 0;
|
300
|
+
this.updateMetricCard('efficiency-metric',
|
301
|
+
`${efficiencyA}/ms`,
|
302
|
+
`${efficiencyB}/ms`,
|
303
|
+
efficiencyA === efficiencyB ? 'Same' : (efficiencyA > efficiencyB ? 'A more efficient' : 'B more efficient')
|
304
|
+
);
|
305
|
+
},
|
306
|
+
|
307
|
+
updateMetricCard(cardId, valueA, valueB, difference) {
|
308
|
+
const card = document.getElementById(cardId);
|
309
|
+
if (!card) return;
|
310
|
+
|
311
|
+
card.querySelector('.value-a').textContent = valueA;
|
312
|
+
card.querySelector('.value-b').textContent = valueB;
|
313
|
+
|
314
|
+
const diffElement = card.querySelector('.metric-difference');
|
315
|
+
diffElement.textContent = difference;
|
316
|
+
|
317
|
+
// Add appropriate class
|
318
|
+
diffElement.className = 'metric-difference';
|
319
|
+
if (difference.includes('faster') || difference.includes('cheaper') || difference.includes('more efficient')) {
|
320
|
+
diffElement.classList.add('better');
|
321
|
+
} else if (difference.includes('slower') || difference.includes('expensive')) {
|
322
|
+
diffElement.classList.add('worse');
|
323
|
+
} else {
|
324
|
+
diffElement.classList.add('same');
|
325
|
+
}
|
326
|
+
},
|
327
|
+
|
328
|
+
renderMetricsTable(data) {
|
329
|
+
const tbody = document.getElementById('metrics-table-body');
|
330
|
+
const execA = data.executions.a;
|
331
|
+
const execB = data.executions.b;
|
332
|
+
const comparison = data.comparison;
|
333
|
+
|
334
|
+
tbody.innerHTML = '';
|
335
|
+
|
336
|
+
// Helper function to create metric rows
|
337
|
+
const createMetricRow = (label, valueA, valueB, difference, impact, betterDirection = 'lower') => {
|
338
|
+
const row = document.createElement('tr');
|
339
|
+
|
340
|
+
let diffClass = 'metric-same';
|
341
|
+
let diffText = 'Same';
|
342
|
+
|
343
|
+
if (difference && difference !== '0%') {
|
344
|
+
const isABetter = betterDirection === 'lower' ?
|
345
|
+
parseFloat(valueA) < parseFloat(valueB) :
|
346
|
+
parseFloat(valueA) > parseFloat(valueB);
|
347
|
+
|
348
|
+
diffClass = isABetter ? 'metric-better' : 'metric-worse';
|
349
|
+
diffText = difference;
|
350
|
+
}
|
351
|
+
|
352
|
+
row.innerHTML = `
|
353
|
+
<td>${label}</td>
|
354
|
+
<td>${valueA || 'N/A'}</td>
|
355
|
+
<td>${valueB || 'N/A'}</td>
|
356
|
+
<td class="${diffClass}">${diffText}</td>
|
357
|
+
<td>${impact}</td>
|
358
|
+
`;
|
359
|
+
|
360
|
+
return row;
|
361
|
+
};
|
362
|
+
|
363
|
+
// Add metric rows with impact assessment
|
364
|
+
tbody.appendChild(createMetricRow(
|
365
|
+
'⏱️ Total Time',
|
366
|
+
execA.metrics.total_time_ms ? `${execA.metrics.total_time_ms}ms` : null,
|
367
|
+
execB.metrics.total_time_ms ? `${execB.metrics.total_time_ms}ms` : null,
|
368
|
+
comparison.performance.time_difference_pct ?
|
369
|
+
`${Math.abs(comparison.performance.time_difference_pct)}% ${comparison.performance.time_faster === 'b' ? 'B faster' : 'A faster'}` : null,
|
370
|
+
this.getPerformanceImpact(comparison.performance.time_difference_pct)
|
371
|
+
));
|
372
|
+
|
373
|
+
tbody.appendChild(createMetricRow(
|
374
|
+
'💰 Query Cost',
|
375
|
+
execA.metrics.query_cost || null,
|
376
|
+
execB.metrics.query_cost || null,
|
377
|
+
comparison.performance.cost_difference_pct ?
|
378
|
+
`${Math.abs(comparison.performance.cost_difference_pct)}% ${comparison.performance.cost_cheaper === 'b' ? 'B cheaper' : 'A cheaper'}` : null,
|
379
|
+
this.getCostImpact(comparison.performance.cost_difference_pct)
|
380
|
+
));
|
381
|
+
|
382
|
+
tbody.appendChild(createMetricRow(
|
383
|
+
'📋 Rows Returned',
|
384
|
+
execA.metrics.rows_returned || null,
|
385
|
+
execB.metrics.rows_returned || null,
|
386
|
+
null,
|
387
|
+
'Data Volume'
|
388
|
+
));
|
389
|
+
|
390
|
+
// Planning Time
|
391
|
+
const planningDiff = this.calculatePercentageDifference(
|
392
|
+
execA.metrics.planning_time_ms,
|
393
|
+
execB.metrics.planning_time_ms
|
394
|
+
);
|
395
|
+
tbody.appendChild(createMetricRow(
|
396
|
+
'🧠 Planning Time',
|
397
|
+
execA.metrics.planning_time_ms ? `${execA.metrics.planning_time_ms}ms` : null,
|
398
|
+
execB.metrics.planning_time_ms ? `${execB.metrics.planning_time_ms}ms` : null,
|
399
|
+
planningDiff,
|
400
|
+
'Query Complexity'
|
401
|
+
));
|
402
|
+
|
403
|
+
// Execution Time
|
404
|
+
const executionDiff = this.calculatePercentageDifference(
|
405
|
+
execA.metrics.execution_time_ms,
|
406
|
+
execB.metrics.execution_time_ms
|
407
|
+
);
|
408
|
+
tbody.appendChild(createMetricRow(
|
409
|
+
'⚡ Execution Time',
|
410
|
+
execA.metrics.execution_time_ms ? `${execA.metrics.execution_time_ms}ms` : null,
|
411
|
+
execB.metrics.execution_time_ms ? `${execB.metrics.execution_time_ms}ms` : null,
|
412
|
+
executionDiff,
|
413
|
+
'Resource Usage'
|
414
|
+
));
|
415
|
+
|
416
|
+
// Rows Scanned
|
417
|
+
tbody.appendChild(createMetricRow(
|
418
|
+
'🔍 Rows Scanned',
|
419
|
+
execA.metrics.rows_scanned || null,
|
420
|
+
execB.metrics.rows_scanned || null,
|
421
|
+
this.calculatePercentageDifference(execA.metrics.rows_scanned, execB.metrics.rows_scanned),
|
422
|
+
'I/O Efficiency'
|
423
|
+
));
|
424
|
+
|
425
|
+
// Memory Usage
|
426
|
+
if (execA.metrics.memory_usage_kb || execB.metrics.memory_usage_kb) {
|
427
|
+
tbody.appendChild(createMetricRow(
|
428
|
+
'💾 Peak Memory',
|
429
|
+
execA.metrics.memory_usage_kb ? `${execA.metrics.memory_usage_kb} KB` : null,
|
430
|
+
execB.metrics.memory_usage_kb ? `${execB.metrics.memory_usage_kb} KB` : null,
|
431
|
+
this.calculatePercentageDifference(execA.metrics.memory_usage_kb, execB.metrics.memory_usage_kb),
|
432
|
+
'Resource Usage'
|
433
|
+
));
|
434
|
+
}
|
435
|
+
|
436
|
+
// Workers
|
437
|
+
if (execA.metrics.workers_launched || execB.metrics.workers_launched) {
|
438
|
+
tbody.appendChild(createMetricRow(
|
439
|
+
'👥 Parallel Workers',
|
440
|
+
execA.metrics.workers_launched || 0,
|
441
|
+
execB.metrics.workers_launched || 0,
|
442
|
+
null,
|
443
|
+
'Parallelization'
|
444
|
+
));
|
445
|
+
}
|
446
|
+
|
447
|
+
// Plan Complexity
|
448
|
+
tbody.appendChild(createMetricRow(
|
449
|
+
'🌳 Plan Nodes',
|
450
|
+
execA.metrics.node_count || null,
|
451
|
+
execB.metrics.node_count || null,
|
452
|
+
null,
|
453
|
+
'Complexity'
|
454
|
+
));
|
455
|
+
|
456
|
+
// Scan Types
|
457
|
+
if (execA.metrics.scan_types?.length || execB.metrics.scan_types?.length) {
|
458
|
+
tbody.appendChild(createMetricRow(
|
459
|
+
'📋 Scan Methods',
|
460
|
+
execA.metrics.scan_types ? execA.metrics.scan_types.join(', ') : 'N/A',
|
461
|
+
execB.metrics.scan_types ? execB.metrics.scan_types.join(', ') : 'N/A',
|
462
|
+
null,
|
463
|
+
'Access Pattern'
|
464
|
+
));
|
465
|
+
}
|
466
|
+
|
467
|
+
// Index Usage
|
468
|
+
if (execA.metrics.index_usage?.length || execB.metrics.index_usage?.length) {
|
469
|
+
tbody.appendChild(createMetricRow(
|
470
|
+
'🔑 Indexes Used',
|
471
|
+
execA.metrics.index_usage?.length ? `${execA.metrics.index_usage.length} indexes` : 'None',
|
472
|
+
execB.metrics.index_usage?.length ? `${execB.metrics.index_usage.length} indexes` : 'None',
|
473
|
+
null,
|
474
|
+
'Index Efficiency'
|
475
|
+
));
|
476
|
+
}
|
477
|
+
|
478
|
+
// Sort Methods
|
479
|
+
if (execA.metrics.sort_methods?.length || execB.metrics.sort_methods?.length) {
|
480
|
+
tbody.appendChild(createMetricRow(
|
481
|
+
'🔢 Sort Methods',
|
482
|
+
execA.metrics.sort_methods ? execA.metrics.sort_methods.join(', ') : 'None',
|
483
|
+
execB.metrics.sort_methods ? execB.metrics.sort_methods.join(', ') : 'None',
|
484
|
+
null,
|
485
|
+
'Memory Efficiency'
|
486
|
+
));
|
487
|
+
}
|
488
|
+
},
|
489
|
+
|
490
|
+
renderWinnerSummary(data) {
|
491
|
+
const winnerEl = document.getElementById('winner-summary');
|
492
|
+
const winner = data.comparison.winner;
|
493
|
+
|
494
|
+
if (winner && winner !== 'unknown') {
|
495
|
+
const winnerQuery = winner === 'a' ? 'Query A' : 'Query B';
|
496
|
+
winnerEl.querySelector('.winner-text').textContent = `${winnerQuery} performs better overall`;
|
497
|
+
winnerEl.style.display = 'block';
|
498
|
+
} else {
|
499
|
+
winnerEl.style.display = 'none';
|
500
|
+
}
|
501
|
+
},
|
502
|
+
|
503
|
+
renderInsights(data) {
|
504
|
+
const insightsList = document.getElementById('insights-list');
|
505
|
+
const insights = data.comparison.insights || [];
|
506
|
+
|
507
|
+
insightsList.innerHTML = '';
|
508
|
+
|
509
|
+
if (insights.length === 0) {
|
510
|
+
document.getElementById('insights-section').style.display = 'none';
|
511
|
+
return;
|
512
|
+
}
|
513
|
+
|
514
|
+
document.getElementById('insights-section').style.display = 'block';
|
515
|
+
|
516
|
+
insights.forEach(insight => {
|
517
|
+
const item = document.createElement('div');
|
518
|
+
item.className = 'insight-item';
|
519
|
+
item.innerHTML = `
|
520
|
+
<div class="insight-icon">💡</div>
|
521
|
+
<div class="insight-text">${insight}</div>
|
522
|
+
`;
|
523
|
+
insightsList.appendChild(item);
|
524
|
+
});
|
525
|
+
},
|
526
|
+
|
527
|
+
renderExecutionPlans(data) {
|
528
|
+
const planAContent = document.getElementById('plan-a-content');
|
529
|
+
const planBContent = document.getElementById('plan-b-content');
|
530
|
+
|
531
|
+
const execA = data.executions.a;
|
532
|
+
const execB = data.executions.b;
|
533
|
+
|
534
|
+
// Render plan A
|
535
|
+
if (execA.sql_text) {
|
536
|
+
planAContent.innerHTML = this.formatPlanPreview(execA.sql_text);
|
537
|
+
} else {
|
538
|
+
planAContent.innerHTML = '<div style="color: #6b7280; font-style: italic;">No execution plan available</div>';
|
539
|
+
}
|
540
|
+
|
541
|
+
// Render plan B
|
542
|
+
if (execB.sql_text) {
|
543
|
+
planBContent.innerHTML = this.formatPlanPreview(execB.sql_text);
|
544
|
+
} else {
|
545
|
+
planBContent.innerHTML = '<div style="color: #6b7280; font-style: italic;">No execution plan available</div>';
|
546
|
+
}
|
547
|
+
},
|
548
|
+
|
549
|
+
formatPlanPreview(sqlText) {
|
550
|
+
// Format SQL text for preview
|
551
|
+
const formattedSql = sqlText
|
552
|
+
.replace(/SELECT/gi, '<span style="color: #059669; font-weight: 600;">SELECT</span>')
|
553
|
+
.replace(/FROM/gi, '<span style="color: #0ea5e9; font-weight: 600;">FROM</span>')
|
554
|
+
.replace(/WHERE/gi, '<span style="color: #8b5cf6; font-weight: 600;">WHERE</span>')
|
555
|
+
.replace(/JOIN/gi, '<span style="color: #f59e0b; font-weight: 600;">JOIN</span>')
|
556
|
+
.replace(/ORDER BY/gi, '<span style="color: #ef4444; font-weight: 600;">ORDER BY</span>')
|
557
|
+
.replace(/GROUP BY/gi, '<span style="color: #ec4899; font-weight: 600;">GROUP BY</span>');
|
558
|
+
|
559
|
+
return `
|
560
|
+
<div style="background: #f4f4f5; padding: 8px; margin-bottom: 8px; font-weight: 600; color: #18181b; font-size: 12px;">
|
561
|
+
SQL Query
|
562
|
+
</div>
|
563
|
+
<div style="line-height: 1.4; white-space: pre-wrap; font-size: 12px;">${formattedSql}</div>
|
564
|
+
`;
|
565
|
+
},
|
566
|
+
|
567
|
+
renderPlanAnalysis(data) {
|
568
|
+
const execA = data.executions.a;
|
569
|
+
const execB = data.executions.b;
|
570
|
+
|
571
|
+
// Use actual extracted plan data
|
572
|
+
document.getElementById('nodes-a').textContent = execA.metrics.node_count || 'N/A';
|
573
|
+
document.getElementById('nodes-b').textContent = execB.metrics.node_count || 'N/A';
|
574
|
+
|
575
|
+
// Calculate depth from node count (rough approximation)
|
576
|
+
const depthA = execA.metrics.node_count ? Math.ceil(Math.log2(execA.metrics.node_count)) : 'N/A';
|
577
|
+
const depthB = execB.metrics.node_count ? Math.ceil(Math.log2(execB.metrics.node_count)) : 'N/A';
|
578
|
+
document.getElementById('depth-a').textContent = depthA;
|
579
|
+
document.getElementById('depth-b').textContent = depthB;
|
580
|
+
|
581
|
+
// Show actual scan types
|
582
|
+
document.getElementById('scans-a').textContent = execA.metrics.scan_types?.join(', ') || 'N/A';
|
583
|
+
document.getElementById('scans-b').textContent = execB.metrics.scan_types?.join(', ') || 'N/A';
|
584
|
+
|
585
|
+
// Enhanced efficiency indicators
|
586
|
+
const efficiencyA = this.calculateEfficiencyScore(execA.metrics);
|
587
|
+
const efficiencyB = this.calculateEfficiencyScore(execB.metrics);
|
588
|
+
|
589
|
+
document.getElementById('efficiency-a').textContent = efficiencyA;
|
590
|
+
document.getElementById('efficiency-b').textContent = efficiencyB;
|
591
|
+
|
592
|
+
// Smart bottleneck detection
|
593
|
+
const bottleneckA = this.detectBottleneck(execA.metrics);
|
594
|
+
const bottleneckB = this.detectBottleneck(execB.metrics);
|
595
|
+
|
596
|
+
document.getElementById('bottleneck-a').textContent = bottleneckA;
|
597
|
+
document.getElementById('bottleneck-b').textContent = bottleneckB;
|
598
|
+
},
|
599
|
+
|
600
|
+
calculateEfficiencyScore(metrics) {
|
601
|
+
if (!metrics.total_time_ms) return 'N/A';
|
602
|
+
|
603
|
+
let score = 100;
|
604
|
+
|
605
|
+
// Penalize based on time
|
606
|
+
if (metrics.total_time_ms > 5000) score -= 40;
|
607
|
+
else if (metrics.total_time_ms > 1000) score -= 20;
|
608
|
+
else if (metrics.total_time_ms > 500) score -= 10;
|
609
|
+
|
610
|
+
// Bonus for parallel execution
|
611
|
+
if (metrics.workers_launched > 0) score += 10;
|
612
|
+
|
613
|
+
// Penalty for high memory usage
|
614
|
+
if (metrics.memory_usage_kb > 50000) score -= 15;
|
615
|
+
|
616
|
+
// Bonus for index usage
|
617
|
+
if (metrics.index_usage?.length > 0) score += 5;
|
618
|
+
|
619
|
+
// Penalty for sequential scans
|
620
|
+
if (metrics.scan_types?.includes('Seq Scan')) score -= 10;
|
621
|
+
|
622
|
+
// Penalty for external sorts
|
623
|
+
if (metrics.sort_methods?.includes('external merge')) score -= 15;
|
624
|
+
|
625
|
+
score = Math.max(0, Math.min(100, score));
|
626
|
+
|
627
|
+
if (score >= 85) return 'Excellent';
|
628
|
+
if (score >= 70) return 'Good';
|
629
|
+
if (score >= 50) return 'Fair';
|
630
|
+
return 'Needs Review';
|
631
|
+
},
|
632
|
+
|
633
|
+
detectBottleneck(metrics) {
|
634
|
+
const issues = [];
|
635
|
+
|
636
|
+
if (metrics.scan_types?.includes('Seq Scan')) {
|
637
|
+
issues.push('Sequential Scans');
|
638
|
+
}
|
639
|
+
|
640
|
+
if (metrics.sort_methods?.includes('external merge')) {
|
641
|
+
issues.push('Disk Sorting');
|
642
|
+
}
|
643
|
+
|
644
|
+
if (metrics.memory_usage_kb > 100000) {
|
645
|
+
issues.push('High Memory');
|
646
|
+
}
|
647
|
+
|
648
|
+
if (metrics.workers_planned > metrics.workers_launched) {
|
649
|
+
issues.push('Worker Shortage');
|
650
|
+
}
|
651
|
+
|
652
|
+
if (!metrics.index_usage?.length && metrics.rows_scanned > 10000) {
|
653
|
+
issues.push('Missing Indexes');
|
654
|
+
}
|
655
|
+
|
656
|
+
return issues.length ? issues.join(', ') : 'None';
|
657
|
+
},
|
658
|
+
|
659
|
+
renderOptimizationAnalysis(data) {
|
660
|
+
const execA = data.executions.a;
|
661
|
+
const execB = data.executions.b;
|
662
|
+
|
663
|
+
// Calculate optimization scores (0-100)
|
664
|
+
const scoreA = this.calculateOptimizationScore(execA.metrics);
|
665
|
+
const scoreB = this.calculateOptimizationScore(execB.metrics);
|
666
|
+
|
667
|
+
document.getElementById('score-a').textContent = scoreA;
|
668
|
+
document.getElementById('score-b').textContent = scoreB;
|
669
|
+
|
670
|
+
// Set grades
|
671
|
+
document.getElementById('grade-a').textContent = this.getScoreGrade(scoreA);
|
672
|
+
document.getElementById('grade-b').textContent = this.getScoreGrade(scoreB);
|
673
|
+
document.getElementById('grade-a').className = `score-grade ${this.getGradeClass(scoreA)}`;
|
674
|
+
document.getElementById('grade-b').className = `score-grade ${this.getGradeClass(scoreB)}`;
|
675
|
+
|
676
|
+
// Score comparison
|
677
|
+
const scoreDiff = Math.abs(scoreA - scoreB);
|
678
|
+
const betterQuery = scoreA > scoreB ? 'A' : 'B';
|
679
|
+
|
680
|
+
if (scoreDiff > 5) {
|
681
|
+
document.getElementById('score-arrow').textContent = scoreA > scoreB ? '→' : '←';
|
682
|
+
document.getElementById('score-improvement').textContent = `${scoreDiff} points better`;
|
683
|
+
document.getElementById('score-improvement').className = 'score-improvement better';
|
684
|
+
} else {
|
685
|
+
document.getElementById('score-arrow').textContent = '↔';
|
686
|
+
document.getElementById('score-improvement').textContent = 'Similar performance';
|
687
|
+
document.getElementById('score-improvement').className = 'score-improvement';
|
688
|
+
}
|
689
|
+
|
690
|
+
// Populate findings
|
691
|
+
this.populateFindings(data);
|
692
|
+
|
693
|
+
// Generate recommendations
|
694
|
+
this.generateRecommendations(data);
|
695
|
+
},
|
696
|
+
|
697
|
+
calculateOptimizationScore(metrics) {
|
698
|
+
let score = 100;
|
699
|
+
|
700
|
+
// Penalize slow queries
|
701
|
+
if (metrics.total_time_ms > 1000) score -= 30;
|
702
|
+
else if (metrics.total_time_ms > 500) score -= 15;
|
703
|
+
else if (metrics.total_time_ms > 100) score -= 5;
|
704
|
+
|
705
|
+
// Penalize high cost
|
706
|
+
if (metrics.query_cost > 10000) score -= 20;
|
707
|
+
else if (metrics.query_cost > 1000) score -= 10;
|
708
|
+
|
709
|
+
// Consider efficiency
|
710
|
+
if (metrics.rows_returned && metrics.total_time_ms) {
|
711
|
+
const efficiency = metrics.rows_returned / metrics.total_time_ms;
|
712
|
+
if (efficiency < 0.1) score -= 15;
|
713
|
+
}
|
714
|
+
|
715
|
+
return Math.max(0, Math.round(score));
|
716
|
+
},
|
717
|
+
|
718
|
+
getScoreGrade(score) {
|
719
|
+
if (score >= 90) return 'A';
|
720
|
+
if (score >= 80) return 'B';
|
721
|
+
if (score >= 70) return 'C';
|
722
|
+
return 'D';
|
723
|
+
},
|
724
|
+
|
725
|
+
getGradeClass(score) {
|
726
|
+
if (score >= 90) return 'grade-a';
|
727
|
+
if (score >= 80) return 'grade-b';
|
728
|
+
if (score >= 70) return 'grade-c';
|
729
|
+
return 'grade-d';
|
730
|
+
},
|
731
|
+
|
732
|
+
populateFindings(data) {
|
733
|
+
const execA = data.executions.a;
|
734
|
+
const execB = data.executions.b;
|
735
|
+
|
736
|
+
// Performance Issues
|
737
|
+
const perfIssues = document.getElementById('performance-issues');
|
738
|
+
perfIssues.innerHTML = '';
|
739
|
+
|
740
|
+
this.analyzePerformanceIssues(execA, 'A', perfIssues);
|
741
|
+
this.analyzePerformanceIssues(execB, 'B', perfIssues);
|
742
|
+
|
743
|
+
if (perfIssues.innerHTML === '') {
|
744
|
+
perfIssues.innerHTML = '<li>No major performance issues detected</li>';
|
745
|
+
}
|
746
|
+
|
747
|
+
// Index Usage Analysis
|
748
|
+
const indexFindings = document.getElementById('index-findings');
|
749
|
+
indexFindings.innerHTML = '';
|
750
|
+
|
751
|
+
this.analyzeIndexUsage(execA, execB, indexFindings);
|
752
|
+
|
753
|
+
// Optimization Opportunities
|
754
|
+
const opportunities = document.getElementById('optimization-opportunities');
|
755
|
+
opportunities.innerHTML = '';
|
756
|
+
|
757
|
+
this.analyzeOptimizationOpportunities(execA, execB, opportunities);
|
758
|
+
|
759
|
+
if (opportunities.innerHTML === '') {
|
760
|
+
opportunities.innerHTML = '<li>Both queries are well-optimized</li>';
|
761
|
+
}
|
762
|
+
},
|
763
|
+
|
764
|
+
analyzePerformanceIssues(exec, label, container) {
|
765
|
+
const metrics = exec.metrics;
|
766
|
+
|
767
|
+
if (metrics.total_time_ms > 5000) {
|
768
|
+
container.innerHTML += `<li>Query ${label}: Very slow execution (${metrics.total_time_ms}ms)</li>`;
|
769
|
+
} else if (metrics.total_time_ms > 1000) {
|
770
|
+
container.innerHTML += `<li>Query ${label}: Slow execution time (${metrics.total_time_ms}ms)</li>`;
|
771
|
+
}
|
772
|
+
|
773
|
+
if (metrics.memory_usage_kb > 100000) {
|
774
|
+
container.innerHTML += `<li>Query ${label}: High memory usage (${metrics.memory_usage_kb}KB)</li>`;
|
775
|
+
}
|
776
|
+
|
777
|
+
if (metrics.sort_methods?.includes('external merge')) {
|
778
|
+
container.innerHTML += `<li>Query ${label}: Sorting spilled to disk</li>`;
|
779
|
+
}
|
780
|
+
|
781
|
+
if (metrics.workers_planned > metrics.workers_launched) {
|
782
|
+
container.innerHTML += `<li>Query ${label}: Only ${metrics.workers_launched}/${metrics.workers_planned} parallel workers launched</li>`;
|
783
|
+
}
|
784
|
+
|
785
|
+
if (metrics.scan_types?.includes('Seq Scan') && metrics.rows_scanned > 100000) {
|
786
|
+
container.innerHTML += `<li>Query ${label}: Large sequential scan (${metrics.rows_scanned} rows)</li>`;
|
787
|
+
}
|
788
|
+
},
|
789
|
+
|
790
|
+
analyzeIndexUsage(execA, execB, container) {
|
791
|
+
const indexCountA = execA.metrics.index_usage?.length || 0;
|
792
|
+
const indexCountB = execB.metrics.index_usage?.length || 0;
|
793
|
+
|
794
|
+
if (indexCountA > 0) {
|
795
|
+
container.innerHTML += `<li>Query A: Uses ${indexCountA} index(es) - ${execA.metrics.index_usage.join(', ')}</li>`;
|
796
|
+
} else {
|
797
|
+
container.innerHTML += '<li>Query A: No indexes used (may cause performance issues)</li>';
|
798
|
+
}
|
799
|
+
|
800
|
+
if (indexCountB > 0) {
|
801
|
+
container.innerHTML += `<li>Query B: Uses ${indexCountB} index(es) - ${execB.metrics.index_usage.join(', ')}</li>`;
|
802
|
+
} else {
|
803
|
+
container.innerHTML += '<li>Query B: No indexes used (may cause performance issues)</li>';
|
804
|
+
}
|
805
|
+
|
806
|
+
const seqScanA = execA.metrics.scan_types?.includes('Seq Scan');
|
807
|
+
const seqScanB = execB.metrics.scan_types?.includes('Seq Scan');
|
808
|
+
|
809
|
+
if (seqScanA && !seqScanB) {
|
810
|
+
container.innerHTML += '<li>Query B has better index utilization than Query A</li>';
|
811
|
+
} else if (seqScanB && !seqScanA) {
|
812
|
+
container.innerHTML += '<li>Query A has better index utilization than Query B</li>';
|
813
|
+
} else if (!seqScanA && !seqScanB) {
|
814
|
+
container.innerHTML += '<li>Both queries efficiently use indexes</li>';
|
815
|
+
}
|
816
|
+
},
|
817
|
+
|
818
|
+
analyzeOptimizationOpportunities(execA, execB, container) {
|
819
|
+
const timeDiffPct = Math.abs(execA.metrics.total_time_ms - execB.metrics.total_time_ms) /
|
820
|
+
Math.max(execA.metrics.total_time_ms, execB.metrics.total_time_ms) * 100;
|
821
|
+
|
822
|
+
if (timeDiffPct > 50) {
|
823
|
+
if (execA.metrics.total_time_ms > execB.metrics.total_time_ms) {
|
824
|
+
container.innerHTML += '<li>Query A could benefit from Query B\'s execution strategy</li>';
|
825
|
+
} else {
|
826
|
+
container.innerHTML += '<li>Query B could benefit from Query A\'s execution strategy</li>';
|
827
|
+
}
|
828
|
+
}
|
829
|
+
|
830
|
+
// Memory optimization opportunities
|
831
|
+
if (execA.metrics.memory_usage_kb > execB.metrics.memory_usage_kb * 2) {
|
832
|
+
container.innerHTML += '<li>Query A uses significantly more memory - consider optimizing</li>';
|
833
|
+
} else if (execB.metrics.memory_usage_kb > execA.metrics.memory_usage_kb * 2) {
|
834
|
+
container.innerHTML += '<li>Query B uses significantly more memory - consider optimizing</li>';
|
835
|
+
}
|
836
|
+
|
837
|
+
// Parallel processing opportunities
|
838
|
+
if (execA.metrics.workers_launched === 0 && execA.metrics.total_time_ms > 1000) {
|
839
|
+
container.innerHTML += '<li>Query A could benefit from parallel execution</li>';
|
840
|
+
}
|
841
|
+
if (execB.metrics.workers_launched === 0 && execB.metrics.total_time_ms > 1000) {
|
842
|
+
container.innerHTML += '<li>Query B could benefit from parallel execution</li>';
|
843
|
+
}
|
844
|
+
|
845
|
+
// Sort optimization
|
846
|
+
if (execA.metrics.sort_methods?.includes('external merge')) {
|
847
|
+
container.innerHTML += '<li>Query A: Increase work_mem to avoid disk-based sorting</li>';
|
848
|
+
}
|
849
|
+
if (execB.metrics.sort_methods?.includes('external merge')) {
|
850
|
+
container.innerHTML += '<li>Query B: Increase work_mem to avoid disk-based sorting</li>';
|
851
|
+
}
|
852
|
+
|
853
|
+
// Index recommendations
|
854
|
+
const seqScanA = execA.metrics.scan_types?.includes('Seq Scan');
|
855
|
+
const seqScanB = execB.metrics.scan_types?.includes('Seq Scan');
|
856
|
+
|
857
|
+
if (seqScanA && execA.metrics.rows_scanned > 10000) {
|
858
|
+
container.innerHTML += '<li>Query A: Consider adding indexes to eliminate sequential scans</li>';
|
859
|
+
}
|
860
|
+
if (seqScanB && execB.metrics.rows_scanned > 10000) {
|
861
|
+
container.innerHTML += '<li>Query B: Consider adding indexes to eliminate sequential scans</li>';
|
862
|
+
}
|
863
|
+
},
|
864
|
+
|
865
|
+
generateRecommendations(data) {
|
866
|
+
const recommendations = document.getElementById('recommendations-list');
|
867
|
+
const execA = data.executions.a;
|
868
|
+
const execB = data.executions.b;
|
869
|
+
|
870
|
+
recommendations.innerHTML = '';
|
871
|
+
|
872
|
+
// Generate specific recommendations based on metrics
|
873
|
+
if (execA.metrics.total_time_ms > 1000 || execB.metrics.total_time_ms > 1000) {
|
874
|
+
recommendations.innerHTML += `
|
875
|
+
<div class="recommendation-item">
|
876
|
+
<div class="recommendation-priority priority-high">HIGH</div>
|
877
|
+
<div class="recommendation-text">Consider adding appropriate indexes to reduce sequential scans and improve query performance.</div>
|
878
|
+
</div>
|
879
|
+
`;
|
880
|
+
}
|
881
|
+
|
882
|
+
if (Math.abs(execA.metrics.total_time_ms - execB.metrics.total_time_ms) > 200) {
|
883
|
+
const fasterQuery = execA.metrics.total_time_ms < execB.metrics.total_time_ms ? 'A' : 'B';
|
884
|
+
recommendations.innerHTML += `
|
885
|
+
<div class="recommendation-item">
|
886
|
+
<div class="recommendation-priority priority-medium">MEDIUM</div>
|
887
|
+
<div class="recommendation-text">Analyze Query ${fasterQuery}'s execution plan to optimize the slower query's performance.</div>
|
888
|
+
</div>
|
889
|
+
`;
|
890
|
+
}
|
891
|
+
|
892
|
+
recommendations.innerHTML += `
|
893
|
+
<div class="recommendation-item">
|
894
|
+
<div class="recommendation-priority priority-low">LOW</div>
|
895
|
+
<div class="recommendation-text">Monitor query performance over time and consider query result caching for frequently accessed data.</div>
|
896
|
+
</div>
|
897
|
+
`;
|
898
|
+
},
|
899
|
+
|
900
|
+
initializePlanToggles() {
|
901
|
+
const toggles = document.querySelectorAll('.plan-toggle');
|
902
|
+
toggles.forEach(toggle => {
|
903
|
+
toggle.addEventListener('click', () => {
|
904
|
+
// Remove active from all toggles
|
905
|
+
toggles.forEach(t => t.classList.remove('active'));
|
906
|
+
// Add active to clicked toggle
|
907
|
+
toggle.classList.add('active');
|
908
|
+
|
909
|
+
// Switch views
|
910
|
+
const view = toggle.dataset.view;
|
911
|
+
this.switchPlanView(view);
|
912
|
+
});
|
913
|
+
});
|
914
|
+
},
|
915
|
+
|
916
|
+
switchPlanView(view) {
|
917
|
+
// Hide all views
|
918
|
+
document.getElementById('side-by-side-view').style.display = 'none';
|
919
|
+
document.getElementById('overlay-view').style.display = 'none';
|
920
|
+
document.getElementById('diff-view').style.display = 'none';
|
921
|
+
|
922
|
+
// Show selected view
|
923
|
+
if (view === 'side-by-side') {
|
924
|
+
document.getElementById('side-by-side-view').style.display = 'grid';
|
925
|
+
} else if (view === 'overlay') {
|
926
|
+
document.getElementById('overlay-view').style.display = 'block';
|
927
|
+
this.renderPlanOverlay();
|
928
|
+
} else if (view === 'diff') {
|
929
|
+
document.getElementById('diff-view').style.display = 'block';
|
930
|
+
this.renderPlanDifferences();
|
931
|
+
}
|
932
|
+
},
|
933
|
+
|
934
|
+
renderPlanDifferences() {
|
935
|
+
const diffSummary = document.getElementById('diff-summary');
|
936
|
+
const diffDetails = document.getElementById('diff-details');
|
937
|
+
|
938
|
+
diffSummary.innerHTML = `
|
939
|
+
<strong>Plan Differences:</strong> Query B uses fewer plan nodes (6 vs 8) and has better index utilization.
|
940
|
+
`;
|
941
|
+
|
942
|
+
diffDetails.innerHTML = `
|
943
|
+
<div style="padding: 8px; background: #dcfce7; border: 1px solid #bbf7d0; border-radius: 3px; margin-bottom: 8px;">
|
944
|
+
<strong>Improved in Query B:</strong> Better index scan usage, reduced sequential scans
|
945
|
+
</div>
|
946
|
+
<div style="padding: 8px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 3px;">
|
947
|
+
<strong>Needs attention in Query A:</strong> Contains sequential scans that could be optimized
|
948
|
+
</div>
|
949
|
+
`;
|
950
|
+
},
|
951
|
+
|
952
|
+
renderPlanOverlay() {
|
953
|
+
const overlayContent = document.getElementById('overlay-content');
|
954
|
+
if (!overlayContent) return;
|
955
|
+
|
956
|
+
// Simulate overlay plan by combining both queries' plan elements
|
957
|
+
// In a real implementation, this would parse actual execution_plan JSON
|
958
|
+
const overlayPlan = this.generateOverlayPlan();
|
959
|
+
overlayContent.innerHTML = overlayPlan;
|
960
|
+
|
961
|
+
// Add interactivity to overlay controls
|
962
|
+
this.initializeOverlayControls();
|
963
|
+
},
|
964
|
+
|
965
|
+
generateOverlayPlan() {
|
966
|
+
// Simulate a merged execution plan showing both queries
|
967
|
+
return `
|
968
|
+
<div class="overlay-node common">
|
969
|
+
<span class="node-text">Hash Join</span>
|
970
|
+
<span class="overlay-metrics">Cost: 1234.56..5678.90</span>
|
971
|
+
</div>
|
972
|
+
|
973
|
+
<div class="overlay-node query-a" style="margin-left: 20px;">
|
974
|
+
<span class="node-text">→ Seq Scan on users (Query A)</span>
|
975
|
+
<span class="overlay-metrics">Cost: 0.00..456.78, Rows: 1000</span>
|
976
|
+
</div>
|
977
|
+
|
978
|
+
<div class="overlay-node query-b" style="margin-left: 20px;">
|
979
|
+
<span class="node-text">→ Index Scan on users (Query B)</span>
|
980
|
+
<span class="overlay-metrics">Cost: 0.42..123.45, Rows: 1000</span>
|
981
|
+
</div>
|
982
|
+
|
983
|
+
<div class="overlay-node different" style="margin-left: 40px;">
|
984
|
+
<span class="node-text">Index: users_email_idx (Query B only)</span>
|
985
|
+
<span class="overlay-metrics">Better performance</span>
|
986
|
+
</div>
|
987
|
+
|
988
|
+
<div class="overlay-node common" style="margin-left: 20px;">
|
989
|
+
<span class="node-text">→ Hash</span>
|
990
|
+
<span class="overlay-metrics">Cost: 234.56..345.67</span>
|
991
|
+
</div>
|
992
|
+
|
993
|
+
<div class="overlay-node common" style="margin-left: 40px;">
|
994
|
+
<span class="node-text">→ Seq Scan on orders</span>
|
995
|
+
<span class="overlay-metrics">Cost: 0.00..567.89, Rows: 5000</span>
|
996
|
+
</div>
|
997
|
+
|
998
|
+
<div class="overlay-node different">
|
999
|
+
<span class="node-text">Sort (Different ordering)</span>
|
1000
|
+
<span class="overlay-metrics">Query A: created_at, Query B: updated_at</span>
|
1001
|
+
</div>
|
1002
|
+
|
1003
|
+
<div class="overlay-node query-a" style="margin-left: 20px;">
|
1004
|
+
<span class="node-text">→ Sort Key: created_at DESC (Query A)</span>
|
1005
|
+
<span class="overlay-metrics">Cost: 789.01..890.12</span>
|
1006
|
+
</div>
|
1007
|
+
|
1008
|
+
<div class="overlay-node query-b" style="margin-left: 20px;">
|
1009
|
+
<span class="node-text">→ Sort Key: updated_at DESC (Query B)</span>
|
1010
|
+
<span class="overlay-metrics">Cost: 678.90..789.01</span>
|
1011
|
+
</div>
|
1012
|
+
`;
|
1013
|
+
},
|
1014
|
+
|
1015
|
+
initializeOverlayControls() {
|
1016
|
+
const highlightDifferencesCheckbox = document.getElementById('highlight-differences');
|
1017
|
+
const showMetricsCheckbox = document.getElementById('show-metrics');
|
1018
|
+
|
1019
|
+
if (highlightDifferencesCheckbox) {
|
1020
|
+
highlightDifferencesCheckbox.addEventListener('change', (e) => {
|
1021
|
+
const overlayNodes = document.querySelectorAll('.overlay-node');
|
1022
|
+
overlayNodes.forEach(node => {
|
1023
|
+
if (e.target.checked) {
|
1024
|
+
// Highlight differences more prominently
|
1025
|
+
if (node.classList.contains('different')) {
|
1026
|
+
node.style.boxShadow = '0 0 0 2px #ef4444';
|
1027
|
+
}
|
1028
|
+
} else {
|
1029
|
+
node.style.boxShadow = 'none';
|
1030
|
+
}
|
1031
|
+
});
|
1032
|
+
});
|
1033
|
+
}
|
1034
|
+
|
1035
|
+
if (showMetricsCheckbox) {
|
1036
|
+
showMetricsCheckbox.addEventListener('change', (e) => {
|
1037
|
+
const metrics = document.querySelectorAll('.overlay-metrics');
|
1038
|
+
metrics.forEach(metric => {
|
1039
|
+
metric.style.display = e.target.checked ? 'block' : 'none';
|
1040
|
+
});
|
1041
|
+
});
|
1042
|
+
}
|
1043
|
+
},
|
1044
|
+
|
1045
|
+
getPerformanceImpact(timeDifferencePct) {
|
1046
|
+
if (!timeDifferencePct) return 'Negligible';
|
1047
|
+
const diff = Math.abs(timeDifferencePct);
|
1048
|
+
|
1049
|
+
if (diff > 50) return 'Critical';
|
1050
|
+
if (diff > 25) return 'High';
|
1051
|
+
if (diff > 10) return 'Moderate';
|
1052
|
+
return 'Low';
|
1053
|
+
},
|
1054
|
+
|
1055
|
+
getCostImpact(costDifferencePct) {
|
1056
|
+
if (!costDifferencePct) return 'Same';
|
1057
|
+
const diff = Math.abs(costDifferencePct);
|
1058
|
+
|
1059
|
+
if (diff > 100) return 'Very High';
|
1060
|
+
if (diff > 50) return 'High';
|
1061
|
+
if (diff > 20) return 'Moderate';
|
1062
|
+
return 'Low';
|
1063
|
+
},
|
1064
|
+
|
1065
|
+
calculatePercentageDifference(valueA, valueB) {
|
1066
|
+
if (!valueA || !valueB || valueA === valueB) return null;
|
1067
|
+
|
1068
|
+
const numA = parseFloat(valueA);
|
1069
|
+
const numB = parseFloat(valueB);
|
1070
|
+
|
1071
|
+
if (isNaN(numA) || isNaN(numB)) return null;
|
1072
|
+
|
1073
|
+
const percentDiff = Math.abs(((numB - numA) / numA) * 100);
|
1074
|
+
const fasterQuery = numA < numB ? 'A' : 'B';
|
1075
|
+
|
1076
|
+
if (percentDiff < 1) return null; // Less than 1% difference is negligible
|
1077
|
+
|
1078
|
+
return `${percentDiff.toFixed(1)}% ${fasterQuery} faster`;
|
1079
|
+
},
|
1080
|
+
|
1081
|
+
updateHistoryCount(count) {
|
1082
|
+
document.querySelector('.history-count').textContent = `(${count})`;
|
1083
|
+
},
|
1084
|
+
|
1085
|
+
showHistoryError() {
|
1086
|
+
const loadingEl = document.querySelector('.history-loading');
|
1087
|
+
const itemsEl = document.querySelector('.history-items');
|
1088
|
+
|
1089
|
+
loadingEl.innerHTML = '<span style="color: #dc2626;">Failed to load query history</span>';
|
1090
|
+
itemsEl.style.display = 'none';
|
1091
|
+
},
|
1092
|
+
|
1093
|
+
showComparisonError(message) {
|
1094
|
+
document.getElementById('comparison-loading').style.display = 'none';
|
1095
|
+
document.getElementById('comparison-results').style.display = 'none';
|
1096
|
+
document.getElementById('comparison-empty').innerHTML = `
|
1097
|
+
<div class="empty-icon">⚠️</div>
|
1098
|
+
<h3>Comparison Failed</h3>
|
1099
|
+
<p>${message}</p>
|
1100
|
+
`;
|
1101
|
+
document.getElementById('comparison-empty').style.display = 'block';
|
1102
|
+
},
|
1103
|
+
|
1104
|
+
initializeCompareTab() {
|
1105
|
+
const compareTab = document.getElementById('compare-tab');
|
1106
|
+
if (!compareTab || compareTab.dataset.initialized) return;
|
1107
|
+
|
1108
|
+
compareTab.addEventListener('click', (e) => {
|
1109
|
+
e.preventDefault();
|
1110
|
+
|
1111
|
+
// Skip if disabled (let tooltip show help)
|
1112
|
+
if (compareTab.classList.contains('disabled')) return;
|
1113
|
+
|
1114
|
+
this.activateCompareTab();
|
1115
|
+
this.loadComparisonInterface();
|
1116
|
+
});
|
1117
|
+
|
1118
|
+
compareTab.dataset.initialized = 'true';
|
1119
|
+
}
|
1120
|
+
};
|
1121
|
+
|
1122
|
+
// Initialize the comparison system
|
1123
|
+
QueryComparison.init();
|
1124
|
+
|
1125
|
+
// Refresh history when a new analysis completes
|
1126
|
+
document.addEventListener('analysisCompleted', function() {
|
1127
|
+
QueryComparison.loadQueryHistory();
|
1128
|
+
});
|
1129
|
+
});
|