pghero 3.7.0 → 4.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -2
  3. data/LICENSE.txt +1 -1
  4. data/README.md +4 -3
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +2228 -5320
  6. data/app/assets/javascripts/pghero/application.js +104 -68
  7. data/app/assets/javascripts/pghero/highlight.min.js +330 -341
  8. data/app/assets/javascripts/pghero/nouislider.js +64 -5
  9. data/app/assets/stylesheets/pghero/application.css +7 -1
  10. data/app/controllers/pg_hero/home_controller.rb +75 -41
  11. data/app/views/layouts/pg_hero/application.html.erb +1 -1
  12. data/app/views/pg_hero/home/_live_queries_table.html.erb +4 -2
  13. data/app/views/pg_hero/home/_queries_table.html.erb +8 -7
  14. data/app/views/pg_hero/home/_query_stats_slider.html.erb +1 -0
  15. data/app/views/pg_hero/home/_suggested_index.html.erb +3 -3
  16. data/app/views/pg_hero/home/explain.html.erb +2 -1
  17. data/app/views/pg_hero/home/index.html.erb +17 -18
  18. data/app/views/pg_hero/home/index_bloat.html.erb +2 -2
  19. data/app/views/pg_hero/home/live_queries.html.erb +7 -3
  20. data/app/views/pg_hero/home/queries.html.erb +31 -6
  21. data/app/views/pg_hero/home/show_query.html.erb +5 -5
  22. data/app/views/pg_hero/home/space.html.erb +5 -4
  23. data/app/views/pg_hero/home/tune.html.erb +1 -1
  24. data/lib/generators/pghero/templates/config.yml.tt +6 -1
  25. data/lib/generators/pghero/templates/query_stats.rb.tt +7 -1
  26. data/lib/generators/pghero/templates/upgrade_query_stats.rb.tt +23 -0
  27. data/lib/generators/pghero/upgrade_query_stats_generator.rb +19 -0
  28. data/lib/pghero/database.rb +6 -9
  29. data/lib/pghero/engine.rb +3 -10
  30. data/lib/pghero/methods/basic.rb +28 -27
  31. data/lib/pghero/methods/connections.rb +16 -32
  32. data/lib/pghero/methods/explain.rb +13 -20
  33. data/lib/pghero/methods/indexes.rb +8 -5
  34. data/lib/pghero/methods/kill.rb +4 -1
  35. data/lib/pghero/methods/maintenance.rb +14 -20
  36. data/lib/pghero/methods/queries.rb +7 -5
  37. data/lib/pghero/methods/query_stats.rb +264 -238
  38. data/lib/pghero/methods/replication.rb +9 -20
  39. data/lib/pghero/methods/sequences.rb +1 -1
  40. data/lib/pghero/methods/settings.rb +6 -11
  41. data/lib/pghero/methods/space.rb +11 -8
  42. data/lib/pghero/methods/suggested_indexes.rb +15 -16
  43. data/lib/pghero/methods/system.rb +6 -106
  44. data/lib/pghero/methods/tables.rb +7 -3
  45. data/lib/pghero/query.rb +8 -0
  46. data/lib/pghero/query_stats.rb +3 -0
  47. data/lib/pghero/version.rb +1 -1
  48. data/lib/pghero.rb +10 -42
  49. data/licenses/LICENSE-chart.js.txt +1 -1
  50. metadata +8 -8
  51. data/app/assets/javascripts/pghero/jquery.js +0 -10993
  52. data/lib/pghero/methods/users.rb +0 -95
  53. data/licenses/LICENSE-jquery.txt +0 -20
@@ -6,7 +6,7 @@
6
6
  <p>Indexes can become <%= link_to "bloated over time", "https://www.compose.com/articles/postgresql-bloat-origins-monitoring-and-managing/", target: "_blank" %>. Recreate them to remove bloat.</p>
7
7
 
8
8
  <p>For each index, run:</p>
