pghero 1.5.3 → 2.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +75 -0
  4. data/Gemfile +1 -1
  5. data/README.md +8 -14
  6. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  7. data/app/assets/javascripts/pghero/application.js +9 -0
  8. data/app/assets/javascripts/pghero/chartkick.js +511 -152
  9. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  10. data/app/assets/stylesheets/pghero/application.css +66 -2
  11. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  12. data/app/controllers/pg_hero/home_controller.rb +166 -63
  13. data/app/helpers/pg_hero/base_helper.rb +15 -0
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -3
  15. data/app/views/pg_hero/home/_connections_table.html.erb +2 -9
  16. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -8
  17. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  18. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  19. data/app/views/pg_hero/home/connections.html.erb +19 -1
  20. data/app/views/pg_hero/home/explain.html.erb +1 -1
  21. data/app/views/pg_hero/home/index.html.erb +125 -66
  22. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  23. data/app/views/pg_hero/home/live_queries.html.erb +3 -1
  24. data/app/views/pg_hero/home/maintenance.html.erb +11 -7
  25. data/app/views/pg_hero/home/queries.html.erb +10 -0
  26. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  27. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  28. data/app/views/pg_hero/home/space.html.erb +64 -10
  29. data/app/views/pg_hero/home/system.html.erb +16 -4
  30. data/config/routes.rb +4 -2
  31. data/guides/Linux.md +20 -0
  32. data/guides/Rails.md +30 -4
  33. data/guides/Suggested-Indexes.md +1 -1
  34. data/lib/generators/pghero/config_generator.rb +13 -0
  35. data/lib/generators/pghero/query_stats_generator.rb +7 -1
  36. data/lib/generators/pghero/space_stats_generator.rb +7 -1
  37. data/lib/generators/pghero/templates/config.yml +23 -0
  38. data/lib/generators/pghero/templates/query_stats.rb +1 -1
  39. data/lib/generators/pghero/templates/space_stats.rb +1 -1
  40. data/lib/pghero/database.rb +6 -2
  41. data/lib/pghero/engine.rb +10 -7
  42. data/lib/pghero/methods/basic.rb +83 -11
  43. data/lib/pghero/methods/connections.rb +16 -56
  44. data/lib/pghero/methods/explain.rb +3 -5
  45. data/lib/pghero/methods/indexes.rb +179 -21
  46. data/lib/pghero/methods/kill.rb +2 -2
  47. data/lib/pghero/methods/maintenance.rb +34 -20
  48. data/lib/pghero/methods/queries.rb +28 -45
  49. data/lib/pghero/methods/query_stats.rb +97 -91
  50. data/lib/pghero/methods/{replica.rb → replication.rb} +20 -4
  51. data/lib/pghero/methods/sequences.rb +4 -5
  52. data/lib/pghero/methods/space.rb +101 -8
  53. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  54. data/lib/pghero/methods/system.rb +23 -13
  55. data/lib/pghero/methods/tables.rb +9 -9
  56. data/lib/pghero/methods/users.rb +10 -12
  57. data/lib/pghero/version.rb +1 -1
  58. data/lib/pghero.rb +34 -36
  59. data/lib/tasks/pghero.rake +5 -0
  60. data/test/basic_test.rb +38 -0
  61. data/test/best_index_test.rb +3 -3
  62. data/test/suggested_indexes_test.rb +0 -2
  63. data/test/test_helper.rb +38 -40
  64. metadata +14 -7
  65. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  66. data/test/explain_test.rb +0 -18
@@ -8,24 +8,27 @@
8
8
  </tr>
9
9
  </thead>
10
10
  <tbody>
11
- <% now = Time.now %>
12
11
  <% queries.reverse.each do |query| %>
13
12
  <tr>
14
- <td><%= query["pid"] %></td>
15
- <td><%= query["started_at"] ? "#{number_with_delimiter(((now - Time.parse(query["started_at"])) * 1000).round)} ms" : nil %></td>
16
- <td><%= query["state"] %></td>
13
+ <td><%= query[:pid] %></td>
14
+ <td><%= number_with_delimiter(query[:duration_ms].round) %> ms</td>
15
+ <td><%= query[:state] %></td>
17
16
  <td class="text-right">
18
- <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query["query"]}}] : [explain_path(query: query["query"]), {}] %>
17
+ <% button_path, button_options = Rails.version >= "4.1" ? [explain_path, {params: {query: query[:query]}}] : [explain_path(query: query[:query]), {}] %>
19
18
  <%= button_to "Explain", button_path, button_options.merge(form: {target: "_blank"}, class: "btn btn-info") %>
20
- <%= button_to "Kill", kill_path(pid: query["pid"]), class: "btn btn-danger" %>
19
+ <%= button_to "Kill", kill_path(pid: query[:pid]), class: "btn btn-danger" %>
21
20
  </td>
22
21
  </tr>
23
22
  <tr>
24
23
  <td colspan="6" style="border-top: none; padding: 0;">
25
- <%= query["source"] %> <span class="text-muted"><%= query["user"] %></span>
26
- <pre><%= query["query"] %></pre>
24
+ <%= query[:source] %> <span class="text-muted"><%= query[:user] %></span>
25
+ <pre style="margin-top: 1em;"><code><%= query[:query] %></code></pre>
27
26
  </td>
28
27
  </tr>
29
28
  <% end %>
30
29
  </tbody>
31
30
  </table>
31
+
32
+ <script>
33
+ highlightQueries();
34
+ </script>
@@ -25,9 +25,9 @@
25
25
  <% queries.each do |query| %>
26
26
  <tr>
27
27
  <td>
28
- <%= number_with_delimiter(query["total_minutes"].to_f.round) %> min
28
+ <%= number_with_delimiter(query[:total_minutes].round) %> min
29
29
  <span class="percent">
30
- <% percent = query["total_percent"].to_f %>
30
+ <% percent = query[:total_percent] %>
31
31
  <% if percent > 1 %>
32
32
  <%= percent.round %>%
33
33
  <% elsif percent > 0.1 %>
@@ -37,21 +37,32 @@
37
37
  <% end %>
38
38
  </span>
39
39
  </td>
40
- <td><%= number_with_delimiter(query["average_time"].to_f.round) %> ms</td>
41
40
  <td>
42
- <%= number_with_delimiter(query["calls"].to_i) %>
43
- <% if query["user"] %>
44
- <span class="user"><%= query["user"] %></span>
45
- <% end %>
41
+ <%= number_with_delimiter(query[:average_time].round) %> ms
42
+ </td>
43
+ <td>
44
+ <%= number_with_delimiter(query[:calls]) %>
45
+
46
+ <span class="user">
47
+ <% if query[:user] %>
48
+ <%= query[:user] %>
49
+ <% if @show_details %>
50
+ &middot;
51
+ <% end %>
52
+ <% end %>
53
+ <% if @show_details %>
54
+ <%= link_to "details", show_query_path(query[:query_hash]), target: "_blank" %>
55
+ <% end %>
56
+ </span>
46
57
  </td>
47
58
  </tr>
48
59
  <tr>
49
60
  <td colspan="3" style="border-top: none; padding: 0;">
50
- <code><pre style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query["query"] %></pre></code>
51
- <% if query["query"] == "<insufficient privilege>" %>
61
+ <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query[:query] %></code></pre>
62
+ <% if query[:query] == "<insufficient privilege>" %>
52
63
  <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
53
64
  <% end %>
54
- <% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query["query"]]) %>
65
+ <% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query[:query]]) && (details[:index] || @debug) %>
55
66
  <%= render partial: "suggested_index", locals: {index: details[:index], details: details} %>
56
67
  <% end %>
57
68
  </td>
