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.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +2 -2
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  6. data/app/assets/javascripts/pghero/application.js +9 -0
  7. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  8. data/app/assets/stylesheets/pghero/application.css +54 -2
  9. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  10. data/app/controllers/pg_hero/home_controller.rb +148 -52
  11. data/app/helpers/pg_hero/base_helper.rb +15 -0
  12. data/app/views/layouts/pg_hero/application.html.erb +1 -1
  13. data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
  14. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -7
  15. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  16. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  17. data/app/views/pg_hero/home/connections.html.erb +2 -14
  18. data/app/views/pg_hero/home/explain.html.erb +1 -1
  19. data/app/views/pg_hero/home/index.html.erb +58 -22
  20. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  21. data/app/views/pg_hero/home/maintenance.html.erb +7 -7
  22. data/app/views/pg_hero/home/queries.html.erb +10 -0
  23. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  24. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  25. data/app/views/pg_hero/home/space.html.erb +64 -10
  26. data/config/routes.rb +4 -2
  27. data/guides/Rails.md +28 -1
  28. data/guides/Suggested-Indexes.md +1 -1
  29. data/lib/pghero.rb +25 -36
  30. data/lib/pghero/database.rb +5 -1
  31. data/lib/pghero/methods/basic.rb +78 -13
  32. data/lib/pghero/methods/connections.rb +16 -56
  33. data/lib/pghero/methods/explain.rb +2 -6
  34. data/lib/pghero/methods/indexes.rb +173 -18
  35. data/lib/pghero/methods/kill.rb +2 -2
  36. data/lib/pghero/methods/maintenance.rb +23 -26
  37. data/lib/pghero/methods/queries.rb +1 -23
  38. data/lib/pghero/methods/query_stats.rb +95 -96
  39. data/lib/pghero/methods/{replica.rb → replication.rb} +17 -4
  40. data/lib/pghero/methods/sequences.rb +4 -5
  41. data/lib/pghero/methods/space.rb +101 -8
  42. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  43. data/lib/pghero/methods/system.rb +14 -10
  44. data/lib/pghero/methods/tables.rb +8 -8
  45. data/lib/pghero/methods/users.rb +10 -12
  46. data/lib/pghero/version.rb +1 -1
  47. data/lib/tasks/pghero.rake +1 -1
  48. data/test/basic_test.rb +38 -0
  49. data/test/best_index_test.rb +3 -3
  50. data/test/suggested_indexes_test.rb +0 -2
  51. data/test/test_helper.rb +38 -40
  52. metadata +11 -6
  53. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  54. 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(options = {})
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(options = {})
5
- current_query_stats = options[:historical] && options[:end_at] && options[:end_at] < Time.now ? [] : current_query_stats(options)
6
- historical_query_stats = options[:historical] ? historical_query_stats(options) : []
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["query_hash"], q["user"]] })
9
- query_stats = combine_query_stats(query_stats.group_by { |q| [normalize_query(q["query"]), q["user"]] })
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 || {})["all_queries_total_minutes"].to_f }
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["average_time"] = query["total_minutes"] * 1000 * 60 / query["calls"]
15
- query["total_percent"] = query["total_minutes"] * 100.0 / all_queries_total_minutes
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 options[:min_average_time]
21
- query_stats.reject! { |q| q["average_time"].to_f < options[:min_average_time] }
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 options[:min_calls]
24
- query_stats.reject! { |q| q["calls"].to_i < options[:min_calls] }
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
- select_all("SELECT COUNT(*) AS count FROM pg_available_extensions WHERE name = 'pg_stat_statements'").first["count"].to_i > 0
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
- query_stats_extension_enabled? && query_stats_readable?
35
+ @query_stats_enabled ||= query_stats_readable?
35
36
  end
36
37
 
37
38
  def query_stats_extension_enabled?
38
- select_all("SELECT COUNT(*) AS count FROM pg_extension WHERE extname = 'pg_stat_statements'").first["count"].to_i > 0
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
- if query_stats_enabled?
59
- execute("SELECT pg_stat_statements_reset()")
60
- true
61
- else
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
- ["PostgreSQL", "PostGIS"].include?(stats_connection.adapter_name) &&
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 supports_query_hash?
89
- @supports_query_hash ||= server_version_num >= 90400 && historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("query_hash")
73
+ def query_stats_table_exists?
74
+ table_exists?("pghero_query_stats")
90
75
  end
91
76
 
92
- def supports_query_stats_user?
93
- @supports_query_stats_user ||= historical_query_stats_enabled? && PgHero::QueryStats.column_names.include?("user")
77
+ def missing_query_stats_columns
78
+ %w(query_hash user) - PgHero::QueryStats.column_names
94
79
  end
95
80
 
96
- def insert_stats(table, columns, values)
97
- values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
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
- values = [
111
+ [
132
112
  db_id,
133
- qs["query"],
134
- qs["total_minutes"].to_f * 60 * 1000,
135
- qs["calls"],
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(options = {})
154
- query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
155
- query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
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
- private
159
-
160
- def stats_connection
161
- ::PgHero::QueryStats.connection
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(options = {})
161
+ def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
166
162
  if query_stats_enabled?
167
- limit = options[:limit] || 100
168
- sort = options[:sort] || "total_minutes"
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
- #{supports_query_stats_user? ? "rolname" : "NULL::text"} AS user,
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(options = {})
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 = options[:sort] || "total_minutes"
211
- stats_connection.select_all squish <<-SQL
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
- #{supports_query_stats_user? ? "pghero_query_stats.user" : "NULL::text"} AS user,
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
- #{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
226
- #{options[:end_at] ? "AND captured_at <= #{quote(options[:end_at])}" : ""}
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
- "query" => (stats2.find { |s| s["query"] } || {})["query"],
259
- "user" => (stats2.find { |s| s["user"] } || {})["user"],
260
- "query_hash" => (stats2.find { |s| s["query"] } || {})["query_hash"],
261
- "total_minutes" => stats2.sum { |s| s["total_minutes"].to_f },
262
- "calls" => stats2.sum { |s| s["calls"].to_i },
263
- "all_queries_total_minutes" => stats2.sum { |s| s["all_queries_total_minutes"].to_f }
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["total_percent"] = value["total_minutes"] * 100.0 / value["all_queries_total_minutes"]
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 Replica
3
+ module Replication
4
4
  def replica?
5
5
  unless defined?(@replica)
6
- @replica = PgHero.truthy?(select_all("SELECT pg_is_in_recovery()").first["pg_is_in_recovery"])
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
- select_all(<<-SQL
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
- ).first["replication_lag"].to_f
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["sequence"]}" }.join(" UNION ALL ")).each_with_index do |row, i|
25
- sequences[i]["last_value"] = row["last_value"]
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(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 }
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
@@ -2,40 +2,133 @@ module PgHero
2
2
  module Methods
3
3
  module Space
4
4
  def database_size
5
- select_all("SELECT pg_size_pretty(pg_database_size(current_database()))").first["pg_size_pretty"]
5
+ PgHero.pretty_size select_one("SELECT pg_database_size(current_database())")
6
6
  end
7
7
 
8
8
  def relation_sizes
9
- select_all <<-SQL
9
+ select_all_size <<-SQL
10
10
  SELECT
11
11
  n.nspname AS schema,
12
- c.relname AS name,
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 (n.oid = c.relnamespace)
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
- name ASC
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[database schema relation size captured_at]
121
+ columns = %w(database schema relation size captured_at)
33
122
  values = []
34
123
  relation_sizes.each do |rs|
35
- values << [id, rs["schema"], rs["name"], rs["size_bytes"].to_i, now]
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