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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +75 -0
  4. data/Gemfile +1 -1
  5. data/README.md +8 -14
  6. data/app/assets/javascripts/pghero/Chart.bundle.js +7512 -5661
  7. data/app/assets/javascripts/pghero/application.js +9 -0
  8. data/app/assets/javascripts/pghero/chartkick.js +511 -152
  9. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  10. data/app/assets/stylesheets/pghero/application.css +66 -2
  11. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  12. data/app/controllers/pg_hero/home_controller.rb +166 -63
  13. data/app/helpers/pg_hero/base_helper.rb +15 -0
  14. data/app/views/layouts/pg_hero/application.html.erb +4 -3
  15. data/app/views/pg_hero/home/_connections_table.html.erb +2 -9
  16. data/app/views/pg_hero/home/_live_queries_table.html.erb +11 -8
  17. data/app/views/pg_hero/home/_queries_table.html.erb +21 -10
  18. data/app/views/pg_hero/home/_suggested_index.html.erb +1 -1
  19. data/app/views/pg_hero/home/connections.html.erb +19 -1
  20. data/app/views/pg_hero/home/explain.html.erb +1 -1
  21. data/app/views/pg_hero/home/index.html.erb +125 -66
  22. data/app/views/pg_hero/home/index_bloat.html.erb +69 -0
  23. data/app/views/pg_hero/home/live_queries.html.erb +3 -1
  24. data/app/views/pg_hero/home/maintenance.html.erb +11 -7
  25. data/app/views/pg_hero/home/queries.html.erb +10 -0
  26. data/app/views/pg_hero/home/relation_space.html.erb +9 -0
  27. data/app/views/pg_hero/home/show_query.html.erb +107 -0
  28. data/app/views/pg_hero/home/space.html.erb +64 -10
  29. data/app/views/pg_hero/home/system.html.erb +16 -4
  30. data/config/routes.rb +4 -2
  31. data/guides/Linux.md +20 -0
  32. data/guides/Rails.md +30 -4
  33. data/guides/Suggested-Indexes.md +1 -1
  34. data/lib/generators/pghero/config_generator.rb +13 -0
  35. data/lib/generators/pghero/query_stats_generator.rb +7 -1
  36. data/lib/generators/pghero/space_stats_generator.rb +7 -1
  37. data/lib/generators/pghero/templates/config.yml +23 -0
  38. data/lib/generators/pghero/templates/query_stats.rb +1 -1
  39. data/lib/generators/pghero/templates/space_stats.rb +1 -1
  40. data/lib/pghero/database.rb +6 -2
  41. data/lib/pghero/engine.rb +10 -7
  42. data/lib/pghero/methods/basic.rb +83 -11
  43. data/lib/pghero/methods/connections.rb +16 -56
  44. data/lib/pghero/methods/explain.rb +3 -5
  45. data/lib/pghero/methods/indexes.rb +179 -21
  46. data/lib/pghero/methods/kill.rb +2 -2
  47. data/lib/pghero/methods/maintenance.rb +34 -20
  48. data/lib/pghero/methods/queries.rb +28 -45
  49. data/lib/pghero/methods/query_stats.rb +97 -91
  50. data/lib/pghero/methods/{replica.rb → replication.rb} +20 -4
  51. data/lib/pghero/methods/sequences.rb +4 -5
  52. data/lib/pghero/methods/space.rb +101 -8
  53. data/lib/pghero/methods/suggested_indexes.rb +49 -108
  54. data/lib/pghero/methods/system.rb +23 -13
  55. data/lib/pghero/methods/tables.rb +9 -9
  56. data/lib/pghero/methods/users.rb +10 -12
  57. data/lib/pghero/version.rb +1 -1
  58. data/lib/pghero.rb +34 -36
  59. data/lib/tasks/pghero.rake +5 -0
  60. data/test/basic_test.rb +38 -0
  61. data/test/best_index_test.rb +3 -3
  62. data/test/suggested_indexes_test.rb +0 -2
  63. data/test/test_helper.rb +38 -40
  64. metadata +14 -7
  65. data/app/views/pg_hero/home/index_usage.html.erb +0 -27
  66. 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(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,46 +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
- PgHero.truthy?(stats_connection.select_all(squish <<-SQL
70
- SELECT EXISTS (
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 supports_query_hash?
87
- @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")
88
75
  end
89
76
 
90
- def supports_query_stats_user?
91
- @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
92
79
  end
93
80
 
94
- def insert_stats(table, columns, values)
95
- values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
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
- values = [
111
+ [
130
112
  db_id,
131
- qs["query"],
132
- qs["total_minutes"].to_f * 60 * 1000,
133
- qs["calls"],
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
- private
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 stats_connection
154
- ::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
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(options = {})
161
+ def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
159
162
  if query_stats_enabled?
160
- limit = options[:limit] || 100
161
- sort = options[:sort] || "total_minutes"
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
- #{supports_query_stats_user? ? "rolname" : "NULL"} AS user,
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(options = {})
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 = options[:sort] || "total_minutes"
204
- stats_connection.select_all squish <<-SQL
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
- #{supports_query_stats_user? ? "pghero_query_stats.user" : "NULL"} AS user,
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
- #{options[:start_at] ? "AND captured_at >= #{quote(options[:start_at])}" : ""}
219
- #{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)}" : ""}
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
- "query" => (stats2.find { |s| s["query"] } || {})["query"],
252
- "user" => (stats2.find { |s| s["user"] } || {})["user"],
253
- "query_hash" => (stats2.find { |s| s["query"] } || {})["query_hash"],
254
- "total_minutes" => stats2.sum { |s| s["total_minutes"].to_f },
255
- "calls" => stats2.sum { |s| s["calls"].to_i },
256
- "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] }
257
258
  }
258
- 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
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 Replica
3
+ module Replication
4
4
  def replica?
5
- select_all("SELECT setting FROM pg_settings WHERE name = 'hot_standby'").first["setting"] == "on"
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
- select_all(<<-SQL
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
- ).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?
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["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