@@ -13,6 +13,6 @@ Row estimates
13
13
  <%= details[:row_estimates].to_a.map { |k, v| "- #{k}: #{v}" }.join("\n") %><% end %><% if details[:table_indexes] %>
14
14
 
15
15
  Existing indexes
16
- <% details[:table_indexes].sort_by { |i| [PgHero.truthy?(i["primary"]) ? 0 : 1, i["columns"]] }.each do |i3| %>- <%= i3["columns"].join(", ") %><% if i3["using"] != "btree" %> <%= i3["using"].to_s.upcase %><% end %><% if PgHero.truthy?(i3["primary"]) %> PRIMARY<% elsif PgHero.truthy?(i3["unique"]) %> UNIQUE<% end %>
16
+ <% details[:table_indexes].sort_by { |i| [i[:primary] ? 0 : 1, i[:columns]] }.each do |i3| %>- <%= i3[:columns].join(", ") %><% if i3[:using] != "btree" %> <%= i3[:using].to_s.upcase %><% end %><% if i3[:primary] %> PRIMARY<% elsif i3[:unique] %> UNIQUE<% end %>
17
17
  <% end %><% end %></pre></code>
18
18
  </div>
@@ -1,5 +1,23 @@
1
1
  <div class="content">
2
2
  <h1>Connections</h1>
3
3
 
4
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: @connection_sources, show_message: false} %>
4
+ <p><%= pluralize(@total_connections, "connection") %></p>
5
+
6
+ <% if @total_connections > 0 %>
7
+ <h3>By Database</h3>
8
+
9
+ <div id="chart-1" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
10
+ <script>
11
+ new Chartkick.PieChart("chart-1", <%= json_escape(@connections_by_database.to_json).html_safe %>);
12
+ </script>
13
+
14
+ <h3>By User</h3>
15
+
16
+ <div id="chart-2" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
17
+ <script>
18
+ new Chartkick.PieChart("chart-2", <%= json_escape(@connections_by_user.to_json).html_safe %>);
19
+ </script>
20
+
21
+ <%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
22
+ <% end %>
5
23
  </div>
@@ -14,7 +14,7 @@
14
14
  <% if @visualize %>
15
15
  <p>Paste the output below into the <%= link_to "Postgres Explain Visualizer", "http://tatiyants.com/pev/#/plans/new", target: "_blank" %></p>
16
16
  <% end %>
17
- <pre><%= @explanation %></pre>
17
+ <pre><code><%= @explanation %></code></pre>
18
18
  <% unless @visualize %>
19
19
  <p><%= link_to "See how to interpret this", "http://www.postgresql.org/docs/current/static/using-explain.html", target: "_blank" %></p>
20
20
  <% end %>
@@ -8,6 +8,10 @@
8
8
  <% end %>
9
9
  <span class="tiny"><%= number_with_delimiter((@replication_lag * 1000).round) %> ms</span>
10
10
  </div>
11
+ <% elsif @inactive_replication_slots.any? %>
12
+ <div class="alert alert-warning">
13
+ <%= pluralize(@inactive_replication_slots.size, "inactive replication slot") %>
14
+ </div>
11
15
  <% end %>
12
16
  <div class="alert alert-<%= @long_running_queries.empty? ? "success" : "warning" %>">
13
17
  <% if @long_running_queries.any? %>
@@ -15,17 +19,22 @@
15
19
  <% else %>
16
20
  No long running queries
17
21
  <% end %>
18
- </div>
19
- <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
20
- <% if @good_cache_rate %>
21
- Cache hit rate above <%= @database.cache_hit_rate_threshold %>%
22
- <% else %>
23
- Low cache hit rate
22
+ <% if @autovacuum_queries.any? %>
23
+ <span class="tiny"><%= @autovacuum_queries.size %> autovacuum</span>
24
24
  <% end %>
25
25
  </div>
