pghero 1.2.1 → 1.2.2
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 +6 -0
- data/Gemfile +0 -2
- data/app/controllers/pg_hero/home_controller.rb +0 -1
- data/app/views/layouts/pg_hero/application.html.erb +1 -0
- data/app/views/pg_hero/home/_queries_table.html.erb +2 -10
- data/app/views/pg_hero/home/_suggested_index.html.erb +18 -1
- data/app/views/pg_hero/home/index.html.erb +2 -52
- data/app/views/pg_hero/home/queries.html.erb +0 -4
- data/lib/pghero.rb +17 -8
- data/lib/pghero/version.rb +1 -1
- data/pghero.gemspec +2 -0
- data/test/best_index_test.rb +1 -1
- data/test/suggested_indexes_test.rb +1 -1
- data/test/test_helper.rb +19 -14
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d444b648639843cbae37f0e554e47ff37462222d
|
4
|
+
data.tar.gz: 2482c623785d74f933b836e89233b0c2c44a6e75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 648695c06b8a8264eb9380f0a6b2700c7846540d8490be0d476ba66d063bc73896baadcf9235057b513a7fdf8dbea8819d60a03526d9c32cb5082ab72264a9df
|
7
|
+
data.tar.gz: d69b31b33eb386ee52e761dfee89884e928012913c963aa1ab4758397007753952a17a2506fe930a667a6e29686eea62a8949008e422a809a9382412a49916a9
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -24,7 +24,6 @@ module PgHero
|
|
24
24
|
end
|
25
25
|
@unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
|
26
26
|
@invalid_indexes = PgHero.invalid_indexes
|
27
|
-
@duplicate_indexes = PgHero.duplicate_indexes
|
28
27
|
@good_cache_rate = @table_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100 && @index_hit_rate >= PgHero.cache_hit_rate_threshold.to_f / 100
|
29
28
|
@query_stats_available = PgHero.query_stats_available?
|
30
29
|
@total_connections = PgHero.total_connections
|
@@ -46,16 +46,8 @@
|
|
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 local_assigns[:suggested_indexes] != false && (
|
50
|
-
|
51
|
-
<%= render partial: "suggested_index", locals: {index: index} %>
|
52
|
-
<% end %>
|
53
|
-
<% if @debug %>
|
54
|
-
<code><pre style="color: #f0ad4e; background-color: #333;"><% if i2[:explanation] %><%= i2[:explanation] %><% end %>
|
55
|
-
<% if i2[:row_estimates] %>Rows: <%= i2[:rows] %>
|
56
|
-
Row estimates: <%= i2[:row_estimates].to_a.map { |k, v| "#{k}=#{v}" }.join(", ") %>
|
57
|
-
Row progression: <%= i2[:row_progression].to_a.join(", ") %><% end %></pre></code>
|
58
|
-
<% end %>
|
49
|
+
<% if local_assigns[:suggested_indexes] != false && (details = @suggested_indexes_by_query[query["query"]]) %>
|
50
|
+
<%= render partial: "suggested_index", locals: {index: details[:index], details: details} %>
|
59
51
|
<% end %>
|
60
52
|
</td>
|
61
53
|
</tr>
|
@@ -1 +1,18 @@
|
|
1
|
-
|
1
|
+
<% if index && !details[:covering_index] %>
|
2
|
+
<% unless @debug %>
|
3
|
+
<div style="float: right; color: #f0ad4e; margin-top: 0px; padding: 10px; cursor: pointer;" onclick="document.getElementById('details-<%= index.object_id %>').style.display = 'block'; this.style.display = 'none';">Details</div>
|
4
|
+
<% end %>
|
5
|
+
<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>
|
6
|
+
<% end %>
|
7
|
+
<div id="details-<%= index.object_id %>" style="<%= "display: none;" unless @debug %>">
|
8
|
+
<code><pre style="color: #f0ad4e; background-color: #333;"><% if details[:explanation] %><%= details[:explanation] %>
|
9
|
+
<% end %><% if details[:row_estimates] %>Rows: <%= details[:rows] %>
|
10
|
+
Row progression: <%= details[:row_progression].to_a.join(", ") %>
|
11
|
+
|
12
|
+
Row estimates
|
13
|
+
<%= details[:row_estimates].to_a.map { |k, v| "- #{k}: #{v}" }.join("\n") %><% end %><% if details[:table_indexes] %>
|
14
|
+
|
15
|
+
Existing indexes
|
16
|
+
<% details[:table_indexes].sort_by { |i| [i["primary"] == "t" ? 0 : 1, i["columns"]] }.each do |i3| %>- <%= i3["columns"].join(", ") %><% if i3["using"] != "btree" %> <%= i3["using"].to_s.upcase %><% end %><% if i3["primary"] == "t" %> PRIMARY<% elsif i3["unique"] != "f" %> UNIQUE<% end %>
|
17
|
+
<% end %><% end %></pre></code>
|
18
|
+
</div>
|
@@ -45,13 +45,6 @@
|
|
45
45
|
No invalid indexes
|
46
46
|
<% end %>
|
47
47
|
</div>
|
48
|
-
<div class="alert alert-<%= @duplicate_indexes.empty? ? "success" : "warning" %>">
|
49
|
-
<% if @duplicate_indexes.any? %>
|
50
|
-
<%= pluralize(@duplicate_indexes.size, "duplicate index", "duplicate indexes") %>
|
51
|
-
<% else %>
|
52
|
-
No duplicate indexes
|
53
|
-
<% end %>
|
54
|
-
</div>
|
55
48
|
<% if PgHero.suggested_indexes_enabled? %>
|
56
49
|
<div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
|
57
50
|
<% if @suggested_indexes.any? %>
|
@@ -169,49 +162,6 @@
|
|
169
162
|
</div>
|
170
163
|
<% end %>
|
171
164
|
|
172
|
-
<% if @duplicate_indexes.any? %>
|
173
|
-
<div class="content">
|
174
|
-
<h1>Duplicate Indexes</h1>
|
175
|
-
|
176
|
-
<p>
|
177
|
-
These indexes exist, but aren’t needed. Remove them
|
178
|
-
<% if @show_migrations %>
|
179
|
-
<a href="javascript: void(0);" onclick="document.getElementById('migration2').style.display = 'block';">with a migration</a>
|
180
|
-
<% end %>
|
181
|
-
for faster writes.
|
182
|
-
</p>
|
183
|
-
|
184
|
-
<div id="migration2" style="display: none;">
|
185
|
-
<pre>rails g migration remove_unneeded_indexes</pre>
|
186
|
-
<p>And paste</p>
|
187
|
-
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
|
188
|
-
remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
|
189
|
-
</div>
|
190
|
-
|
191
|
-
<table class="table">
|
192
|
-
<thead>
|
193
|
-
<tr>
|
194
|
-
<th>Details</th>
|
195
|
-
</tr>
|
196
|
-
</thead>
|
197
|
-
<tbody>
|
198
|
-
<% @duplicate_indexes.each do |index| %>
|
199
|
-
<% unneeded_index = index["unneeded_index"] %>
|
200
|
-
<% covering_index = index["covering_index"] %>
|
201
|
-
<tr>
|
202
|
-
<td style="padding-top: 15px; padding-bottom: 5px;">
|
203
|
-
On <%= unneeded_index["table"] %>
|
204
|
-
<pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
|
205
|
-
is covered by
|
206
|
-
<pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
|
207
|
-
</td>
|
208
|
-
</tr>
|
209
|
-
<% end %>
|
210
|
-
</tbody>
|
211
|
-
</table>
|
212
|
-
</div>
|
213
|
-
<% end %>
|
214
|
-
|
215
165
|
<% if @suggested_indexes.any? %>
|
216
166
|
<div class="content">
|
217
167
|
<h1>Suggested Indexes</h1>
|
@@ -230,12 +180,12 @@ remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= q
|
|
230
180
|
<% if index[:using] == "gist" %>
|
231
181
|
connection.execute("CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)")
|
232
182
|
<% else %>
|
233
|
-
add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join("
|
183
|
+
add_index <%= index[:table].to_sym.inspect %>, [<%= index[:columns].map(&:to_sym).map(&:inspect).join(", ") %>], algorithm: :concurrently<% end %><% end %></pre>
|
234
184
|
</div>
|
235
185
|
|
236
186
|
<% @suggested_indexes.each_with_index do |index, i| %>
|
237
187
|
<hr />
|
238
|
-
<%= render partial: "suggested_index", locals: {index: index} %>
|
188
|
+
<%= render partial: "suggested_index", locals: {index: index, details: index[:details]} %>
|
239
189
|
<p>to speed up</p>
|
240
190
|
<%= render partial: "queries_table", locals: {queries: index[:queries].map { |q| @query_stats_by_query[q] }, suggested_indexes: false} %>
|
241
191
|
<% end %>
|
@@ -9,10 +9,6 @@
|
|
9
9
|
<%= render partial: "query_stats_slider" %>
|
10
10
|
<% end %>
|
11
11
|
|
12
|
-
<% if PgHero.suggested_indexes_enabled? %>
|
13
|
-
<p style="text-align: center; margin-top: 7px;">PgHero now suggests indexes. <%= link_to "See how it thinks", {debug: true} %>.</p>
|
14
|
-
<% end %>
|
15
|
-
|
16
12
|
<% if @query_stats_enabled %>
|
17
13
|
<% if @error %>
|
18
14
|
<div class="alert alert-danger">Cannot understand start or end time.</div>
|
data/lib/pghero.rb
CHANGED
@@ -757,13 +757,14 @@ module PgHero
|
|
757
757
|
SELECT
|
758
758
|
t.relname AS table,
|
759
759
|
ix.relname AS name,
|
760
|
-
regexp_replace(pg_get_indexdef(indexrelid), '
|
761
|
-
regexp_replace(pg_get_indexdef(indexrelid), '.* USING (
|
760
|
+
regexp_replace(pg_get_indexdef(indexrelid), '^[^\\(]*\\((.*)\\)$', '\\1') AS columns,
|
761
|
+
regexp_replace(pg_get_indexdef(indexrelid), '.* USING ([^ ]*) \\(.*', '\\1') AS using,
|
762
762
|
indisunique AS unique,
|
763
763
|
indisprimary AS primary,
|
764
764
|
indisvalid AS valid,
|
765
765
|
indexprs::text,
|
766
|
-
indpred::text
|
766
|
+
indpred::text,
|
767
|
+
pg_get_indexdef(indexrelid) AS definition
|
767
768
|
FROM
|
768
769
|
pg_index i
|
769
770
|
INNER JOIN
|
@@ -773,7 +774,7 @@ module PgHero
|
|
773
774
|
ORDER BY
|
774
775
|
1, 2
|
775
776
|
SQL
|
776
|
-
).map { |v| v["columns"] = v["columns"].split(", "); v }
|
777
|
+
).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", "); v }
|
777
778
|
end
|
778
779
|
|
779
780
|
def duplicate_indexes
|
@@ -807,15 +808,18 @@ module PgHero
|
|
807
808
|
|
808
809
|
if best_indexes.any?
|
809
810
|
existing_columns = Hash.new { |hash, key| hash[key] = Hash.new { |hash2, key2| hash2[key2] = [] } }
|
810
|
-
|
811
|
+
indexes = self.indexes
|
812
|
+
indexes.group_by { |g| g["using"] }.each do |group, inds|
|
811
813
|
inds.each do |i|
|
812
814
|
existing_columns[group][i["table"]] << i["columns"]
|
813
815
|
end
|
814
816
|
end
|
817
|
+
indexes_by_table = indexes.group_by { |i| i["table"] }
|
815
818
|
|
816
819
|
best_indexes.each do |query, best_index|
|
817
820
|
if best_index[:found]
|
818
821
|
index = best_index[:index]
|
822
|
+
best_index[:table_indexes] = indexes_by_table[index[:table]].to_a
|
819
823
|
covering_index = existing_columns[index[:using] || "btree"][index[:table]].find { |e| index_covers?(e, index[:columns]) }
|
820
824
|
if covering_index
|
821
825
|
best_index[:covering_index] = covering_index
|
@@ -833,7 +837,11 @@ module PgHero
|
|
833
837
|
indexes = []
|
834
838
|
|
835
839
|
(options[:suggested_indexes_by_query] || suggested_indexes_by_query(options)).select { |s, i| i[:found] && !i[:covering_index] }.group_by { |s, i| i[:index] }.each do |index, group|
|
836
|
-
|
840
|
+
details = {}
|
841
|
+
group.map(&:second).each do |g|
|
842
|
+
details = details.except(:index).deep_merge(g)
|
843
|
+
end
|
844
|
+
indexes << index.merge(queries: group.map(&:first), details: details)
|
837
845
|
end
|
838
846
|
|
839
847
|
indexes.sort_by { |i| [i[:table], i[:columns]] }
|
@@ -951,13 +959,14 @@ module PgHero
|
|
951
959
|
end
|
952
960
|
where = where.sort_by { |c| [row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]), c[:column]] } + sort
|
953
961
|
|
954
|
-
index[:row_estimates] = Hash[where.map { |c| [c[:column], row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }]
|
962
|
+
index[:row_estimates] = Hash[where.map { |c| ["#{c[:column]} (#{c[:op] || "sort"})", row_estimates(ranks[c[:column]], total_rows, total_rows, c[:op]).round] }]
|
955
963
|
|
956
964
|
# no index needed if less than 500 rows
|
957
965
|
if total_rows >= 500
|
958
966
|
|
959
967
|
if ["~~", "~~*"].include?(where.first[:op])
|
960
968
|
index[:found] = true
|
969
|
+
index[:row_progression] = [total_rows, index[:row_estimates].values.first]
|
961
970
|
index[:index] = {table: table, columns: ["#{where.first[:column]} gist_trgm_ops"], using: "gist"}
|
962
971
|
else
|
963
972
|
# if most values are unique, no need to index others
|
@@ -969,7 +978,7 @@ module PgHero
|
|
969
978
|
final_where << c[:column]
|
970
979
|
rows_left = row_estimates(ranks[c[:column]], total_rows, rows_left, c[:op])
|
971
980
|
prev_rows_left << rows_left
|
972
|
-
if rows_left < 50 || final_where.size >=
|
981
|
+
if rows_left < 50 || final_where.size >= 2 || [">", ">=", "<", "<=", "~~", "~~*"].include?(c[:op])
|
973
982
|
break
|
974
983
|
end
|
975
984
|
end
|
data/lib/pghero/version.rb
CHANGED
data/pghero.gemspec
CHANGED
@@ -23,10 +23,12 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.6"
|
24
24
|
spec.add_development_dependency "rake"
|
25
25
|
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency "activerecord-import"
|
26
27
|
|
27
28
|
if RUBY_PLATFORM == "java"
|
28
29
|
spec.add_development_dependency "activerecord-jdbcpostgresql-adapter"
|
29
30
|
else
|
30
31
|
spec.add_development_dependency "pg"
|
32
|
+
spec.add_development_dependency "pg_query"
|
31
33
|
end
|
32
34
|
end
|
data/test/best_index_test.rb
CHANGED
@@ -12,7 +12,7 @@ class BestIndexTest < Minitest::Test
|
|
12
12
|
structure: {table: "users", where: [{column: "login_attempts", op: "="}], sort: [{column: "created_at", direction: "asc"}]},
|
13
13
|
index: {table: "users", columns: ["login_attempts", "created_at"]},
|
14
14
|
rows: 10000,
|
15
|
-
row_estimates: {"login_attempts" => 333, "created_at" => 1},
|
15
|
+
row_estimates: {"login_attempts (=)" => 333, "created_at (sort)" => 1},
|
16
16
|
row_progression: [10000, 333, 0]
|
17
17
|
}
|
18
18
|
assert_equal expected, index
|
@@ -9,6 +9,6 @@ class SuggestedIndexesTest < Minitest::Test
|
|
9
9
|
User.where(email: "person1@example.org").first
|
10
10
|
# User.where(email: "person1@example.org", city_id: 1).first
|
11
11
|
# User.where(city_id: 1).to_a
|
12
|
-
assert_equal [{table: "users", columns: ["email"]}], PgHero.suggested_indexes.map { |q| q.except(:queries) }
|
12
|
+
assert_equal [{table: "users", columns: ["email"]}], PgHero.suggested_indexes.map { |q| q.except(:queries, :details) }
|
13
13
|
end
|
14
14
|
end
|
data/test/test_helper.rb
CHANGED
@@ -3,6 +3,7 @@ Bundler.require(:default)
|
|
3
3
|
require "minitest/autorun"
|
4
4
|
require "minitest/pride"
|
5
5
|
require "pg_query"
|
6
|
+
require "activerecord-import"
|
6
7
|
|
7
8
|
# for Minitest < 5
|
8
9
|
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
|
@@ -33,17 +34,19 @@ if ENV["SEED"]
|
|
33
34
|
end
|
34
35
|
|
35
36
|
User.transaction do
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
users =
|
38
|
+
10000.times.map do |i|
|
39
|
+
city_id = i % 100
|
40
|
+
User.new(
|
41
|
+
city_id: city_id,
|
42
|
+
email: "person#{i}@example.org",
|
43
|
+
login_attempts: rand(30),
|
44
|
+
zip_code: i % 40 == 0 ? nil : "12345",
|
45
|
+
active: true,
|
46
|
+
created_at: Time.now - rand(50).days
|
47
|
+
)
|
48
|
+
end
|
49
|
+
User.import users, validate: false
|
47
50
|
end
|
48
51
|
ActiveRecord::Base.connection.execute("VACUUM ANALYZE users")
|
49
52
|
|
@@ -52,9 +55,11 @@ if ENV["SEED"]
|
|
52
55
|
end
|
53
56
|
|
54
57
|
State.transaction do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
states =
|
59
|
+
50.times.map do |i|
|
60
|
+
State.new(name: "State #{i}")
|
61
|
+
end
|
62
|
+
State.import states, validate: false
|
58
63
|
end
|
59
64
|
ActiveRecord::Base.connection.execute("VACUUM ANALYZE states")
|
60
65
|
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord-import
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: pg
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - ">="
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg_query
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description: The missing dashboard for Postgres
|
84
112
|
email:
|
85
113
|
- andrew@chartkick.com
|