9
- <pre><code>CREATE INDEX CONCURRENTLY new_index ...;
9
+ <pre><code class="language-pgsql">CREATE INDEX CONCURRENTLY new_index ...;
10
10
  ANALYZE table;
11
11
  DROP INDEX CONCURRENTLY index;
12
12
  ANALYZE table;
@@ -45,7 +45,7 @@ ALTER INDEX new_index RENAME TO index;</code></pre>
45
45
  <tr>
46
46
  <td colspan="3" class="query-row">
47
47
  <% new_index = "new_#{index[:index]}".first(63) %>
48
- <pre><code><%= index[:definition].sub(" INDEX ", " INDEX CONCURRENTLY \n ").sub(index[:index], new_index) %>;
48
+ <pre><code class="language-pgsql"><%= index[:definition].sub(" INDEX ", " INDEX CONCURRENTLY \n ").sub(index[:index], new_index) %>;
49
49
 
50
50
  ANALYZE <%= pghero_pretty_ident(index[:table], schema: index[:schema]) %>;
51
51
 
@@ -3,9 +3,13 @@
3
3
 
4
4
  <p><%= pluralize(@running_queries.size, "query") %></p>
5
5
 
6
- <%= render partial: "live_queries_table", locals: {queries: @running_queries, vacuum_progress: @vacuum_progress} %>
6
+ <% if @running_queries.any? %>
7
+ <%= render partial: "live_queries_table", locals: {queries: @running_queries, vacuum_progress: @vacuum_progress} %>
7
8
 
8
- <p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
9
+ <% if @kill_enabled %>
10
+ <p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
9
11
 
10
- <p class="text-muted">You may need to restart your app servers afterwards.</p>
12
+ <p class="text-muted">You may need to restart your app servers afterwards.</p>
13
+ <% end %>
14
+ <% end %>
11
15
  </div>
@@ -1,6 +1,6 @@
1
1
  <div class="content">
2
- <% if @query_stats_enabled && !@historical_query_stats_enabled %>
3
- <%= button_to "Reset", reset_query_stats_path, class: "btn btn-danger", style: "float: right;" %>
2
+ <% if @query_stats_enabled && !@historical_query_stats_enabled && !@database.query_stats_table_exists? %>
3
+ <%= button_to "Reset", reset_query_stats_path, class: "btn btn-danger push-right" %>
4
4
  <% end %>
5
5
 
6
6
  <% if !@historical_query_stats_enabled %>
@@ -9,11 +9,31 @@
9
9
 
10
10
  <% if @historical_query_stats_enabled %>
11
11
  <%= render partial: "query_stats_slider" %>
12
- <% elsif @database.query_stats_table_exists? && (columns = @database.missing_query_stats_columns).any? %>
12
+ <% elsif @database.query_stats_table_exists? && !@database.queries_table_exists? %>
13
13
  <div class="clear-both">
14
- <p>Add missing columns to re-enable historical query stats.</p>
15
- <pre><code><% @database.missing_query_stats_columns.each do |column| %>ALTER TABLE pghero_query_stats ADD COLUMN "<%= column %>" <%= column == "query_hash" ? "bigint" : "text" %>;
16
- <% end %></code></pre>
14
+ <p>Update the database to re-enable historical query stats.</p>
15
+ <% if PgHero.show_migrations %>
16
+ <pre><code>rails generate pghero:upgrade_query_stats
17
+ rails db:migrate</code></pre>
18
+ <% else %>
19
+ <pre><code class="language-pgsql">CREATE TABLE pghero_queries (
20
+ id bigserial PRIMARY KEY,
21
+ query text
22
+ );
23
+ CREATE INDEX ON pghero_queries USING hash (query);
24
+
25
+ ALTER TABLE pghero_query_stats ADD COLUMN query_id bigint;
26
+
27
+ INSERT INTO pghero_queries (query)
28
+ SELECT DISTINCT query FROM pghero_query_stats WHERE query IS NOT NULL;
29
+
30
+ UPDATE pghero_query_stats SET query_id = pghero_queries.id, query = NULL
31
+ FROM pghero_queries WHERE pghero_queries.query = pghero_query_stats.query;
32
+
33
+ ALTER TABLE pghero_query_stats DROP COLUMN query;
34
+
35
+ VACUUM (FULL, ANALYZE) pghero_query_stats;</code></pre>
36
+ <% end %>
17
37
  <p>Then restart the web server.</p>
