pghero 1.2.4 → 1.3.0
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/app/controllers/pg_hero/home_controller.rb +2 -0
- data/app/views/layouts/pg_hero/application.html.erb +1 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +90 -0
- data/guides/Rails.md +17 -0
- data/lib/generators/pghero/templates/query_stats.rb +1 -0
- data/lib/pghero.rb +2 -0
- data/lib/pghero/methods/basic.rb +8 -0
- data/lib/pghero/methods/indexes.rb +1 -1
- data/lib/pghero/methods/kill.rb +1 -0
- data/lib/pghero/methods/queries.rb +7 -2
- data/lib/pghero/methods/query_stats.rb +82 -30
- data/lib/pghero/methods/sequences.rb +37 -0
- data/lib/pghero/methods/suggested_indexes.rb +9 -1
- data/lib/pghero/methods/system.rb +6 -2
- data/lib/pghero/version.rb +1 -1
- data/test/best_index_test.rb +9 -0
- data/test/gemfiles/activerecord41.gemfile +6 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5de5f457151a5d2e77e515b117225033f2f5153b
|
4
|
+
data.tar.gz: fa0a4eea1887464cc49e376aa5675610819f92fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af9c72a1eaf428e6c2b7b43a5df732f848633e5f1fa4835c697ff801384fd3b8920d43516a7a5fef9f212b46ee70b9f33ef81e7557fa78b1d086d7e180321aab
|
7
|
+
data.tar.gz: 2a687404ad19e82ee2bd7517cd7a81837fe9ea469110028f2fe656b82849977ad139c5407b52168678dfd006e547a316ebc886f953e7db034ea8daae99eb79b4
|
data/CHANGELOG.md
CHANGED
@@ -31,6 +31,7 @@ module PgHero
|
|
31
31
|
end
|
32
32
|
@unused_indexes = PgHero.unused_indexes.select { |q| q["index_scans"].to_i == 0 }
|
33
33
|
@invalid_indexes = PgHero.invalid_indexes
|
34
|
+
@duplicate_indexes = PgHero.duplicate_indexes if params[:duplicate_indexes]
|
34
35
|
@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
|
35
36
|
@query_stats_available = PgHero.query_stats_available?
|
36
37
|
@total_connections = PgHero.total_connections
|
@@ -42,6 +43,7 @@ module PgHero
|
|
42
43
|
@transaction_id_danger = PgHero.transaction_id_danger(threshold: 1500000000)
|
43
44
|
set_suggested_indexes((params[:min_average_time] || 20).to_f, (params[:min_calls] || 50).to_i)
|
44
45
|
@show_migrations = PgHero.show_migrations
|
46
|
+
@sequence_danger = PgHero.sequence_danger(threshold: params[:sequence_threshold])
|
45
47
|
end
|
46
48
|
|
47
49
|
def index_usage
|
@@ -422,6 +422,7 @@
|
|
422
422
|
<li class="<%= controller.action_name == "space" ? "active" : "" %>"><%= link_to "Space", space_path %></li>
|
423
423
|
<li class="<%= controller.action_name == "connections" ? "active" : "" %>"><%= link_to "Connections", connections_path %></li>
|
424
424
|
<li class="<%= controller.action_name == "live_queries" ? "active" : "" %>"><%= link_to "Live Queries", live_queries_path %></li>
|
425
|
+
<li class="<%= controller.action_name == "maintenance" ? "active" : "" %>"><%= link_to "Maintenance", maintenance_path %></li>
|
425
426
|
<li class="<%= controller.action_name == "explain" ? "active" : "" %>"><%= link_to "Explain", explain_path %></li>
|
426
427
|
<li class="<%= controller.action_name == "tune" ? "active" : "" %>"><%= link_to "Tune", tune_path %></li>
|
427
428
|
</ul>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
<td><%= query["pid"] %></td>
|
16
16
|
<td><%= query["state"] %></td>
|
17
17
|
<td><%= query["started_at"] ? "#{number_with_delimiter(((now - Time.parse(query["started_at"])) * 1000).round)} ms" : nil %></td>
|
18
|
-
<td><%= query["source"] %></td>
|
18
|
+
<td><%= query["source"] %> <span class="text-muted"><%= query["user"] %></span></td>
|
19
19
|
<td class="text-right">
|
20
20
|
<%= button_to "Explain", explain_path(query: query["query"]), form: {target: "_blank"}, class: "btn btn-info" %>
|
21
21
|
<%= button_to "Kill", kill_path(pid: query["pid"]), class: "btn btn-danger" %>
|
@@ -38,6 +38,13 @@
|
|
38
38
|
Vacuuming healthy
|
39
39
|
<% end %>
|
40
40
|
</div>
|
41
|
+
<div class="alert alert-<%= @sequence_danger.empty? ? "success" : "warning" %>">
|
42
|
+
<% if @sequence_danger.any? %>
|
43
|
+
<%= pluralize(@sequence_danger.size, "columns") %> approaching overflow
|
44
|
+
<% else %>
|
45
|
+
No columns near overflow
|
46
|
+
<% end %>
|
47
|
+
</div>
|
41
48
|
<div class="alert alert-<%= @invalid_indexes.empty? ? "success" : "warning" %>">
|
42
49
|
<% if @invalid_indexes.any? %>
|
43
50
|
<%= pluralize(@invalid_indexes.size, "invalid index", "invalid indexes") %>
|
@@ -45,6 +52,15 @@
|
|
45
52
|
No invalid indexes
|
46
53
|
<% end %>
|
47
54
|
</div>
|
55
|
+
<% if @duplicate_indexes %>
|
56
|
+
<div class="alert alert-<%= @duplicate_indexes.empty? ? "success" : "warning" %>">
|
57
|
+
<% if @duplicate_indexes.any? %>
|
58
|
+
<%= pluralize(@duplicate_indexes.size, "duplicate index", "duplicate indexes") %>
|
59
|
+
<% else %>
|
60
|
+
No duplicate indexes
|
61
|
+
<% end %>
|
62
|
+
</div>
|
63
|
+
<% end %>
|
48
64
|
<% if PgHero.suggested_indexes_enabled? %>
|
49
65
|
<div class="alert alert-<%= @suggested_indexes.empty? ? "success" : "warning" %>">
|
50
66
|
<% if @suggested_indexes.any? %>
|
@@ -139,6 +155,37 @@
|
|
139
155
|
</div>
|
140
156
|
<% end %>
|
141
157
|
|
158
|
+
<% if @sequence_danger.any? %>
|
159
|
+
<div class="content">
|
160
|
+
<h2>Columns Near Overflow</h2>
|
161
|
+
<p>Consider changing the column type to support a larger range of values.</p>
|
162
|
+
<table class="table">
|
163
|
+
<thead>
|
164
|
+
<tr>
|
165
|
+
<th>Column</th>
|
166
|
+
<th style="width: 20%;">Type</th>
|
167
|
+
<th style="width: 20%;">Values Left</th>
|
168
|
+
</tr>
|
169
|
+
</thead>
|
170
|
+
<tbody>
|
171
|
+
<% @sequence_danger.sort_by { |s| [s["table"], s["column"]] }.each do |query| %>
|
172
|
+
<tr>
|
173
|
+
<td>
|
174
|
+
<%= query["table"] %>.<%= query["column"] %>
|
175
|
+
</td>
|
176
|
+
<td>
|
177
|
+
<span class="text-muted"><%= query["column_type"] %></span>
|
178
|
+
</td>
|
179
|
+
<td>
|
180
|
+
<%= number_with_delimiter(query["max_value"].to_i - query["last_value"].to_i) %>
|
181
|
+
</td>
|
182
|
+
</tr>
|
183
|
+
<% end %>
|
184
|
+
</tbody>
|
185
|
+
</table>
|
186
|
+
</div>
|
187
|
+
<% end %>
|
188
|
+
|
142
189
|
<% if @invalid_indexes.any? %>
|
143
190
|
<div class="content">
|
144
191
|
<h1>Invalid Indexes</h1>
|
@@ -162,6 +209,49 @@
|
|
162
209
|
</div>
|
163
210
|
<% end %>
|
164
211
|
|
212
|
+
<% if @duplicate_indexes && @duplicate_indexes.any? %>
|
213
|
+
<div class="content">
|
214
|
+
<h1>Duplicate Indexes</h1>
|
215
|
+
|
216
|
+
<p>
|
217
|
+
These indexes exist, but aren’t needed. Remove them
|
218
|
+
<% if @show_migrations %>
|
219
|
+
<a href="javascript: void(0);" onclick="document.getElementById('migration2').style.display = 'block';">with a migration</a>
|
220
|
+
<% end %>
|
221
|
+
for faster writes.
|
222
|
+
</p>
|
223
|
+
|
224
|
+
<div id="migration2" style="display: none;">
|
225
|
+
<pre>rails g migration remove_unneeded_indexes</pre>
|
226
|
+
<p>And paste</p>
|
227
|
+
<pre style="overflow: scroll; white-space: pre; word-break: normal;"><% @duplicate_indexes.each do |query| %>
|
228
|
+
remove_index <%= query["unneeded_index"]["table"].to_sym.inspect %>, name: <%= query["unneeded_index"]["name"].to_s.inspect %><% end %></pre>
|
229
|
+
</div>
|
230
|
+
|
231
|
+
<table class="table">
|
232
|
+
<thead>
|
233
|
+
<tr>
|
234
|
+
<th>Details</th>
|
235
|
+
</tr>
|
236
|
+
</thead>
|
237
|
+
<tbody>
|
238
|
+
<% @duplicate_indexes.each do |index| %>
|
239
|
+
<% unneeded_index = index["unneeded_index"] %>
|
240
|
+
<% covering_index = index["covering_index"] %>
|
241
|
+
<tr>
|
242
|
+
<td style="padding-top: 15px; padding-bottom: 5px;">
|
243
|
+
On <%= unneeded_index["table"] %>
|
244
|
+
<pre><%= unneeded_index["name"] %> (<%= unneeded_index["columns"].join(", ") %>)</pre>
|
245
|
+
is covered by
|
246
|
+
<pre><%= covering_index["name"] %> (<%= covering_index["columns"].join(", ") %>)</pre>
|
247
|
+
</td>
|
248
|
+
</tr>
|
249
|
+
<% end %>
|
250
|
+
</tbody>
|
251
|
+
</table>
|
252
|
+
</div>
|
253
|
+
<% end %>
|
254
|
+
|
165
255
|
<% if @suggested_indexes.any? %>
|
166
256
|
<div class="content">
|
167
257
|
<h1>Suggested Indexes</h1>
|
data/guides/Rails.md
CHANGED
@@ -242,6 +242,23 @@ Minimum connections for high connections warning
|
|
242
242
|
PgHero.total_connections_threshold = 100 # default
|
243
243
|
```
|
244
244
|
|
245
|
+
## Upgrading
|
246
|
+
|
247
|
+
### 1.3.0
|
248
|
+
|
249
|
+
For better query stats grouping with Postgres 9.4+, create a migration with:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
add_column :pghero_query_stats, :query_hash, :integer, limit: 8
|
253
|
+
```
|
254
|
+
|
255
|
+
If you get an error with `queryid`, recreate the `pg_stat_statements` extension.
|
256
|
+
|
257
|
+
```sql
|
258
|
+
DROP EXTENSION pg_stat_statements;
|
259
|
+
CREATE EXTENSION pg_stat_statements;
|
260
|
+
```
|
261
|
+
|
245
262
|
## Bonus
|
246
263
|
|
247
264
|
- See where queries come from with [Marginalia](https://github.com/basecamp/marginalia) - comments appear on the Live Queries tab.
|
data/lib/pghero.rb
CHANGED
@@ -18,6 +18,7 @@ require "pghero/methods/maintenance"
|
|
18
18
|
require "pghero/methods/queries"
|
19
19
|
require "pghero/methods/query_stats"
|
20
20
|
require "pghero/methods/replica"
|
21
|
+
require "pghero/methods/sequences"
|
21
22
|
require "pghero/methods/space"
|
22
23
|
require "pghero/methods/suggested_indexes"
|
23
24
|
require "pghero/methods/system"
|
@@ -47,6 +48,7 @@ module PgHero
|
|
47
48
|
extend Methods::Queries
|
48
49
|
extend Methods::QueryStats
|
49
50
|
extend Methods::Replica
|
51
|
+
extend Methods::Sequences
|
50
52
|
extend Methods::Space
|
51
53
|
extend Methods::SuggestedIndexes
|
52
54
|
extend Methods::System
|
data/lib/pghero/methods/basic.rb
CHANGED
@@ -133,7 +133,7 @@ module PgHero
|
|
133
133
|
ORDER BY
|
134
134
|
1, 2
|
135
135
|
SQL
|
136
|
-
).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", "); v }
|
136
|
+
).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
|
137
137
|
end
|
138
138
|
|
139
139
|
def duplicate_indexes
|
data/lib/pghero/methods/kill.rb
CHANGED
@@ -10,13 +10,15 @@ module PgHero
|
|
10
10
|
age(now(), xact_start) AS duration,
|
11
11
|
waiting,
|
12
12
|
query,
|
13
|
-
xact_start AS started_at
|
13
|
+
xact_start AS started_at,
|
14
|
+
usename AS user
|
14
15
|
FROM
|
15
16
|
pg_stat_activity
|
16
17
|
WHERE
|
17
18
|
query <> '<insufficient privilege>'
|
18
19
|
AND state <> 'idle'
|
19
20
|
AND pid <> pg_backend_pid()
|
21
|
+
AND datname = current_database()
|
20
22
|
ORDER BY
|
21
23
|
query_start DESC
|
22
24
|
SQL
|
@@ -31,7 +33,8 @@ module PgHero
|
|
31
33
|
age(now(), xact_start) AS duration,
|
32
34
|
waiting,
|
33
35
|
query,
|
34
|
-
xact_start AS started_at
|
36
|
+
xact_start AS started_at,
|
37
|
+
usename AS user
|
35
38
|
FROM
|
36
39
|
pg_stat_activity
|
37
40
|
WHERE
|
@@ -39,6 +42,7 @@ module PgHero
|
|
39
42
|
AND state <> 'idle'
|
40
43
|
AND pid <> pg_backend_pid()
|
41
44
|
AND now() - query_start > interval '#{long_running_query_sec.to_i} seconds'
|
45
|
+
AND datname = current_database()
|
42
46
|
ORDER BY
|
43
47
|
query_start DESC
|
44
48
|
SQL
|
@@ -63,6 +67,7 @@ module PgHero
|
|
63
67
|
pg_stat_activity.query <> '<insufficient privilege>'
|
64
68
|
AND pg_locks.mode = 'ExclusiveLock'
|
65
69
|
AND pg_stat_activity.pid <> pg_backend_pid()
|
70
|
+
AND pg_stat_activity.datname = current_database()
|
66
71
|
ORDER BY
|
67
72
|
pid,
|
68
73
|
query_start
|
@@ -2,20 +2,21 @@ module PgHero
|
|
2
2
|
module Methods
|
3
3
|
module QueryStats
|
4
4
|
def query_stats(options = {})
|
5
|
-
current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["
|
6
|
-
historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["
|
5
|
+
current_query_stats = (options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)).index_by { |q| q["query_hash"] }
|
6
|
+
historical_query_stats = (options[:historical] ? historical_query_stats(options) : []).index_by { |q| q["query_hash"] }
|
7
7
|
current_query_stats.default = {}
|
8
8
|
historical_query_stats.default = {}
|
9
9
|
|
10
10
|
query_stats = []
|
11
|
-
(current_query_stats.keys + historical_query_stats.keys).uniq.each do |
|
11
|
+
(current_query_stats.keys + historical_query_stats.keys).uniq.each do |query_hash|
|
12
12
|
value = {
|
13
|
-
"query" => query,
|
14
|
-
"
|
15
|
-
"
|
13
|
+
"query" => current_query_stats[query_hash]["query"] || historical_query_stats[query_hash]["query"],
|
14
|
+
"query_hash" => query_hash,
|
15
|
+
"total_minutes" => current_query_stats[query_hash]["total_minutes"].to_f + historical_query_stats[query_hash]["total_minutes"].to_f,
|
16
|
+
"calls" => current_query_stats[query_hash]["calls"].to_i + historical_query_stats[query_hash]["calls"].to_i
|
16
17
|
}
|
17
18
|
value["average_time"] = value["total_minutes"] * 1000 * 60 / value["calls"]
|
18
|
-
value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[
|
19
|
+
value["total_percent"] = value["total_minutes"] * 100.0 / (current_query_stats[query_hash]["all_queries_total_minutes"].to_f + historical_query_stats[query_hash]["all_queries_total_minutes"].to_f)
|
19
20
|
query_stats << value
|
20
21
|
end
|
21
22
|
sort = options[:sort] || "total_minutes"
|
@@ -61,24 +62,56 @@ module PgHero
|
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
65
|
+
# resetting query stats will reset across the entire Postgres instance
|
66
|
+
# this is problematic if multiple PgHero databases use the same Postgres instance
|
67
|
+
#
|
68
|
+
# to get around this, we capture queries for every Postgres database before we
|
69
|
+
# reset query stats for the Postgres instance with the `capture_query_stats` option
|
64
70
|
def capture_query_stats
|
65
|
-
|
71
|
+
# get database names
|
72
|
+
pg_databases = {}
|
73
|
+
supports_query_hash = {}
|
74
|
+
config["databases"].each do |k, _|
|
75
|
+
with(k) do
|
76
|
+
pg_databases[k] = execute("SELECT current_database()").first["current_database"]
|
77
|
+
supports_query_hash[k] = supports_query_hash?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
config["databases"].reject { |_, v| v["capture_query_stats"] && v["capture_query_stats"] != true }.each do |database, _|
|
66
82
|
with(database) do
|
83
|
+
mapping = {database => pg_databases[database]}
|
84
|
+
config["databases"].select { |_, v| v["capture_query_stats"] == database }.each do |k, _|
|
85
|
+
mapping[k] = pg_databases[k]
|
86
|
+
end
|
87
|
+
|
67
88
|
now = Time.now
|
68
|
-
query_stats =
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
89
|
+
query_stats = {}
|
90
|
+
mapping.each do |db, pg_database|
|
91
|
+
query_stats[db] = self.query_stats(limit: 1000000, database: pg_database)
|
92
|
+
end
|
93
|
+
|
94
|
+
if query_stats.any? { |_, v| v.any? } && reset_query_stats
|
95
|
+
query_stats.each do |db, db_query_stats|
|
96
|
+
if db_query_stats.any?
|
97
|
+
values =
|
98
|
+
db_query_stats.map do |qs|
|
99
|
+
values = [
|
100
|
+
db,
|
101
|
+
qs["query"],
|
102
|
+
qs["total_minutes"].to_f * 60 * 1000,
|
103
|
+
qs["calls"],
|
104
|
+
now
|
105
|
+
]
|
106
|
+
values << qs["query_hash"] if supports_query_hash[db]
|
107
|
+
values.map { |v| quote(v) }.join(",")
|
108
|
+
end.map { |v| "(#{v})" }.join(",")
|
109
|
+
|
110
|
+
columns = %w[database query total_time calls captured_at]
|
111
|
+
columns << "query_hash" if supports_query_hash[db]
|
112
|
+
stats_connection.execute("INSERT INTO pghero_query_stats (#{columns.join(", ")}) VALUES #{values}")
|
113
|
+
end
|
114
|
+
end
|
82
115
|
end
|
83
116
|
end
|
84
117
|
end
|
@@ -115,22 +148,25 @@ module PgHero
|
|
115
148
|
if query_stats_enabled?
|
116
149
|
limit = options[:limit] || 100
|
117
150
|
sort = options[:sort] || "total_minutes"
|
151
|
+
database = options[:database] ? quote(options[:database]) : "current_database()"
|
118
152
|
select_all <<-SQL
|
119
153
|
WITH query_stats AS (
|
120
154
|
SELECT
|
121
155
|
query,
|
122
|
-
|
123
|
-
(total_time /
|
156
|
+
#{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
|
157
|
+
(total_time / 1000 / 60) AS total_minutes,
|
158
|
+
(total_time / calls) AS average_time,
|
124
159
|
calls
|
125
160
|
FROM
|
126
161
|
pg_stat_statements
|
127
162
|
INNER JOIN
|
128
163
|
pg_database ON pg_database.oid = pg_stat_statements.dbid
|
129
164
|
WHERE
|
130
|
-
pg_database.datname =
|
165
|
+
pg_database.datname = #{database}
|
131
166
|
)
|
132
167
|
SELECT
|
133
168
|
query,
|
169
|
+
query_hash,
|
134
170
|
total_minutes,
|
135
171
|
average_time,
|
136
172
|
calls,
|
@@ -153,20 +189,23 @@ module PgHero
|
|
153
189
|
stats_connection.select_all squish <<-SQL
|
154
190
|
WITH query_stats AS (
|
155
191
|
SELECT
|
156
|
-
query,
|
157
|
-
(
|
158
|
-
(SUM(total_time) /
|
159
|
-
SUM(calls)
|
192
|
+
#{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
|
193
|
+
MAX(query) AS query,
|
194
|
+
(SUM(total_time) / 1000 / 60) AS total_minutes,
|
195
|
+
(SUM(total_time) / SUM(calls)) AS average_time,
|
196
|
+
SUM(calls) AS calls
|
160
197
|
FROM
|
161
198
|
pghero_query_stats
|
162
199
|
WHERE
|
163
200
|
database = #{quote(current_database)}
|
201
|
+
#{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
|
164
202
|
#{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
|
165
203
|
#{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
|
166
204
|
GROUP BY
|
167
|
-
|
205
|
+
1
|
168
206
|
)
|
169
207
|
SELECT
|
208
|
+
query_hash,
|
170
209
|
query,
|
171
210
|
total_minutes,
|
172
211
|
average_time,
|
@@ -183,6 +222,19 @@ module PgHero
|
|
183
222
|
[]
|
184
223
|
end
|
185
224
|
end
|
225
|
+
|
226
|
+
def supports_query_hash?
|
227
|
+
@supports_query_hash ||= {}
|
228
|
+
if @supports_query_hash[current_database].nil?
|
229
|
+
@supports_query_hash[current_database] = server_version >= 90400 && historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("query_hash")
|
230
|
+
end
|
231
|
+
@supports_query_hash[current_database]
|
232
|
+
end
|
233
|
+
|
234
|
+
def server_version
|
235
|
+
@server_version ||= {}
|
236
|
+
@server_version[current_database] ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
|
237
|
+
end
|
186
238
|
end
|
187
239
|
end
|
188
240
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Sequences
|
4
|
+
def sequences
|
5
|
+
sequences = select_all <<-SQL
|
6
|
+
SELECT
|
7
|
+
sequence_schema AS schema,
|
8
|
+
table_name AS table,
|
9
|
+
column_name AS column,
|
10
|
+
c.data_type AS column_type,
|
11
|
+
CASE WHEN c.data_type = 'integer' THEN 2147483647::bigint ELSE maximum_value::bigint END AS max_value,
|
12
|
+
sequence_name AS sequence
|
13
|
+
FROM
|
14
|
+
information_schema.columns c
|
15
|
+
INNER JOIN
|
16
|
+
information_schema.sequences iss ON iss.sequence_name = regexp_replace(c.column_default, '^nextval\\(''(.*)''\\:\\:regclass\\)$', '\\1')
|
17
|
+
WHERE
|
18
|
+
column_default LIKE 'nextval%'
|
19
|
+
AND table_catalog = current_database()
|
20
|
+
ORDER BY
|
21
|
+
sequence_name ASC
|
22
|
+
SQL
|
23
|
+
|
24
|
+
select_all(sequences.map { |s| "SELECT last_value FROM #{s["sequence"]}" }.join(" UNION ALL ")).each_with_index do |row, i|
|
25
|
+
sequences[i]["last_value"] = row["last_value"]
|
26
|
+
end
|
27
|
+
|
28
|
+
sequences
|
29
|
+
end
|
30
|
+
|
31
|
+
def sequence_danger(options = {})
|
32
|
+
threshold = (options[:threshold] || 0.9).to_f
|
33
|
+
sequences.select { |s| s["last_value"].to_i / s["max_value"].to_f > threshold }.sort_by { |s| s["max_value"].to_i - s["last_value"].to_i }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -274,11 +274,17 @@ module PgHero
|
|
274
274
|
if tree["BOOLEXPR"]
|
275
275
|
if tree["BOOLEXPR"]["boolop"] == 0
|
276
276
|
tree["BOOLEXPR"]["args"].flat_map { |v| parse_where(v) }
|
277
|
+
else
|
278
|
+
raise "Not Implemented"
|
277
279
|
end
|
278
280
|
elsif tree["AEXPR AND"]
|
279
281
|
left = parse_where(tree["AEXPR AND"]["lexpr"])
|
280
282
|
right = parse_where(tree["AEXPR AND"]["rexpr"])
|
281
|
-
|
283
|
+
if left && right
|
284
|
+
left + right
|
285
|
+
else
|
286
|
+
raise "Not Implemented"
|
287
|
+
end
|
282
288
|
elsif aexpr && ["=", "<>", ">", ">=", "<", "<=", "~~", "~~*", "BETWEEN"].include?(aexpr["name"].first)
|
283
289
|
[{column: aexpr["lexpr"]["COLUMNREF"]["fields"].last, op: aexpr["name"].first}]
|
284
290
|
elsif tree["AEXPR IN"] && ["=", "<>"].include?(tree["AEXPR IN"]["name"].first)
|
@@ -286,6 +292,8 @@ module PgHero
|
|
286
292
|
elsif tree["NULLTEST"]
|
287
293
|
op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
|
288
294
|
[{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
|
295
|
+
else
|
296
|
+
raise "Not Implemented"
|
289
297
|
end
|
290
298
|
end
|
291
299
|
|
@@ -25,9 +25,9 @@ module PgHero
|
|
25
25
|
if system_stats_enabled?
|
26
26
|
client =
|
27
27
|
if defined?(Aws)
|
28
|
-
Aws::CloudWatch::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key)
|
28
|
+
Aws::CloudWatch::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region)
|
29
29
|
else
|
30
|
-
AWS::CloudWatch.new(access_key_id: access_key_id, secret_access_key: secret_access_key).client
|
30
|
+
AWS::CloudWatch.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region).client
|
31
31
|
end
|
32
32
|
|
33
33
|
now = Time.now
|
@@ -62,6 +62,10 @@ module PgHero
|
|
62
62
|
ENV["PGHERO_SECRET_ACCESS_KEY"] || ENV["AWS_SECRET_ACCESS_KEY"]
|
63
63
|
end
|
64
64
|
|
65
|
+
def region
|
66
|
+
ENV["PGHERO_REGION"] || ENV["AWS_REGION"] || (defined?(Aws) && Aws.config[:region]) || "us-east-1"
|
67
|
+
end
|
68
|
+
|
65
69
|
def db_instance_identifier
|
66
70
|
databases[current_database].db_instance_identifier
|
67
71
|
end
|
data/lib/pghero/version.rb
CHANGED
data/test/best_index_test.rb
CHANGED
@@ -130,6 +130,15 @@ class BestIndexTest < Minitest::Test
|
|
130
130
|
assert_no_index "Unknown structure", "SELECT NOW()"
|
131
131
|
end
|
132
132
|
|
133
|
+
def test_where_or
|
134
|
+
assert_no_index "Unknown structure", "SELECT FROM users WHERE login_attempts = 0 OR login_attempts = 1"
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_where_nested_or
|
138
|
+
assert_no_index "Unknown structure", "SELECT FROM users WHERE city_id = 1 AND (login_attempts = 0 OR login_attempts = 1)"
|
139
|
+
end
|
140
|
+
|
141
|
+
|
133
142
|
def test_multiple_tables
|
134
143
|
assert_no_index "JOIN not supported yet", "SELECT * FROM users INNER JOIN cities ON cities.id = users.city_id"
|
135
144
|
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.
|
4
|
+
version: 1.3.0
|
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-
|
11
|
+
date: 2016-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -162,6 +162,7 @@ files:
|
|
162
162
|
- lib/pghero/methods/queries.rb
|
163
163
|
- lib/pghero/methods/query_stats.rb
|
164
164
|
- lib/pghero/methods/replica.rb
|
165
|
+
- lib/pghero/methods/sequences.rb
|
165
166
|
- lib/pghero/methods/space.rb
|
166
167
|
- lib/pghero/methods/suggested_indexes.rb
|
167
168
|
- lib/pghero/methods/system.rb
|
@@ -173,6 +174,7 @@ files:
|
|
173
174
|
- pghero.gemspec
|
174
175
|
- test/best_index_test.rb
|
175
176
|
- test/explain_test.rb
|
177
|
+
- test/gemfiles/activerecord41.gemfile
|
176
178
|
- test/suggested_indexes_test.rb
|
177
179
|
- test/test_helper.rb
|
178
180
|
homepage: https://github.com/ankane/pghero
|
@@ -208,6 +210,7 @@ test_files:
|
|
208
210
|
- guides/Suggested-Indexes.md
|
209
211
|
- test/best_index_test.rb
|
210
212
|
- test/explain_test.rb
|
213
|
+
- test/gemfiles/activerecord41.gemfile
|
211
214
|
- test/suggested_indexes_test.rb
|
212
215
|
- test/test_helper.rb
|
213
216
|
has_rdoc:
|