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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3975a93a4da7c4199e2e3a5f98c4c98e35c991e9
4
- data.tar.gz: 7a2a1ba863c86c42d09316c624cbd9fa411bda41
3
+ metadata.gz: d444b648639843cbae37f0e554e47ff37462222d
4
+ data.tar.gz: 2482c623785d74f933b836e89233b0c2c44a6e75
5
5
  SHA512:
6
- metadata.gz: 745ac33fea1c485e3263075b4a17c04b79894f240776a65331818a160b8ef3a0f308f362aeb590c433be96e1f684dd36d764ae3087bf16ef4826d284c168f8c7
7
- data.tar.gz: c671471aa2a742f428a73b8d05c4af7624baa324e80654ae3d20c3b4fbf1c615ee942860b383c6c03c76ae798efe27476f4adb2152a4744f007de924bdbcc0c5
6
+ metadata.gz: 648695c06b8a8264eb9380f0a6b2700c7846540d8490be0d476ba66d063bc73896baadcf9235057b513a7fdf8dbea8819d60a03526d9c32cb5082ab72264a9df
7
+ data.tar.gz: d69b31b33eb386ee52e761dfee89884e928012913c963aa1ab4758397007753952a17a2506fe930a667a6e29686eea62a8949008e422a809a9382412a49916a9
@@ -1,3 +1,9 @@
1
+ ## 1.2.2
2
+
3
+ - Better suggested indexes
4
+ - Removed duplicate indexes noise
5
+ - Fixed partial and expression indexes
6
+
1
7
  ## 1.2.1
2
8
 
3
9
  - Better suggested indexes
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in pghero.gemspec
4
4
  gemspec
5
-
6
- gem "pg_query"
@@ -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
@@ -82,6 +82,7 @@
82
82
  border: none;
83
83
  height: 0;
84
84
  border-top: solid 1px #ddd;
85
+ margin-bottom: 15px;
85
86
  }
86
87
 
87
88
  .container {
@@ -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 && (i2 = @suggested_indexes_by_query[query["query"]]) %>
50
- <% if (index = i2[:index]) && !i2[:covering_index] %>
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
- <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
+ <% 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(" ,") %>], algorithm: :concurrently<% end %><% end %></pre>
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>
@@ -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), '.*\\((.*)\\)', '\\1') AS columns,
761
- regexp_replace(pg_get_indexdef(indexrelid), '.* USING (.*) \\(.*', '\\1') AS 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
- self.indexes.group_by { |g| g["using"] }.each do |group, inds|
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
- indexes << index.merge(queries: group.map(&:first))
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 >= 3 || [">", ">=", "<", "<=", "~~", "~~*"].include?(c[:op])
981
+ if rows_left < 50 || final_where.size >= 2 || [">", ">=", "<", "<=", "~~", "~~*"].include?(c[:op])
973
982
  break
974
983
  end
975
984
  end
@@ -1,3 +1,3 @@
1
1
  module PgHero
2
- VERSION = "1.2.1"
2
+ VERSION = "1.2.2"
3
3
  end
@@ -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
@@ -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
@@ -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
- 10000.times do |i|
37
- city_id = i % 100
38
- User.create!(
39
- city_id: city_id,
40
- email: "person#{i}@example.org",
41
- login_attempts: rand(30),
42
- zip_code: i % 40 == 0 ? nil : "12345",
43
- active: true,
44
- created_at: Time.now - rand(50).days
45
- )
46
- end
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
- 50.times do |i|
56
- State.create!(name: "State #{i}")
57
- end
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.1
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-07 00:00:00.000000000 Z
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