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
@@ -2,64 +2,24 @@ module PgHero
2
2
  module Methods
3
3
  module Connections
4
4
  def total_connections
5
- select_all("SELECT COUNT(*) FROM pg_stat_activity WHERE pid <> pg_backend_pid()").first["count"].to_i
5
+ select_one("SELECT COUNT(*) FROM pg_stat_activity")
6
6
  end
7
7
 
8
- def connection_sources(options = {})
9
- if options[:by_database_and_user]
10
- select_all <<-SQL
11
- SELECT
12
- datname AS database,
13
- usename AS user,
14
- application_name AS source,
15
- client_addr AS ip,
16
- COUNT(*) AS total_connections
17
- FROM
18
- pg_stat_activity
19
- WHERE
20
- pid <> pg_backend_pid()
21
- GROUP BY
22
- 1, 2, 3, 4
23
- ORDER BY
24
- 5 DESC, 1, 2, 3, 4
25
- SQL
26
- elsif options[:by_database]
27
- select_all <<-SQL
28
- SELECT
29
- application_name AS source,
30
- client_addr AS ip,
31
- datname AS database,
32
- COUNT(*) AS total_connections
33
- FROM
34
- pg_stat_activity
35
- WHERE
36
- pid <> pg_backend_pid()
37
- GROUP BY
38
- 1, 2, 3
39
- ORDER BY
40
- COUNT(*) DESC,
41
- application_name ASC,
42
- client_addr ASC
43
- SQL
44
- else
45
- select_all <<-SQL
46
- SELECT
47
- application_name AS source,
48
- client_addr AS ip,
49
- COUNT(*) AS total_connections
50
- FROM
51
- pg_stat_activity
52
- WHERE
53
- pid <> pg_backend_pid()
54
- GROUP BY
55
- application_name,
56
- ip
57
- ORDER BY
58
- COUNT(*) DESC,
59
- application_name ASC,
60
- client_addr ASC
61
- SQL
62
- end
8
+ def connection_sources
9
+ select_all <<-SQL
10
+ SELECT
11
+ datname AS database,
12
+ usename AS user,
13
+ application_name AS source,
14
+ client_addr AS ip,
15
+ COUNT(*) AS total_connections
16
+ FROM
17
+ pg_stat_activity
18
+ GROUP BY
19
+ 1, 2, 3, 4
20
+ ORDER BY
21
+ 5 DESC, 1, 2, 3, 4
22
+ SQL
63
23
  end
64
24
  end
65
25
  end
@@ -4,15 +4,13 @@ module PgHero
4
4
  def explain(sql)
5
5
  sql = squish(sql)
6
6
  explanation = nil
7
- explain_safe = explain_safe?
8
7
 
9
8
  # use transaction for safety
