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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c032f7f0fd5b3d6484cae193d125d3899ee87594
4
- data.tar.gz: 02db1720b5201380dff89ac2dceba86c42d8dc49
3
+ metadata.gz: 3975a93a4da7c4199e2e3a5f98c4c98e35c991e9
4
+ data.tar.gz: 7a2a1ba863c86c42d09316c624cbd9fa411bda41
5
5
  SHA512:
6
- metadata.gz: f07fe4ece79f34731bb6bc28dc19cd09d365a9f9968caeb0918fb347d9be9d050ce5c1286b09feccc0312d2e379afbf3278fd65ca5c802f4c817e6b1ee7f83c0
7
- data.tar.gz: dd334b3251a68372218f765b2c5d986e4bd71172e3a428511f05f4dfb7580123b38421a08eebacc204360cc51b60a9b24bc74dea31889eda9f1d6013bd4bc204
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
- Postgres insights made easy
3
+ The missing dashboard for Postgres
4
4
 
5
- [View the demo](https://pghero.herokuapp.com/)
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 = PgHero.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
- @autovacuum_danger = PgHero.autovacuum_danger
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] %><% else %>Rows: <%= i2[:rows] %>
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 progresssion: <%= i2[:row_progression].to_a.join(", ") %><% end %></pre></code>
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-<%= @autovacuum_danger.empty? ? "success" : "warning" %>">
71
- <% if @autovacuum_danger.any? %>
72
- <%= pluralize(@autovacuum_danger.size, "table") %> approaching autovacuum freeze max age
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
- No autovacuum danger
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
- <div class="alert alert-<%= @missing_indexes.empty? ? "success" : "warning" %>">
101
- <% if @missing_indexes.any? %>
102
- <%= pluralize(@missing_indexes.size, "table appears", "tables appear") %> to be missing indexes
103
- <% else %>
104
- No missing indexes
105
- <% end %>
106
- </div>
107
- <div class="alert alert-<%= @unused_indexes.empty? ? "success" : "warning" %>">
108
- <% if @unused_indexes.any? %>
109
- <%= pluralize(@unused_indexes.size, "unused index", "unused indexes") %>
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 unused indexes
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 @autovacuum_danger.any? %>
124
+ <% if @transaction_id_danger.any? %>
187
125
  <div class="content">
188
- <h1>Autovacuum Danger</h1>
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 Before Autovacuum</th>
134
+ <th style="width: 20%;">Transactions Left</th>
197
135
  </tr>
198
136
  </thead>
199
137
  <tbody>
200
- <% @autovacuum_danger.each do |query| %>
138
+ <% @transaction_id_danger.each do |query| %>
201
139
  <tr>
202
140
  <td><%= query["table"] %></td>
203
- <td><%= number_with_delimiter(query["transactions_before_autovacuum"]) %></td>
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 @unused_indexes.any? %>
272
+ <% if !@query_stats_enabled %>
334
273
  <div class="content">
335
- <h1>Unused Indexes</h1>
274
+ <h1>Query Stats</h1>
336
275
 
337
- <p>
338
- Unused indexes cause unnecessary overhead. Remove them
339
- <% if @show_migrations %>
340
- <a href="javascript: void(0);" onclick="document.getElementById('migration').style.display = 'block';">with a migration</a>
341
- <% end %>
342
- for faster writes.
343
- </p>
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
- <div id="migration" style="display: none;">
346
- <pre>rails g migration remove_unused_indexes</pre>
347
- <p>And paste</p>
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
- <table class="table">
353
- <thead>
354
- <tr>
355
- <th>Name</th>
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))) < 10000000
390
+ AND (2146483648 - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{threshold}
390
391
  ORDER BY
391
- transactions_before_shutdown
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 |i|
810
- existing_columns[i["table"]] << i["columns"]
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
- pg_class ON pg_class.relname = pg_stats.tablename
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
- #{tables ? "pg_class.relname IN (#{tables.map { |t| quote(t) }.join(", ")})" : "1 = 1"}
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
- column_stats = self.column_stats(table: tables).group_by { |i| i["table"] }
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
- ranks = Hash[column_stats[table].to_a.map { |r| [r["column"], r] }]
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? && columns.all? { |c| ranks[c] }
920
- first_desc = sort.index { |c| c[:direction] == "desc" }
921
- if first_desc
922
- sort = sort.first(first_desc + 1)
923
- end
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
- index[:row_progression] = prev_rows_left.map(&:round)
977
+ index[:row_progression] = prev_rows_left.map(&:round)
948
978
 
949
- # if the last indexes don't give us much, don't include
950
- if prev_rows_left.last > 50
951
- prev_rows_left.reverse!
952
- (prev_rows_left.size - 1).times do |i|
953
- if prev_rows_left[i] > prev_rows_left[i + 1] * 0.1
954
- final_where.pop
955
- else
956
- break
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
- if final_where.any?
962
- index[:found] = true
963
- index[:index] = {table: table, columns: final_where}
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] = "No index needed if less than 500 rows"
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
- else
997
- "Unknown structure"
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 nil
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 = nil, op = nil)
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
- if stats["n_distinct"].to_f == 0
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
- rows_left / stats["n_distinct"].to_f
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: "in"}]
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}]
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.2.0"
2
+ VERSION = "1.2.1"
3
3
  end
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 = "Database insights made easy"
12
- spec.description = "Database insights made easy"
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
 
@@ -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 "Unknown structure", "SELECT * FROM users INNER JOIN cities ON cities.id = users.city_id"
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.0
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: 2015-12-31 00:00:00.000000000 Z
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: Database insights made easy
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: Database insights made easy
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: