pg_reports 0.4.0 → 0.5.1
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 +130 -0
- data/README.md +170 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +348 -47
- data/app/views/layouts/pg_reports/application.html.erb +370 -79
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -1
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +254 -29
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
- data/app/views/pg_reports/dashboard/index.html.erb +485 -5
- data/config/locales/en.yml +45 -0
- data/config/locales/ru.yml +45 -0
- data/config/routes.rb +8 -0
- data/lib/pg_reports/configuration.rb +21 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
- data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
- data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
- data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
- data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
- data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
- data/lib/pg_reports/explain_analyzer.rb +338 -0
- data/lib/pg_reports/modules/schema_analysis.rb +4 -6
- data/lib/pg_reports/modules/system.rb +26 -3
- data/lib/pg_reports/query_monitor.rb +293 -0
- data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
- data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
- data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
- data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
- data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
- data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
- data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
- data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
- data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
- data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
- data/lib/pg_reports/sql/system/databases_list.sql +8 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +2 -0
- metadata +55 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module PgReports
|
|
8
|
+
class QueryMonitor
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
attr_reader :enabled, :session_id
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@enabled = false
|
|
15
|
+
@subscriber = nil
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@session_id = nil
|
|
18
|
+
@queries = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
if @enabled
|
|
24
|
+
return {success: false, message: "Monitoring already active"}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@session_id = SecureRandom.uuid
|
|
28
|
+
@queries = []
|
|
29
|
+
@enabled = true
|
|
30
|
+
|
|
31
|
+
# Subscribe to sql.active_record events
|
|
32
|
+
@subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |name, started, finished, unique_id, payload|
|
|
33
|
+
handle_sql_event(name, started, finished, unique_id, payload)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Write session start marker to file
|
|
37
|
+
write_session_marker("session_start")
|
|
38
|
+
|
|
39
|
+
{success: true, message: "Query monitoring started", session_id: @session_id}
|
|
40
|
+
end
|
|
41
|
+
rescue => e
|
|
42
|
+
@enabled = false
|
|
43
|
+
{success: false, error: e.message}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
unless @enabled
|
|
49
|
+
return {success: false, message: "Monitoring not active"}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Unsubscribe from notifications
|
|
53
|
+
if @subscriber
|
|
54
|
+
ActiveSupport::Notifications.unsubscribe(@subscriber)
|
|
55
|
+
@subscriber = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Write session end marker to file
|
|
59
|
+
write_session_marker("session_end")
|
|
60
|
+
|
|
61
|
+
# Flush queries to file
|
|
62
|
+
flush_to_file
|
|
63
|
+
|
|
64
|
+
@enabled = false
|
|
65
|
+
@queries = []
|
|
66
|
+
session_id = @session_id
|
|
67
|
+
@session_id = nil
|
|
68
|
+
|
|
69
|
+
{success: true, message: "Query monitoring stopped", session_id: session_id}
|
|
70
|
+
end
|
|
71
|
+
rescue => e
|
|
72
|
+
{success: false, error: e.message}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def status
|
|
76
|
+
{
|
|
77
|
+
enabled: @enabled,
|
|
78
|
+
session_id: @session_id,
|
|
79
|
+
query_count: @queries.size
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def queries(limit: nil, session_id: nil)
|
|
84
|
+
result = @queries.dup
|
|
85
|
+
|
|
86
|
+
# Filter by session_id if provided
|
|
87
|
+
if session_id
|
|
88
|
+
result = result.select { |q| q[:session_id] == session_id }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Limit results if requested
|
|
92
|
+
if limit
|
|
93
|
+
result = result.last(limit)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def load_from_log(session_id: nil, limit: nil)
|
|
100
|
+
return [] unless log_file_enabled?
|
|
101
|
+
return [] unless File.exist?(log_file_path)
|
|
102
|
+
|
|
103
|
+
queries = []
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
File.open(log_file_path, "r") do |f|
|
|
107
|
+
f.each_line do |line|
|
|
108
|
+
entry = JSON.parse(line.strip, symbolize_names: true)
|
|
109
|
+
next unless entry[:type] == "query"
|
|
110
|
+
|
|
111
|
+
# Filter by session_id if provided
|
|
112
|
+
next if session_id && entry[:session_id] != session_id
|
|
113
|
+
|
|
114
|
+
queries << entry
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
# Skip malformed lines
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Limit results if requested
|
|
122
|
+
queries = queries.last(limit) if limit
|
|
123
|
+
|
|
124
|
+
queries
|
|
125
|
+
rescue => e
|
|
126
|
+
Rails.logger.warn("PgReports: Failed to load queries from log: #{e.message}") if defined?(Rails)
|
|
127
|
+
[]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def handle_sql_event(name, started, finished, unique_id, payload)
|
|
134
|
+
return unless @enabled
|
|
135
|
+
|
|
136
|
+
# Skip if should be filtered
|
|
137
|
+
return if should_skip?(payload)
|
|
138
|
+
|
|
139
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
140
|
+
sql = payload[:sql]
|
|
141
|
+
query_name = payload[:name]
|
|
142
|
+
|
|
143
|
+
# Extract source location
|
|
144
|
+
source_location = extract_source_location
|
|
145
|
+
|
|
146
|
+
# Build query entry
|
|
147
|
+
query_entry = {
|
|
148
|
+
type: "query",
|
|
149
|
+
session_id: @session_id,
|
|
150
|
+
sql: sql,
|
|
151
|
+
duration_ms: duration_ms,
|
|
152
|
+
name: query_name,
|
|
153
|
+
source_location: source_location,
|
|
154
|
+
timestamp: Time.current.iso8601
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
add_to_buffer(query_entry)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def should_skip?(payload)
|
|
161
|
+
sql = payload[:sql]
|
|
162
|
+
name = payload[:name]
|
|
163
|
+
|
|
164
|
+
# Skip if query is from pg_reports itself (check by name)
|
|
165
|
+
return true if name&.start_with?("PgReports")
|
|
166
|
+
|
|
167
|
+
# Skip if query is from pg_reports gem (check backtrace)
|
|
168
|
+
if query_from_pg_reports?
|
|
169
|
+
return true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Skip SCHEMA queries
|
|
173
|
+
return true if name&.start_with?("SCHEMA")
|
|
174
|
+
|
|
175
|
+
# Skip CACHE queries
|
|
176
|
+
return true if name == "CACHE"
|
|
177
|
+
|
|
178
|
+
# Skip if cached
|
|
179
|
+
return true if payload[:cached]
|
|
180
|
+
|
|
181
|
+
# Skip EXPLAIN queries
|
|
182
|
+
return true if sql&.match?(/\bEXPLAIN\b/i)
|
|
183
|
+
|
|
184
|
+
# Skip DDL statements
|
|
185
|
+
return true if sql&.match?(/\b(CREATE|ALTER|DROP)\b/i)
|
|
186
|
+
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def query_from_pg_reports?
|
|
191
|
+
# Check if query originates from pg_reports gem internal code
|
|
192
|
+
locations = caller_locations(0, 30)
|
|
193
|
+
return false unless locations
|
|
194
|
+
|
|
195
|
+
locations.any? do |location|
|
|
196
|
+
path = location.path
|
|
197
|
+
# Exclude test paths
|
|
198
|
+
next if path.include?("/spec/")
|
|
199
|
+
|
|
200
|
+
# Filter queries from pg_reports internal modules only:
|
|
201
|
+
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
202
|
+
# - Local gem: /pg_reports/lib/pg_reports/modules/
|
|
203
|
+
# - Dashboard controller: /pg_reports/app/controllers/pg_reports/
|
|
204
|
+
path.match?(%r{/gems/pg_reports[-\d.]+/lib/}) ||
|
|
205
|
+
path.match?(%r{/pg_reports/lib/pg_reports/modules/}) ||
|
|
206
|
+
path.match?(%r{/pg_reports/app/controllers/pg_reports/dashboard_controller\.rb})
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def extract_source_location
|
|
211
|
+
# Get caller locations, skip first few frames (this file, active_support)
|
|
212
|
+
# Increase limit to 50 to capture more of the stack
|
|
213
|
+
locations = caller_locations(5, 50)
|
|
214
|
+
|
|
215
|
+
return nil unless locations
|
|
216
|
+
|
|
217
|
+
# Find first application code location
|
|
218
|
+
# Look for paths that are NOT from gems/ruby/railties
|
|
219
|
+
app_location = locations.find do |location|
|
|
220
|
+
path = location.path
|
|
221
|
+
|
|
222
|
+
# Skip framework and gem paths
|
|
223
|
+
next if path.match?(%r{/(gems|ruby|railties)/})
|
|
224
|
+
|
|
225
|
+
# Skip pg_reports internal paths
|
|
226
|
+
next if path.match?(%r{/pg_reports/lib/pg_reports/})
|
|
227
|
+
next if path.match?(%r{/pg_reports/app/controllers/pg_reports/})
|
|
228
|
+
|
|
229
|
+
# This is likely application code
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
return nil unless app_location
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
file: app_location.path,
|
|
237
|
+
line: app_location.lineno,
|
|
238
|
+
method: app_location.label
|
|
239
|
+
}
|
|
240
|
+
rescue
|
|
241
|
+
# If source extraction fails, return nil
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def add_to_buffer(query_entry)
|
|
246
|
+
@queries << query_entry
|
|
247
|
+
|
|
248
|
+
# Trim to max_queries to prevent memory bloat
|
|
249
|
+
max_queries = PgReports.config.query_monitor_max_queries
|
|
250
|
+
if @queries.size > max_queries
|
|
251
|
+
@queries = @queries.last(max_queries)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def write_session_marker(marker_type)
|
|
256
|
+
return unless log_file_enabled?
|
|
257
|
+
|
|
258
|
+
marker = {
|
|
259
|
+
type: marker_type,
|
|
260
|
+
session_id: @session_id,
|
|
261
|
+
timestamp: Time.current.iso8601
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
File.open(log_file_path, "a") do |f|
|
|
265
|
+
f.puts marker.to_json
|
|
266
|
+
end
|
|
267
|
+
rescue => e
|
|
268
|
+
# Silently fail - don't break monitoring if file write fails
|
|
269
|
+
Rails.logger.warn("PgReports: Failed to write session marker: #{e.message}")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def flush_to_file
|
|
273
|
+
return unless log_file_enabled?
|
|
274
|
+
return if @queries.empty?
|
|
275
|
+
|
|
276
|
+
File.open(log_file_path, "a") do |f|
|
|
277
|
+
@queries.each do |query|
|
|
278
|
+
f.puts query.to_json
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
rescue => e
|
|
282
|
+
Rails.logger.warn("PgReports: Failed to flush queries to file: #{e.message}")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def log_file_enabled?
|
|
286
|
+
PgReports.config.query_monitor_log_file.present?
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def log_file_path
|
|
290
|
+
PgReports.config.query_monitor_log_file
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- Connection Churn Analysis
|
|
2
|
+
-- Identifies applications with excessive connection turnover
|
|
3
|
+
|
|
4
|
+
WITH connection_ages AS (
|
|
5
|
+
SELECT
|
|
6
|
+
datname AS database,
|
|
7
|
+
COALESCE(application_name, 'unknown') AS application,
|
|
8
|
+
EXTRACT(EPOCH FROM (NOW() - backend_start)) AS connection_age_seconds
|
|
9
|
+
FROM pg_stat_activity
|
|
10
|
+
WHERE pid != pg_backend_pid()
|
|
11
|
+
AND datname IS NOT NULL
|
|
12
|
+
AND backend_type = 'client backend'
|
|
13
|
+
),
|
|
14
|
+
connection_stats AS (
|
|
15
|
+
SELECT
|
|
16
|
+
database,
|
|
17
|
+
application,
|
|
18
|
+
COUNT(*) AS total_connections,
|
|
19
|
+
ROUND(AVG(connection_age_seconds)::numeric, 2) AS avg_connection_age_seconds,
|
|
20
|
+
ROUND(MIN(connection_age_seconds)::numeric, 2) AS min_connection_age_seconds,
|
|
21
|
+
ROUND(MAX(connection_age_seconds)::numeric, 2) AS max_connection_age_seconds,
|
|
22
|
+
COUNT(*) FILTER (WHERE connection_age_seconds < 10) AS short_lived_connections
|
|
23
|
+
FROM connection_ages
|
|
24
|
+
GROUP BY database, application
|
|
25
|
+
)
|
|
26
|
+
SELECT
|
|
27
|
+
database,
|
|
28
|
+
application,
|
|
29
|
+
total_connections,
|
|
30
|
+
avg_connection_age_seconds,
|
|
31
|
+
min_connection_age_seconds,
|
|
32
|
+
max_connection_age_seconds,
|
|
33
|
+
short_lived_connections,
|
|
34
|
+
ROUND((short_lived_connections::numeric / total_connections::numeric) * 100, 2) AS churn_rate_pct
|
|
35
|
+
FROM connection_stats
|
|
36
|
+
WHERE total_connections > 1 -- Filter out single connections
|
|
37
|
+
ORDER BY churn_rate_pct DESC, short_lived_connections DESC;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
-- Connection Pool Saturation Analysis
|
|
2
|
+
-- Provides overall pool health metrics with warnings
|
|
3
|
+
|
|
4
|
+
WITH settings AS (
|
|
5
|
+
SELECT
|
|
6
|
+
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn,
|
|
7
|
+
(SELECT setting::int FROM pg_settings WHERE name = 'superuser_reserved_connections') AS reserved_conn
|
|
8
|
+
),
|
|
9
|
+
current_state AS (
|
|
10
|
+
SELECT
|
|
11
|
+
COUNT(*) AS total_connections,
|
|
12
|
+
COUNT(*) FILTER (WHERE state = 'active') AS active_conn,
|
|
13
|
+
COUNT(*) FILTER (WHERE state = 'idle') AS idle_conn,
|
|
14
|
+
COUNT(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_txn,
|
|
15
|
+
COUNT(*) FILTER (WHERE state = 'idle in transaction (aborted)') AS idle_in_txn_aborted,
|
|
16
|
+
COUNT(*) FILTER (WHERE wait_event IS NOT NULL) AS waiting_conn
|
|
17
|
+
FROM pg_stat_activity
|
|
18
|
+
WHERE pid != pg_backend_pid()
|
|
19
|
+
),
|
|
20
|
+
metrics AS (
|
|
21
|
+
SELECT
|
|
22
|
+
'Total Connections' AS metric,
|
|
23
|
+
cs.total_connections AS current_value,
|
|
24
|
+
s.max_conn - s.reserved_conn AS max_value,
|
|
25
|
+
ROUND((cs.total_connections::numeric / (s.max_conn - s.reserved_conn)::numeric) * 100, 2) AS utilization_pct
|
|
26
|
+
FROM current_state cs, settings s
|
|
27
|
+
|
|
28
|
+
UNION ALL
|
|
29
|
+
|
|
30
|
+
SELECT
|
|
31
|
+
'Active Connections',
|
|
32
|
+
cs.active_conn,
|
|
33
|
+
s.max_conn - s.reserved_conn,
|
|
34
|
+
ROUND((cs.active_conn::numeric / (s.max_conn - s.reserved_conn)::numeric) * 100, 2)
|
|
35
|
+
FROM current_state cs, settings s
|
|
36
|
+
|
|
37
|
+
UNION ALL
|
|
38
|
+
|
|
39
|
+
SELECT
|
|
40
|
+
'Idle Connections',
|
|
41
|
+
cs.idle_conn,
|
|
42
|
+
s.max_conn - s.reserved_conn,
|
|
43
|
+
ROUND((cs.idle_conn::numeric / (s.max_conn - s.reserved_conn)::numeric) * 100, 2)
|
|
44
|
+
FROM current_state cs, settings s
|
|
45
|
+
|
|
46
|
+
UNION ALL
|
|
47
|
+
|
|
48
|
+
SELECT
|
|
49
|
+
'Idle in Transaction',
|
|
50
|
+
cs.idle_in_txn,
|
|
51
|
+
s.max_conn / 4, -- Should be max 25% of pool
|
|
52
|
+
ROUND((cs.idle_in_txn::numeric / (s.max_conn / 4)::numeric) * 100, 2)
|
|
53
|
+
FROM current_state cs, settings s
|
|
54
|
+
|
|
55
|
+
UNION ALL
|
|
56
|
+
|
|
57
|
+
SELECT
|
|
58
|
+
'Waiting Connections',
|
|
59
|
+
cs.waiting_conn,
|
|
60
|
+
s.max_conn / 10, -- Should be max 10% of pool
|
|
61
|
+
ROUND((cs.waiting_conn::numeric / GREATEST(s.max_conn / 10, 1)::numeric) * 100, 2)
|
|
62
|
+
FROM current_state cs, settings s
|
|
63
|
+
)
|
|
64
|
+
SELECT
|
|
65
|
+
metric,
|
|
66
|
+
current_value,
|
|
67
|
+
max_value,
|
|
68
|
+
utilization_pct,
|
|
69
|
+
CASE
|
|
70
|
+
WHEN utilization_pct >= 85 THEN '🔴 Critical'
|
|
71
|
+
WHEN utilization_pct >= 70 THEN '🟡 Warning'
|
|
72
|
+
WHEN utilization_pct >= 50 THEN '🟡 Elevated'
|
|
73
|
+
ELSE '🟢 Normal'
|
|
74
|
+
END AS status,
|
|
75
|
+
CASE
|
|
76
|
+
WHEN metric = 'Total Connections' AND utilization_pct >= 85 THEN
|
|
77
|
+
'CRITICAL: Pool near exhaustion. Scale up max_connections or implement connection pooling immediately.'
|
|
78
|
+
WHEN metric = 'Total Connections' AND utilization_pct >= 70 THEN
|
|
79
|
+
'WARNING: High pool utilization. Consider increasing max_connections or adding connection pooler.'
|
|
80
|
+
WHEN metric = 'Idle in Transaction' AND utilization_pct >= 70 THEN
|
|
81
|
+
'WARNING: Too many idle in transaction connections. Review application transaction handling and set idle_in_transaction_session_timeout.'
|
|
82
|
+
WHEN metric = 'Waiting Connections' AND utilization_pct >= 70 THEN
|
|
83
|
+
'WARNING: Many connections waiting. Check for lock contention and long-running queries.'
|
|
84
|
+
WHEN metric = 'Active Connections' AND utilization_pct >= 85 THEN
|
|
85
|
+
'High active connection count. Monitor query performance and consider read replicas.'
|
|
86
|
+
ELSE
|
|
87
|
+
'Pool is healthy.'
|
|
88
|
+
END AS recommendation
|
|
89
|
+
FROM metrics
|
|
90
|
+
ORDER BY utilization_pct DESC;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Connection Pool Usage Statistics
|
|
2
|
+
-- Shows current pool utilization across databases
|
|
3
|
+
|
|
4
|
+
WITH connection_counts AS (
|
|
5
|
+
SELECT
|
|
6
|
+
COALESCE(datname, 'system') AS database,
|
|
7
|
+
COUNT(*) AS total_connections,
|
|
8
|
+
COUNT(*) FILTER (WHERE state = 'active') AS active_connections,
|
|
9
|
+
COUNT(*) FILTER (WHERE state = 'idle') AS idle_connections,
|
|
10
|
+
COUNT(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction
|
|
11
|
+
FROM pg_stat_activity
|
|
12
|
+
WHERE pid != pg_backend_pid()
|
|
13
|
+
GROUP BY datname
|
|
14
|
+
),
|
|
15
|
+
limits AS (
|
|
16
|
+
SELECT setting::int AS max_connections
|
|
17
|
+
FROM pg_settings
|
|
18
|
+
WHERE name = 'max_connections'
|
|
19
|
+
)
|
|
20
|
+
SELECT
|
|
21
|
+
cc.database,
|
|
22
|
+
cc.total_connections,
|
|
23
|
+
cc.active_connections,
|
|
24
|
+
cc.idle_connections,
|
|
25
|
+
cc.idle_in_transaction,
|
|
26
|
+
l.max_connections,
|
|
27
|
+
ROUND((cc.total_connections::numeric / l.max_connections::numeric) * 100, 2) AS utilization_pct,
|
|
28
|
+
l.max_connections - cc.total_connections AS available_connections
|
|
29
|
+
FROM connection_counts cc
|
|
30
|
+
CROSS JOIN limits l
|
|
31
|
+
ORDER BY cc.total_connections DESC;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Connection Pool Wait Time Analysis
|
|
2
|
+
-- Shows queries currently waiting for resources
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
pid,
|
|
6
|
+
datname AS database,
|
|
7
|
+
usename AS username,
|
|
8
|
+
wait_event_type,
|
|
9
|
+
wait_event,
|
|
10
|
+
state,
|
|
11
|
+
ROUND(EXTRACT(EPOCH FROM (NOW() - state_change))::numeric, 2) AS wait_duration_seconds,
|
|
12
|
+
state_change AS query_start,
|
|
13
|
+
LEFT(query, 500) AS query
|
|
14
|
+
FROM pg_stat_activity
|
|
15
|
+
WHERE wait_event IS NOT NULL
|
|
16
|
+
AND pid != pg_backend_pid()
|
|
17
|
+
AND datname IS NOT NULL
|
|
18
|
+
AND state != 'idle'
|
|
19
|
+
ORDER BY wait_duration_seconds DESC;
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
-- Requires pg_stat_statements extension
|
|
3
3
|
|
|
4
4
|
SELECT
|
|
5
|
-
query,
|
|
6
|
-
calls,
|
|
7
|
-
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
8
|
-
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
9
|
-
ROUND((min_exec_time)::numeric, 2) AS min_time_ms,
|
|
10
|
-
ROUND((max_exec_time)::numeric, 2) AS max_time_ms,
|
|
11
|
-
ROUND((stddev_exec_time)::numeric, 2) AS stddev_time_ms,
|
|
12
|
-
rows,
|
|
13
|
-
shared_blks_hit,
|
|
14
|
-
shared_blks_read,
|
|
15
|
-
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
16
|
-
FROM pg_stat_statements
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
s.query,
|
|
6
|
+
s.calls,
|
|
7
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
8
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
9
|
+
ROUND((s.min_exec_time)::numeric, 2) AS min_time_ms,
|
|
10
|
+
ROUND((s.max_exec_time)::numeric, 2) AS max_time_ms,
|
|
11
|
+
ROUND((s.stddev_exec_time)::numeric, 2) AS stddev_time_ms,
|
|
12
|
+
s.rows,
|
|
13
|
+
s.shared_blks_hit,
|
|
14
|
+
s.shared_blks_read,
|
|
15
|
+
ROUND((s.shared_blks_hit * 100.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
16
|
+
FROM pg_stat_statements s
|
|
17
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
18
|
+
WHERE s.calls > 0
|
|
19
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
20
|
+
AND d.datname = current_database()
|
|
21
|
+
ORDER BY s.total_exec_time DESC
|
|
20
22
|
LIMIT 200;
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
-- Requires pg_stat_statements extension
|
|
3
3
|
|
|
4
4
|
WITH total AS (
|
|
5
|
-
SELECT SUM(total_exec_time) AS total_time
|
|
6
|
-
FROM pg_stat_statements
|
|
7
|
-
|
|
5
|
+
SELECT SUM(s.total_exec_time) AS total_time
|
|
6
|
+
FROM pg_stat_statements s
|
|
7
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
8
|
+
WHERE s.calls > 0
|
|
9
|
+
AND d.datname = current_database()
|
|
8
10
|
)
|
|
9
11
|
SELECT
|
|
10
12
|
s.query,
|
|
@@ -13,10 +15,13 @@ SELECT
|
|
|
13
15
|
ROUND((s.total_exec_time * 100.0 / t.total_time)::numeric, 2) AS percent_of_total,
|
|
14
16
|
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
15
17
|
s.rows
|
|
16
|
-
FROM pg_stat_statements s
|
|
18
|
+
FROM pg_stat_statements s
|
|
19
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
20
|
+
CROSS JOIN total t
|
|
17
21
|
WHERE s.calls > 0
|
|
18
22
|
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
19
23
|
AND s.query NOT LIKE 'COMMIT%'
|
|
20
24
|
AND s.query NOT LIKE 'BEGIN%'
|
|
25
|
+
AND d.datname = current_database()
|
|
21
26
|
ORDER BY s.total_exec_time DESC
|
|
22
27
|
LIMIT 100;
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
-- Requires pg_stat_statements extension
|
|
3
3
|
|
|
4
4
|
SELECT
|
|
5
|
-
query,
|
|
6
|
-
calls,
|
|
7
|
-
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
8
|
-
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
9
|
-
rows,
|
|
10
|
-
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
-
FROM pg_stat_statements
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
AND query NOT LIKE '
|
|
15
|
-
AND query NOT LIKE '
|
|
16
|
-
|
|
5
|
+
s.query,
|
|
6
|
+
s.calls,
|
|
7
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
8
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
9
|
+
s.rows,
|
|
10
|
+
ROUND((s.shared_blks_hit * 100.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
+
FROM pg_stat_statements s
|
|
12
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
13
|
+
WHERE s.calls > 0
|
|
14
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
15
|
+
AND s.query NOT LIKE 'COMMIT%'
|
|
16
|
+
AND s.query NOT LIKE 'BEGIN%'
|
|
17
|
+
AND d.datname = current_database()
|
|
18
|
+
ORDER BY s.calls DESC
|
|
17
19
|
LIMIT 100;
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
-- Requires pg_stat_statements extension
|
|
3
3
|
|
|
4
4
|
SELECT
|
|
5
|
-
query,
|
|
6
|
-
calls,
|
|
7
|
-
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio,
|
|
8
|
-
shared_blks_hit,
|
|
9
|
-
shared_blks_read,
|
|
10
|
-
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
11
|
-
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms
|
|
12
|
-
FROM pg_stat_statements
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
AND
|
|
16
|
-
AND query NOT LIKE '
|
|
17
|
-
AND query NOT LIKE '
|
|
18
|
-
|
|
5
|
+
s.query,
|
|
6
|
+
s.calls,
|
|
7
|
+
ROUND((s.shared_blks_hit * 100.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio,
|
|
8
|
+
s.shared_blks_hit,
|
|
9
|
+
s.shared_blks_read,
|
|
10
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
11
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms
|
|
12
|
+
FROM pg_stat_statements s
|
|
13
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
14
|
+
WHERE s.calls > 10
|
|
15
|
+
AND (s.shared_blks_hit + s.shared_blks_read) > 0
|
|
16
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
17
|
+
AND s.query NOT LIKE 'COMMIT%'
|
|
18
|
+
AND s.query NOT LIKE 'BEGIN%'
|
|
19
|
+
AND d.datname = current_database()
|
|
20
|
+
ORDER BY (s.shared_blks_hit * 1.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0)) ASC
|
|
19
21
|
LIMIT 100;
|
|
@@ -3,23 +3,25 @@
|
|
|
3
3
|
-- Requires pg_stat_statements extension
|
|
4
4
|
|
|
5
5
|
SELECT
|
|
6
|
-
query,
|
|
7
|
-
calls,
|
|
8
|
-
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
-
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
10
|
-
rows,
|
|
6
|
+
s.query,
|
|
7
|
+
s.calls,
|
|
8
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
10
|
+
s.rows,
|
|
11
11
|
-- Heuristic: high rows examined per call may indicate missing index
|
|
12
|
-
ROUND((rows / NULLIF(calls, 0))::numeric, 0) AS rows_per_call,
|
|
12
|
+
ROUND((s.rows / NULLIF(s.calls, 0))::numeric, 0) AS rows_per_call,
|
|
13
13
|
-- High read/hit ratio suggests disk access (possible seq scan)
|
|
14
|
-
ROUND((shared_blks_read * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS disk_read_ratio
|
|
15
|
-
FROM pg_stat_statements
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
AND
|
|
19
|
-
AND
|
|
20
|
-
AND query NOT LIKE '
|
|
21
|
-
AND query NOT LIKE '
|
|
14
|
+
ROUND((s.shared_blks_read * 100.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0))::numeric, 2) AS disk_read_ratio
|
|
15
|
+
FROM pg_stat_statements s
|
|
16
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
17
|
+
WHERE s.calls > 10
|
|
18
|
+
AND (s.rows / NULLIF(s.calls, 0)) > 100
|
|
19
|
+
AND s.mean_exec_time > 10
|
|
20
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
21
|
+
AND s.query NOT LIKE 'COMMIT%'
|
|
22
|
+
AND s.query NOT LIKE 'BEGIN%'
|
|
22
23
|
-- Focus on SELECT statements
|
|
23
|
-
AND (query ILIKE 'SELECT%' OR query ILIKE '%WHERE%')
|
|
24
|
-
|
|
24
|
+
AND (s.query ILIKE 'SELECT%' OR s.query ILIKE '%WHERE%')
|
|
25
|
+
AND d.datname = current_database()
|
|
26
|
+
ORDER BY (s.rows / NULLIF(s.calls, 0)) * s.calls DESC
|
|
25
27
|
LIMIT 100;
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
-- Requires pg_stat_statements extension
|
|
3
3
|
|
|
4
4
|
SELECT
|
|
5
|
-
query,
|
|
6
|
-
calls,
|
|
7
|
-
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
8
|
-
ROUND((total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
-
ROUND((rows / NULLIF(calls, 0))::numeric, 2) AS rows_per_call,
|
|
10
|
-
ROUND((shared_blks_hit * 100.0 / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
-
FROM pg_stat_statements
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
AND query NOT LIKE '
|
|
15
|
-
AND query NOT LIKE '
|
|
16
|
-
|
|
5
|
+
s.query,
|
|
6
|
+
s.calls,
|
|
7
|
+
ROUND((s.mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
8
|
+
ROUND((s.total_exec_time)::numeric, 2) AS total_time_ms,
|
|
9
|
+
ROUND((s.rows / NULLIF(s.calls, 0))::numeric, 2) AS rows_per_call,
|
|
10
|
+
ROUND((s.shared_blks_hit * 100.0 / NULLIF(s.shared_blks_hit + s.shared_blks_read, 0))::numeric, 2) AS cache_hit_ratio
|
|
11
|
+
FROM pg_stat_statements s
|
|
12
|
+
JOIN pg_database d ON s.dbid = d.oid
|
|
13
|
+
WHERE s.calls > 0
|
|
14
|
+
AND s.query NOT LIKE '%pg_stat_statements%'
|
|
15
|
+
AND s.query NOT LIKE 'COMMIT%'
|
|
16
|
+
AND s.query NOT LIKE 'BEGIN%'
|
|
17
|
+
AND d.datname = current_database()
|
|
18
|
+
ORDER BY s.mean_exec_time DESC
|
|
17
19
|
LIMIT 100;
|