pghero_fork 2.7.3
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 +7 -0
- data/CHANGELOG.md +391 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +3 -0
- data/app/assets/images/pghero/favicon.png +0 -0
- data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
- data/app/assets/javascripts/pghero/application.js +158 -0
- data/app/assets/javascripts/pghero/chartkick.js +2436 -0
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/javascripts/pghero/jquery.js +10872 -0
- data/app/assets/javascripts/pghero/nouislider.js +2672 -0
- data/app/assets/stylesheets/pghero/application.css +514 -0
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/assets/stylesheets/pghero/nouislider.css +310 -0
- data/app/controllers/pg_hero/home_controller.rb +449 -0
- data/app/helpers/pg_hero/home_helper.rb +30 -0
- data/app/views/layouts/pg_hero/application.html.erb +68 -0
- data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
- data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
- data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
- data/app/views/pg_hero/home/connections.html.erb +32 -0
- data/app/views/pg_hero/home/explain.html.erb +27 -0
- data/app/views/pg_hero/home/index.html.erb +518 -0
- data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
- data/app/views/pg_hero/home/live_queries.html.erb +11 -0
- data/app/views/pg_hero/home/maintenance.html.erb +55 -0
- data/app/views/pg_hero/home/queries.html.erb +33 -0
- data/app/views/pg_hero/home/relation_space.html.erb +14 -0
- data/app/views/pg_hero/home/show_query.html.erb +106 -0
- data/app/views/pg_hero/home/space.html.erb +83 -0
- data/app/views/pg_hero/home/system.html.erb +34 -0
- data/app/views/pg_hero/home/tune.html.erb +53 -0
- data/config/routes.rb +32 -0
- data/lib/generators/pghero/config_generator.rb +13 -0
- data/lib/generators/pghero/query_stats_generator.rb +18 -0
- data/lib/generators/pghero/space_stats_generator.rb +18 -0
- data/lib/generators/pghero/templates/config.yml.tt +46 -0
- data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
- data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
- data/lib/pghero.rb +246 -0
- data/lib/pghero/connection.rb +5 -0
- data/lib/pghero/database.rb +175 -0
- data/lib/pghero/engine.rb +16 -0
- data/lib/pghero/methods/basic.rb +160 -0
- data/lib/pghero/methods/connections.rb +77 -0
- data/lib/pghero/methods/constraints.rb +30 -0
- data/lib/pghero/methods/explain.rb +29 -0
- data/lib/pghero/methods/indexes.rb +332 -0
- data/lib/pghero/methods/kill.rb +28 -0
- data/lib/pghero/methods/maintenance.rb +93 -0
- data/lib/pghero/methods/queries.rb +75 -0
- data/lib/pghero/methods/query_stats.rb +349 -0
- data/lib/pghero/methods/replication.rb +74 -0
- data/lib/pghero/methods/sequences.rb +124 -0
- data/lib/pghero/methods/settings.rb +37 -0
- data/lib/pghero/methods/space.rb +141 -0
- data/lib/pghero/methods/suggested_indexes.rb +329 -0
- data/lib/pghero/methods/system.rb +287 -0
- data/lib/pghero/methods/tables.rb +68 -0
- data/lib/pghero/methods/users.rb +87 -0
- data/lib/pghero/query_stats.rb +5 -0
- data/lib/pghero/space_stats.rb +5 -0
- data/lib/pghero/stats.rb +6 -0
- data/lib/pghero/version.rb +3 -0
- data/lib/tasks/pghero.rake +27 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-nouislider.txt +21 -0
- metadata +130 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Kill
|
4
|
+
def kill(pid)
|
5
|
+
select_one("SELECT pg_terminate_backend(#{pid.to_i})")
|
6
|
+
end
|
7
|
+
|
8
|
+
def kill_long_running_queries(min_duration: nil)
|
9
|
+
running_queries(min_duration: min_duration || long_running_query_sec).each { |query| kill(query[:pid]) }
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def kill_all
|
14
|
+
select_all <<-SQL
|
15
|
+
SELECT
|
16
|
+
pg_terminate_backend(pid)
|
17
|
+
FROM
|
18
|
+
pg_stat_activity
|
19
|
+
WHERE
|
20
|
+
pid <> pg_backend_pid()
|
21
|
+
AND query <> '<insufficient privilege>'
|
22
|
+
AND datname = current_database()
|
23
|
+
SQL
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Maintenance
|
4
|
+
# https://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
|
5
|
+
# "the system will shut down and refuse to start any new transactions
|
6
|
+
# once there are fewer than 1 million transactions left until wraparound"
|
7
|
+
# warn when 10,000,000 transactions left
|
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
|
13
|
+
SELECT
|
14
|
+
n.nspname AS schema,
|
15
|
+
c.relname AS table,
|
16
|
+
#{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_left
|
17
|
+
FROM
|
18
|
+
pg_class c
|
19
|
+
INNER JOIN
|
20
|
+
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
21
|
+
LEFT JOIN
|
22
|
+
pg_class t ON c.reltoastrelid = t.oid
|
23
|
+
WHERE
|
24
|
+
c.relkind = 'r'
|
25
|
+
AND (#{quote(max_value)} - GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid))) < #{quote(threshold)}
|
26
|
+
ORDER BY
|
27
|
+
3, 1, 2
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
|
31
|
+
def autovacuum_danger
|
32
|
+
max_value = select_one("SHOW autovacuum_freeze_max_age").to_i
|
33
|
+
transaction_id_danger(threshold: 2000000, max_value: max_value)
|
34
|
+
end
|
35
|
+
|
36
|
+
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
|
50
|
+
end
|
51
|
+
|
52
|
+
def maintenance_info
|
53
|
+
select_all <<-SQL
|
54
|
+
SELECT
|
55
|
+
schemaname AS schema,
|
56
|
+
relname AS table,
|
57
|
+
last_vacuum,
|
58
|
+
last_autovacuum,
|
59
|
+
last_analyze,
|
60
|
+
last_autoanalyze,
|
61
|
+
n_dead_tup AS dead_rows,
|
62
|
+
n_live_tup AS live_rows
|
63
|
+
FROM
|
64
|
+
pg_stat_user_tables
|
65
|
+
ORDER BY
|
66
|
+
1, 2
|
67
|
+
SQL
|
68
|
+
end
|
69
|
+
|
70
|
+
def analyze(table, verbose: false)
|
71
|
+
execute "ANALYZE #{verbose ? "VERBOSE " : ""}#{quote_table_name(table)}"
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def analyze_tables(verbose: false, min_size: nil, tables: nil)
|
76
|
+
tables = table_stats(table: tables).reject { |s| %w(information_schema pg_catalog).include?(s[:schema]) }
|
77
|
+
tables = tables.select { |s| s[:size_bytes] > min_size } if min_size
|
78
|
+
tables.map { |s| s.slice(:schema, :table) }.each do |stats|
|
79
|
+
begin
|
80
|
+
with_transaction(lock_timeout: 5000, statement_timeout: 120000) do
|
81
|
+
analyze "#{stats[:schema]}.#{stats[:table]}", verbose: verbose
|
82
|
+
end
|
83
|
+
success = true
|
84
|
+
rescue ActiveRecord::StatementInvalid => e
|
85
|
+
$stderr.puts e.message
|
86
|
+
success = false
|
87
|
+
end
|
88
|
+
stats[:success] = success
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module Queries
|
4
|
+
def running_queries(min_duration: nil, all: false)
|
5
|
+
query = <<-SQL
|
6
|
+
SELECT
|
7
|
+
pid,
|
8
|
+
state,
|
9
|
+
application_name AS source,
|
10
|
+
age(NOW(), COALESCE(query_start, xact_start)) AS duration,
|
11
|
+
#{server_version_num >= 90600 ? "(wait_event IS NOT NULL) AS waiting" : "waiting"},
|
12
|
+
query,
|
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
|
+
usename AS user
|
16
|
+
FROM
|
17
|
+
pg_stat_activity
|
18
|
+
WHERE
|
19
|
+
state <> 'idle'
|
20
|
+
AND pid <> pg_backend_pid()
|
21
|
+
AND datname = current_database()
|
22
|
+
#{min_duration ? "AND NOW() - COALESCE(query_start, xact_start) > interval '#{min_duration.to_i} seconds'" : nil}
|
23
|
+
#{all ? nil : "AND query <> '<insufficient privilege>'"}
|
24
|
+
ORDER BY
|
25
|
+
COALESCE(query_start, xact_start) DESC
|
26
|
+
SQL
|
27
|
+
|
28
|
+
select_all(query, query_columns: [:query])
|
29
|
+
end
|
30
|
+
|
31
|
+
def long_running_queries
|
32
|
+
running_queries(min_duration: long_running_query_sec)
|
33
|
+
end
|
34
|
+
|
35
|
+
# from https://wiki.postgresql.org/wiki/Lock_Monitoring
|
36
|
+
# and https://big-elephants.com/2013-09/exploring-query-locks-in-postgres/
|
37
|
+
def blocked_queries
|
38
|
+
query = <<-SQL
|
39
|
+
SELECT
|
40
|
+
COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
|
41
|
+
blockeda.pid AS blocked_pid,
|
42
|
+
blockeda.usename AS blocked_user,
|
43
|
+
blockeda.query as blocked_query,
|
44
|
+
age(now(), blockeda.query_start) AS blocked_duration,
|
45
|
+
blockedl.mode as blocked_mode,
|
46
|
+
blockinga.pid AS blocking_pid,
|
47
|
+
blockinga.usename AS blocking_user,
|
48
|
+
blockinga.state AS state_of_blocking_process,
|
49
|
+
blockinga.query AS current_or_recent_query_in_blocking_process,
|
50
|
+
age(now(), blockinga.query_start) AS blocking_duration,
|
51
|
+
blockingl.mode as blocking_mode
|
52
|
+
FROM
|
53
|
+
pg_catalog.pg_locks blockedl
|
54
|
+
LEFT JOIN
|
55
|
+
pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
|
56
|
+
LEFT JOIN
|
57
|
+
pg_catalog.pg_locks blockingl ON blockedl.pid != blockingl.pid AND (
|
58
|
+
blockingl.transactionid = blockedl.transactionid
|
59
|
+
OR (blockingl.relation = blockedl.relation AND blockingl.locktype = blockedl.locktype)
|
60
|
+
)
|
61
|
+
LEFT JOIN
|
62
|
+
pg_stat_activity blockinga ON blockingl.pid = blockinga.pid AND blockinga.datid = blockeda.datid
|
63
|
+
WHERE
|
64
|
+
NOT blockedl.granted
|
65
|
+
AND blockeda.query <> '<insufficient privilege>'
|
66
|
+
AND blockeda.datname = current_database()
|
67
|
+
ORDER BY
|
68
|
+
blocked_duration DESC
|
69
|
+
SQL
|
70
|
+
|
71
|
+
select_all(query, query_columns: [:blocked_query, :current_or_recent_query_in_blocking_process])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
module PgHero
|
2
|
+
module Methods
|
3
|
+
module QueryStats
|
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
|
+
|
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
|
+
|
11
|
+
# add percentages
|
12
|
+
all_queries_total_minutes = [current_query_stats, historical_query_stats].sum { |s| (s.first || {})[:all_queries_total_minutes] || 0 }
|
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
|
16
|
+
end
|
17
|
+
|
18
|
+
sort = options[:sort] || "total_minutes"
|
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
|
+
end
|
23
|
+
if min_calls
|
24
|
+
query_stats.reject! { |q| q[:calls] < min_calls }
|
25
|
+
end
|
26
|
+
query_stats
|
27
|
+
end
|
28
|
+
|
29
|
+
def query_stats_available?
|
30
|
+
select_one("SELECT COUNT(*) AS count FROM pg_available_extensions WHERE name = 'pg_stat_statements'") > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
# only cache if true
|
34
|
+
def query_stats_enabled?
|
35
|
+
@query_stats_enabled ||= query_stats_readable?
|
36
|
+
end
|
37
|
+
|
38
|
+
def query_stats_extension_enabled?
|
39
|
+
select_one("SELECT COUNT(*) AS count FROM pg_extension WHERE extname = 'pg_stat_statements'") > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def query_stats_readable?
|
43
|
+
select_all("SELECT * FROM pg_stat_statements LIMIT 1")
|
44
|
+
true
|
45
|
+
rescue ActiveRecord::StatementInvalid
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def enable_query_stats
|
50
|
+
execute("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def disable_query_stats
|
55
|
+
execute("DROP EXTENSION IF EXISTS pg_stat_statements")
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO scope by database in PgHero 3.0
|
60
|
+
# (add database: database_name to options)
|
61
|
+
def reset_query_stats(**options)
|
62
|
+
reset_instance_query_stats(**options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# resets query stats for the entire instance
|
66
|
+
# it's possible to reset stats for a specific
|
67
|
+
# database, user or query hash in Postgres 12+
|
68
|
+
def reset_instance_query_stats(database: nil, user: nil, query_hash: nil, raise_errors: false)
|
69
|
+
if database || user || query_hash
|
70
|
+
raise PgHero::Error, "Requires PostgreSQL 12+" if server_version_num < 120000
|
71
|
+
|
72
|
+
if database
|
73
|
+
database_id = execute("SELECT oid FROM pg_database WHERE datname = #{quote(database)}").first.try(:[], "oid")
|
74
|
+
raise PgHero::Error, "Database not found: #{database}" unless database_id
|
75
|
+
else
|
76
|
+
database_id = 0
|
77
|
+
end
|
78
|
+
|
79
|
+
if user
|
80
|
+
user_id = execute("SELECT usesysid FROM pg_user WHERE usename = #{quote(user)}").first.try(:[], "usesysid")
|
81
|
+
raise PgHero::Error, "User not found: #{user}" unless user_id
|
82
|
+
else
|
83
|
+
user_id = 0
|
84
|
+
end
|
85
|
+
|
86
|
+
if query_hash
|
87
|
+
query_id = query_hash.to_i
|
88
|
+
# may not be needed
|
89
|
+
# but not intuitive that all query hashes are reset with 0
|
90
|
+
raise PgHero::Error, "Invalid query hash: #{query_hash}" if query_id == 0
|
91
|
+
else
|
92
|
+
query_id = 0
|
93
|
+
end
|
94
|
+
|
95
|
+
execute("SELECT pg_stat_statements_reset(#{quote(user_id.to_i)}, #{quote(database_id.to_i)}, #{quote(query_id.to_i)})")
|
96
|
+
else
|
97
|
+
execute("SELECT pg_stat_statements_reset()")
|
98
|
+
end
|
99
|
+
true
|
100
|
+
rescue ActiveRecord::StatementInvalid => e
|
101
|
+
raise e if raise_errors
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
# https://stackoverflow.com/questions/20582500/how-to-check-if-a-table-exists-in-a-given-schema
|
106
|
+
def historical_query_stats_enabled?
|
107
|
+
# TODO use schema from config
|
108
|
+
# make sure primary database is PostgreSQL first
|
109
|
+
query_stats_table_exists? && capture_query_stats? && !missing_query_stats_columns.any?
|
110
|
+
end
|
111
|
+
|
112
|
+
def query_stats_table_exists?
|
113
|
+
table_exists?("pghero_query_stats")
|
114
|
+
end
|
115
|
+
|
116
|
+
def missing_query_stats_columns
|
117
|
+
%w(query_hash user) - PgHero::QueryStats.column_names
|
118
|
+
end
|
119
|
+
|
120
|
+
def supports_query_hash?
|
121
|
+
server_version_num >= 90400
|
122
|
+
end
|
123
|
+
|
124
|
+
# resetting query stats will reset across the entire Postgres instance
|
125
|
+
# this is problematic if multiple PgHero databases use the same Postgres instance
|
126
|
+
#
|
127
|
+
# to get around this, we capture queries for every Postgres database before we
|
128
|
+
# reset query stats for the Postgres instance with the `capture_query_stats` option
|
129
|
+
def capture_query_stats(raise_errors: false)
|
130
|
+
return if config["capture_query_stats"] && config["capture_query_stats"] != true
|
131
|
+
|
132
|
+
# get all databases that use same query stats and build mapping
|
133
|
+
mapping = {id => database_name}
|
134
|
+
PgHero.databases.select { |_, d| d.config["capture_query_stats"] == id }.each do |_, d|
|
135
|
+
mapping[d.id] = d.database_name
|
136
|
+
end
|
137
|
+
|
138
|
+
now = Time.now
|
139
|
+
|
140
|
+
query_stats = {}
|
141
|
+
mapping.each do |database_id, database_name|
|
142
|
+
query_stats[database_id] = query_stats(limit: 1000000, database: database_name)
|
143
|
+
end
|
144
|
+
|
145
|
+
query_stats = query_stats.select { |_, v| v.any? }
|
146
|
+
|
147
|
+
# nothing to do
|
148
|
+
return if query_stats.empty?
|
149
|
+
|
150
|
+
# use mapping, not query stats here
|
151
|
+
# TODO add option for this, and make default in PgHero 3.0
|
152
|
+
if false # mapping.size == 1 && server_version_num >= 120000
|
153
|
+
query_stats.each do |db_id, db_query_stats|
|
154
|
+
if reset_query_stats(database: mapping[db_id], raise_errors: raise_errors)
|
155
|
+
insert_query_stats(db_id, db_query_stats, now)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
else
|
159
|
+
if reset_query_stats(raise_errors: raise_errors)
|
160
|
+
query_stats.each do |db_id, db_query_stats|
|
161
|
+
insert_query_stats(db_id, db_query_stats, now)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def clean_query_stats
|
168
|
+
PgHero::QueryStats.where(database: id).where("captured_at < ?", 14.days.ago).delete_all
|
169
|
+
end
|
170
|
+
|
171
|
+
def slow_queries(query_stats: nil, **options)
|
172
|
+
query_stats ||= self.query_stats(options)
|
173
|
+
query_stats.select { |q| q[:calls].to_i >= slow_query_calls.to_i && q[:average_time].to_f >= slow_query_ms.to_f }
|
174
|
+
end
|
175
|
+
|
176
|
+
def query_hash_stats(query_hash, user: nil)
|
177
|
+
if historical_query_stats_enabled? && supports_query_hash?
|
178
|
+
start_at = 24.hours.ago
|
179
|
+
select_all_stats <<-SQL
|
180
|
+
SELECT
|
181
|
+
captured_at,
|
182
|
+
total_time / 1000 / 60 AS total_minutes,
|
183
|
+
(total_time / calls) AS average_time,
|
184
|
+
calls,
|
185
|
+
(SELECT regexp_matches(query, '.*/\\*(.+?)\\*/'))[1] AS origin
|
186
|
+
FROM
|
187
|
+
pghero_query_stats
|
188
|
+
WHERE
|
189
|
+
database = #{quote(id)}
|
190
|
+
AND captured_at >= #{quote(start_at)}
|
191
|
+
AND query_hash = #{quote(query_hash)}
|
192
|
+
#{user ? "AND \"user\" = #{quote(user)}" : ""}
|
193
|
+
ORDER BY
|
194
|
+
1 ASC
|
195
|
+
SQL
|
196
|
+
else
|
197
|
+
raise NotEnabled, "Query hash stats not enabled"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
# http://www.craigkerstiens.com/2013/01/10/more-on-postgres-performance/
|
204
|
+
def current_query_stats(limit: nil, sort: nil, database: nil, query_hash: nil)
|
205
|
+
if query_stats_enabled?
|
206
|
+
limit ||= 100
|
207
|
+
sort ||= "total_minutes"
|
208
|
+
total_time = server_version_num >= 130000 ? "(total_plan_time + total_exec_time)" : "total_time"
|
209
|
+
query = <<-SQL
|
210
|
+
WITH query_stats AS (
|
211
|
+
SELECT
|
212
|
+
LEFT(query, 10000) AS query,
|
213
|
+
#{supports_query_hash? ? "queryid" : "md5(query)"} AS query_hash,
|
214
|
+
rolname AS user,
|
215
|
+
(#{total_time} / 1000 / 60) AS total_minutes,
|
216
|
+
(#{total_time} / calls) AS average_time,
|
217
|
+
calls
|
218
|
+
FROM
|
219
|
+
pg_stat_statements
|
220
|
+
INNER JOIN
|
221
|
+
pg_database ON pg_database.oid = pg_stat_statements.dbid
|
222
|
+
INNER JOIN
|
223
|
+
pg_roles ON pg_roles.oid = pg_stat_statements.userid
|
224
|
+
WHERE
|
225
|
+
calls > 0 AND
|
226
|
+
pg_database.datname = #{database ? quote(database) : "current_database()"}
|
227
|
+
#{query_hash ? "AND queryid = #{quote(query_hash)}" : nil}
|
228
|
+
)
|
229
|
+
SELECT
|
230
|
+
query,
|
231
|
+
query_hash,
|
232
|
+
query_stats.user,
|
233
|
+
total_minutes,
|
234
|
+
average_time,
|
235
|
+
calls,
|
236
|
+
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
|
237
|
+
(SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
|
238
|
+
FROM
|
239
|
+
query_stats
|
240
|
+
ORDER BY
|
241
|
+
#{quote_table_name(sort)} DESC
|
242
|
+
LIMIT #{limit.to_i}
|
243
|
+
SQL
|
244
|
+
|
245
|
+
# we may be able to skip query_columns
|
246
|
+
# in more recent versions of Postgres
|
247
|
+
# as pg_stat_statements should be already normalized
|
248
|
+
select_all(query, query_columns: [:query])
|
249
|
+
else
|
250
|
+
raise NotEnabled, "Query stats not enabled"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def historical_query_stats(sort: nil, start_at: nil, end_at: nil, query_hash: nil)
|
255
|
+
if historical_query_stats_enabled?
|
256
|
+
sort ||= "total_minutes"
|
257
|
+
query = <<-SQL
|
258
|
+
WITH query_stats AS (
|
259
|
+
SELECT
|
260
|
+
#{supports_query_hash? ? "query_hash" : "md5(query)"} AS query_hash,
|
261
|
+
pghero_query_stats.user AS user,
|
262
|
+
array_agg(LEFT(query, 10000) ORDER BY REPLACE(LEFT(query, 1000), '?', '!') COLLATE "C" ASC) AS query,
|
263
|
+
(SUM(total_time) / 1000 / 60) AS total_minutes,
|
264
|
+
(SUM(total_time) / SUM(calls)) AS average_time,
|
265
|
+
SUM(calls) AS calls
|
266
|
+
FROM
|
267
|
+
pghero_query_stats
|
268
|
+
WHERE
|
269
|
+
database = #{quote(id)}
|
270
|
+
#{supports_query_hash? ? "AND query_hash IS NOT NULL" : ""}
|
271
|
+
#{start_at ? "AND captured_at >= #{quote(start_at)}" : ""}
|
272
|
+
#{end_at ? "AND captured_at <= #{quote(end_at)}" : ""}
|
273
|
+
#{query_hash ? "AND query_hash = #{quote(query_hash)}" : ""}
|
274
|
+
GROUP BY
|
275
|
+
1, 2
|
276
|
+
)
|
277
|
+
SELECT
|
278
|
+
query_hash,
|
279
|
+
query_stats.user,
|
280
|
+
query[1] AS query,
|
281
|
+
query[array_length(query, 1)] AS explainable_query,
|
282
|
+
total_minutes,
|
283
|
+
average_time,
|
284
|
+
calls,
|
285
|
+
total_minutes * 100.0 / (SELECT SUM(total_minutes) FROM query_stats) AS total_percent,
|
286
|
+
(SELECT SUM(total_minutes) FROM query_stats) AS all_queries_total_minutes
|
287
|
+
FROM
|
288
|
+
query_stats
|
289
|
+
ORDER BY
|
290
|
+
#{quote_table_name(sort)} DESC
|
291
|
+
LIMIT 100
|
292
|
+
SQL
|
293
|
+
|
294
|
+
# we can skip query_columns if all stored data is normalized
|
295
|
+
# for now, assume it's not
|
296
|
+
select_all_stats(query, query_columns: [:query, :explainable_query])
|
297
|
+
else
|
298
|
+
raise NotEnabled, "Historical query stats not enabled"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def combine_query_stats(grouped_stats)
|
303
|
+
query_stats = []
|
304
|
+
grouped_stats.each do |_, stats2|
|
305
|
+
value = {
|
306
|
+
query: (stats2.find { |s| s[:query] } || {})[:query],
|
307
|
+
user: (stats2.find { |s| s[:user] } || {})[:user],
|
308
|
+
query_hash: (stats2.find { |s| s[:query_hash] } || {})[:query_hash],
|
309
|
+
total_minutes: stats2.sum { |s| s[:total_minutes] },
|
310
|
+
calls: stats2.sum { |s| s[:calls] }.to_i,
|
311
|
+
all_queries_total_minutes: stats2.sum { |s| s[:all_queries_total_minutes] }
|
312
|
+
}
|
313
|
+
value[:total_percent] = value[:total_minutes] * 100.0 / value[:all_queries_total_minutes]
|
314
|
+
value[:explainable_query] = stats2.map { |s| s[:explainable_query] }.select { |q| q && explainable?(q) }.first
|
315
|
+
query_stats << value
|
316
|
+
end
|
317
|
+
query_stats
|
318
|
+
end
|
319
|
+
|
320
|
+
def explainable?(query)
|
321
|
+
query =~ /select/i && !query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i
|
322
|
+
end
|
323
|
+
|
324
|
+
# removes comments
|
325
|
+
# combines ?, ?, ? => ?
|
326
|
+
def normalize_query(query)
|
327
|
+
squish(query.to_s.gsub(/\?(, ?\?)+/, "?").gsub(/\/\*.+?\*\//, ""))
|
328
|
+
end
|
329
|
+
|
330
|
+
def insert_query_stats(db_id, db_query_stats, now)
|
331
|
+
values =
|
332
|
+
db_query_stats.map do |qs|
|
333
|
+
[
|
334
|
+
db_id,
|
335
|
+
qs[:query],
|
336
|
+
qs[:total_minutes] * 60 * 1000,
|
337
|
+
qs[:calls],
|
338
|
+
now,
|
339
|
+
supports_query_hash? ? qs[:query_hash] : nil,
|
340
|
+
qs[:user]
|
341
|
+
]
|
342
|
+
end
|
343
|
+
|
344
|
+
columns = %w[database query total_time calls captured_at query_hash user]
|
345
|
+
insert_stats("pghero_query_stats", columns, values)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|