10
- connection_model.transaction do
11
- if !explain_safe && (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT"))
9
+ with_transaction(statement_timeout: 10000, rollback: true) do
10
+ if (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
12
11
  raise ActiveRecord::StatementInvalid, "Unsafe statement"
13
12
  end
14
- explanation = select_all("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
15
- raise ActiveRecord::Rollback
13
+ explanation = select_all("EXPLAIN #{sql}").map { |v| v[:"QUERY PLAN"] }.join("\n")
16
14
  end
17
15
 
18
16
  explanation
@@ -2,20 +2,20 @@ module PgHero
2
2
  module Methods
3
3
  module Indexes
4
4
  def index_hit_rate
5
- select_all(<<-SQL
5
+ select_one <<-SQL
6
6
  SELECT
7
7
  (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate
8
8
  FROM
9
9
  pg_statio_user_indexes
10
10
  SQL
11
- ).first["rate"].to_f
12
11
  end
13
12
 
14
13
  def index_caching
15
14
  select_all <<-SQL
16
15
  SELECT
17
- indexrelname AS index,
16
+ schemaname AS schema,
18
17
  relname AS table,
18
+ indexrelname AS index,
19
19
  CASE WHEN idx_blks_hit + idx_blks_read = 0 THEN
20
20
  0
21
21
  ELSE
@@ -37,7 +37,7 @@ module PgHero
37
37
  WHEN 0 THEN 'Insufficient data'
38
38
  ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
39
39
  END percent_of_times_index_used,
40
- n_live_tup rows_in_table
40
+ n_live_tup AS estimated_rows
41
41
  FROM
42
42
  pg_stat_user_tables
43
43
  ORDER BY
@@ -55,7 +55,7 @@ module PgHero
55
55
  WHEN 0 THEN 'Insufficient data'
56
56
  ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
57
57
  END percent_of_times_index_used,
58
- n_live_tup rows_in_table
58
+ n_live_tup AS estimated_rows
59
59
  FROM
60
60
  pg_stat_user_tables
61
61
  WHERE
@@ -68,13 +68,13 @@ module PgHero
68
68
  SQL
69
69
  end
70
70
 
71
- def unused_indexes
72
- select_all <<-SQL
71
+ def unused_indexes(max_scans: 50)
72
+ select_all_size <<-SQL
73
73
  SELECT
74
74
  schemaname AS schema,
75
75
  relname AS table,
76
76
  indexrelname AS index,
77
- pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
77
+ pg_relation_size(i.indexrelid) AS size_bytes,
78
78
  idx_scan as index_scans
79
79
  FROM
80
80
  pg_stat_user_indexes ui
@@ -82,13 +82,29 @@ module PgHero
82
82
  pg_index i ON ui.indexrelid = i.indexrelid
83
83
  WHERE
84
84
  NOT indisunique
85
- AND idx_scan < 50
85
+ AND idx_scan <= #{max_scans.to_i}
86
86
  ORDER BY
87
87
  pg_relation_size(i.indexrelid) DESC,
88
88
  relname ASC
89
89
  SQL
90
90
  end
91
91
 
92
+ def reset_stats
93
+ execute("SELECT pg_stat_reset()")
94
+ true
95
+ end
96
+
97
+ def last_stats_reset_time
98
+ select_one <<-SQL
99
+ SELECT
100
+ pg_stat_get_db_stat_reset_time(oid) AS reset_time
101
+ FROM
102
+ pg_database
103
+ WHERE
104
+ datname = current_database()
105
+ SQL
106
+ end
107
+
92
108
  def invalid_indexes
93
109
  select_all <<-SQL
94
110
  SELECT
@@ -114,40 +130,182 @@ module PgHero
114
130
  def indexes
115
131
  select_all(<<-SQL
116
132
  SELECT
133
+ schemaname AS schema,
117
134
  t.relname AS table,
118
135
  ix.relname AS name,
119
- regexp_replace(pg_get_indexdef(indexrelid), '^[^\\(]*\\((.*)\\)$', '\\1') AS columns,
120
- regexp_replace(pg_get_indexdef(indexrelid), '.* USING ([^ ]*) \\(.*', '\\1') AS using,
136
+ regexp_replace(pg_get_indexdef(i.indexrelid), '^[^\\(]*\\((.*)\\)$', '\\1') AS columns,
137
+ regexp_replace(pg_get_indexdef(i.indexrelid), '.* USING ([^ ]*) \\(.*', '\\1') AS using,
121
138
  indisunique AS unique,
122
139
  indisprimary AS primary,
123
140
  indisvalid AS valid,
124
141
  indexprs::text,
125
142
  indpred::text,
126
- pg_get_indexdef(indexrelid) AS definition
143
+ pg_get_indexdef(i.indexrelid) AS definition
127
144
  FROM
128
145
  pg_index i
129
146
  INNER JOIN
130
147
  pg_class t ON t.oid = i.indrelid
131
148
  INNER JOIN
132
149
  pg_class ix ON ix.oid = i.indexrelid
150
+ LEFT JOIN
151
+ pg_stat_user_indexes ui ON ui.indexrelid = i.indexrelid
152
+ WHERE
153
+ schemaname IS NOT NULL
133
154
  ORDER BY
134
155
  1, 2
135
156
  SQL
136
- ).map { |v| v["columns"] = v["columns"].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
157
+ ).map { |v| v[:columns] = v[:columns].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
137
158
  end
138
159
 
139
- def duplicate_indexes
140
- indexes = []
160
+ def duplicate_indexes(indexes: nil)
161
+ dup_indexes = []
141
162
 
142
- indexes_by_table = self.indexes.group_by { |i| i["table"] }
143
- indexes_by_table.values.flatten.select { |i| PgHero.falsey?(i["primary"]) && PgHero.falsey?(i["unique"]) && !i["indexprs"] && !i["indpred"] && PgHero.truthy?(i["valid"]) }.each do |index|
144
- covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && !i["indexprs"] && !i["indpred"] && PgHero.truthy?(i["valid"]) }
145
- if covering_index
146
- indexes << {"unneeded_index" => index, "covering_index" => covering_index}
163
+ indexes_by_table = (indexes || self.indexes).group_by { |i| i[:table] }
164
+ indexes_by_table.values.flatten.select { |i| !i[:primary] && !i[:unique] && !i[:indexprs] && !i[:indpred] && i[:valid] }.each do |index|
165
+ covering_index = indexes_by_table[index[:table]].find { |i| index_covers?(i[:columns], index[:columns]) && i[:using] == index[:using] && i[:name] != index[:name] && i[:schema] == index[:schema] && !i[:indexprs] && !i[:indpred] && i[:valid] }
166
+ if covering_index && (covering_index[:columns] != index[:columns] || index[:name] > covering_index[:name])
167
+ dup_indexes << {unneeded_index: index, covering_index: covering_index}
147
168
  end
148
169
  end
149
170
 
150
- indexes.sort_by { |i| ui = i["unneeded_index"]; [ui["table"], ui["columns"]] }
171
+ dup_indexes.sort_by { |i| ui = i[:unneeded_index]; [ui[:table], ui[:columns]] }
172
+ end
173
+
174
+ # https://gist.github.com/mbanck/9976015/71888a24e464e2f772182a7eb54f15a125edf398
175
+ # thanks @jberkus and @mbanck
176
+ def index_bloat(min_size: nil)
177
+ min_size ||= index_bloat_bytes
178
+ select_all <<-SQL
179
+ WITH btree_index_atts AS (
180
+ SELECT
181
+ nspname, relname, reltuples, relpages, indrelid, relam,
182
+ regexp_split_to_table(indkey::text, ' ')::smallint AS attnum,
183
+ indexrelid as index_oid
184
+ FROM
185
+ pg_index
186
+ JOIN
187
+ pg_class ON pg_class.oid=pg_index.indexrelid
188
+ JOIN
189
+ pg_namespace ON pg_namespace.oid = pg_class.relnamespace
190
+ JOIN
191
+ pg_am ON pg_class.relam = pg_am.oid
192
+ WHERE
193
+ pg_am.amname = 'btree'
194
+ ),
195
+ index_item_sizes AS (
196
+ SELECT
197
+ i.nspname,
198
+ i.relname,
199
+ i.reltuples,
200
+ i.relpages,
201
+ i.relam,
202
+ (quote_ident(s.schemaname) || '.' || quote_ident(s.tablename))::regclass AS starelid,
203
+ a.attrelid AS table_oid, index_oid,
204
+ current_setting('block_size')::numeric AS bs,
205
+ /* MAXALIGN: 4 on 32bits, 8 on 64bits (and mingw32 ?) */
206
+ CASE
207
+ WHEN version() ~ 'mingw32' OR version() ~ '64-bit' THEN 8
208
+ ELSE 4
209
+ END AS maxalign,
210
+ 24 AS pagehdr,
211
+ /* per tuple header: add index_attribute_bm if some cols are null-able */
212
+ CASE WHEN max(coalesce(s.null_frac,0)) = 0
213
+ THEN 2
214
+ ELSE 6
215
+ END AS index_tuple_hdr,
216
+ /* data len: we remove null values save space using it fractionnal part from stats */
217
+ sum( (1-coalesce(s.null_frac, 0)) * coalesce(s.avg_width, 2048) ) AS nulldatawidth
218
+ FROM
219
+ pg_attribute AS a
220
+ JOIN
221
+ pg_stats AS s ON (quote_ident(s.schemaname) || '.' || quote_ident(s.tablename))::regclass=a.attrelid AND s.attname = a.attname
222
+ JOIN
223
+ btree_index_atts AS i ON i.indrelid = a.attrelid AND a.attnum = i.attnum
224
+ WHERE
225
+ a.attnum > 0
226
+ GROUP BY
227
+ 1, 2, 3, 4, 5, 6, 7, 8, 9
228
+ ),
229
+ index_aligned AS (
230
+ SELECT
231
+ maxalign,
232
+ bs,
233
+ nspname,
234
+ relname AS index_name,
235
+ reltuples,
236
+ relpages,
237
+ relam,
238
+ table_oid,
239
+ index_oid,
240
+ ( 2 +
241
+ maxalign - CASE /* Add padding to the index tuple header to align on MAXALIGN */
242
+ WHEN index_tuple_hdr%maxalign = 0 THEN maxalign
243
+ ELSE index_tuple_hdr%maxalign
244
+ END
245
+ + nulldatawidth + maxalign - CASE /* Add padding to the data to align on MAXALIGN */
246
+ WHEN nulldatawidth::integer%maxalign = 0 THEN maxalign
247
+ ELSE nulldatawidth::integer%maxalign
248
+ END
249
+ )::numeric AS nulldatahdrwidth, pagehdr
250
+ FROM
251
+ index_item_sizes AS s1
252
+ ),
253
+ otta_calc AS (
254
+ SELECT
255
+ bs,
256
+ nspname,
257
+ table_oid,
258
+ index_oid,
259
+ index_name,
260
+ relpages,
261
+ coalesce(
262
+ ceil((reltuples*(4+nulldatahdrwidth))/(bs-pagehdr::float)) +
263
+ CASE WHEN am.amname IN ('hash','btree') THEN 1 ELSE 0 END , 0 /* btree and hash have a metadata reserved block */
264
+ ) AS otta
265
+ FROM
266
+ index_aligned AS s2
267
+ LEFT JOIN
268
+ pg_am am ON s2.relam = am.oid
269
+ ),
270
+ raw_bloat AS (
271
+ SELECT
272
+ nspname,
273
+ c.relname AS table_name,
274
+ index_name,
275
+ bs*(sub.relpages)::bigint AS totalbytes,
276
+ CASE
277
+ WHEN sub.relpages <= otta THEN 0
278
+ ELSE bs*(sub.relpages-otta)::bigint END
279
+ AS wastedbytes,
280
+ CASE
281
+ WHEN sub.relpages <= otta
282
+ THEN 0 ELSE bs*(sub.relpages-otta)::bigint * 100 / (bs*(sub.relpages)::bigint) END
283
+ AS realbloat,
284
+ pg_relation_size(sub.table_oid) as table_bytes,
285
+ stat.idx_scan as index_scans,
286
+ stat.indexrelid
287
+ FROM
288
+ otta_calc AS sub
289
+ JOIN
290
+ pg_class AS c ON c.oid=sub.table_oid
291
+ JOIN
292
+ pg_stat_user_indexes AS stat ON sub.index_oid = stat.indexrelid
293
+ )
294
+ SELECT
295
+ nspname AS schema,
296
+ table_name AS table,
297
+ index_name AS index,
298
+ wastedbytes AS bloat_bytes,
299
+ totalbytes AS index_bytes,
300
+ pg_get_indexdef(indexrelid) AS definition
301
+ FROM
302
+ raw_bloat
303
+ WHERE
304
+ wastedbytes >= #{min_size.to_i}
305
+ ORDER BY
306
+ wastedbytes DESC,
307
+ index_name
308
+ SQL
151
309
  end
152
310
  end
153
311
  end
@@ -2,11 +2,11 @@ module PgHero
2
2
  module Methods
3
3
  module Kill
4
4
  def kill(pid)
5
- PgHero.truthy? execute("SELECT pg_terminate_backend(#{pid.to_i})").first["pg_terminate_backend"]
5
+ select_one("SELECT pg_terminate_backend(#{pid.to_i})")
6
6
  end
7
7
 
8
8
  def kill_long_running_queries
9
- long_running_queries.each { |query| kill(query["pid"]) }
9
+ long_running_queries.each { |query| kill(query[:pid]) }
10
10
  true
11
11
  end
12
12
 
@@ -5,40 +5,32 @@ module PgHero
5
5
  # "the system will shut down and refuse to start any new transactions
6
6
  # once there are fewer than 1 million transactions left until wraparound"
7
7
  # warn when 10,000,000 transactions left
8
- def transaction_id_danger(options = {})
9
- threshold = options[:threshold] || 10000000
8
+ def transaction_id_danger(threshold: 10000000, max_value: 2146483648)
9
+ max_value = max_value.to_i
10
+ threshold = threshold.to_i
11
+
10
12
  select_all <<-SQL
11
13
  SELECT
12
- c.oid::regclass::text AS table,
13
- 2146483648 - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_before_shutdown
14
+ n.nspname AS schema,
15
+ c.relname AS table,
16
+ #{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_left
14
17
  FROM
15
18
  pg_class c
19
+ INNER JOIN
20
+ pg_catalog.pg_namespace n ON n.oid = c.relnamespace
16
21
  LEFT JOIN
17
22
  pg_class t ON c.reltoastrelid = t.oid
18
23
  WHERE
19
24
  c.relkind = 'r'
20
- AND (2146483648 - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{threshold}
25
+ AND (#{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{quote(threshold)}
21
26
  ORDER BY
22
27
  2, 1
23
28
  SQL
24
29
  end
25
30
 
26
31
  def autovacuum_danger
27
- select_all <<-SQL
28
- SELECT
29
- c.oid::regclass::text as table,
30
- (SELECT setting FROM pg_settings WHERE name = 'autovacuum_freeze_max_age')::int -
31
- GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_before_autovacuum
32
- FROM
33
- pg_class c
34
- LEFT JOIN
35
- pg_class t ON c.reltoastrelid = t.oid
36
- WHERE
37
- c.relkind = 'r'
38
- AND (SELECT setting FROM pg_settings WHERE name = 'autovacuum_freeze_max_age')::int - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) < 2000000
39
- ORDER BY
40
- transactions_before_autovacuum
41
- SQL
32
+ max_value = select_one("SHOW autovacuum_freeze_max_age").to_i
33
+ transaction_id_danger(threshold: 2000000, max_value: max_value)
42
34
  end
43
35
 
44
36
  def maintenance_info
@@ -56,6 +48,28 @@ module PgHero
56
48
  1, 2
57
49
  SQL
58
50
  end
51
+
52
+ def analyze(table, verbose: false)
53
+ execute "ANALYZE #{verbose ? "VERBOSE " : ""}#{quote_table_name(table)}"
54
+ true
55
+ end
56
+
57
+ def analyze_tables(verbose: false, min_size: nil, tables: nil)
58
+ tables = table_stats(table: tables).reject { |s| %w(information_schema pg_catalog).include?(s[:schema]) }
59
+ tables = tables.select { |s| s[:size_bytes] > min_size } if min_size
60
+ tables.map { |s| s.slice(:schema, :table) }.each do |stats|
61
+ begin
62
+ with_transaction(lock_timeout: 5000, statement_timeout: 120000) do
63
+ analyze "#{stats[:schema]}.#{stats[:table]}", verbose: verbose
64
+ end
65
+ success = true
66
+ rescue ActiveRecord::StatementInvalid => e
67
+ $stderr.puts e.message
68
+ success = false
69
+ end
70
+ stats[:success] = success
71
+ end
72
+ end
59
73
  end
60
74
  end
61
75
  end
@@ -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,
@@ -12,6 +11,7 @@ module PgHero
12
11
  #{server_version_num >= 90600 ? "(wait_event IS NOT NULL) AS waiting" : "waiting"},
13
12
  query,
14
13
  COALESCE(query_start, xact_start) AS started_at,
14
+ EXTRACT(EPOCH FROM NOW() - COALESCE(query_start, xact_start)) * 1000.0 AS duration_ms,
15
15
  usename AS user
16
16
  FROM
17
17
  pg_stat_activity
@@ -30,55 +30,38 @@ module PgHero
30
30
  running_queries(min_duration: long_running_query_sec)
31
31
  end
32
32
 
33
- def slow_queries(options = {})
34
- query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
35
- query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
36
- end
37
-
38
- def locks
39
- select_all <<-SQL
40
- SELECT DISTINCT ON (pid)
41
- pg_stat_activity.pid,
42
- pg_stat_activity.query,
43
- age(now(), pg_stat_activity.query_start) AS age
44
- FROM
45
- pg_stat_activity
46
- INNER JOIN
47
- pg_locks ON pg_locks.pid = pg_stat_activity.pid
48
- WHERE
49
- pg_stat_activity.query <> '<insufficient privilege>'
50
- AND pg_locks.mode = 'ExclusiveLock'
51
- AND pg_stat_activity.pid <> pg_backend_pid()
52
- AND pg_stat_activity.datname = current_database()
53
- ORDER BY
54
- pid,
55
- query_start
56
- SQL
57
- end
58
-
59
33
  # from https://wiki.postgresql.org/wiki/Lock_Monitoring
34
+ # and http://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
60
35
  def blocked_queries
61
36
  select_all <<-SQL
62
37
  SELECT
63
- bl.pid AS blocked_pid,
64
- a.usename AS blocked_user,
65
- ka.query AS current_or_recent_query_in_blocking_process,
66
- ka.state AS state_of_blocking_process,
67
- age(now(), ka.query_start) AS blocking_duration,
68
- kl.pid AS blocking_pid,
69
- ka.usename AS blocking_user,
70
- a.query AS blocked_query,
71
- age(now(), a.query_start) AS blocked_duration
38
+ COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
39
+ blockeda.pid AS blocked_pid,
40
+ blockeda.usename AS blocked_user,
41
+ blockeda.query as blocked_query,
42
+ age(now(), blockeda.query_start) AS blocked_duration,
43
+ blockedl.mode as blocked_mode,
44
+ blockinga.pid AS blocking_pid,
45
+ blockinga.usename AS blocking_user,
46
+ blockinga.state AS state_of_blocking_process,
47
+ blockinga.query AS current_or_recent_query_in_blocking_process,
48
+ age(now(), blockinga.query_start) AS blocking_duration,
49
+ blockingl.mode as blocking_mode
72
50
  FROM
73
- pg_catalog.pg_locks bl
74
- JOIN
75
- pg_catalog.pg_stat_activity a ON a.pid = bl.pid
76
- JOIN
77
- pg_catalog.pg_locks kl ON kl.transactionid = bl.transactionid AND kl.pid != bl.pid
78
- JOIN
79
- pg_catalog.pg_stat_activity ka ON ka.pid = kl.pid
51
+ pg_catalog.pg_locks blockedl
52
+ LEFT JOIN
53
+ pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
54
+ LEFT JOIN
55
+ pg_catalog.pg_locks blockingl ON blockedl.pid != blockingl.pid AND (
56
+ blockingl.transactionid = blockedl.transactionid
57
+ OR (blockingl.relation = blockedl.relation AND blockingl.locktype = blockedl.locktype)
58
+ )
59
+ LEFT JOIN
60
+ pg_stat_activity blockinga ON blockingl.pid = blockinga.pid AND blockinga.datid = blockeda.datid
80
61
  WHERE
81
- NOT bl.GRANTED
62
+ NOT blockedl.granted
63
+ AND blockeda.query <> '<insufficient privilege>'
64
+ AND blockeda.datname = current_database()
82
65
  ORDER BY
83
66
  blocked_duration DESC
84
67
  SQL