26
+ <% if @extended %>
27
+ <div class="alert alert-<%= @good_cache_rate ? "success" : "warning" %>">
28
+ <% if @good_cache_rate %>
29
+ Cache hit rate above <%= @database.cache_hit_rate_threshold %>%
30
+ <% else %>
31
+ Low cache hit rate
32
+ <% end %>
33
+ </div>
34
+ <% end %>
26
35
  <div class="alert alert-<%= @good_total_connections ? "success" : "warning" %>">
27
36
  <% if @good_total_connections %>
28
- Healthy number of connections
37
+ Number of connections healthy
29
38
  <% else %>
30
39
  High number of connections
31
40
  <% end %>
@@ -42,7 +51,7 @@
42
51
  <% if @sequence_danger.any? %>
43
52
  <%= pluralize(@sequence_danger.size, "columns") %> approaching overflow
44
53
  <% else %>
45
- No columns near overflow
54
+ No columns near integer overflow
46
55
  <% end %>
47
56
  </div>
48
57
  <div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
@@ -69,14 +78,6 @@
69
78
  No suggested indexes
70
79
  <% end %>
71
80
  </div>
72
- <% else %>
73
- <div class="alert alert-<%= @missing_indexes.empty? ? "success" : "warning" %>">
74
- <% if @missing_indexes.any? %>
75
- <%= pluralize(@missing_indexes.size, "table appears", "tables appear") %> to be missing indexes
76
- <% else %>
77
- No missing indexes
78
- <% end %>
79
- </div>
80
81
  <% end %>
81
82
  <div class="alert alert-<%= @query_stats_enabled && @slow_queries.empty? ? "success" : "warning" %>">
82
83
  <% if !@query_stats_enabled %>
@@ -87,6 +88,15 @@
87
88
  No slow queries
88
89
  <% end %>
89
90
  </div>
91
+ <% if @extended %>
92
+ <div class="alert alert-<%= @unused_indexes.empty? ? "success" : "warning" %>">
93
+ <% if @unused_indexes.any? %>
94
+ <%= pluralize(@unused_indexes.size, "unused index", "unused indexes") %>
95
+ <% else %>
96
+ No unused indexes
97
+ <% end %>
98
+ </div>
99
+ <% end %>
90
100
  </div>
91
101
 
92
102
  <% if @replica && !@good_replication_lag %>
@@ -97,6 +107,29 @@
97
107
  </div>
98
108
  <% end %>
99
109
 
110
+ <% if @inactive_replication_slots && @inactive_replication_slots.any? %>
111
+ <div class="content">
112
+ <h1>Inactive Replication Slots</h1>
113
+ <p>Inactive replication slots can cause a lot of disk space to be consumed.</p>
114
+ <p>For each, run:</p>
115
+ <pre><code>SELECT pg_drop_replication_slot('slot_name');</code></pre>
116
+ <table class="table">
117
+ <thead>
118
+ <tr>
119
+ <th>Name</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ <% @inactive_replication_slots.each do |slot| %>
124
+ <tr>
125
+ <td><%= slot[:slot_name] %></td>
126
+ </tr>
127
+ <% end %>
128
+ </tbody>
129
+ </table>
130
+ </div>
131
+ <% end %>
132
+
100
133
  <% if @long_running_queries.any? %>
101
134
  <div class="content">
102
135
  <%= button_to "Kill All", kill_long_running_queries_path, class: "btn btn-danger", style: "float: right;" %>
@@ -106,7 +139,7 @@
106
139
  </div>
107
140
  <% end %>
108
141
 
109
- <% if !@good_cache_rate %>
142
+ <% if @extended && !@good_cache_rate %>
110
143
  <div class="content">
111
144
  <h1>Low Cache Hit Rate</h1>
112
145
 
@@ -126,7 +159,11 @@
126
159
  <% if !@good_total_connections %>
127
160
  <div class="content">