18
38
  </div>
19
39
  <% end %>
@@ -22,6 +42,11 @@
22
42
  <% if @error %>
23
43
  <div class="alert alert-danger">Cannot understand start or end time.</div>
24
44
  <% elsif @query_stats.any? || @historical_query_stats_enabled %>
45
+ <% if @user %>
46
+ <div class="clear-both">
47
+ <p>Showing queries for <code><%= @user %></code> user. <%= link_to "Show all", @link_options, id: "show-all" %></p>
48
+ </div>
49
+ <% end %>
25
50
  <%= render partial: "queries_table", locals: {queries: @query_stats, sort_headers: true} %>
26
51
  <%= javascript_tag nonce: true do %>
27
52
  highlightQueries();
@@ -1,5 +1,5 @@
1
1
  <div class="content">
2
- <pre><code class="query-code"><%= @query %></code></pre>
2
+ <pre><code class="query-code language-pgsql"><%= @query %></code></pre>
3
3
  <%= javascript_tag nonce: true do %>
4
4
  highlightQueries()
5
5
  <% end %>
@@ -46,16 +46,16 @@
46
46
 
47
47
  <!-- chart -->
48
48
  <% if @chart_data %>
49
- <h1>Total Time <small>ms</small></h1>
49
+ <h1>Total Time</h1>
50
50
  <div id="chart-1" class="chart">Loading...</div>
51
51
  <%= javascript_tag nonce: true do %>
