pghero 1.5.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -1
- data/README.md +8 -14
- data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
- data/app/assets/javascripts/pghero/application.js +9 -0
- data/app/assets/javascripts/pghero/chartkick.js +511 -152
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/stylesheets/pghero/application.css +66 -2
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/controllers/pg_hero/home_controller.rb +166 -63
- data/app/helpers/pg_hero/base_helper.rb +15 -0
- data/app/views/layouts/pg_hero/application.html.erb +4 -3
- data/app/views/pg_hero/home/_connections_table.html.erb +2 -9
- data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -8
- 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 +19 -1
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +125 -66
- data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
- data/app/views/pg_hero/home/live_queries.html.erb +3 -1
- data/app/views/pg_hero/home/maintenance.html.erb +11 -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/app/views/pg_hero/home/system.html.erb +16 -4
- data/config/routes.rb +4 -2
- data/guides/Linux.md +20 -0
- data/guides/Rails.md +30 -4
- data/guides/Suggested-Indexes.md +1 -1
- data/lib/generators/pghero/config_generator.rb +13 -0
- data/lib/generators/pghero/query_stats_generator.rb +7 -1
- data/lib/generators/pghero/space_stats_generator.rb +7 -1
- data/lib/generators/pghero/templates/config.yml +23 -0
- data/lib/generators/pghero/templates/query_stats.rb +1 -1
- data/lib/generators/pghero/templates/space_stats.rb +1 -1
- data/lib/pghero/database.rb +6 -2
- data/lib/pghero/engine.rb +10 -7
- data/lib/pghero/methods/basic.rb +83 -11
- data/lib/pghero/methods/connections.rb +16 -56
- data/lib/pghero/methods/explain.rb +3 -5
- data/lib/pghero/methods/indexes.rb +179 -21
- data/lib/pghero/methods/kill.rb +2 -2
- data/lib/pghero/methods/maintenance.rb +34 -20
- data/lib/pghero/methods/queries.rb +28 -45
- data/lib/pghero/methods/query_stats.rb +97 -91
- data/lib/pghero/methods/{replica.rb → replication.rb} +20 -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 +23 -13
- data/lib/pghero/methods/tables.rb +9 -9
- data/lib/pghero/methods/users.rb +10 -12
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +34 -36
- data/lib/tasks/pghero.rake +5 -0
- 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 +14 -7
- data/app/views/pg_hero/home/index_usage.html.erb +0 -27
- data/test/explain_test.rb +0 -18
|
@@ -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,46 +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
|
-
|
|
70
|
-
|
|
71
|
-
SELECT
|
|
72
|
-
1
|
|
73
|
-
FROM
|
|
74
|
-
pg_catalog.pg_class c
|
|
75
|
-
INNER JOIN
|
|
76
|
-
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
77
|
-
WHERE
|
|
78
|
-
n.nspname = 'public'
|
|
79
|
-
AND c.relname = 'pghero_query_stats'
|
|
80
|
-
AND c.relkind = 'r'
|
|
81
|
-
)
|
|
82
|
-
SQL
|
|
83
|
-
).to_a.first["exists"]) && capture_query_stats?
|
|
69
|
+
# make sure primary database is PostgreSQL first
|
|
70
|
+
query_stats_table_exists? && capture_query_stats? && !missing_query_stats_columns.any?
|
|
84
71
|
end
|
|
85
72
|
|
|
86
|
-
def
|
|
87
|
-
|
|
73
|
+
def query_stats_table_exists?
|
|
74
|
+
table_exists?("pghero_query_stats")
|
|
88
75
|
end
|
|
89
76
|
|
|
90
|
-
def
|
|
91
|
-
|
|
77
|
+
def missing_query_stats_columns
|
|
78
|
+
%w(query_hash user) - PgHero::QueryStats.column_names
|
|
92
79
|
end
|
|
93
80
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
columns = columns.map { |v| quote_table_name(v) }.join(",")
|
|
97
|
-
stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
|
|
81
|
+
def supports_query_hash?
|
|
82
|
+
server_version_num >= 90400
|
|
98
83
|
end
|
|
99
84
|
|
|
100
85
|
# resetting query stats will reset across the entire Postgres instance
|
|
@@ -121,51 +106,68 @@ module PgHero
|
|
|
121
106
|
if query_stats.any? { |_, v| v.any? } && reset_query_stats
|
|
122
107
|
query_stats.each do |db_id, db_query_stats|
|
|
123
108
|
if db_query_stats.any?
|
|
124
|
-
supports_query_hash = PgHero.databases[db_id].supports_query_hash?
|
|
125
|
-
supports_query_stats_user = PgHero.databases[db_id].supports_query_stats_user?
|
|
126
|
-
|
|
127
109
|
values =
|
|
128
110
|
db_query_stats.map do |qs|
|
|
129
|
-
|
|
111
|
+
[
|
|
130
112
|
db_id,
|
|
131
|
-
qs[
|
|
132
|
-
qs[
|
|
133
|
-
qs[
|
|
134
|
-
now
|
|
113
|
+
qs[:query],
|
|
114
|
+
qs[:total_minutes] * 60 * 1000,
|
|
115
|
+
qs[:calls],
|
|
116
|
+
now,
|
|
117
|
+
qs[:query_hash],
|
|
118
|
+
qs[:user]
|
|
135
119
|
]
|
|
136
|
-
values << qs["query_hash"] if supports_query_hash
|
|
137
|
-
values << qs["user"] if supports_query_stats_user
|
|
138
|
-
values
|
|
139
120
|
end
|
|
140
121
|
|
|
141
|
-
columns = %w[database query total_time calls captured_at]
|
|
142
|
-
columns << "query_hash" if supports_query_hash
|
|
143
|
-
columns << "user" if supports_query_stats_user
|
|
144
|
-
|
|
122
|
+
columns = %w[database query total_time calls captured_at query_hash user]
|
|
145
123
|
insert_stats("pghero_query_stats", columns, values)
|
|
146
124
|
end
|
|
147
125
|
end
|
|
148
126
|
end
|
|
149
127
|
end
|
|
150
128
|
|
|
151
|
-
|
|
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 }
|
|
132
|
+
end
|
|
152
133
|
|
|
153
|
-
def
|
|
154
|
-
|
|
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
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
private
|
|
159
|
+
|
|
157
160
|
# http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
|
|
158
|
-
def current_query_stats(
|
|
161
|
+
def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
|
|
159
162
|
if query_stats_enabled?
|
|
160
|
-
limit
|
|
161
|
-
sort
|
|
162
|
-
database = options[:database] ? quote(options[:database]) : "current_database()"
|
|
163
|
+
limit ||= 100
|
|
164
|
+
sort ||= "total_minutes"
|
|
163
165
|
select_all <<-SQL
|
|
164
166
|
WITH query_stats AS (
|
|
165
167
|
SELECT
|
|
166
168
|
LEFT(query, 10000) AS query,
|
|
167
169
|
#{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
|
|
168
|
-
|
|
170
|
+
rolname AS user,
|
|
169
171
|
(total_time / 1000 / 60) AS total_minutes,
|
|
170
172
|
(total_time / calls) AS average_time,
|
|
171
173
|
calls
|
|
@@ -176,7 +178,8 @@ module PgHero
|
|
|
176
178
|
INNER JOIN
|
|
177
179
|
pg_roles ON pg_roles.oid = pg_stat_statements.userid
|
|
178
180
|
WHERE
|
|
179
|
-
pg_database.datname = #{database}
|
|
181
|
+
pg_database.datname = #{database ? quote(database) : "current_database()"}
|
|
182
|
+
#{query_hash ? "AND queryid = #{quote(query_hash)}" : nil}
|
|
180
183
|
)
|
|
181
184
|
SELECT
|
|
182
185
|
query,
|
|
@@ -194,19 +197,19 @@ module PgHero
|
|
|
194
197
|
LIMIT #{limit.to_i}
|
|
195
198
|
SQL
|
|
196
199
|
else
|
|
197
|
-
|
|
200
|
+
raise NotEnabled, "Query stats not enabled"
|
|
198
201
|
end
|
|
199
202
|
end
|
|
200
203
|
|
|
201
|
-
def historical_query_stats(
|
|
204
|
+
def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
|
|
202
205
|
if historical_query_stats_enabled?
|
|
203
|
-
sort
|
|
204
|
-
|
|
206
|
+
sort ||= "total_minutes"
|
|
207
|
+
select_all_stats <<-SQL
|
|
205
208
|
WITH query_stats AS (
|
|
206
209
|
SELECT
|
|
207
210
|
#{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
|
|
208
|
-
|
|
209
|
-
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,
|
|
210
213
|
(SUM(total_time) / 1000 / 60) AS total_minutes,
|
|
211
214
|
(SUM(total_time) / SUM(calls)) AS average_time,
|
|
212
215
|
SUM(calls) AS calls
|
|
@@ -215,15 +218,17 @@ module PgHero
|
|
|
215
218
|
WHERE
|
|
216
219
|
database = #{quote(id)}
|
|
217
220
|
#{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
|
|
218
|
-
#{
|
|
219
|
-
#{
|
|
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)}" : ""}
|
|
220
224
|
GROUP BY
|
|
221
225
|
1, 2
|
|
222
226
|
)
|
|
223
227
|
SELECT
|
|
224
228
|
query_hash,
|
|
225
229
|
query_stats.user,
|
|
226
|
-
query[1],
|
|
230
|
+
query[1] AS query,
|
|
231
|
+
query[array_length(query, 1)] AS explainable_query,
|
|
227
232
|
total_minutes,
|
|
228
233
|
average_time,
|
|
229
234
|
calls,
|
|
@@ -236,31 +241,32 @@ module PgHero
|
|
|
236
241
|
LIMIT 100
|
|
237
242
|
SQL
|
|
238
243
|
else
|
|
239
|
-
|
|
244
|
+
raise NotEnabled, "Historical query stats not enabled"
|
|
240
245
|
end
|
|
241
246
|
end
|
|
242
247
|
|
|
243
|
-
def server_version_num
|
|
244
|
-
@server_version ||= select_all("SHOW server_version_num").first["server_version_num"].to_i
|
|
245
|
-
end
|
|
246
|
-
|
|
247
248
|
def combine_query_stats(grouped_stats)
|
|
248
249
|
query_stats = []
|
|
249
250
|
grouped_stats.each do |_, stats2|
|
|
250
251
|
value = {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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] }
|
|
257
258
|
}
|
|
258
|
-
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
|
|
259
261
|
query_stats << value
|
|
260
262
|
end
|
|
261
263
|
query_stats
|
|
262
264
|
end
|
|
263
265
|
|
|
266
|
+
def explainable?(query)
|
|
267
|
+
query =~ /select/i && !query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i
|
|
268
|
+
end
|
|
269
|
+
|
|
264
270
|
# removes comments
|
|
265
271
|
# combines ?, ?, ? => ?
|
|
266
272
|
def normalize_query(query)
|
|
@@ -1,13 +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 = select_one("SELECT pg_is_in_recovery()")
|
|
7
|
+
end
|
|
8
|
+
@replica
|
|
6
9
|
end
|
|
7
10
|
|
|
8
11
|
# http://www.postgresql.org/message-id/CADKbJJWz9M0swPT3oqe8f9+tfD4-F54uE6Xtkh4nERpVsQnjnw@mail.gmail.com
|
|
9
12
|
def replication_lag
|
|
10
|
-
|
|
13
|
+
select_one <<-SQL
|
|
11
14
|
SELECT
|
|
12
15
|
CASE
|
|
13
16
|
WHEN pg_last_xlog_receive_location() = pg_last_xlog_replay_location() THEN 0
|
|
@@ -15,7 +18,20 @@ module PgHero
|
|
|
15
18
|
END
|
|
16
19
|
AS replication_lag
|
|
17
20
|
SQL
|
|
18
|
-
|
|
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?
|
|
19
35
|
end
|
|
20
36
|
end
|
|
21
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
|