128
161
  <h1>High Number of Connections</h1>
129
- <%= render partial: "connections_table", locals: {total_connections: @total_connections, connection_sources: @database.connection_sources(by_database_and_user: true).first(10), show_message: true} %>
162
+ <p><%= pluralize(@total_connections, "connection") %></p>
163
+
164
+ <p><%= link_to "Use connection pooling", "http://www.craigkerstiens.com/2014/05/22/on-connection-pooling/", target: "_blank" %> for better performance. <%= link_to "PgBouncer", "https://wiki.postgresql.org/wiki/PgBouncer", target: "_blank" %> is a solid option.</p>
165
+
166
+ <%= render partial: "connections_table", locals: {connection_sources: @database.connection_sources.first(10)} %>
130
167
  </div>
131
168
  <% end %>
132
169
 
@@ -135,7 +172,7 @@
135
172
  <h2>Vacuuming Needed</h2>
136
173
  <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>
137
174
  <p>For each table, run:</p>
138
- <code><pre>VACUUM FREEZE VERBOSE table</pre></code>
175
+ <pre><code>VACUUM FREEZE VERBOSE table;</code></pre>
139
176
  <table class="table">
140
177
  <thead>
141
178
  <tr>
@@ -146,8 +183,13 @@
146
183
  <tbody>
147
184
  <% @transaction_id_danger.each do |query| %>
148
185
  <tr>
149
- <td><%= query["table"] %></td>
150
- <td><%= number_with_delimiter(query["transactions_before_shutdown"]) %></td>
186
+ <td>
187
+ <%= query[:table] %>
188
+ <% if query[:schema] != "public" %>
189
+ <span class="text-muted"><%= query[:schema] %></span>
190
+ <% end %>
191
+ </td>
192
+ <td><%= number_with_delimiter(query[:transactions_left]) %></td>
151
193
  </tr>
152
194
  <% end %>
153
195
  </tbody>
@@ -169,19 +211,19 @@
169
211
  </tr>
170
212
  </thead>
171
213
  <tbody>
172
- <% @sequence_danger.sort_by { |s| [s["table"], s["column"]] }.each do |query| %>
214
+ <% @sequence_danger.sort_by { |s| [s[:table], s[:column]] }.each do |query| %>
173
215
  <tr>
174
216
  <td>
175
- <%= query["table"] %>.<%= query["column"] %>
217
+ <%= query[:table] %>.<%= query[:column] %>
176
218
  </td>
177
219
  <td>
178
- <%= query["column_type"] %>
220
+ <%= query[:column_type] %>
179
221
  </td>
180
222
  <td>
181
- <%= number_with_delimiter(query["max_value"].to_i - query["last_value"].to_i) %>
223
+ <%= number_with_delimiter(query[:max_value] - query[:last_value]) %>
182
224
  </td>
183
225
  <td>
184
- <%= number_to_percentage((query["max_value"].to_i - query["last_value"].to_i) * 100.0 / query["max_value"].to_i, precision: 2, significant: true) %>
226
+ <%= number_to_percentage((query[:max_value] - query[:last_value]) * 100.0 / query[:max_value], precision: 2, significant: true) %>
185
227
  </td>
186
228
  </tr>
187
229
  <% end %>
@@ -203,9 +245,9 @@
203
245
  </tr>
204
246
  </thead>
205
247
  <tbody>
206
- <% @invalid_indexes.each do |query| %>
248
+ <% @invalid_indexes.each do |index| %>
207
249
  <tr>
208
- <td><%= query["index"] %></td>
250
+ <td><%= index[:name] %></td>
209
251
  </tr>
210
252
  <% end %>
211
253
  </tbody>
@@ -229,10 +271,10 @@
229
271
  <pre>rails g migration remove_unneeded_indexes</pre>
230
272
  <p>And paste</p>
231
273
  <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
