pghero 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pghero might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +2 -2
- data/app/controllers/pg_hero/home_controller.rb +12 -7
- data/app/views/layouts/pg_hero/application.html.erb +0 -3
- data/app/views/pg_hero/home/_queries_table.html.erb +5 -4
- data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +85 -155
- data/lib/pghero.rb +116 -74
- data/lib/pghero/version.rb +1 -1
- data/pghero.gemspec +2 -2
- data/test/best_index_test.rb +42 -2
- data/test/test_helper.rb +2 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3975a93a4da7c4199e2e3a5f98c4c98e35c991e9
|
4
|
+
data.tar.gz: 7a2a1ba863c86c42d09316c624cbd9fa411bda41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 745ac33fea1c485e3263075b4a17c04b79894f240776a65331818a160b8ef3a0f308f362aeb590c433be96e1f684dd36d764ae3087bf16ef4826d284c168f8c7
|
7
|
+
data.tar.gz: c671471aa2a742f428a73b8d05c4af7624baa324e80654ae3d20c3b4fbf1c615ee942860b383c6c03c76ae798efe27476f4adb2152a4744f007de924bdbcc0c5
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## 1.2.1
|
2
|
+
|
3
|
+
- Better suggested indexes
|
4
|
+
- Removed unused indexes noise
|
5
|
+
- Removed autovacuum danger noise
|
6
|
+
- Removed maintenance tab
|
7
|
+
- Fixed suggested indexes for replicas
|
8
|
+
- Fixed issue w/ suggested indexes where same table name exists in multiple schemas
|
9
|
+
|
1
10
|
## 1.2.0
|
2
11
|
|
3
12
|
- Added suggested indexes
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# PgHero
|
2
2
|
|
3
|
-
|
3
|
+
The missing dashboard for Postgres
|
4
4
|
|
5
|
-
[
|
5
|
+
[See it in action](https://pghero.herokuapp.com/)
|
6
6
|
|
7
7
|
[![Screenshot](https://pghero.herokuapp.com/assets/screenshot-34a33ee68c77d64c1f89f143f6297a47.png)](https://pghero.herokuapp.com/)
|
8
8
|
|
@@ -16,7 +16,12 @@ module PgHero
|
|
16
16
|
@long_running_queries = PgHero.long_running_queries
|
17
17
|
@index_hit_rate = PgHero.index_hit_rate
|
18
18
|
@table_hit_rate = PgHero.table_hit_rate
|
19
|
-
@missing_indexes =
|
19
|
+
@missing_indexes =
|
20
|
+
if PgHero.suggested_indexes_enabled?
|
21
|
+
[]
|
22
|
+
else
|
23
|
+
PgHero.missing_indexes
|
24
|
+
end
|
20
25
|
@unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
|
21
26
|
@invalid_indexes = PgHero.invalid_indexes
|
22
27
|
@duplicate_indexes = PgHero.duplicate_indexes
|
@@ -28,9 +33,8 @@ module PgHero
|
|
28
33
|
@replication_lag = PgHero.replication_lag
|
29
34
|
@good_replication_lag = @replication_lag < 5
|
30
35
|
end
|
31
|
-
@transaction_id_danger = PgHero.transaction_id_danger
|
32
|
-
|
33
|
-
set_suggested_indexes
|
36
|
+
@transaction_id_danger = PgHero.transaction_id_danger(threshold: 1500000000)
|
37
|
+
set_suggested_indexes((params[:min_average_time] || 20).to_f, (params[:min_calls] || 50).to_i)
|
34
38
|
@show_migrations = PgHero.show_migrations
|
35
39
|
end
|
36
40
|
|
@@ -56,7 +60,6 @@ module PgHero
|
|
56
60
|
@sort = %w[average_time calls].include?(params[:sort]) ? params[:sort] : nil
|
57
61
|
@min_average_time = params[:min_average_time] ? params[:min_average_time].to_i : nil
|
58
62
|
@min_calls = params[:min_calls] ? params[:min_calls].to_i : nil
|
59
|
-
@debug = params[:debug] == "true"
|
60
63
|
|
61
64
|
@query_stats =
|
62
65
|
begin
|
@@ -199,9 +202,11 @@ module PgHero
|
|
199
202
|
@replica = PgHero.replica?
|
200
203
|
end
|
201
204
|
|
202
|
-
def set_suggested_indexes
|
203
|
-
@suggested_indexes_by_query = PgHero.suggested_indexes_by_query(query_stats: @query_stats)
|
205
|
+
def set_suggested_indexes(min_average_time = 0, min_calls = 0)
|
206
|
+
@suggested_indexes_by_query = PgHero.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs["average_time"].to_f >= min_average_time && qs["calls"].to_i >= min_calls })
|
204
207
|
@suggested_indexes = PgHero.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query)
|
208
|
+
@query_stats_by_query = @query_stats.index_by { |q| q["query"] }
|
209
|
+
@debug = params[:debug] == "true"
|
205
210
|
end
|
206
211
|
end
|
207
212
|
end
|
@@ -420,9 +420,6 @@
|
|
420
420
|
<li class="<%= controller.action_name == "index_usage" ? "active" : "" %>"><%= link_to "Index Usage", index_usage_path %></li>
|
421
421
|
<li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
|
422
422
|
<li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
|
423
|
-
<% unless @replica %>
|
424
|
-
<li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
|
425
|
-
<% end %>
|
426
423
|
<li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
|
427
424
|
<li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
|
428
425
|
<li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
|
@@ -42,18 +42,19 @@
|
|
42
42
|
</tr>
|
43
43
|
<tr>
|
44
44
|
<td colspan="3" style="border-top: none; padding: 0;">
|
45
|
-
<pre><%= query["query"] %></pre>
|
45
|
+
<code><pre style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query["query"] %></pre></code>
|
46
46
|
<% if query["query"] == "<insufficient privilege>" %>
|
47
47
|
<p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
|
48
48
|
<% end %>
|
49
|
-
<% if (i2 = @suggested_indexes_by_query[query["query"]]) %>
|
49
|
+
<% if local_assigns[:suggested_indexes] != false && (i2 = @suggested_indexes_by_query[query["query"]]) %>
|
50
50
|
<% if (index = i2[:index]) && !i2[:covering_index] %>
|
51
51
|
<%= render partial: "suggested_index", locals: {index: index} %>
|
52
52
|
<% end %>
|
53
53
|
<% if @debug %>
|
54
|
-
<code><pre style="color: #f0ad4e; background-color: #333;"><% if i2[:explanation] %><%= i2[:explanation] %><%
|
54
|
+
<code><pre style="color: #f0ad4e; background-color: #333;"><% if i2[:explanation] %><%= i2[:explanation] %><% end %>
|
55
|
+
<% if i2[:row_estimates] %>Rows: <%= i2[:rows] %>
|
55
56
|
Row estimates: <%= i2[:row_estimates].to_a.map { |k, v| "#{k}=#{v}" }.join(", ") %>
|
56
|
-
Row
|
57
|
+
Row progression: <%= i2[:row_progression].to_a.join(", ") %><% end %></pre></code>
|
57
58
|
<% end %>
|
58
59
|
<% end %>
|
59
60
|
</td>
|
@@ -1 +1 @@
|
|
1
|
-
<code><pre style="color: #eee; background-color: #333;">CREATE INDEX CONCURRENTLY ON <%= index[:table] %> (<%= index[:columns].join(", ") %>)</pre></code>
|
1
|
+
<code><pre style="color: #eee; background-color: #333;">CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</pre></code>
|
@@ -1,30 +1,3 @@
|
|
1
|
-
<% if @transaction_id_danger.any? %>
|
2
|
-
<div class="alert alert-danger">
|
3
|
-
Database shutdown imminent due to transaction ID wraparound failure
|
4
|
-
</div>
|
5
|
-
<div class="content">
|
6
|
-
<p>For each table, run:</p>
|
7
|
-
<code><pre>VACUUM FREEZE VERBOSE table</pre></code>
|
8
|
-
<p><%= link_to "Read more", "http://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND", target: "_blank" %></p>
|
9
|
-
<table class="table">
|
10
|
-
<thead>
|
11
|
-
<tr>
|
12
|
-
<th>Table</th>
|
13
|
-
<th style="width: 20%;">Transactions Before Shutdown</th>
|
14
|
-
</tr>
|
15
|
-
</thead>
|
16
|
-
<tbody>
|
17
|
-
<% @transaction_id_danger.each do |query| %>
|
18
|
-
<tr>
|
19
|
-
<td><%= query["table"] %></td>
|
20
|
-
<td><%= number_with_delimiter(query["transactions_before_shutdown"]) %></td>
|
21
|
-
</tr>
|
22
|
-
<% end %>
|
23
|
-
</tbody>
|
24
|
-
</table>
|
25
|
-
</div>
|
26
|
-
<% end %>
|
27
|
-
|
28
1
|
<div id="status">
|
29
2
|
<% if @replica %>
|
30
3
|
<div class="alert alert-<%= @good_replication_lag ? "success" : "warning" %>">
|
@@ -43,15 +16,6 @@
|
|
43
16
|
No long running queries
|
44
17
|
<% end %>
|
45
18
|
</div>
|
46
|
-
<% if PgHero.suggested_indexes_enabled? %>
|
47
|
-
<div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
|
48
|
-
<% if @suggested_indexes.any? %>
|
49
|
-
<%= pluralize(@suggested_indexes.size, "suggested index", "suggested indexes") %>
|
50
|
-
<% else %>
|
51
|
-
No suggested indexes
|
52
|
-
<% end %>
|
53
|
-
</div>
|
54
|
-
<% end %>
|
55
19
|
<div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
|
56
20
|
<% if @good_cache_rate %>
|
57
21
|
Cache hit rate above <%= PgHero.cache_hit_rate_threshold %>%
|
@@ -67,20 +31,11 @@
|
|
67
31
|
<% end %>
|
68
32
|
<span class="tiny"><%= @total_connections %></span>
|
69
33
|
</div>
|
70
|
-
<div class="alert alert-<%= @
|
71
|
-
<% if @
|
72
|
-
<%= pluralize(@
|
34
|
+
<div class="alert alert-<%= @transaction_id_danger.empty? ? "success" : "warning" %>">
|
35
|
+
<% if @transaction_id_danger.any? %>
|
36
|
+
<%= pluralize(@transaction_id_danger.size, "table") %> not vacuuming properly
|
73
37
|
<% else %>
|
74
|
-
|
75
|
-
<% end %>
|
76
|
-
</div>
|
77
|
-
<div class="alert alert-<%= @query_stats_enabled && @slow_queries.empty? ? "success" : "warning" %>">
|
78
|
-
<% if !@query_stats_enabled %>
|
79
|
-
Query stats must be enabled for slow queries
|
80
|
-
<% elsif @slow_queries.any? %>
|
81
|
-
<%= pluralize(@slow_queries.size, "slow query") %>
|
82
|
-
<% else %>
|
83
|
-
No slow queries
|
38
|
+
Vacuuming healthy
|
84
39
|
<% end %>
|
85
40
|
</div>
|
86
41
|
<div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
|
@@ -97,18 +52,30 @@
|
|
97
52
|
No duplicate indexes
|
98
53
|
<% end %>
|
99
54
|
</div>
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
55
|
+
<% if PgHero.suggested_indexes_enabled? %>
|
56
|
+
<div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
|
57
|
+
<% if @suggested_indexes.any? %>
|
58
|
+
<%= pluralize(@suggested_indexes.size, "suggested index", "suggested indexes") %>
|
59
|
+
<% else %>
|
60
|
+
No suggested indexes
|
61
|
+
<% end %>
|
62
|
+
</div>
|
63
|
+
<% else %>
|
64
|
+
<div class="alert alert-<%= @missing_indexes.empty? ? "success" : "warning" %>">
|
65
|
+
<% if @missing_indexes.any? %>
|
66
|
+
<%= pluralize(@missing_indexes.size, "table appears", "tables appear") %> to be missing indexes
|
67
|
+
<% else %>
|
68
|
+
No missing indexes
|
69
|
+
<% end %>
|
70
|
+
</div>
|
71
|
+
<% end %>
|
72
|
+
<div class="alert alert-<%= @query_stats_enabled && @slow_queries.empty? ? "success" : "warning" %>">
|
73
|
+
<% if !@query_stats_enabled %>
|
74
|
+
Query stats must be enabled for slow queries
|
75
|
+
<% elsif @slow_queries.any? %>
|
76
|
+
<%= pluralize(@slow_queries.size, "slow query") %>
|
110
77
|
<% else %>
|
111
|
-
No
|
78
|
+
No slow queries
|
112
79
|
<% end %>
|
113
80
|
</div>
|
114
81
|
</div>
|
@@ -130,35 +97,6 @@
|
|
130
97
|
</div>
|
131
98
|
<% end %>
|
132
99
|
|
133
|
-
<% if @suggested_indexes.any? %>
|
134
|
-
<div class="content">
|
135
|
-
<h1>Suggested Indexes</h1>
|
136
|
-
<p>
|
137
|
-
Add indexes to speed up queries.
|
138
|
-
<% if @show_migrations %>
|
139
|
-
Here’s a
|
140
|
-
<a href="javascript: void(0);" onclick="document.getElementById('migration3').style.display = 'block';">migration</a> to help.
|
141
|
-
<% end %>
|
142
|
-
</p>
|
143
|
-
|
144
|
-
<div id="migration3" style="display: none;">
|
145
|
-
<pre>rails g migration add_suggested_indexes</pre>
|
146
|
-
<p>And paste</p>
|
147
|
-
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @suggested_indexes.each do |index| %>
|
148
|
-
add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(" ,") %>], algorithm: :concurrently<% end %></pre>
|
149
|
-
</div>
|
150
|
-
|
151
|
-
<% @suggested_indexes.each_with_index do |index, i| %>
|
152
|
-
<hr />
|
153
|
-
<%= render partial: "suggested_index", locals: {index: index} %>
|
154
|
-
<p>to speed up</p>
|
155
|
-
<% index[:queries].each do |query| %>
|
156
|
-
<code><pre><%= query %></pre></code>
|
157
|
-
<% end %>
|
158
|
-
<% end %>
|
159
|
-
</div>
|
160
|
-
<% end %>
|
161
|
-
|
162
100
|
<% if !@good_cache_rate %>
|
163
101
|
<div class="content">
|
164
102
|
<h1>Low Cache Hit Rate</h1>
|
@@ -183,24 +121,24 @@ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym
|
|
183
121
|
</div>
|
184
122
|
<% end %>
|
185
123
|
|
186
|
-
<% if @
|
124
|
+
<% if @transaction_id_danger.any? %>
|
187
125
|
<div class="content">
|
188
|
-
<
|
126
|
+
<h2>Vacuuming Needed</h2>
|
127
|
+
<p>The database <strong>will shutdown</strong> when there are fewer than 1,000,000 transactions left. <%= link_to "Read more", "http://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND", target: "_blank" %>.</p>
|
189
128
|
<p>For each table, run:</p>
|
190
129
|
<code><pre>VACUUM FREEZE VERBOSE table</pre></code>
|
191
|
-
<p><%= link_to "Read more", "http://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND", target: "_blank" %></p>
|
192
130
|
<table class="table">
|
193
131
|
<thead>
|
194
132
|
<tr>
|
195
133
|
<th>Table</th>
|
196
|
-
<th style="width: 20%;">Transactions
|
134
|
+
<th style="width: 20%;">Transactions Left</th>
|
197
135
|
</tr>
|
198
136
|
</thead>
|
199
137
|
<tbody>
|
200
|
-
<% @
|
138
|
+
<% @transaction_id_danger.each do |query| %>
|
201
139
|
<tr>
|
202
140
|
<td><%= query["table"] %></td>
|
203
|
-
<td><%= number_with_delimiter(query["
|
141
|
+
<td><%= number_with_delimiter(query["transactions_before_shutdown"]) %></td>
|
204
142
|
</tr>
|
205
143
|
<% end %>
|
206
144
|
</tbody>
|
@@ -208,35 +146,6 @@ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym
|
|
208
146
|
</div>
|
209
147
|
<% end %>
|
210
148
|
|
211
|
-
<% if !@query_stats_enabled %>
|
212
|
-
<div class="content">
|
213
|
-
<h1>Query Stats</h1>
|
214
|
-
|
215
|
-
<% if @query_stats_available %>
|
216
|
-
<p>
|
217
|
-
Query stats are available but not enabled.
|
218
|
-
<%= button_to "Enable", enable_query_stats_path, class: "btn btn-info" %>
|
219
|
-
</p>
|
220
|
-
<% else %>
|
221
|
-
<p>Make them available by adding the following lines to <code>postgresql.conf</code>:</p>
|
222
|
-
<pre>shared_preload_libraries = 'pg_stat_statements'
|
223
|
-
pg_stat_statements.track = all</pre>
|
224
|
-
<p>Restart the server for the changes to take effect.</p>
|
225
|
-
<% end %>
|
226
|
-
</div>
|
227
|
-
<% end %>
|
228
|
-
|
229
|
-
<% if @query_stats_enabled && @slow_queries.any? %>
|
230
|
-
<div class="content">
|
231
|
-
<h1>Slow Queries</h1>
|
232
|
-
|
233
|
-
<p>Slow queries take <%= PgHero.slow_query_ms %> ms or more on average and have been called at least <%= PgHero.slow_query_calls %> times.</p>
|
234
|
-
<p><%= link_to "Explain queries", explain_path %> to see where to add indexes.</p>
|
235
|
-
|
236
|
-
<%= render partial: "queries_table", locals: {queries: @slow_queries} %>
|
237
|
-
</div>
|
238
|
-
<% end %>
|
239
|
-
|
240
149
|
<% if @invalid_indexes.any? %>
|
241
150
|
<div class="content">
|
242
151
|
<h1>Invalid Indexes</h1>
|
@@ -303,6 +212,36 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
|
|
303
212
|
</div>
|
304
213
|
<% end %>
|
305
214
|
|
215
|
+
<% if @suggested_indexes.any? %>
|
216
|
+
<div class="content">
|
217
|
+
<h1>Suggested Indexes</h1>
|
218
|
+
<p>
|
219
|
+
Add indexes to speed up queries.
|
220
|
+
<% if @show_migrations %>
|
221
|
+
Here’s a
|
222
|
+
<a href="javascript: void(0);" onclick="document.getElementById('migration3').style.display = 'block';">migration</a> to help.
|
223
|
+
<% end %>
|
224
|
+
</p>
|
225
|
+
|
226
|
+
<div id="migration3" style="display: none;">
|
227
|
+
<pre>rails g migration add_suggested_indexes</pre>
|
228
|
+
<p>And paste</p>
|
229
|
+
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @suggested_indexes.each do |index| %>
|
230
|
+
<% if index[:using] == "gist" %>
|
231
|
+
connection.execute("CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)")
|
232
|
+
<% else %>
|
233
|
+
add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(" ,") %>], algorithm: :concurrently<% end %><% end %></pre>
|
234
|
+
</div>
|
235
|
+
|
236
|
+
<% @suggested_indexes.each_with_index do |index, i| %>
|
237
|
+
<hr />
|
238
|
+
<%= render partial: "suggested_index", locals: {index: index} %>
|
239
|
+
<p>to speed up</p>
|
240
|
+
<%= render partial: "queries_table", locals: {queries: index[:queries].map { |q| @query_stats_by_query[q] }, suggested_indexes: false} %>
|
241
|
+
<% end %>
|
242
|
+
</div>
|
243
|
+
<% end %>
|
244
|
+
|
306
245
|
<% if @missing_indexes.any? %>
|
307
246
|
<div class="content">
|
308
247
|
<h1>Missing Indexes</h1>
|
@@ -330,40 +269,31 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
|
|
330
269
|
</div>
|
331
270
|
<% end %>
|
332
271
|
|
333
|
-
<% if
|
272
|
+
<% if !@query_stats_enabled %>
|
334
273
|
<div class="content">
|
335
|
-
<h1>
|
274
|
+
<h1>Query Stats</h1>
|
336
275
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
276
|
+
<% if @query_stats_available %>
|
277
|
+
<p>
|
278
|
+
Query stats are available but not enabled.
|
279
|
+
<%= button_to "Enable", enable_query_stats_path, class: "btn btn-info" %>
|
280
|
+
</p>
|
281
|
+
<% else %>
|
282
|
+
<p>Make them available by adding the following lines to <code>postgresql.conf</code>:</p>
|
283
|
+
<pre>shared_preload_libraries = 'pg_stat_statements'
|
284
|
+
pg_stat_statements.track = all</pre>
|
285
|
+
<p>Restart the server for the changes to take effect.</p>
|
286
|
+
<% end %>
|
287
|
+
</div>
|
288
|
+
<% end %>
|
344
289
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
|
349
|
-
remove_index <%= query["table"].to_sym.inspect %>, name: <%= query["index"].to_s.inspect %><% end %></pre>
|
350
|
-
</div>
|
290
|
+
<% if @query_stats_enabled && @slow_queries.any? %>
|
291
|
+
<div class="content">
|
292
|
+
<h1>Slow Queries</h1>
|
351
293
|
|
352
|
-
<
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
<th style="width: 20%;">Index Size</th>
|
357
|
-
</tr>
|
358
|
-
</thead>
|
359
|
-
<tbody>
|
360
|
-
<% @unused_indexes.each do |query| %>
|
361
|
-
<tr>
|
362
|
-
<td><%= query["index"] %><div class="text-muted">on <%= query["table"] %></div></td>
|
363
|
-
<td><%= query["index_size"] %></td>
|
364
|
-
</tr>
|
365
|
-
<% end %>
|
366
|
-
</tbody>
|
367
|
-
</table>
|
294
|
+
<p>Slow queries take <%= PgHero.slow_query_ms %> ms or more on average and have been called at least <%= PgHero.slow_query_calls %> times.</p>
|
295
|
+
<p><%= link_to "Explain queries", explain_path %> to see where to add indexes.</p>
|
296
|
+
|
297
|
+
<%= render partial: "queries_table", locals: {queries: @slow_queries} %>
|
368
298
|
</div>
|
369
299
|
<% end %>
|
data/lib/pghero.rb
CHANGED
@@ -375,7 +375,8 @@ module PgHero
|
|
375
375
|
# "the system will shut down and refuse to start any new transactions
|
376
376
|
# once there are fewer than 1 million transactions left until wraparound"
|
377
377
|
# warn when 10,000,000 transactions left
|
378
|
-
def transaction_id_danger
|
378
|
+
def transaction_id_danger(options = {})
|
379
|
+
threshold = options[:threshold] || 10000000
|
379
380
|
select_all <<-SQL
|
380
381
|
SELECT
|
381
382
|
c.oid::regclass::text AS table,
|
@@ -386,9 +387,9 @@ module PgHero
|
|
386
387
|
pg_class t ON c.reltoastrelid = t.oid
|
387
388
|
WHERE
|
388
389
|
c.relkind = 'r'
|
389
|
-
AND (2146483648 - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) <
|
390
|
+
AND (2146483648 - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{threshold}
|
390
391
|
ORDER BY
|
391
|
-
|
392
|
+
2, 1
|
392
393
|
SQL
|
393
394
|
end
|
394
395
|
|
@@ -805,15 +806,17 @@ module PgHero
|
|
805
806
|
best_indexes = best_index_helper(queries)
|
806
807
|
|
807
808
|
if best_indexes.any?
|
808
|
-
existing_columns = Hash.new { |hash, key| hash[key] = [] }
|
809
|
-
self.indexes.each do |
|
810
|
-
|
809
|
+
existing_columns = Hash.new { |hash, key| hash[key] = Hash.new { |hash2, key2| hash2[key2] = [] } }
|
810
|
+
self.indexes.group_by { |g| g["using"] }.each do |group, inds|
|
811
|
+
inds.each do |i|
|
812
|
+
existing_columns[group][i["table"]] << i["columns"]
|
813
|
+
end
|
811
814
|
end
|
812
815
|
|
813
816
|
best_indexes.each do |query, best_index|
|
814
817
|
if best_index[:found]
|
815
818
|
index = best_index[:index]
|
816
|
-
covering_index = existing_columns[index[:table]].find { |e| index_covers?(e, index[:columns]) }
|
819
|
+
covering_index = existing_columns[index[:using] || "btree"][index[:table]].find { |e| index_covers?(e, index[:columns]) }
|
817
820
|
if covering_index
|
818
821
|
best_index[:covering_index] = covering_index
|
819
822
|
best_index[:explanation] = "Covered by index on (#{covering_index.join(", ")})"
|
@@ -859,22 +862,41 @@ module PgHero
|
|
859
862
|
end
|
860
863
|
|
861
864
|
def column_stats(options = {})
|
865
|
+
schema = options[:schema]
|
862
866
|
tables = options[:table] ? Array(options[:table]) : nil
|
863
867
|
select_all <<-SQL
|
864
868
|
SELECT
|
869
|
+
schemaname AS schema,
|
865
870
|
tablename AS table,
|
866
871
|
attname AS column,
|
867
872
|
null_frac,
|
868
|
-
n_distinct
|
869
|
-
n_live_tup
|
873
|
+
n_distinct
|
870
874
|
FROM
|
871
875
|
pg_stats
|
876
|
+
WHERE
|
877
|
+
#{tables ? "tablename IN (#{tables.map { |t| quote(t) }.join(", ")})" : "1 = 1"}
|
878
|
+
AND schemaname = #{quote(schema)}
|
879
|
+
ORDER BY
|
880
|
+
1, 2, 3
|
881
|
+
SQL
|
882
|
+
end
|
883
|
+
|
884
|
+
def table_stats(options = {})
|
885
|
+
schema = options[:schema]
|
886
|
+
tables = options[:table] ? Array(options[:table]) : nil
|
887
|
+
select_all <<-SQL
|
888
|
+
SELECT
|
889
|
+
nspname AS schema,
|
890
|
+
relname AS table,
|
891
|
+
reltuples::bigint
|
892
|
+
FROM
|
893
|
+
pg_class
|
872
894
|
INNER JOIN
|
873
|
-
|
874
|
-
INNER JOIN
|
875
|
-
pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
|
895
|
+
pg_namespace ON pg_namespace.oid = pg_class.relnamespace
|
876
896
|
WHERE
|
877
|
-
|
897
|
+
relkind = 'r'
|
898
|
+
AND nspname = #{quote(schema)}
|
899
|
+
#{tables ? "AND relname IN (#{tables.map { |t| quote(t) }.join(", ")})" : nil}
|
878
900
|
ORDER BY
|
879
901
|
1, 2
|
880
902
|
SQL
|
@@ -893,8 +915,11 @@ module PgHero
|
|
893
915
|
|
894
916
|
# get stats about columns for relevant tables
|
895
917
|
tables = parts.values.map { |t| t[:table] }.uniq
|
918
|
+
# TODO get schema from query structure, then try search path
|
919
|
+
schema = connection_model.connection_config[:schema] || "public"
|
896
920
|
if tables.any?
|
897
|
-
|
921
|
+
row_stats = Hash[self.table_stats(table: tables, schema: schema).map { |i| [i["table"], i["reltuples"]] }]
|
922
|
+
column_stats = self.column_stats(table: tables, schema: schema).group_by { |i| i["table"] }
|
898
923
|
end
|
899
924
|
|
900
925
|
# find best index based on query structure and column stats
|
@@ -909,61 +934,68 @@ module PgHero
|
|
909
934
|
index[:structure] = structure
|
910
935
|
|
911
936
|
table = structure[:table]
|
912
|
-
where = structure[:where]
|
937
|
+
where = structure[:where].uniq
|
913
938
|
sort = structure[:sort]
|
914
939
|
|
915
|
-
|
940
|
+
total_rows = row_stats[table].to_i
|
941
|
+
index[:rows] = total_rows
|
916
942
|
|
943
|
+
ranks = Hash[column_stats[table].to_a.map { |r| [r["column"], r] }]
|
917
944
|
columns = (where + sort).map { |c| c[:column] }.uniq
|
918
945
|
|
919
|
-
if columns.any?
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
where = where.sort_by { |c| [row_estimates(ranks[c[:column]], nil, c[:op]), c[:column]] } + sort
|
925
|
-
|
926
|
-
index[:row_estimates] = Hash[where.map { |c| [c[:column], row_estimates(ranks[c[:column]], nil, c[:op]).round] }]
|
927
|
-
|
928
|
-
rows_left = ranks[where.first[:column]]["n_live_tup"].to_i
|
929
|
-
index[:rows] = rows_left
|
930
|
-
|
931
|
-
# no index needed if less than 500 rows
|
932
|
-
if rows_left >= 500
|
933
|
-
|
934
|
-
# if most values are unique, no need to index others
|
935
|
-
final_where = []
|
936
|
-
prev_rows_left = [rows_left]
|
937
|
-
where.each do |c|
|
938
|
-
next if final_where.include?(c[:column])
|
939
|
-
final_where << c[:column]
|
940
|
-
rows_left = row_estimates(ranks[c[:column]], rows_left, c[:op])
|
941
|
-
prev_rows_left << rows_left
|
942
|
-
if rows_left < 50
|
943
|
-
break
|
944
|
-
end
|
946
|
+
if columns.any?
|
947
|
+
if columns.all? { |c| ranks[c] }
|
948
|
+
first_desc = sort.index { |c| c[:direction] == "desc" }
|
949
|
+
if first_desc
|
950
|
+
sort = sort.first(first_desc + 1)
|
945
951
|
end
|
952
|
+
where = where.sort_by { |c| [row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]), c[:column]] } + sort
|
953
|
+
|
954
|
+
index[:row_estimates] = Hash[where.map { |c| [c[:column], row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }]
|
955
|
+
|
956
|
+
# no index needed if less than 500 rows
|
957
|
+
if total_rows >= 500
|
958
|
+
|
959
|
+
if ["~~", "~~*"].include?(where.first[:op])
|
960
|
+
index[:found] = true
|
961
|
+
index[:index] = {table: table, columns: ["#{where.first[:column]} gist_trgm_ops"], using: "gist"}
|
962
|
+
else
|
963
|
+
# if most values are unique, no need to index others
|
964
|
+
rows_left = total_rows
|
965
|
+
final_where = []
|
966
|
+
prev_rows_left = [rows_left]
|
967
|
+
where.reject { |c| ["~~", "~~*"].include?(c[:op]) }.each do |c|
|
968
|
+
next if final_where.include?(c[:column])
|
969
|
+
final_where << c[:column]
|
970
|
+
rows_left = row_estimates(ranks[c[:column]], total_rows, rows_left, c[:op])
|
971
|
+
prev_rows_left << rows_left
|
972
|
+
if rows_left < 50 || final_where.size >= 3 || [">", ">=", "<", "<=", "~~", "~~*"].include?(c[:op])
|
973
|
+
break
|
974
|
+
end
|
975
|
+
end
|
946
976
|
|
947
|
-
|
977
|
+
index[:row_progression] = prev_rows_left.map(&:round)
|
948
978
|
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
979
|
+
# if the last indexes don't give us much, don't include
|
980
|
+
prev_rows_left.reverse!
|
981
|
+
(prev_rows_left.size - 1).times do |i|
|
982
|
+
if prev_rows_left[i] > prev_rows_left[i + 1] * 0.3
|
983
|
+
final_where.pop
|
984
|
+
else
|
985
|
+
break
|
986
|
+
end
|
957
987
|
end
|
958
|
-
end
|
959
|
-
end
|
960
988
|
|
961
|
-
|
962
|
-
|
963
|
-
|
989
|
+
if final_where.any?
|
990
|
+
index[:found] = true
|
991
|
+
index[:index] = {table: table, columns: final_where}
|
992
|
+
end
|
993
|
+
end
|
994
|
+
else
|
995
|
+
index[:explanation] = "No index needed if less than 500 rows"
|
964
996
|
end
|
965
997
|
else
|
966
|
-
index[:explanation] = "
|
998
|
+
index[:explanation] = "Stats not found"
|
967
999
|
end
|
968
1000
|
else
|
969
1001
|
index[:explanation] = "No columns to index"
|
@@ -993,18 +1025,19 @@ module PgHero
|
|
993
1025
|
"INSERT statement"
|
994
1026
|
when "SET"
|
995
1027
|
"SET statement"
|
996
|
-
|
997
|
-
"
|
1028
|
+
when "SELECT"
|
1029
|
+
if (tree["SELECT"]["fromClause"].first["JOINEXPR"] rescue false)
|
1030
|
+
"JOIN not supported yet"
|
1031
|
+
end
|
998
1032
|
end
|
999
|
-
return {error: error}
|
1033
|
+
return {error: error || "Unknown structure"}
|
1000
1034
|
end
|
1001
1035
|
|
1002
1036
|
select = tree["SELECT"] || tree["DELETE FROM"] || tree["UPDATE"]
|
1003
1037
|
where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
|
1004
1038
|
return {error: "Unknown structure"} unless where
|
1005
1039
|
|
1006
|
-
sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue
|
1007
|
-
return {error: "Unknown structure"} unless sort
|
1040
|
+
sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue []
|
1008
1041
|
|
1009
1042
|
{table: table, where: where, sort: sort}
|
1010
1043
|
end
|
@@ -1015,8 +1048,7 @@ module PgHero
|
|
1015
1048
|
|
1016
1049
|
# TODO better row estimation
|
1017
1050
|
# http://www.postgresql.org/docs/current/static/row-estimation-examples.html
|
1018
|
-
def row_estimates(stats, rows_left
|
1019
|
-
rows_left ||= stats["n_live_tup"].to_i
|
1051
|
+
def row_estimates(stats, total_rows, rows_left, op)
|
1020
1052
|
case op
|
1021
1053
|
when "null"
|
1022
1054
|
rows_left * stats["null_frac"].to_f
|
@@ -1024,16 +1056,26 @@ module PgHero
|
|
1024
1056
|
rows_left * (1 - stats["null_frac"].to_f)
|
1025
1057
|
else
|
1026
1058
|
rows_left *= (1 - stats["null_frac"].to_f)
|
1027
|
-
|
1028
|
-
0
|
1029
|
-
elsif stats["n_distinct"].to_f < 0
|
1030
|
-
if stats["n_live_tup"].to_i > 0
|
1031
|
-
(-1 / stats["n_distinct"].to_f) * (rows_left / stats["n_live_tup"].to_f)
|
1032
|
-
else
|
1059
|
+
ret =
|
1060
|
+
if stats["n_distinct"].to_f == 0
|
1033
1061
|
0
|
1062
|
+
elsif stats["n_distinct"].to_f < 0
|
1063
|
+
if total_rows > 0
|
1064
|
+
(-1 / stats["n_distinct"].to_f) * (rows_left / total_rows.to_f)
|
1065
|
+
else
|
1066
|
+
0
|
1067
|
+
end
|
1068
|
+
else
|
1069
|
+
rows_left / stats["n_distinct"].to_f
|
1034
1070
|
end
|
1071
|
+
|
1072
|
+
case op
|
1073
|
+
when ">", ">=", "<", "<=", "~~", "~~*"
|
1074
|
+
(rows_left + ret) / 10.0 # TODO better approximation
|
1075
|
+
when "<>"
|
1076
|
+
rows_left - ret
|
1035
1077
|
else
|
1036
|
-
|
1078
|
+
ret
|
1037
1079
|
end
|
1038
1080
|
end
|
1039
1081
|
end
|
@@ -1059,10 +1101,10 @@ module PgHero
|
|
1059
1101
|
if left && right
|
1060
1102
|
left + right
|
1061
1103
|
end
|
1062
|
-
elsif tree["AEXPR"] && ["="].include?(tree["AEXPR"]["name"].first)
|
1104
|
+
elsif tree["AEXPR"] && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*"].include?(tree["AEXPR"]["name"].first)
|
1063
1105
|
[{column: tree["AEXPR"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR"]["name"].first}]
|
1064
|
-
elsif tree["AEXPR IN"] && tree["AEXPR IN"]["name"].first
|
1065
|
-
[{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: "
|
1106
|
+
elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
|
1107
|
+
[{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR IN"]["name"].first}]
|
1066
1108
|
elsif tree["NULLTEST"]
|
1067
1109
|
op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
|
1068
1110
|
[{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
|
data/lib/pghero/version.rb
CHANGED
data/pghero.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = PgHero::VERSION
|
9
9
|
spec.authors = ["Andrew Kane"]
|
10
10
|
spec.email = ["andrew@chartkick.com"]
|
11
|
-
spec.summary = "
|
12
|
-
spec.description = "
|
11
|
+
spec.summary = "The missing dashboard for Postgres"
|
12
|
+
spec.description = "The missing dashboard for Postgres"
|
13
13
|
spec.homepage = "https://github.com/ankane/pghero"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
data/test/best_index_test.rb
CHANGED
@@ -50,15 +50,46 @@ class BestIndexTest < Minitest::Test
|
|
50
50
|
assert_best_index ({table: "users", columns: ["login_attempts", "created_at"]}), "SELECT * FROM users WHERE login_attempts = 1 ORDER BY created_at"
|
51
51
|
end
|
52
52
|
|
53
|
+
def test_where_order_unknown
|
54
|
+
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE login_attempts = 1 ORDER BY NOW()"
|
55
|
+
end
|
56
|
+
|
53
57
|
def test_where_in
|
54
58
|
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id IN (1, 2)"
|
55
59
|
end
|
56
60
|
|
61
|
+
def test_like
|
62
|
+
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email LIKE ?"
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_like_where
|
66
|
+
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = ? AND email LIKE ?"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_like_where2
|
70
|
+
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email LIKE ? AND active = ?"
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_ilike
|
74
|
+
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email ILIKE ?"
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_not_equals
|
78
|
+
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE city_id != ? and login_attempts = 2"
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_not_in
|
82
|
+
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE city_id NOT IN (?) and login_attempts = 2"
|
83
|
+
end
|
84
|
+
|
57
85
|
def test_between
|
58
|
-
skip
|
59
86
|
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id BETWEEN 1 AND 2"
|
60
87
|
end
|
61
88
|
|
89
|
+
def test_multiple_range
|
90
|
+
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id > ? and login_attempts > ?"
|
91
|
+
end
|
92
|
+
|
62
93
|
def test_where_prepared
|
63
94
|
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = $1"
|
64
95
|
end
|
@@ -91,8 +122,16 @@ class BestIndexTest < Minitest::Test
|
|
91
122
|
assert_no_index "Parse error", "SELECT *123'"
|
92
123
|
end
|
93
124
|
|
125
|
+
def test_stats_not_found
|
126
|
+
assert_no_index "Stats not found", "SELECT * FROM non_existent_table WHERE id = 1"
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_unknown_structure
|
130
|
+
assert_no_index "Unknown structure", "SELECT NOW()"
|
131
|
+
end
|
132
|
+
|
94
133
|
def test_multiple_tables
|
95
|
-
assert_no_index "
|
134
|
+
assert_no_index "JOIN not supported yet", "SELECT * FROM users INNER JOIN cities ON cities.id = users.city_id"
|
96
135
|
end
|
97
136
|
|
98
137
|
def test_no_columns
|
@@ -119,6 +158,7 @@ class BestIndexTest < Minitest::Test
|
|
119
158
|
|
120
159
|
def assert_best_index(expected, statement)
|
121
160
|
index = PgHero.best_index(statement)
|
161
|
+
assert_nil index[:explanation]
|
122
162
|
assert index[:found]
|
123
163
|
assert_equal expected, index[:index]
|
124
164
|
end
|
data/test/test_helper.rb
CHANGED
@@ -28,6 +28,7 @@ if ENV["SEED"]
|
|
28
28
|
t.integer :login_attempts
|
29
29
|
t.string :email
|
30
30
|
t.string :zip_code
|
31
|
+
t.boolean :active
|
31
32
|
t.timestamp :created_at
|
32
33
|
end
|
33
34
|
|
@@ -39,6 +40,7 @@ if ENV["SEED"]
|
|
39
40
|
email: "person#{i}@example.org",
|
40
41
|
login_attempts: rand(30),
|
41
42
|
zip_code: i % 40 == 0 ? nil : "12345",
|
43
|
+
active: true,
|
42
44
|
created_at: Time.now - rand(50).days
|
43
45
|
)
|
44
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pghero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
description:
|
83
|
+
description: The missing dashboard for Postgres
|
84
84
|
email:
|
85
85
|
- andrew@chartkick.com
|
86
86
|
executables: []
|
@@ -155,7 +155,7 @@ rubyforge_project:
|
|
155
155
|
rubygems_version: 2.4.5.1
|
156
156
|
signing_key:
|
157
157
|
specification_version: 4
|
158
|
-
summary:
|
158
|
+
summary: The missing dashboard for Postgres
|
159
159
|
test_files:
|
160
160
|
- guides/Docker.md
|
161
161
|
- guides/Heroku.md
|
@@ -170,3 +170,4 @@ test_files:
|
|
170
170
|
- test/gemfiles/activerecord40.gemfile
|
171
171
|
- test/suggested_indexes_test.rb
|
172
172
|
- test/test_helper.rb
|
173
|
+
has_rdoc:
|