pghero 1.7.0 → 2.0.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/.travis.yml +2 -0
- data/CHANGELOG.md +31 -0
- data/README.md +2 -2
- data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
- data/app/assets/javascripts/pghero/application.js +9 -0
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/stylesheets/pghero/application.css +54 -2
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/controllers/pg_hero/home_controller.rb +148 -52
- data/app/helpers/pg_hero/base_helper.rb +15 -0
- data/app/views/layouts/pg_hero/application.html.erb +1 -1
- data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
- data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -7
- data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
- data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
- data/app/views/pg_hero/home/connections.html.erb +2 -14
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +58 -22
- data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
- data/app/views/pg_hero/home/maintenance.html.erb +7 -7
- data/app/views/pg_hero/home/queries.html.erb +10 -0
- data/app/views/pg_hero/home/relation_space.html.erb +9 -0
- data/app/views/pg_hero/home/show_query.html.erb +107 -0
- data/app/views/pg_hero/home/space.html.erb +64 -10
- data/config/routes.rb +4 -2
- data/guides/Rails.md +28 -1
- data/guides/Suggested-Indexes.md +1 -1
- data/lib/pghero.rb +25 -36
- data/lib/pghero/database.rb +5 -1
- data/lib/pghero/methods/basic.rb +78 -13
- data/lib/pghero/methods/connections.rb +16 -56
- data/lib/pghero/methods/explain.rb +2 -6
- data/lib/pghero/methods/indexes.rb +173 -18
- data/lib/pghero/methods/kill.rb +2 -2
- data/lib/pghero/methods/maintenance.rb +23 -26
- data/lib/pghero/methods/queries.rb +1 -23
- data/lib/pghero/methods/query_stats.rb +95 -96
- data/lib/pghero/methods/{replica.rb → replication.rb} +17 -4
- data/lib/pghero/methods/sequences.rb +4 -5
- data/lib/pghero/methods/space.rb +101 -8
- data/lib/pghero/methods/suggested_indexes.rb +49 -108
- data/lib/pghero/methods/system.rb +14 -10
- data/lib/pghero/methods/tables.rb +8 -8
- data/lib/pghero/methods/users.rb +10 -12
- data/lib/pghero/version.rb +1 -1
- data/lib/tasks/pghero.rake +1 -1
- data/test/basic_test.rb +38 -0
- data/test/best_index_test.rb +3 -3
- data/test/suggested_indexes_test.rb +0 -2
- data/test/test_helper.rb +38 -40
- metadata +11 -6
- data/app/views/pg_hero/home/index_usage.html.erb +0 -27
- data/test/explain_test.rb +0 -18
@@ -1,8 +1,7 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module Queries
|
4
|
-
def running_queries(
|
5
|
-
min_duration = options[:min_duration]
|
4
|
+
def running_queries(min_duration: nil)
|
6
5
|
select_all <<-SQL
|
7
6
|
SELECT
|
8
7
|
pid,
|
@@ -31,27 +30,6 @@ module PgHero
|
|
31
30
|
running_queries(min_duration: long_running_query_sec)
|
32
31
|
end
|
33
32
|
|
34
|
-
def locks
|
35
|
-
select_all <<-SQL
|
36
|
-
SELECT DISTINCT ON (pid)
|
37
|
-
pg_stat_activity.pid,
|
38
|
-
pg_stat_activity.query,
|
39
|
-
age(now(), pg_stat_activity.query_start) AS age
|
40
|
-
FROM
|
41
|
-
pg_stat_activity
|
42
|
-
INNER JOIN
|
43
|
-
pg_locks ON pg_locks.pid = pg_stat_activity.pid
|
44
|
-
WHERE
|
45
|
-
pg_stat_activity.query <> '<insufficient privilege>'
|
46
|
-
AND pg_locks.mode = 'ExclusiveLock'
|
47
|
-
AND pg_stat_activity.pid <> pg_backend_pid()
|
48
|
-
AND pg_stat_activity.datname = current_database()
|
49
|
-
ORDER BY
|
50
|
-
pid,
|
51
|
-
query_start
|
52
|
-
SQL
|
53
|
-
end
|
54
|
-
|
55
33
|
# from https://wiki.postgresql.org/wiki/Lock_Monitoring
|
56
34
|
# and http://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
|
57
35
|
def blocked_queries
|
@@ -1,41 +1,42 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
3
|
module QueryStats
|
4
|
-
def query_stats(
|
5
|
-
current_query_stats =
|
6
|
-
historical_query_stats =
|
4
|
+
def query_stats(historical: false, start_at: nil, end_at: nil, min_average_time: nil, min_calls: nil, **options)
|
5
|
+
current_query_stats = historical && end_at && end_at < Time.now ? [] : current_query_stats(options)
|
6
|
+
historical_query_stats = historical && historical_query_stats_enabled? ? historical_query_stats(start_at: start_at, end_at: end_at, **options) : []
|
7
7
|
|
8
|
-
query_stats = combine_query_stats((current_query_stats + historical_query_stats).group_by { |q| [q[
|
9
|
-
query_stats = combine_query_stats(query_stats.group_by { |q| [normalize_query(q[
|
8
|
+
query_stats = combine_query_stats((current_query_stats + historical_query_stats).group_by { |q| [q[:query_hash], q[:user]] })
|
9
|
+
query_stats = combine_query_stats(query_stats.group_by { |q| [normalize_query(q[:query]), q[:user]] })
|
10
10
|
|
11
11
|
# add percentages
|
12
|
-
all_queries_total_minutes = [current_query_stats, historical_query_stats].sum { |s| (s.first || {})[
|
12
|
+
all_queries_total_minutes = [current_query_stats, historical_query_stats].sum { |s| (s.first || {})[:all_queries_total_minutes] || 0 }
|
13
13
|
query_stats.each do |query|
|
14
|
-
query[
|
15
|
-
query[
|
14
|
+
query[:average_time] = query[:total_minutes] * 1000 * 60 / query[:calls]
|
15
|
+
query[:total_percent] = query[:total_minutes] * 100.0 / all_queries_total_minutes
|
16
16
|
end
|
17
17
|
|
18
18
|
sort = options[:sort] || "total_minutes"
|
19
|
-
query_stats = query_stats.sort_by { |q| -q[sort] }.first(100)
|
20
|
-
if
|
21
|
-
query_stats.reject! { |q| q[
|
19
|
+
query_stats = query_stats.sort_by { |q| -q[sort.to_sym] }.first(100)
|
20
|
+
if min_average_time
|
21
|
+
query_stats.reject! { |q| q[:average_time] < min_average_time }
|
22
22
|
end
|
23
|
-
if
|
24
|
-
query_stats.reject! { |q| q[
|
23
|
+
if min_calls
|
24
|
+
query_stats.reject! { |q| q[:calls] < min_calls }
|
25
25
|
end
|
26
26
|
query_stats
|
27
27
|
end
|
28
28
|
|
29
29
|
def query_stats_available?
|
30
|
-
|
30
|
+
select_one("SELECT COUNT(*) AS count FROM pg_available_extensions WHERE name = 'pg_stat_statements'") > 0
|
31
31
|
end
|
32
32
|
|
33
|
+
# only cache if true
|
33
34
|
def query_stats_enabled?
|
34
|
-
|
35
|
+
@query_stats_enabled ||= query_stats_readable?
|
35
36
|
end
|
36
37
|
|
37
38
|
def query_stats_extension_enabled?
|
38
|
-
|
39
|
+
select_one("SELECT COUNT(*) AS count FROM pg_extension WHERE extname = 'pg_stat_statements'") > 0
|
39
40
|
end
|
40
41
|
|
41
42
|
def query_stats_readable?
|
@@ -46,7 +47,8 @@ module PgHero
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def enable_query_stats
|
49
|
-
execute("CREATE EXTENSION pg_stat_statements")
|
50
|
+
execute("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
51
|
+
true
|
50
52
|
end
|
51
53
|
|
52
54
|
def disable_query_stats
|
@@ -55,48 +57,29 @@ module PgHero
|
|
55
57
|
end
|
56
58
|
|
57
59
|
def reset_query_stats
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
false
|
63
|
-
end
|
60
|
+
execute("SELECT pg_stat_statements_reset()")
|
61
|
+
true
|
62
|
+
rescue ActiveRecord::StatementInvalid
|
63
|
+
false
|
64
64
|
end
|
65
65
|
|
66
66
|
# http://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
|
67
67
|
def historical_query_stats_enabled?
|
68
68
|
# TODO use schema from config
|
69
69
|
# make sure primary database is PostgreSQL first
|
70
|
-
|
71
|
-
PgHero.truthy?(stats_connection.select_all(squish <<-SQL
|
72
|
-
SELECT EXISTS (
|
73
|
-
SELECT
|
74
|
-
1
|
75
|
-
FROM
|
76
|
-
pg_catalog.pg_class c
|
77
|
-
INNER JOIN
|
78
|
-
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
79
|
-
WHERE
|
80
|
-
n.nspname = 'public'
|
81
|
-
AND c.relname = 'pghero_query_stats'
|
82
|
-
AND c.relkind = 'r'
|
83
|
-
)
|
84
|
-
SQL
|
85
|
-
).to_a.first["exists"]) && capture_query_stats?
|
70
|
+
query_stats_table_exists? && capture_query_stats? && !missing_query_stats_columns.any?
|
86
71
|
end
|
87
72
|
|
88
|
-
def
|
89
|
-
|
73
|
+
def query_stats_table_exists?
|
74
|
+
table_exists?("pghero_query_stats")
|
90
75
|
end
|
91
76
|
|
92
|
-
def
|
93
|
-
|
77
|
+
def missing_query_stats_columns
|
78
|
+
%w(query_hash user) - PgHero::QueryStats.column_names
|
94
79
|
end
|
95
80
|
|
96
|
-
def
|
97
|
-
|
98
|
-
columns = columns.map { |v| quote_table_name(v) }.join(",")
|
99
|
-
stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
|
81
|
+
def supports_query_hash?
|
82
|
+
server_version_num >= 90400
|
100
83
|
end
|
101
84
|
|
102
85
|
# resetting query stats will reset across the entire Postgres instance
|
@@ -123,56 +106,68 @@ module PgHero
|
|
123
106
|
if query_stats.any? { |_, v| v.any? } && reset_query_stats
|
124
107
|
query_stats.each do |db_id, db_query_stats|
|
125
108
|
if db_query_stats.any?
|
126
|
-
supports_query_hash = PgHero.databases[db_id].supports_query_hash?
|
127
|
-
supports_query_stats_user = PgHero.databases[db_id].supports_query_stats_user?
|
128
|
-
|
129
109
|
values =
|
130
110
|
db_query_stats.map do |qs|
|
131
|
-
|
111
|
+
[
|
132
112
|
db_id,
|
133
|
-
qs[
|
134
|
-
qs[
|
135
|
-
qs[
|
136
|
-
now
|
113
|
+
qs[:query],
|
114
|
+
qs[:total_minutes] * 60 * 1000,
|
115
|
+
qs[:calls],
|
116
|
+
now,
|
117
|
+
qs[:query_hash],
|
118
|
+
qs[:user]
|
137
119
|
]
|
138
|
-
values << qs["query_hash"] if supports_query_hash
|
139
|
-
values << qs["user"] if supports_query_stats_user
|
140
|
-
values
|
141
120
|
end
|
142
121
|
|
143
|
-
columns = %w[database query total_time calls captured_at]
|
144
|
-
columns << "query_hash" if supports_query_hash
|
145
|
-
columns << "user" if supports_query_stats_user
|
146
|
-
|
122
|
+
columns = %w[database query total_time calls captured_at query_hash user]
|
147
123
|
insert_stats("pghero_query_stats", columns, values)
|
148
124
|
end
|
149
125
|
end
|
150
126
|
end
|
151
127
|
end
|
152
128
|
|
153
|
-
def slow_queries(
|
154
|
-
query_stats
|
155
|
-
query_stats.select { |q| q[
|
129
|
+
def slow_queries(query_stats: nil, **options)
|
130
|
+
query_stats ||= self.query_stats(options)
|
131
|
+
query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
|
156
132
|
end
|
157
133
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
134
|
+
def query_hash_stats(query_hash)
|
135
|
+
if historical_query_stats_enabled? && supports_query_hash?
|
136
|
+
start_at = 24.hours.ago
|
137
|
+
select_all_stats <<-SQL
|
138
|
+
SELECT
|
139
|
+
captured_at,
|
140
|
+
total_time / 1000 / 60 AS total_minutes,
|
141
|
+
(total_time / calls) AS average_time,
|
142
|
+
calls,
|
143
|
+
(SELECT regexp_matches(query, '/\\*(.+)\\*/'))[1] AS origin
|
144
|
+
FROM
|
145
|
+
pghero_query_stats
|
146
|
+
WHERE
|
147
|
+
database = #{quote(id)}
|
148
|
+
AND captured_at >= #{quote(start_at)}
|
149
|
+
AND query_hash = #{quote(query_hash)}
|
150
|
+
ORDER BY
|
151
|
+
1 ASC
|
152
|
+
SQL
|
153
|
+
else
|
154
|
+
raise NotEnabled, "Query hash stats not enabled"
|
155
|
+
end
|
162
156
|
end
|
163
157
|
|
158
|
+
private
|
159
|
+
|
164
160
|
# http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
|
165
|
-
def current_query_stats(
|
161
|
+
def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
|
166
162
|
if query_stats_enabled?
|
167
|
-
limit
|
168
|
-
sort
|
169
|
-
database = options[:database] ? quote(options[:database]) : "current_database()"
|
163
|
+
limit ||= 100
|
164
|
+
sort ||= "total_minutes"
|
170
165
|
select_all <<-SQL
|
171
166
|
WITH query_stats AS (
|
172
167
|
SELECT
|
173
168
|
LEFT(query, 10000) AS query,
|
174
169
|
#{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
|
175
|
-
|
170
|
+
rolname AS user,
|
176
171
|
(total_time / 1000 / 60) AS total_minutes,
|
177
172
|
(total_time / calls) AS average_time,
|
178
173
|
calls
|
@@ -183,7 +178,8 @@ module PgHero
|
|
183
178
|
INNER JOIN
|
184
179
|
pg_roles ON pg_roles.oid = pg_stat_statements.userid
|
185
180
|
WHERE
|
186
|
-
pg_database.datname = #{database}
|
181
|
+
pg_database.datname = #{database ? quote(database) : "current_database()"}
|
182
|
+
#{query_hash ? "AND queryid = #{quote(query_hash)}" : nil}
|
187
183
|
)
|
188
184
|
SELECT
|
189
185
|
query,
|
@@ -201,19 +197,19 @@ module PgHero
|
|
201
197
|
LIMIT #{limit.to_i}
|
202
198
|
SQL
|
203
199
|
else
|
204
|
-
|
200
|
+
raise NotEnabled, "Query stats not enabled"
|
205
201
|
end
|
206
202
|
end
|
207
203
|
|
208
|
-
def historical_query_stats(
|
204
|
+
def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
|
209
205
|
if historical_query_stats_enabled?
|
210
|
-
sort
|
211
|
-
|
206
|
+
sort ||= "total_minutes"
|
207
|
+
select_all_stats <<-SQL
|
212
208
|
WITH query_stats AS (
|
213
209
|
SELECT
|
214
210
|
#{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
|
215
|
-
|
216
|
-
array_agg(LEFT(query, 10000)) AS query,
|
211
|
+
pghero_query_stats.user AS user,
|
212
|
+
array_agg(LEFT(query, 10000) ORDER BY REPLACE(LEFT(query, 1000), '?', '!') COLLATE "C" ASC) AS query,
|
217
213
|
(SUM(total_time) / 1000 / 60) AS total_minutes,
|
218
214
|
(SUM(total_time) / SUM(calls)) AS average_time,
|
219
215
|
SUM(calls) AS calls
|
@@ -222,15 +218,17 @@ module PgHero
|
|
222
218
|
WHERE
|
223
219
|
database = #{quote(id)}
|
224
220
|
#{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
|
225
|
-
#{
|
226
|
-
#{
|
221
|
+
#{start_at ? "AND captured_at >= #{quote(start_at)}" : ""}
|
222
|
+
#{end_at ? "AND captured_at <= #{quote(end_at)}" : ""}
|
223
|
+
#{query_hash ? "AND query_hash = #{quote(query_hash)}" : ""}
|
227
224
|
GROUP BY
|
228
225
|
1, 2
|
229
226
|
)
|
230
227
|
SELECT
|
231
228
|
query_hash,
|
232
229
|
query_stats.user,
|
233
|
-
query[1],
|
230
|
+
query[1] AS query,
|
231
|
+
query[array_length(query, 1)] AS explainable_query,
|
234
232
|
total_minutes,
|
235
233
|
average_time,
|
236
234
|
calls,
|
@@ -243,31 +241,32 @@ module PgHero
|
|
243
241
|
LIMIT 100
|
244
242
|
SQL
|
245
243
|
else
|
246
|
-
|
244
|
+
raise NotEnabled, "Historical query stats not enabled"
|
247
245
|
end
|
248
246
|
end
|
249
247
|
|
250
|
-
def server_version_num
|
251
|
-
@server_version ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
|
252
|
-
end
|
253
|
-
|
254
248
|
def combine_query_stats(grouped_stats)
|
255
249
|
query_stats = []
|
256
250
|
grouped_stats.each do |_, stats2|
|
257
251
|
value = {
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
252
|
+
query: (stats2.find { |s| s[:query] } || {})[:query],
|
253
|
+
user: (stats2.find { |s| s[:user] } || {})[:user],
|
254
|
+
query_hash: (stats2.find { |s| s[:query_hash] } || {})[:query_hash],
|
255
|
+
total_minutes: stats2.sum { |s| s[:total_minutes] },
|
256
|
+
calls: stats2.sum { |s| s[:calls] }.to_i,
|
257
|
+
all_queries_total_minutes: stats2.sum { |s| s[:all_queries_total_minutes] }
|
264
258
|
}
|
265
|
-
value[
|
259
|
+
value[:total_percent] = value[:total_minutes] * 100.0 / value[:all_queries_total_minutes]
|
260
|
+
value[:explainable_query] = stats2.map { |s| s[:explainable_query] }.select { |q| q && explainable?(q) }.first
|
266
261
|
query_stats << value
|
267
262
|
end
|
268
263
|
query_stats
|
269
264
|
end
|
270
265
|
|
266
|
+
def explainable?(query)
|
267
|
+
query =~ /select/i && !query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i
|
268
|
+
end
|
269
|
+
|
271
270
|
# removes comments
|
272
271
|
# combines ?, ?, ? => ?
|
273
272
|
def normalize_query(query)
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module PgHero
|
2
2
|
module Methods
|
3
|
-
module
|
3
|
+
module Replication
|
4
4
|
def replica?
|
5
5
|
unless defined?(@replica)
|
6
|
-
@replica =
|
6
|
+
@replica = select_one("SELECT pg_is_in_recovery()")
|
7
7
|
end
|
8
8
|
@replica
|
9
9
|
end
|
10
10
|
|
11
11
|
# http://www.postgresql.org/message-id/CADKbJJWz9M0swPT3oqe8f9+tfD4-F54uE6Xtkh4nERpVsQnjnw@mail.gmail.com
|
12
12
|
def replication_lag
|
13
|
-
|
13
|
+
select_one <<-SQL
|
14
14
|
SELECT
|
15
15
|
CASE
|
16
16
|
WHEN pg_last_xlog_receive_location() = pg_last_xlog_replay_location() THEN 0
|
@@ -18,7 +18,20 @@ module PgHero
|
|
18
18
|
END
|
19
19
|
AS replication_lag
|
20
20
|
SQL
|
21
|
-
|
21
|
+
end
|
22
|
+
|
23
|
+
def replication_slots
|
24
|
+
select_all <<-SQL
|
25
|
+
SELECT
|
26
|
+
slot_name,
|
27
|
+
database,
|
28
|
+
active
|
29
|
+
FROM pg_replication_slots
|
30
|
+
SQL
|
31
|
+
end
|
32
|
+
|
33
|
+
def replicating?
|
34
|
+
select_all("SELECT state FROM pg_stat_replication").any?
|
22
35
|
end
|
23
36
|
end
|
24
37
|
end
|
@@ -21,16 +21,15 @@ module PgHero
|
|
21
21
|
sequence_name ASC
|
22
22
|
SQL
|
23
23
|
|
24
|
-
select_all(sequences.map { |s| "SELECT last_value FROM #{s[
|
25
|
-
sequences[i][
|
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
26
|
end
|
27
27
|
|
28
28
|
sequences
|
29
29
|
end
|
30
30
|
|
31
|
-
def sequence_danger(
|
32
|
-
|
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 }
|
31
|
+
def sequence_danger(threshold: 0.9)
|
32
|
+
sequences.select { |s| s[:last_value] / s[:max_value].to_f > threshold }.sort_by { |s| s[:max_value] - s[:last_value] }
|
34
33
|
end
|
35
34
|
end
|
36
35
|
end
|
data/lib/pghero/methods/space.rb
CHANGED
@@ -2,40 +2,133 @@ module PgHero
|
|
2
2
|
module Methods
|
3
3
|
module Space
|
4
4
|
def database_size
|
5
|
-
|
5
|
+
PgHero.pretty_size select_one("SELECT pg_database_size(current_database())")
|
6
6
|
end
|
7
7
|
|
8
8
|
def relation_sizes
|
9
|
-
|
9
|
+
select_all_size <<-SQL
|
10
10
|
SELECT
|
11
11
|
n.nspname AS schema,
|
12
|
-
c.relname AS
|
12
|
+
c.relname AS relation,
|
13
13
|
CASE WHEN c.relkind = 'r' THEN 'table' ELSE 'index' END AS type,
|
14
|
-
pg_size_pretty(pg_table_size(c.oid)) AS size,
|
15
14
|
pg_table_size(c.oid) AS size_bytes
|
16
15
|
FROM
|
17
16
|
pg_class c
|
18
17
|
LEFT JOIN
|
19
|
-
pg_namespace n ON
|
18
|
+
pg_namespace n ON n.oid = c.relnamespace
|
20
19
|
WHERE
|
21
20
|
n.nspname NOT IN ('pg_catalog', 'information_schema')
|
22
21
|
AND n.nspname !~ '^pg_toast'
|
23
22
|
AND c.relkind IN ('r', 'i')
|
24
23
|
ORDER BY
|
25
24
|
pg_table_size(c.oid) DESC,
|
26
|
-
|
25
|
+
2 ASC
|
27
26
|
SQL
|
28
27
|
end
|
29
28
|
|
29
|
+
def table_sizes
|
30
|
+
select_all_size <<-SQL
|
31
|
+
SELECT
|
32
|
+
n.nspname AS schema,
|
33
|
+
c.relname AS table,
|
34
|
+
pg_total_relation_size(c.oid) AS size_bytes
|
35
|
+
FROM
|
36
|
+
pg_class c
|
37
|
+
LEFT JOIN
|
38
|
+
pg_namespace n ON n.oid = c.relnamespace
|
39
|
+
WHERE
|
40
|
+
n.nspname NOT IN ('pg_catalog', 'information_schema')
|
41
|
+
AND n.nspname !~ '^pg_toast'
|
42
|
+
AND c.relkind = 'r'
|
43
|
+
ORDER BY
|
44
|
+
pg_total_relation_size(c.oid) DESC,
|
45
|
+
2 ASC
|
46
|
+
SQL
|
47
|
+
end
|
48
|
+
|
49
|
+
def space_growth(days: 7, relation_sizes: nil)
|
50
|
+
if space_stats_enabled?
|
51
|
+
relation_sizes ||= self.relation_sizes
|
52
|
+
sizes = Hash[ relation_sizes.map { |r| [r[:relation], r[:size_bytes]] } ]
|
53
|
+
start_at = days.days.ago
|
54
|
+
|
55
|
+
stats = select_all_stats <<-SQL
|
56
|
+
WITH t AS (
|
57
|
+
SELECT
|
58
|
+
relation,
|
59
|
+
array_agg(size ORDER BY captured_at) AS sizes
|
60
|
+
FROM
|
61
|
+
pghero_space_stats
|
62
|
+
WHERE
|
63
|
+
database = #{quote(id)}
|
64
|
+
AND captured_at >= #{quote(start_at)}
|
65
|
+
GROUP BY
|
66
|
+
1
|
67
|
+
)
|
68
|
+
SELECT
|
69
|
+
relation,
|
70
|
+
sizes[1] AS size_bytes
|
71
|
+
FROM
|
72
|
+
t
|
73
|
+
ORDER BY
|
74
|
+
1
|
75
|
+
SQL
|
76
|
+
|
77
|
+
stats.each do |r|
|
78
|
+
relation = r[:relation]
|
79
|
+
if sizes[relation]
|
80
|
+
r[:growth_bytes] = sizes[relation] - r[:size_bytes]
|
81
|
+
end
|
82
|
+
r.delete(:size_bytes)
|
83
|
+
end
|
84
|
+
stats
|
85
|
+
else
|
86
|
+
raise NotEnabled, "Space stats not enabled"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def relation_space_stats(relation)
|
91
|
+
if space_stats_enabled?
|
92
|
+
relation_sizes ||= self.relation_sizes
|
93
|
+
sizes = Hash[ relation_sizes.map { |r| [r[:relation], r[:size_bytes]] } ]
|
94
|
+
start_at = 30.days.ago
|
95
|
+
|
96
|
+
stats = select_all_stats <<-SQL
|
97
|
+
SELECT
|
98
|
+
captured_at,
|
99
|
+
size AS size_bytes
|
100
|
+
FROM
|
101
|
+
pghero_space_stats
|
102
|
+
WHERE
|
103
|
+
database = #{quote(id)}
|
104
|
+
AND captured_at >= #{quote(start_at)}
|
105
|
+
AND relation = #{quote(relation)}
|
106
|
+
ORDER BY
|
107
|
+
1 ASC
|
108
|
+
SQL
|
109
|
+
|
110
|
+
stats << {
|
111
|
+
captured_at: Time.now,
|
112
|
+
size_bytes: sizes[relation].to_i
|
113
|
+
}
|
114
|
+
else
|
115
|
+
raise NotEnabled, "Space stats not enabled"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
30
119
|
def capture_space_stats
|
31
120
|
now = Time.now
|
32
|
-
columns = %w
|
121
|
+
columns = %w(database schema relation size captured_at)
|
33
122
|
values = []
|
34
123
|
relation_sizes.each do |rs|
|
35
|
-
values << [id, rs[
|
124
|
+
values << [id, rs[:schema], rs[:name], rs[:size_bytes].to_i, now]
|
36
125
|
end
|
37
126
|
insert_stats("pghero_space_stats", columns, values)
|
38
127
|
end
|
128
|
+
|
129
|
+
def space_stats_enabled?
|
130
|
+
table_exists?("pghero_space_stats")
|
131
|
+
end
|
39
132
|
end
|
40
133
|
end
|
41
134
|
end
|