232
- remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
274
+ remove_index <%= query[:unneeded_index][:table].to_sym.inspect %>, name: <%= query[:unneeded_index][:name].to_s.inspect %><% end %></pre>
233
275
  </div>
234
276
 
235
- <table class="table">
277
+ <table class="table duplicate-indexes">
236
278
  <thead>
237
279
  <tr>
238
280
  <th>Details</th>
@@ -240,14 +282,14 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
240
282
  </thead>
241
283
  <tbody>
242
284
  <% @duplicate_indexes.each do |index| %>
243
- <% unneeded_index = index["unneeded_index"] %>
244
- <% covering_index = index["covering_index"] %>
285
+ <% unneeded_index = index[:unneeded_index] %>
286
+ <% covering_index = index[:covering_index] %>
245
287
  <tr>
246
- <td style="padding-top: 15px; padding-bottom: 5px;">
247
- On <%= unneeded_index["table"] %>
248
- <pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
288
+ <td>
289
+ On <%= unneeded_index[:table] %>
290
+ <pre><%= unneeded_index[:name] %> (<%= unneeded_index[:columns].join(", ") %>)</pre>
249
291
  is covered by
250
- <pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
292
+ <pre><%= covering_index[:name] %> (<%= covering_index[:columns].join(", ") %>)</pre>
251
293
  </td>
252
294
  </tr>
253
295
  <% end %>
@@ -270,11 +312,13 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
270
312
  <div id="migration3" style="display: none;">
271
313
  <pre>rails g migration add_suggested_indexes</pre>
272
314
  <p>And paste</p>
273
- <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @suggested_indexes.each do |index| %>
274
- <% if index[:using] == "gist" %>
315
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;">commit_db_transaction
316
+ <% @suggested_indexes.each do |index| %>
317
+ <% if index[:using] && index[:using] != "btree" %>
275
318
  connection.execute("CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)")
276
319
  <% else %>
277
- add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %><% end %></pre>
320
+ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %>
321
+ <% end %></pre>
278
322
  </div>
279
323
 
280
324
  <% @suggested_indexes.each_with_index do |index, i| %>
@@ -286,33 +330,6 @@ add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym
286
330
  </div>
287
331
  <% end %>
288
332
 
289
- <% if @missing_indexes.any? %>
290
- <div class="content">
291
- <h1>Missing Indexes</h1>
292
-
293
- <p>These tables have a large number of rows but indexes are not used often. Add indexes for faster queries.</p>
294
-
295
- <table class="table">
296
- <thead>
297
- <tr>
298
- <th>Table</th>
299
- <th>% of Time Index Used</th>
300
- <th>Rows in Table</th>
301
- </tr>
302
- </thead>
303
- <tbody>
304
- <% @missing_indexes.each do |query| %>
305
- <tr>
306
- <td><%= query["table"] %></td>
307
- <td style="width: 30%;"><%= query["percent_of_times_index_used"] %></td>
308
- <td style="width: 20%;"><%= number_with_delimiter(query["rows_in_table"]) %></td>
309
- </tr>
310
- <% end %>
311
- </tbody>
312
- </table>
313
- </div>
314
- <% end %>
315
-
316
333
  <% if !@query_stats_enabled %>
317
334
  <div class="content">
318
335
  <h1>Query Stats</h1>
@@ -341,3 +358,45 @@ pg_stat_statements.track = all</pre>
341
358
  <%= render partial: "queries_table", locals: {queries: @slow_queries} %>
342
359
  </div>
343
360
  <% end %>
