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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -2
- data/LICENSE.txt +1 -1
- data/README.md +4 -3
- data/app/assets/javascripts/pghero/Chart.bundle.js +2228 -5320
- data/app/assets/javascripts/pghero/application.js +104 -68
- data/app/assets/javascripts/pghero/highlight.min.js +330 -341
- data/app/assets/javascripts/pghero/nouislider.js +64 -5
- data/app/assets/stylesheets/pghero/application.css +7 -1
- data/app/controllers/pg_hero/home_controller.rb +75 -41
- data/app/views/layouts/pg_hero/application.html.erb +1 -1
- data/app/views/pg_hero/home/_live_queries_table.html.erb +4 -2
- data/app/views/pg_hero/home/_queries_table.html.erb +8 -7
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +1 -0
- data/app/views/pg_hero/home/_suggested_index.html.erb +3 -3
- data/app/views/pg_hero/home/explain.html.erb +2 -1
- data/app/views/pg_hero/home/index.html.erb +17 -18
- data/app/views/pg_hero/home/index_bloat.html.erb +2 -2
- data/app/views/pg_hero/home/live_queries.html.erb +7 -3
- data/app/views/pg_hero/home/queries.html.erb +31 -6
- data/app/views/pg_hero/home/show_query.html.erb +5 -5
- data/app/views/pg_hero/home/space.html.erb +5 -4
- data/app/views/pg_hero/home/tune.html.erb +1 -1
- data/lib/generators/pghero/templates/config.yml.tt +6 -1
- data/lib/generators/pghero/templates/query_stats.rb.tt +7 -1
- data/lib/generators/pghero/templates/upgrade_query_stats.rb.tt +23 -0
- data/lib/generators/pghero/upgrade_query_stats_generator.rb +19 -0
- data/lib/pghero/database.rb +6 -9
- data/lib/pghero/engine.rb +3 -10
- data/lib/pghero/methods/basic.rb +28 -27
- data/lib/pghero/methods/connections.rb +16 -32
- data/lib/pghero/methods/explain.rb +13 -20
- data/lib/pghero/methods/indexes.rb +8 -5
- data/lib/pghero/methods/kill.rb +4 -1
- data/lib/pghero/methods/maintenance.rb +14 -20
- data/lib/pghero/methods/queries.rb +7 -5
- data/lib/pghero/methods/query_stats.rb +264 -238
- data/lib/pghero/methods/replication.rb +9 -20
- data/lib/pghero/methods/sequences.rb +1 -1
- data/lib/pghero/methods/settings.rb +6 -11
- data/lib/pghero/methods/space.rb +11 -8
- data/lib/pghero/methods/suggested_indexes.rb +15 -16
- data/lib/pghero/methods/system.rb +6 -106
- data/lib/pghero/methods/tables.rb +7 -3
- data/lib/pghero/query.rb +8 -0
- data/lib/pghero/query_stats.rb +3 -0
- data/lib/pghero/version.rb +1 -1
- data/lib/pghero.rb +10 -42
- data/licenses/LICENSE-chart.js.txt +1 -1
- metadata +8 -8
- data/app/assets/javascripts/pghero/jquery.js +0 -10993
- data/lib/pghero/methods/users.rb +0 -95
- 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
|
-
|
|
6
|
+
<% if @running_queries.any? %>
|
|
7
|
+
<%= render partial: "live_queries_table", locals: {queries: @running_queries, vacuum_progress: @vacuum_progress} %>
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
<% if @kill_enabled %>
|
|
10
|
+
<p><%= button_to "Kill all connections", kill_all_path, class: "btn btn-danger" %></p>
|
|
9
11
|
|
|
10
|
-
|
|
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
|
|
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? &&
|
|
12
|
+
<% elsif @database.query_stats_table_exists? && !@database.queries_table_exists? %>
|
|
13
13
|
<div class="clear-both">
|
|
14
|
-
<p>
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
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://
|
|
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.
|
|
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
|
data/lib/pghero/database.rb
CHANGED
|
@@ -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
|
-
|
|
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"] ||
|
|
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,
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 || {}
|
data/lib/pghero/methods/basic.rb
CHANGED
|
@@ -36,17 +36,28 @@ module PgHero
|
|
|
36
36
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
|
-
def
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
144
|
-
select_all
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <=
|
|
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
|
-
|
|
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 >=
|
|
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
|
data/lib/pghero/methods/kill.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 #{
|
|
65
|
+
execute "ANALYZE #{"VERBOSE " if verbose}#{quote_table_name(table)}"
|
|
72
66
|
true
|
|
73
67
|
end
|
|
74
68
|
|