52
- new Chartkick.LineChart("chart-1", <%= pghero_js_value(@chart_data) %>, {colors: ["#5bc0de"], legend: false, library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
52
+ new Chartkick.LineChart("chart-1", <%= pghero_js_value(@chart_data) %>, {colors: ["#5bc0de"], legend: false, suffix: " ms", library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
53
53
  <% end %>
54
54
 
55
- <h1>Average Time <small>ms</small></h1>
55
+ <h1>Average Time</h1>
56
56
  <div id="chart-2" class="chart">Loading...</div>
57
57
  <%= javascript_tag nonce: true do %>
58
- new Chartkick.LineChart("chart-2", <%= pghero_js_value(@chart2_data) %>, {colors: ["#5bc0de"], legend: false, library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
58
+ new Chartkick.LineChart("chart-2", <%= pghero_js_value(@chart2_data) %>, {colors: ["#5bc0de"], legend: false, suffix: " ms", library: {plugins: {tooltip: {intersect: false, mode: "index"}}}})
59
59
  <% end %>
60
60
 
61
61
  <h1>Calls</h1>
@@ -18,7 +18,7 @@
18
18
 
19
19
  <% if @unused_indexes.any? %>
20
20
  <p>
21
- <%= pluralize(@unused_indexes.size, "unused index") %>. Remove them
21
+ <%= pluralize(@unused_indexes.size, "unused index", "unused indexes") %><%= @database.unused_index_bytes > 0 ? " over #{PgHero.pretty_size(@database.unused_index_bytes)}" : "" %>. Remove them
22
22
  <% if @show_migrations %>
23
23
  <a href="#" class="migration-link">with a migration</a>
24
24
  <% end %>
@@ -30,11 +30,12 @@
30
30
  </p>
31
31
 
32
32
  <div class="migration">
33
- <pre>rails generate migration remove_unused_indexes</pre>
33
+ <pre><code>rails generate migration remove_unused_indexes</code></pre>
34
34
  <p>And paste</p>
35
- <pre><% @unused_indexes.sort_by { |q| [-q[:size_bytes], q[:index]] }.each do |query| %>
36
- <%= pghero_remove_index(query) %><% end %></pre>
35
+ <pre><code><%= @unused_indexes.sort_by { |q| [-q[:size_bytes], q[:index]] }.map { |q| pghero_remove_index(q) }.join("\n") %></code></pre>
37
36
  </div>
37
+ <% else %>
38
+ <p>No unused indexes<%= @database.unused_index_bytes > 0 ? " over #{PgHero.pretty_size(@database.unused_index_bytes)}" : "" %></p>
38
39
  <% end %>
39
40
 
40
41
  <% if @sizes_timeout %>
@@ -48,6 +48,6 @@
48
48
  </tbody>
49
49
  </table>
50
50
 
51
- <p>Check out <%= link_to "Autovacuum Tuning Basics", "https://blog.2ndquadrant.com/autovacuum-tuning-basics/", target: "_blank" %> for recommendations.</p>
51
+ <p>Check out <%= link_to "Autovacuum Tuning Basics", "https://www.enterprisedb.com/blog/autovacuum-tuning-basics", target: "_blank" %> for recommendations.</p>
52
52
  </div>
53
53
  <% end %>
@@ -6,7 +6,6 @@ databases:
6
6
  # System stats
7
7
  # aws_db_instance_identifier: my-instance
8
8
  # gcp_database_id: my-project:my-instance
9
- # azure_resource_id: my-resource-id
10
9
 
11
10
  # Add more databases
12
11
  # other:
@@ -24,6 +23,9 @@ databases:
24
23
  # Minimum connections for high connections warning
25
24
  # total_connections_threshold: 500
26
25
 
26
+ # Minimum size for unused indexes
27
+ # unused_index_megabytes: 10
28
+
27
29
  # Explain functionality
28
30
  # explain: true / false / analyze
29
31
 
@@ -48,5 +50,8 @@ databases:
48
50
  # aws_secret_access_key: <%%= ENV["AWS_SECRET_ACCESS_KEY"] %>
49
51
  # aws_region: us-east-1
50
52
 
53
+ # Disable killing queries and connections
54
+ # disable_kill: true
55
+
51
56
  # Filter data from queries (experimental)
52
57
  # filter_data: true
@@ -1,9 +1,15 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
+ create_table :pghero_queries do |t|
4
+ t.text :query
5
+ end
6
+
7
+ add_index :pghero_queries, :query, using: :hash
8
+
3
9
  create_table :pghero_query_stats do |t|
4
10
  t.text :database
5
11
  t.text :user
6
- t.text :query
12
+ t.references :query, index: false
7
13
  t.integer :query_hash, limit: 8
8
14
  t.float :total_time
9
15
  t.integer :calls, limit: 8
@@ -0,0 +1,23 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :pghero_queries do |t|
4
+ t.text :query
5
+ end
6
+
7
+ add_index :pghero_queries, :query, using: :hash
8
+
9
+ add_reference :pghero_query_stats, :query, index: false
10
+
11
+ execute <<~SQL
12
+ INSERT INTO pghero_queries (query)
13
+ SELECT DISTINCT query FROM pghero_query_stats WHERE query IS NOT NULL
14
+ SQL
15
+
16
+ execute <<~SQL
17
+ UPDATE pghero_query_stats SET query_id = pghero_queries.id, query = NULL
18
+ FROM pghero_queries WHERE pghero_queries.query = pghero_query_stats.query
19
+ SQL
20
+
21
+ remove_column :pghero_query_stats, :query
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Pghero
5
+ module Generators
6
+ class UpgradeQueryStatsGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+ source_root File.join(__dir__, "templates")
9
+
10
+ def copy_migration
11
+ migration_template "upgrade_query_stats.rb", "db/migrate/upgrade_pghero_query_stats.rb", migration_version: migration_version
12
+ end
13
+
14
+ def migration_version
15
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -16,7 +16,6 @@ module PgHero
16
16
  include Methods::SuggestedIndexes
17
17
  include Methods::System
18
18
  include Methods::Tables
19
- include Methods::Users
20
19
 
21
20
  attr_reader :id, :config
22
21
 
@@ -62,9 +61,12 @@ module PgHero
62
61
  (config["long_running_query_sec"] || PgHero.config["long_running_query_sec"] || PgHero.long_running_query_sec).to_i
63
62
  end
64
63
 
65
- # defaults to 100 megabytes
64
+ def unused_index_bytes
65
+ ((config["unused_index_megabytes"] || PgHero.config["unused_index_megabytes"] || 10).to_f * 1.megabyte).to_i
66
+ end
67
+
66
68
  def index_bloat_bytes
67
- (config["index_bloat_bytes"] || PgHero.config["index_bloat_bytes"] || 104857600).to_i
69
+ (config["index_bloat_bytes"] || PgHero.config["index_bloat_bytes"] || 100.megabytes).to_i
68
70
  end
69
71
 
70
72
  def aws_access_key_id
@@ -89,11 +91,6 @@ module PgHero
89
91
  @gcp_database_id ||= config["gcp_database_id"]
90
92
  end
91
93
 
92
- # environment variable is only used if no config file
93
- def azure_resource_id
94
- @azure_resource_id ||= config["azure_resource_id"]
95
- end
96
-
97
94
  # must check keys for booleans
98
95
  def filter_data
99
96
  unless defined?(@filter_data)
@@ -142,7 +139,7 @@ module PgHero
142
139
 
143
140
  # resolve spec
144
141
  if !url && config["spec"]
145
- config_options = {env_name: PgHero.env, PgHero.spec_name_key => config["spec"], PgHero.include_replicas_key => true}
142
+ config_options = {env_name: PgHero.env, name: config["spec"], include_hidden: true}
146
143
  resolved = ActiveRecord::Base.configurations.configs_for(**config_options)
147
144
  raise Error, "Spec not found: #{config["spec"]}" unless resolved
148
145
  url = resolved.configuration_hash
data/lib/pghero/engine.rb CHANGED
@@ -5,16 +5,9 @@ module PgHero
5
5
  initializer "pghero", group: :all do |app|
6
6
  # check if Rails api mode
7
7
  if app.config.respond_to?(:assets) && defined?(Sprockets)
8
- if Sprockets::VERSION.to_i >= 4
9
- app.config.assets.precompile << "pghero/application.js"
10
- app.config.assets.precompile << "pghero/application.css"
11
- app.config.assets.precompile << "pghero/favicon.png"
12
- else
13
- # use a proc instead of a string
14
- app.config.assets.precompile << proc { |path| path == "pghero/application.js" }
15
- app.config.assets.precompile << proc { |path| path == "pghero/application.css" }
16
- app.config.assets.precompile << proc { |path| path == "pghero/favicon.png" }
17
- end
8
+ app.config.assets.precompile << "pghero/application.js"
9
+ app.config.assets.precompile << "pghero/application.css"
10
+ app.config.assets.precompile << "pghero/favicon.png"
18
11
  end
19
12
 
20
13
  file_config = PgHero.file_config || {}
@@ -36,17 +36,28 @@ module PgHero
36
36
 
37
37
  private
38
38
 
39
- def select_all(sql, stats: false, query_columns: [])
39
+ def prepare_sql(sql, binds = nil, stats: false)
40
+ if binds && !binds.empty?
41
+ model = stats ? PgHero::Stats : connection_model
42
+ sql = model.sanitize_sql_array([sql, binds])
43
+ end
44
+
40
45
  with_connection(stats: stats) do |conn|
46
+ yield conn, sql
47
+ end
48
+ end
49
+
50
+ def select_all(sql, binds = nil, stats: false, query_columns: [])
51
+ # squish for logs
52
+ prepare_sql(sql.to_str.squish, binds, stats: stats) do |conn, sql|
41
53
  select_all_leased(sql, conn: conn, query_columns: query_columns)
42
54
  end
43
55
  end
44
56
 
45
57
  def select_all_leased(sql, conn:, query_columns:)
46
- # squish for logs
47
58
  retries = 0
48
59
  begin
49
- result = conn.select_all(add_source(squish(sql)))
60
+ result = conn.select_all(sql)
50
61
  if ActiveRecord::VERSION::MAJOR >= 8
51
62
  result = result.to_a.map(&:symbolize_keys)
52
63
  else
@@ -56,7 +67,7 @@ module PgHero
56
67
  query_columns.each do |column|
57
68
  result.each do |row|
58
69
  begin
59
- row[column] = PgQuery.normalize(row[column])
70
+ row[column] = PgQuery.normalize(row[column]) unless row[column].nil?
60
71
  rescue PgQuery::ParseError
61
72
  # try replacing "interval $1" with "$1::interval"
62
73
  # see https://github.com/lfittl/pg_query/issues/169 for more info
@@ -85,43 +96,33 @@ module PgHero
85
96
  end
86
97
  end
87
98
 
88
- def select_all_stats(sql, **options)
89
- select_all(sql, **options, stats: true)
99
+ def select_all_stats(sql, binds = nil, **options)
100
+ select_all(sql, binds, **options, stats: true)
90
101
  end
91
102
 
92
- def select_all_size(sql)
93
- result = select_all(sql)
103
+ def select_all_size(sql, binds = nil)
104
+ result = select_all(sql, binds)
94
105
  result.each do |row|
95
106
  row[:size] = PgHero.pretty_size(row[:size_bytes])
96
107
  end
97
108
  result
98
109
  end
99
110
 
100
- def select_one(sql)
101
- select_all(sql).first.values.first
111
+ def select_one(sql, binds = nil)
112
+ select_all(sql, binds).first&.values&.first
102
113
  end
103
114
 
104
- def execute(sql)
105
- with_connection { |c| c.execute(add_source(sql)) }
115
+ def execute(sql, binds = nil)
116
+ prepare_sql(sql, binds) do |conn, sql|
117
+ conn.execute(sql)
118
+ end
106
119
  end
107
120
 
108
121
  def with_connection(stats: false, &block)
109
- model = stats ? ::PgHero::Stats : connection_model
122
+ model = stats ? PgHero::Stats : connection_model
110
123
  model.connection_pool.with_connection(&block)
111
124
  end
112
125
 
113
- def squish(str)
114
- str.to_s.squish
115
- end
116
-
117
- def add_source(sql)
118
- "#{sql} /*pghero*/"
119
- end
120
-
121
- def quote(value)
122
- with_connection { |c| c.quote(value) }
123
- end
124
-
125
126
  def quote_table_name(value)
126
127
  with_connection { |c| c.quote_table_name(value) }
127
128
  end
@@ -140,8 +141,8 @@ module PgHero
140
141
 
141
142
  def with_transaction(lock_timeout: nil, statement_timeout: nil, rollback: false)
142
143
  connection_model.transaction do
143
- select_all "SET LOCAL statement_timeout = #{statement_timeout.to_i}" if statement_timeout
144
- select_all "SET LOCAL lock_timeout = #{lock_timeout.to_i}" if lock_timeout
144
+ select_all("SET LOCAL statement_timeout = :timeout", {timeout: statement_timeout.to_i}) if statement_timeout
145
+ select_all("SET LOCAL lock_timeout = :timeout", {timeout: lock_timeout.to_i}) if lock_timeout
145
146
  yield
146
147
  raise ActiveRecord::Rollback if rollback
147
148
  end
@@ -2,38 +2,22 @@ module PgHero
2
2
  module Methods
3
3
  module Connections
4
4
  def connections
5
- if server_version_num >= 90500
6
- select_all <<~SQL
7
- SELECT
8
- pg_stat_activity.pid,
9
- datname AS database,
10
- usename AS user,
11
- application_name AS source,
12
- client_addr AS ip,
13
- state,
14
- ssl
15
- FROM
16
- pg_stat_activity
17
- LEFT JOIN
18
- pg_stat_ssl ON pg_stat_activity.pid = pg_stat_ssl.pid
19
- ORDER BY
20
- pg_stat_activity.pid
21
- SQL
22
- else
23
- select_all <<~SQL
24
- SELECT
25
- pid,
26
- datname AS database,
27
- usename AS user,
28
- application_name AS source,
29
- client_addr AS ip,
30
- state
31
- FROM
32
- pg_stat_activity
33
- ORDER BY
34
- pid
35
- SQL
36
- end
5
+ select_all <<~SQL
6
+ SELECT
7
+ pg_stat_activity.pid,
8
+ datname AS database,
9
+ usename AS user,
10
+ application_name AS source,
11
+ client_addr AS ip,
12
+ state,
13
+ ssl
14
+ FROM
15
+ pg_stat_activity
16
+ LEFT JOIN
17
+ pg_stat_ssl ON pg_stat_activity.pid = pg_stat_ssl.pid
18
+ ORDER BY
19
+ pg_stat_activity.pid
20
+ SQL
37
21
  end
38
22
 
39
23
  def total_connections
@@ -1,26 +1,8 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module Explain
4
- # TODO remove in 4.0
5
4
  # note: this method is not affected by the explain option
6
- def explain(sql)
7
- sql = squish(sql)
8
- explanation = nil
9
-
10
- # use transaction for safety
11
- with_transaction(statement_timeout: (explain_timeout_sec * 1000).round, rollback: true) do
12
- if (sql.delete_suffix(";").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
13
- raise ActiveRecord::StatementInvalid, "Unsafe statement"
14
- end
15
- explanation = execute("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
16
- end
17
-
18
- explanation
19
- end
20
-
21
- # TODO rename to explain in 4.0
22
- # note: this method is not affected by the explain option
23
- def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, generic_plan: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
5
+ def explain(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, generic_plan: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
24
6
  options = []
25
7
  add_explain_option(options, "ANALYZE", analyze)
26
8
  add_explain_option(options, "VERBOSE", verbose)
@@ -33,7 +15,18 @@ module PgHero
33
15
  add_explain_option(options, "SUMMARY", summary)
34
16
  options << "FORMAT #{explain_format(format)}"
35
17
 
36
- explain("(#{options.join(", ")}) #{sql}")
18
+ sql = "(#{options.join(", ")}) #{sql}"
19
+ explanation = nil
20
+
21
+ # use transaction for safety
22
+ with_transaction(statement_timeout: (explain_timeout_sec * 1000).round, rollback: true) do
23
+ if (sql.delete_suffix(";").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
24
+ raise ActiveRecord::StatementInvalid, "Unsafe statement"
25
+ end
26
+ explanation = execute("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
27
+ end
28
+
29
+ explanation
37
30
  end
38
31
 
39
32
  private
@@ -68,8 +68,8 @@ module PgHero
68
68
  SQL
69
69
  end
70
70
 
71
- def unused_indexes(max_scans: 50, across: [])
72
- result = select_all_size <<~SQL
71
+ def unused_indexes(max_scans: 50, min_size: 0, across: [])
72
+ sql = <<~SQL
73
73
  SELECT
74
74
  schemaname AS schema,
75
75
  relname AS table,
@@ -82,11 +82,13 @@ module PgHero
82
82
  pg_index i ON ui.indexrelid = i.indexrelid
83
83
  WHERE
84
84
  NOT indisunique
85
- AND idx_scan <= #{max_scans.to_i}
85
+ AND idx_scan <= :max_scans
86
+ AND pg_relation_size(i.indexrelid) >= :min_size
86
87
  ORDER BY
87
88
  pg_relation_size(i.indexrelid) DESC,
88
89
  relname ASC
89
90
  SQL
91
+ result = select_all_size(sql, {max_scans: max_scans.to_i, min_size: min_size.to_i})
90
92
 
91
93
  across.each do |database_id|
92
94
  database = PgHero.databases.values.find { |d| d.id == database_id }
@@ -186,7 +188,7 @@ module PgHero
186
188
  # thanks @jberkus and @mbanck
187
189
  def index_bloat(min_size: nil)
188
190
  min_size ||= index_bloat_bytes
189
- select_all <<~SQL
191
+ sql = <<~SQL
190
192
  WITH btree_index_atts AS (
191
193
  SELECT
192
194
  nspname, relname, reltuples, relpages, indrelid, relam,
@@ -315,11 +317,12 @@ module PgHero
315
317
  INNER JOIN
316
318
  pg_index i ON i.indexrelid = rb.indexrelid
317
319
  WHERE
318
- wastedbytes >= #{min_size.to_i}
320
+ wastedbytes >= :min_size
319
321
  ORDER BY
320
322
  wastedbytes DESC,
321
323
  index_name
322
324
  SQL
325
+ select_all(sql, {min_size: min_size.to_i})
323
326
  end
324
327
 
325
328
  protected
@@ -1,15 +1,18 @@
1
1
  module PgHero
2
2
  module Methods
3
3
  module Kill
4
+ # note: this method is not affected by disable_kill option
4
5
  def kill(pid)
5
- select_one("SELECT pg_terminate_backend(#{pid.to_i})")
6
+ select_one("SELECT pg_terminate_backend(:pid)", {pid: pid.to_i})
6
7
  end
7
8
 
9
+ # note: this method is not affected by disable_kill option
8
10
  def kill_long_running_queries(min_duration: nil)
9
11
  running_queries(min_duration: min_duration || long_running_query_sec).each { |query| kill(query[:pid]) }
10
12
  true
11
13
  end
12
14
 
15
+ # note: this method is not affected by disable_kill option
13
16
  def kill_all
14
17
  select_all <<~SQL
15
18
  SELECT
@@ -6,14 +6,11 @@ module PgHero
6
6
  # once there are fewer than 1 million transactions left until wraparound"
7
7
  # warn when 10,000,000 transactions left
8
8
  def transaction_id_danger(threshold: 10000000, max_value: 2146483648)
9
- max_value = max_value.to_i
10
- threshold = threshold.to_i
11
-
12
- select_all <<~SQL
9
+ sql = <<~SQL
13
10
  SELECT
14
11
  n.nspname AS schema,
15
12
  c.relname AS table,
16
- #{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_left
13
+ :max_value - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_left
17
14
  FROM
18
15
  pg_class c
19
16
  INNER JOIN
@@ -22,10 +19,11 @@ module PgHero
22
19
  pg_class t ON c.reltoastrelid = t.oid
23
20
  WHERE
24
21
  c.relkind = 'r'
25
- AND (#{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{quote(threshold)}
22
+ AND (:max_value - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < :threshold
26
23
  ORDER BY
27
24
  3, 1, 2
28
25
  SQL
26
+ select_all(sql, {max_value: max_value.to_i, threshold: threshold.to_i})
29
27
  end
30
28
 
31
29
  def autovacuum_danger
@@ -34,19 +32,15 @@ module PgHero
34
32
  end
35
33
 
36
34
  def vacuum_progress
37
- if server_version_num >= 90600
38
- select_all <<~SQL
39
- SELECT
40
- pid,
41
- phase
42
- FROM
43
- pg_stat_progress_vacuum
44
- WHERE
45
- datname = current_database()
46
- SQL
47
- else
48
- []
49
- end
35
+ select_all <<~SQL
36
+ SELECT
37
+ pid,
38
+ phase
39
+ FROM
40
+ pg_stat_progress_vacuum
41
+ WHERE
42
+ datname = current_database()
43
+ SQL
50
44
  end
51
45
 
52
46
  def maintenance_info
@@ -68,7 +62,7 @@ module PgHero
68
62
  end
69
63
 
70
64
  def analyze(table, verbose: false)
71
- execute "ANALYZE #{verbose ? "VERBOSE " : ""}#{quote_table_name(table)}"
65
+ execute "ANALYZE #{"VERBOSE " if verbose}#{quote_table_name(table)}"
72
66
  true
73
67
  end
74
68