361
+
362
+ <% if @extended && @unused_indexes.any? %>
363
+ <div class="content">
364
+ <h1>Unused Indexes</h1>
365
+
366
+ <p>
367
+ Unused indexes cause unnecessary overhead. Remove them
368
+ <% if @show_migrations %>
369
+ <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
370
+ <% end %>
371
+ for faster writes.
372
+ </p>
373
+
374
+ <div id="migration" style="display: none;">
375
+ <pre>rails g migration remove_unused_indexes</pre>
376
+ <p>And paste</p>
377
+ <pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @unused_indexes.each do |query| %>
378
+ remove_index <%= query[:table].to_sym.inspect %>, name: <%= query[:index].to_s.inspect %><% end %></pre>
379
+ </div>
380
+
381
+ <table class="table">
382
+ <thead>
383
+ <tr>
384
+ <th>Name</th>
385
+ <th style="width: 20%;">Index Size</th>
386
+ </tr>
387
+ </thead>
388
+ <tbody>
389
+ <% @unused_indexes.each do |query| %>
390
+ <tr>
391
+ <td><%= query[:index] %><div class="text-muted">on <%= query[:table] %></div></td>
392
+ <td><%= query[:size] %></td>
393
+ </tr>
394
+ <% end %>
395
+ </tbody>
396
+ </table>
397
+ </div>
398
+ <% end %>
399
+
400
+ <script>
401
+ highlightQueries();
402
+ </script>
@@ -0,0 +1,69 @@
1
+ <div class="content">
2
+ <h1>Index Bloat</h1>
3
+
4
+ <% if @index_bloat.any? %>
5
+
6
+ <p>Indexes can become <%= link_to "bloated over time", "https://www.compose.com/articles/postgresql-bloat-origins-monitoring-and-managing/", target: "_blank" %>. Recreate them to remove bloat.</p>
7
+
8
+ <p>For each index, run:</p>
9
+ <pre><code>CREATE INDEX CONCURRENTLY new_index ...;
10
+ ANALYZE table;
11
+ DROP INDEX CONCURRENTLY index;
12
+ ANALYZE table;
13
+ ALTER INDEX new_index RENAME TO index;</code></pre>
14
+
15
+ <p>
16
+ <% if @show_sql %>
17
+ <%= link_to "Hide SQL", {} %>
18
+ <% else %>
19
+ <%= link_to "Show SQL", {sql: "t"} %>
20
+ <% end %>
21
+ for each index
22
+ </p>
23
+
24
+ <table class="table">
25
+ <thead>
26
+ <tr>
27
+ <th>Index</th>
28
+ <th style="width: 15%;">Bloat</th>
29
+ <th style="width: 15%;">Size</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% @index_bloat.each do |index| %>
34
+ <tr>
35
+ <td>
36
+ <span style="word-break: break-all;"><%= index[:index] %></span>
37
+ </td>
38
+ <td><%= PgHero.pretty_size(index[:bloat_bytes]) %></td>
39
+ <td><%= PgHero.pretty_size(index[:index_bytes]) %></td>
40
+ </tr>
41
+ <% if @show_sql %>
42
+ <tr>
43
+ <td colspan="3" style="border-top: none; padding: 0;">
44
+ <% new_index = "new_#{index[:index]}".first(63) %>
45
+ <pre><code><%= index[:definition].sub(" INDEX ", " INDEX CONCURRENTLY \n ").sub(index[:index], new_index) %>;
46
+
47
+ ANALYZE <%= pghero_pretty_ident(index[:table], schema: index[:schema]) %>;
48
+
49
+ DROP INDEX CONCURRENTLY
50
+ <%= pghero_pretty_ident(index[:index]) %>;
51
+
52
+ ANALYZE <%= pghero_pretty_ident(index[:table], schema: index[:schema]) %>;
53
+
54
+ ALTER INDEX <%= pghero_pretty_ident(new_index) %>
55
+ RENAME TO <%= pghero_pretty_ident(index[:index]) %>;</code></pre>
56
+ </td>
57
+ </tr>
58
+ <% end %>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ <% else %>
63
+ <p>No significant index bloat!</p>
64
+ <% end %>
65
+ </div>
66
+
67
+ <script>
68
+ highlightQueries();
69
+ </script>
@@ -1,9 +1,11 @@
1
1
  <div class="content">
2
2
  <h1>Live Queries</h1>
3
3
 
4
+ <p><%= pluralize(@running_queries.size, "query") %></p>
5
+
4
6
  <%= render partial: "live_queries_table", locals: {queries: @running_queries} %>
5
7
 
6
8
  <p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
7
9
 
8
- <p class="text-muted">You may need to restart your Rails server afterwards.</p>
10
+ <p class="text-muted">You may need to restart your app server afterwards.</p>
9
11
  </div>
@@ -13,21 +13,25 @@
13
13
  <% @maintenance_info.each do |table| %>
14
14
  <tr>
15
15
  <td>
16
- <%= table["table"] %>
17
- <% if table["schema"] != "public" %>
18
- <span class="text-muted"><%= table["schema"] %></span>
16
+ <%= table[:table] %>
17
+ <% if table[:schema] != "public" %>
18
+ <span class="text-muted"><%= table[:schema] %></span>
19
19
  <% end %>
20
20
  </td>
21
21
  <td>
22
- <% time = [table["last_autovacuum"], table["last_vacuum"]].compact.max %>
22
+ <% time = [table[:last_autovacuum], table[:last_vacuum]].compact.max %>
23
23
  <% if time %>
24
- <%= @time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
24
+ <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
25
+ <% else %>
26
+ <span class="text-muted">Unknown</span>
25
27
  <% end %>
26
28
  </td>
27
29
  <td>
28
- <% time = [table["last_autoanalyze"], table["last_analyze"]].compact.max %>
30
+ <% time = [table[:last_autoanalyze], table[:last_analyze]].compact.max %>
29
31
  <% if time %>
30
- <%= @time_zone.parse(time).strftime("%m/%-e %l:%M %P") %>
32
+ <%= time.in_time_zone(@time_zone).strftime("%-m/%-e %l:%M %P") %>
33
+ <% else %>
34
+ <span class="text-muted">Unknown</span>
31
35
  <% end %>
32
36
  </td>
33
37
  </tr>
@@ -7,6 +7,13 @@
7
7
 
8
8
  <% if @historical_query_stats_enabled %>
9
9
  <%= render partial: "query_stats_slider" %>
10
+ <% elsif @database.query_stats_table_exists? && (columns = @database.missing_query_stats_columns).any? %>
11
+ <div style="clear: both;">
12
+ <p>Add missing columns to re-enable historical query stats.</p>
13
+ <pre><code><% @database.missing_query_stats_columns.each do |column| %>ALTER TABLE pghero_query_stats ADD COLUMN "<%= column %>" <%= column == "query_hash" ? "bigint" : "text" %>;
14
+ <% end %></code></pre>
15
+ <p>Then restart the web server.</p>
16
+ </div>
10
17
  <% end %>
11
18
 
12
19
  <% if @query_stats_enabled %>
@@ -14,6 +21,9 @@
14
21
  <div class="alert alert-danger">Cannot understand start or end time.</div>
15
22
  <% elsif @query_stats.any? || @historical_query_stats_enabled %>
16
23
  <%= render partial: "queries_table", locals: {queries: @query_stats, sort_headers: true} %>
24
+ <script>
25
+ highlightQueries();
26
+ </script>
17
27
  <% else %>
18
28
  <p>Stats are not available yet. Come back soon!</p>
19
29
  <% end %>
@@ -0,0 +1,9 @@
1
+ <div class="content">
2
+ <h1><%= @relation %></h1>
3
+
4
+ <h1>Size <small>MB</small></h1>
5
+ <div id="chart-1" class="chart" style="margin-bottom: 20px;">Loading...</div>
6
+ <script>
7
+ new Chartkick.LineChart("chart-1", <%= json_escape(@chart_data.to_json).html_safe %>, {colors: ["#5bc0de"], legend: false, min: null})
8
+ </script>
9
+ </div>