pg_peek 0.1.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 (120) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +217 -0
  4. data/Rakefile +16 -0
  5. data/app/assets/javascripts/pg_peek/application.js +147 -0
  6. data/app/assets/stylesheets/pg_peek/application.css +362 -0
  7. data/app/controllers/pg_peek/activity_controller.rb +16 -0
  8. data/app/controllers/pg_peek/application_controller.rb +36 -0
  9. data/app/controllers/pg_peek/databases_controller.rb +96 -0
  10. data/app/controllers/pg_peek/endpoints_controller.rb +10 -0
  11. data/app/controllers/pg_peek/indexes_controller.rb +7 -0
  12. data/app/controllers/pg_peek/jobs_controller.rb +44 -0
  13. data/app/controllers/pg_peek/pg_stat_statements_controller.rb +11 -0
  14. data/app/controllers/pg_peek/queries_controller.rb +13 -0
  15. data/app/controllers/pg_peek/tables_controller.rb +6 -0
  16. data/app/helpers/pg_peek/application_helper.rb +179 -0
  17. data/app/helpers/pg_peek/report_helper.rb +58 -0
  18. data/app/jobs/pg_peek/application_job.rb +4 -0
  19. data/app/mailers/pg_peek/application_mailer.rb +6 -0
  20. data/app/models/pg_peek/application_record.rb +3 -0
  21. data/app/models/pg_peek/database.rb +131 -0
  22. data/app/models/pg_peek/pg_stat_statements.rb +132 -0
  23. data/app/models/pg_peek/sql_comment.rb +25 -0
  24. data/app/models/pg_peek/table.rb +266 -0
  25. data/app/queries/pg_peek/pg14/activity/sessions.sql +26 -0
  26. data/app/queries/pg_peek/pg14/activity/summary.sql +8 -0
  27. data/app/queries/pg_peek/pg14/activity/waiting_locks.sql +11 -0
  28. data/app/queries/pg_peek/pg14/database/vitals.sql +11 -0
  29. data/app/queries/pg_peek/pg14/indexes/all.sql +17 -0
  30. data/app/queries/pg_peek/pg14/pg_stat_statements/endpoints.sql +43 -0
  31. data/app/queries/pg_peek/pg14/pg_stat_statements/jobs.sql +12 -0
  32. data/app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql +13 -0
  33. data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql +14 -0
  34. data/app/queries/pg_peek/pg14/tables/dead_tuples.sql +11 -0
  35. data/app/queries/pg_peek/pg14/tables/inline_stats.sql +9 -0
  36. data/app/queries/pg_peek/pg14/tables/stats.sql +32 -0
  37. data/app/queries/pg_peek/pg15/activity/sessions.sql +26 -0
  38. data/app/queries/pg_peek/pg15/activity/summary.sql +8 -0
  39. data/app/queries/pg_peek/pg15/activity/waiting_locks.sql +11 -0
  40. data/app/queries/pg_peek/pg15/database/vitals.sql +11 -0
  41. data/app/queries/pg_peek/pg15/indexes/all.sql +17 -0
  42. data/app/queries/pg_peek/pg15/pg_stat_statements/endpoints.sql +43 -0
  43. data/app/queries/pg_peek/pg15/pg_stat_statements/jobs.sql +12 -0
  44. data/app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql +13 -0
  45. data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql +14 -0
  46. data/app/queries/pg_peek/pg15/tables/dead_tuples.sql +11 -0
  47. data/app/queries/pg_peek/pg15/tables/inline_stats.sql +9 -0
  48. data/app/queries/pg_peek/pg15/tables/stats.sql +32 -0
  49. data/app/queries/pg_peek/pg16/activity/sessions.sql +26 -0
  50. data/app/queries/pg_peek/pg16/activity/summary.sql +8 -0
  51. data/app/queries/pg_peek/pg16/activity/waiting_locks.sql +11 -0
  52. data/app/queries/pg_peek/pg16/database/vitals.sql +11 -0
  53. data/app/queries/pg_peek/pg16/indexes/all.sql +17 -0
  54. data/app/queries/pg_peek/pg16/pg_stat_statements/endpoints.sql +43 -0
  55. data/app/queries/pg_peek/pg16/pg_stat_statements/jobs.sql +12 -0
  56. data/app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql +13 -0
  57. data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql +14 -0
  58. data/app/queries/pg_peek/pg16/tables/dead_tuples.sql +11 -0
  59. data/app/queries/pg_peek/pg16/tables/inline_stats.sql +9 -0
  60. data/app/queries/pg_peek/pg16/tables/stats.sql +32 -0
  61. data/app/queries/pg_peek/pg17/activity/sessions.sql +26 -0
  62. data/app/queries/pg_peek/pg17/activity/summary.sql +8 -0
  63. data/app/queries/pg_peek/pg17/activity/waiting_locks.sql +11 -0
  64. data/app/queries/pg_peek/pg17/database/vitals.sql +11 -0
  65. data/app/queries/pg_peek/pg17/indexes/all.sql +17 -0
  66. data/app/queries/pg_peek/pg17/pg_stat_statements/endpoints.sql +43 -0
  67. data/app/queries/pg_peek/pg17/pg_stat_statements/jobs.sql +12 -0
  68. data/app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql +13 -0
  69. data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql +14 -0
  70. data/app/queries/pg_peek/pg17/tables/dead_tuples.sql +11 -0
  71. data/app/queries/pg_peek/pg17/tables/inline_stats.sql +9 -0
  72. data/app/queries/pg_peek/pg17/tables/stats.sql +32 -0
  73. data/app/queries/pg_peek/pg18/activity/sessions.sql +26 -0
  74. data/app/queries/pg_peek/pg18/activity/summary.sql +8 -0
  75. data/app/queries/pg_peek/pg18/activity/waiting_locks.sql +11 -0
  76. data/app/queries/pg_peek/pg18/database/vitals.sql +11 -0
  77. data/app/queries/pg_peek/pg18/indexes/all.sql +17 -0
  78. data/app/queries/pg_peek/pg18/pg_stat_statements/endpoints.sql +43 -0
  79. data/app/queries/pg_peek/pg18/pg_stat_statements/jobs.sql +12 -0
  80. data/app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql +13 -0
  81. data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql +14 -0
  82. data/app/queries/pg_peek/pg18/tables/dead_tuples.sql +11 -0
  83. data/app/queries/pg_peek/pg18/tables/inline_stats.sql +9 -0
  84. data/app/queries/pg_peek/pg18/tables/stats.sql +32 -0
  85. data/app/reports/pg_peek/column.rb +27 -0
  86. data/app/reports/pg_peek/report.rb +63 -0
  87. data/app/reports/pg_peek/reports/blocked.rb +60 -0
  88. data/app/reports/pg_peek/reports/databases.rb +47 -0
  89. data/app/reports/pg_peek/reports/endpoints.rb +35 -0
  90. data/app/reports/pg_peek/reports/indexes.rb +44 -0
  91. data/app/reports/pg_peek/reports/job_queries.rb +14 -0
  92. data/app/reports/pg_peek/reports/jobs.rb +21 -0
  93. data/app/reports/pg_peek/reports/queries.rb +23 -0
  94. data/app/reports/pg_peek/reports/sessions.rb +88 -0
  95. data/app/reports/pg_peek/reports/tables.rb +25 -0
  96. data/app/views/layouts/pg_peek/application.html.erb +56 -0
  97. data/app/views/pg_peek/activity/show.html.erb +70 -0
  98. data/app/views/pg_peek/authentication_required.html.erb +31 -0
  99. data/app/views/pg_peek/databases/index.html.erb +43 -0
  100. data/app/views/pg_peek/databases/show.html.erb +169 -0
  101. data/app/views/pg_peek/endpoints/index.html.erb +24 -0
  102. data/app/views/pg_peek/indexes/index.html.erb +31 -0
  103. data/app/views/pg_peek/jobs/index.html.erb +51 -0
  104. data/app/views/pg_peek/jobs/show.html.erb +17 -0
  105. data/app/views/pg_peek/pg_stat_statements/_preload_instructions.html.erb +17 -0
  106. data/app/views/pg_peek/pg_stat_statements/not_preloaded.html.erb +5 -0
  107. data/app/views/pg_peek/queries/index.html.erb +10 -0
  108. data/app/views/pg_peek/reports/_report.html.erb +32 -0
  109. data/app/views/pg_peek/tables/show.html.erb +128 -0
  110. data/config/routes.rb +16 -0
  111. data/lib/generators/pg_peek/install/install_generator.rb +40 -0
  112. data/lib/generators/pg_peek/install/templates/README +31 -0
  113. data/lib/generators/pg_peek/install/templates/initializer.rb +38 -0
  114. data/lib/pg_peek/configuration.rb +59 -0
  115. data/lib/pg_peek/engine.rb +7 -0
  116. data/lib/pg_peek/query_loader.rb +59 -0
  117. data/lib/pg_peek/version.rb +3 -0
  118. data/lib/pg_peek.rb +10 -0
  119. data/lib/tasks/pg_peek_tasks.rake +4 -0
  120. metadata +192 -0
@@ -0,0 +1,266 @@
1
+ class PgPeek::Table
2
+ attr_reader :database, :name
3
+
4
+ STATS_RESET_QUERY = PgPeek::QueryLoader.mark("SELECT stats_reset FROM pg_stat_bgwriter").freeze
5
+
6
+ def self.inline_stats_for(database, table_names)
7
+ return {} if table_names.empty?
8
+
9
+ query = PgPeek::QueryLoader.load("tables/inline_stats",
10
+ pg_version: database.major_version,
11
+ count: table_names.size)
12
+
13
+ result = database.connection.exec_query(query, "PgPeek::Table InlineStats", table_names)
14
+ result.rows.each_with_object({}) do |row, hash|
15
+ table_name = row[0]
16
+ heap_blks_read = row[1].to_i
17
+ heap_blks_hit = row[2].to_i
18
+ idx_blks_read = row[3].to_i
19
+ idx_blks_hit = row[4].to_i
20
+
21
+ hash[table_name] = {
22
+ heap_hit_ratio: cache_hit_ratio(heap_blks_hit, heap_blks_read),
23
+ index_hit_ratio: cache_hit_ratio(idx_blks_hit, idx_blks_read)
24
+ }
25
+ end
26
+ end
27
+
28
+ def self.cache_hit_ratio(hits, reads)
29
+ total = hits + reads
30
+ return nil if total.zero?
31
+ (hits.to_f / total * 100).round(1)
32
+ end
33
+
34
+ def self.excluded?(table_name)
35
+ PgPeek.config.excluded_tables.any? do |pattern|
36
+ case pattern
37
+ when Regexp
38
+ pattern.match?(table_name)
39
+ when String
40
+ pattern == table_name
41
+ else
42
+ false
43
+ end
44
+ end
45
+ end
46
+
47
+ def initialize(database, name)
48
+ @database = database
49
+ @name = name
50
+ end
51
+
52
+ def stats
53
+ @stats ||= fetch_stats
54
+ end
55
+
56
+ def stats_reset_at
57
+ @stats_reset_at ||= begin
58
+ result = database.connection.execute(STATS_RESET_QUERY)
59
+ result.first&.dig("stats_reset")
60
+ end
61
+ end
62
+
63
+ def seconds_since_reset
64
+ return nil unless stats_reset_at
65
+ Time.current - stats_reset_at
66
+ end
67
+
68
+ # Cache metrics
69
+ def heap_cache_hit_ratio
70
+ self.class.cache_hit_ratio(heap_blks_hit, heap_blks_read)
71
+ end
72
+
73
+ def index_cache_hit_ratio
74
+ self.class.cache_hit_ratio(idx_blks_hit, idx_blks_read)
75
+ end
76
+
77
+ def toast_cache_hit_ratio
78
+ self.class.cache_hit_ratio(toast_blks_hit, toast_blks_read)
79
+ end
80
+
81
+ def toast_index_cache_hit_ratio
82
+ self.class.cache_hit_ratio(tidx_blks_hit, tidx_blks_read)
83
+ end
84
+
85
+ # Row stats
86
+ def seq_scan
87
+ stats["seq_scan"].to_i
88
+ end
89
+
90
+ def idx_scan
91
+ stats["idx_scan"].to_i
92
+ end
93
+
94
+ def total_scans
95
+ seq_scan + idx_scan
96
+ end
97
+
98
+ def seq_scan_percentage
99
+ return nil if total_scans.zero?
100
+ (seq_scan.to_f / total_scans * 100).round(1)
101
+ end
102
+
103
+ def idx_scan_percentage
104
+ return nil if total_scans.zero?
105
+ (idx_scan.to_f / total_scans * 100).round(1)
106
+ end
107
+
108
+ def index_usage_percentage
109
+ idx_scan_percentage
110
+ end
111
+
112
+ def seq_tup_read
113
+ stats["seq_tup_read"].to_i
114
+ end
115
+
116
+ def idx_tup_fetch
117
+ stats["idx_tup_fetch"].to_i
118
+ end
119
+
120
+ def rows_fetched
121
+ seq_tup_read + idx_tup_fetch
122
+ end
123
+
124
+ def rows_inserted
125
+ stats["n_tup_ins"].to_i
126
+ end
127
+
128
+ def rows_updated
129
+ stats["n_tup_upd"].to_i
130
+ end
131
+
132
+ def rows_deleted
133
+ stats["n_tup_del"].to_i
134
+ end
135
+
136
+ def live_tuples
137
+ stats["n_live_tup"].to_i
138
+ end
139
+
140
+ def dead_tuples
141
+ stats["n_dead_tup"].to_i
142
+ end
143
+
144
+ def dead_tuple_ratio
145
+ total = live_tuples + dead_tuples
146
+ return nil if total.zero?
147
+ (dead_tuples.to_f / total * 100).round(1)
148
+ end
149
+
150
+ def dead_tuple_warning?
151
+ ratio = dead_tuple_ratio
152
+ return false if ratio.nil?
153
+ ratio >= PgPeek.config.dead_tuple_warning_threshold
154
+ end
155
+
156
+ def row_estimate
157
+ estimate = stats["row_estimate"]
158
+ estimate.nil? ? 0 : estimate.to_i
159
+ end
160
+
161
+ # Vacuum/maintenance stats
162
+ def last_vacuum
163
+ stats["last_vacuum"]
164
+ end
165
+
166
+ def last_autovacuum
167
+ stats["last_autovacuum"]
168
+ end
169
+
170
+ def last_analyze
171
+ stats["last_analyze"]
172
+ end
173
+
174
+ def last_autoanalyze
175
+ stats["last_autoanalyze"]
176
+ end
177
+
178
+ def vacuum_count
179
+ stats["vacuum_count"].to_i
180
+ end
181
+
182
+ def autovacuum_count
183
+ stats["autovacuum_count"].to_i
184
+ end
185
+
186
+ def analyze_count
187
+ stats["analyze_count"].to_i
188
+ end
189
+
190
+ def autoanalyze_count
191
+ stats["autoanalyze_count"].to_i
192
+ end
193
+
194
+ # Cache block stats (raw)
195
+ def heap_blks_read
196
+ stats["heap_blks_read"].to_i
197
+ end
198
+
199
+ def heap_blks_hit
200
+ stats["heap_blks_hit"].to_i
201
+ end
202
+
203
+ def idx_blks_read
204
+ stats["idx_blks_read"].to_i
205
+ end
206
+
207
+ def idx_blks_hit
208
+ stats["idx_blks_hit"].to_i
209
+ end
210
+
211
+ def toast_blks_read
212
+ stats["toast_blks_read"].to_i
213
+ end
214
+
215
+ def toast_blks_hit
216
+ stats["toast_blks_hit"].to_i
217
+ end
218
+
219
+ def tidx_blks_read
220
+ stats["tidx_blks_read"].to_i
221
+ end
222
+
223
+ def tidx_blks_hit
224
+ stats["tidx_blks_hit"].to_i
225
+ end
226
+
227
+ private
228
+
229
+ def fetch_stats
230
+ query = PgPeek::QueryLoader.load("tables/stats", pg_version: database.major_version)
231
+ result = database.connection.exec_query(query, "PgPeek::Table Stats", [ name ])
232
+ result.first || empty_stats
233
+ end
234
+
235
+ def empty_stats
236
+ {
237
+ "table_name" => name,
238
+ "seq_scan" => 0,
239
+ "seq_tup_read" => 0,
240
+ "idx_scan" => 0,
241
+ "idx_tup_fetch" => 0,
242
+ "n_tup_ins" => 0,
243
+ "n_tup_upd" => 0,
244
+ "n_tup_del" => 0,
245
+ "n_live_tup" => 0,
246
+ "n_dead_tup" => 0,
247
+ "last_vacuum" => nil,
248
+ "last_autovacuum" => nil,
249
+ "last_analyze" => nil,
250
+ "last_autoanalyze" => nil,
251
+ "vacuum_count" => 0,
252
+ "autovacuum_count" => 0,
253
+ "analyze_count" => 0,
254
+ "autoanalyze_count" => 0,
255
+ "heap_blks_read" => 0,
256
+ "heap_blks_hit" => 0,
257
+ "idx_blks_read" => 0,
258
+ "idx_blks_hit" => 0,
259
+ "toast_blks_read" => 0,
260
+ "toast_blks_hit" => 0,
261
+ "tidx_blks_read" => 0,
262
+ "tidx_blks_hit" => 0,
263
+ "row_estimate" => 0
264
+ }
265
+ end
266
+ end
@@ -0,0 +1,26 @@
1
+ -- Client sessions on the current database, except the one asking. Every
2
+ -- duration is taken from one clock reading, so they agree with each other;
3
+ -- clock_timestamp() rather than now() because the reader may be inside a
4
+ -- long transaction of its own.
5
+ SELECT
6
+ pid,
7
+ usename,
8
+ application_name,
9
+ client_addr::text AS client_addr,
10
+ state,
11
+ wait_event_type,
12
+ wait_event,
13
+ backend_start,
14
+ xact_start,
15
+ query_start,
16
+ state_change,
17
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - query_start)))::bigint AS query_ms,
18
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - xact_start)))::bigint AS xact_ms,
19
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - state_change)))::bigint AS state_ms,
20
+ array_to_string(pg_blocking_pids(pid), ',') AS blocked_by,
21
+ query
22
+ FROM pg_stat_activity
23
+ WHERE datname = current_database()
24
+ AND backend_type = 'client backend'
25
+ AND pid <> pg_backend_pid()
26
+ ORDER BY pid
@@ -0,0 +1,8 @@
1
+ -- Server-wide connection use. max_connections is a cluster setting, so the
2
+ -- count spans every database. current_user is who pg_peek connects as, for
3
+ -- the hint about hidden query text.
4
+ SELECT
5
+ current_setting('max_connections')::int AS max_connections,
6
+ count(*) FILTER (WHERE backend_type = 'client backend') AS client_connections,
7
+ current_user AS username
8
+ FROM pg_stat_activity
@@ -0,0 +1,11 @@
1
+ -- What each waiting session is waiting for. A backend waits for at most one
2
+ -- lock at a time, so pid identifies the row. Relations from other databases
3
+ -- render as a bare oid, which the caller filters out by pid anyway.
4
+ SELECT
5
+ pid,
6
+ locktype,
7
+ mode,
8
+ CASE WHEN relation IS NOT NULL THEN relation::regclass::text END AS relation
9
+ FROM pg_locks
10
+ WHERE NOT granted
11
+ AND pid <> pg_backend_pid()
@@ -0,0 +1,11 @@
1
+ -- One row of health figures for the current database.
2
+ SELECT
3
+ round(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 1) AS cache_hit_ratio,
4
+ numbackends AS connections,
5
+ xact_commit,
6
+ xact_rollback,
7
+ deadlocks,
8
+ temp_files,
9
+ stats_reset
10
+ FROM pg_stat_database
11
+ WHERE datname = current_database()
@@ -0,0 +1,17 @@
1
+ -- Every index on a user table, with how often it has been scanned since the
2
+ -- statistics were last reset. Constraint indexes are marked so a zero scan
3
+ -- count is not mistaken for waste: they earn their keep on writes.
4
+ SELECT
5
+ s.relname AS table_name,
6
+ s.indexrelname AS index_name,
7
+ s.idx_scan AS scans,
8
+ s.idx_tup_read AS tuples_read,
9
+ pg_relation_size(s.indexrelid) AS size_bytes,
10
+ pg_size_pretty(pg_relation_size(s.indexrelid)) AS size,
11
+ i.indisprimary AS primary_key,
12
+ i.indisunique AS unique_index,
13
+ pg_get_indexdef(s.indexrelid) AS definition
14
+ FROM pg_stat_user_indexes s
15
+ JOIN pg_index i ON i.indexrelid = s.indexrelid
16
+ WHERE s.schemaname = ANY (current_schemas(false))
17
+ ORDER BY pg_relation_size(s.indexrelid) DESC, s.relname, s.indexrelname
@@ -0,0 +1,43 @@
1
+ -- Database cost per controller#action, from SQLcommenter tags.
2
+ --
3
+ -- Catalog queries are excluded throughout: Rails' schema introspection runs
4
+ -- once when a model loads and is tagged with whichever endpoint happened to
5
+ -- trigger it, so it both inflates that endpoint's cost and, sitting at calls=1
6
+ -- forever, destroys the min(calls) request-count estimate below.
7
+ WITH application_queries AS (
8
+ SELECT
9
+ COALESCE(
10
+ substring(query from 'namespaced_controller=''([^'']+)'''),
11
+ substring(query from 'controller=''([^'']+)''')
12
+ ) AS controller,
13
+ substring(query from 'action=''([^'']+)''') AS action,
14
+ calls, total_exec_time, rows
15
+ FROM pg_stat_statements
16
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
17
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
18
+ AND query NOT LIKE '%/* pg_peek */%'
19
+ AND query ~ 'controller=''[^'']+'''
20
+ AND query ~ 'action=''[^'']+'''
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ),
24
+ requests AS (
25
+ -- Some query runs exactly once per request, so the smallest call count for an
26
+ -- endpoint approximates how many times it was served.
27
+ SELECT controller, action, min(calls) AS request_count
28
+ FROM application_queries
29
+ GROUP BY controller, action
30
+ )
31
+ SELECT
32
+ q.controller || '#' || q.action AS endpoint,
33
+ SUM(q.total_exec_time) AS total_exec_time_ms,
34
+ SUM(q.calls) AS total_calls,
35
+ COUNT(*) AS query_count,
36
+ SUM(q.rows) AS total_rows,
37
+ r.request_count,
38
+ ROUND((MAX(q.calls)::numeric / NULLIF(r.request_count, 0)), 1) AS max_queries_per_request
39
+ FROM application_queries q
40
+ JOIN requests r ON r.controller = q.controller AND r.action = q.action
41
+ GROUP BY q.controller, q.action, r.request_count
42
+ ORDER BY total_exec_time_ms DESC
43
+ LIMIT {{limit}}
@@ -0,0 +1,12 @@
1
+ SELECT
2
+ substring(query from 'job=''([^'']+)''') AS job_class,
3
+ SUM(calls) AS total_calls,
4
+ SUM(total_exec_time) AS total_exec_time_ms,
5
+ COUNT(*) AS query_count
6
+ FROM pg_stat_statements
7
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
8
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
9
+ AND query NOT LIKE '%/* pg_peek */%'
10
+ AND query ~ 'job=''[^'']+'''
11
+ GROUP BY job_class
12
+ ORDER BY total_exec_time_ms DESC
@@ -0,0 +1,13 @@
1
+ SELECT
2
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
3
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
5
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
6
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
+ query
8
+ FROM pg_stat_statements
9
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
10
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
11
+ AND query NOT LIKE '%/* pg_peek */%'
12
+ ORDER BY total_exec_time DESC
13
+ LIMIT {{limit}}
@@ -0,0 +1,14 @@
1
+ SELECT
2
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
3
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
5
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
6
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
+ query
8
+ FROM pg_stat_statements
9
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
10
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
11
+ AND query NOT LIKE '%/* pg_peek */%'
12
+ AND query LIKE {{job_pattern}}
13
+ ORDER BY total_exec_time DESC
14
+ LIMIT {{limit}}
@@ -0,0 +1,11 @@
1
+ -- Tables whose dead-tuple share is at or above the warning threshold.
2
+ SELECT
3
+ relname,
4
+ n_live_tup,
5
+ n_dead_tup,
6
+ round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
+ last_autovacuum
8
+ FROM pg_stat_user_tables
9
+ WHERE n_live_tup + n_dead_tup > 0
10
+ AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
11
+ ORDER BY dead_ratio DESC
@@ -0,0 +1,9 @@
1
+ SELECT
2
+ t.relname AS table_name,
3
+ s.heap_blks_read,
4
+ s.heap_blks_hit,
5
+ s.idx_blks_read,
6
+ s.idx_blks_hit
7
+ FROM pg_stat_user_tables t
8
+ LEFT JOIN pg_statio_user_tables s ON t.relid = s.relid
9
+ WHERE t.relname IN ({{placeholders:count}})
@@ -0,0 +1,32 @@
1
+ SELECT
2
+ t.relname AS table_name,
3
+ t.seq_scan,
4
+ t.seq_tup_read,
5
+ t.idx_scan,
6
+ t.idx_tup_fetch,
7
+ t.n_tup_ins,
8
+ t.n_tup_upd,
9
+ t.n_tup_del,
10
+ t.n_live_tup,
11
+ t.n_dead_tup,
12
+ t.last_vacuum,
13
+ t.last_autovacuum,
14
+ t.last_analyze,
15
+ t.last_autoanalyze,
16
+ t.vacuum_count,
17
+ t.autovacuum_count,
18
+ t.analyze_count,
19
+ t.autoanalyze_count,
20
+ s.heap_blks_read,
21
+ s.heap_blks_hit,
22
+ s.idx_blks_read,
23
+ s.idx_blks_hit,
24
+ s.toast_blks_read,
25
+ s.toast_blks_hit,
26
+ s.tidx_blks_read,
27
+ s.tidx_blks_hit,
28
+ c.reltuples AS row_estimate
29
+ FROM pg_stat_user_tables t
30
+ LEFT JOIN pg_statio_user_tables s ON t.relid = s.relid
31
+ LEFT JOIN pg_class c ON t.relid = c.oid
32
+ WHERE t.relname = $1
@@ -0,0 +1,26 @@
1
+ -- Client sessions on the current database, except the one asking. Every
2
+ -- duration is taken from one clock reading, so they agree with each other;
3
+ -- clock_timestamp() rather than now() because the reader may be inside a
4
+ -- long transaction of its own.
5
+ SELECT
6
+ pid,
7
+ usename,
8
+ application_name,
9
+ client_addr::text AS client_addr,
10
+ state,
11
+ wait_event_type,
12
+ wait_event,
13
+ backend_start,
14
+ xact_start,
15
+ query_start,
16
+ state_change,
17
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - query_start)))::bigint AS query_ms,
18
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - xact_start)))::bigint AS xact_ms,
19
+ round(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - state_change)))::bigint AS state_ms,
20
+ array_to_string(pg_blocking_pids(pid), ',') AS blocked_by,
21
+ query
22
+ FROM pg_stat_activity
23
+ WHERE datname = current_database()
24
+ AND backend_type = 'client backend'
25
+ AND pid <> pg_backend_pid()
26
+ ORDER BY pid
@@ -0,0 +1,8 @@
1
+ -- Server-wide connection use. max_connections is a cluster setting, so the
2
+ -- count spans every database. current_user is who pg_peek connects as, for
3
+ -- the hint about hidden query text.
4
+ SELECT
5
+ current_setting('max_connections')::int AS max_connections,
6
+ count(*) FILTER (WHERE backend_type = 'client backend') AS client_connections,
7
+ current_user AS username
8
+ FROM pg_stat_activity
@@ -0,0 +1,11 @@
1
+ -- What each waiting session is waiting for. A backend waits for at most one
2
+ -- lock at a time, so pid identifies the row. Relations from other databases
3
+ -- render as a bare oid, which the caller filters out by pid anyway.
4
+ SELECT
5
+ pid,
6
+ locktype,
7
+ mode,
8
+ CASE WHEN relation IS NOT NULL THEN relation::regclass::text END AS relation
9
+ FROM pg_locks
10
+ WHERE NOT granted
11
+ AND pid <> pg_backend_pid()
@@ -0,0 +1,11 @@
1
+ -- One row of health figures for the current database.
2
+ SELECT
3
+ round(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 1) AS cache_hit_ratio,
4
+ numbackends AS connections,
5
+ xact_commit,
6
+ xact_rollback,
7
+ deadlocks,
8
+ temp_files,
9
+ stats_reset
10
+ FROM pg_stat_database
11
+ WHERE datname = current_database()
@@ -0,0 +1,17 @@
1
+ -- Every index on a user table, with how often it has been scanned since the
2
+ -- statistics were last reset. Constraint indexes are marked so a zero scan
3
+ -- count is not mistaken for waste: they earn their keep on writes.
4
+ SELECT
5
+ s.relname AS table_name,
6
+ s.indexrelname AS index_name,
7
+ s.idx_scan AS scans,
8
+ s.idx_tup_read AS tuples_read,
9
+ pg_relation_size(s.indexrelid) AS size_bytes,
10
+ pg_size_pretty(pg_relation_size(s.indexrelid)) AS size,
11
+ i.indisprimary AS primary_key,
12
+ i.indisunique AS unique_index,
13
+ pg_get_indexdef(s.indexrelid) AS definition
14
+ FROM pg_stat_user_indexes s
15
+ JOIN pg_index i ON i.indexrelid = s.indexrelid
16
+ WHERE s.schemaname = ANY (current_schemas(false))
17
+ ORDER BY pg_relation_size(s.indexrelid) DESC, s.relname, s.indexrelname
@@ -0,0 +1,43 @@
1
+ -- Database cost per controller#action, from SQLcommenter tags.
2
+ --
3
+ -- Catalog queries are excluded throughout: Rails' schema introspection runs
4
+ -- once when a model loads and is tagged with whichever endpoint happened to
5
+ -- trigger it, so it both inflates that endpoint's cost and, sitting at calls=1
6
+ -- forever, destroys the min(calls) request-count estimate below.
7
+ WITH application_queries AS (
8
+ SELECT
9
+ COALESCE(
10
+ substring(query from 'namespaced_controller=''([^'']+)'''),
11
+ substring(query from 'controller=''([^'']+)''')
12
+ ) AS controller,
13
+ substring(query from 'action=''([^'']+)''') AS action,
14
+ calls, total_exec_time, rows
15
+ FROM pg_stat_statements
16
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
17
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
18
+ AND query NOT LIKE '%/* pg_peek */%'
19
+ AND query ~ 'controller=''[^'']+'''
20
+ AND query ~ 'action=''[^'']+'''
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ),
24
+ requests AS (
25
+ -- Some query runs exactly once per request, so the smallest call count for an
26
+ -- endpoint approximates how many times it was served.
27
+ SELECT controller, action, min(calls) AS request_count
28
+ FROM application_queries
29
+ GROUP BY controller, action
30
+ )
31
+ SELECT
32
+ q.controller || '#' || q.action AS endpoint,
33
+ SUM(q.total_exec_time) AS total_exec_time_ms,
34
+ SUM(q.calls) AS total_calls,
35
+ COUNT(*) AS query_count,
36
+ SUM(q.rows) AS total_rows,
37
+ r.request_count,
38
+ ROUND((MAX(q.calls)::numeric / NULLIF(r.request_count, 0)), 1) AS max_queries_per_request
39
+ FROM application_queries q
40
+ JOIN requests r ON r.controller = q.controller AND r.action = q.action
41
+ GROUP BY q.controller, q.action, r.request_count
42
+ ORDER BY total_exec_time_ms DESC
43
+ LIMIT {{limit}}
@@ -0,0 +1,12 @@
1
+ SELECT
2
+ substring(query from 'job=''([^'']+)''') AS job_class,
3
+ SUM(calls) AS total_calls,
4
+ SUM(total_exec_time) AS total_exec_time_ms,
5
+ COUNT(*) AS query_count
6
+ FROM pg_stat_statements
7
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
8
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
9
+ AND query NOT LIKE '%/* pg_peek */%'
10
+ AND query ~ 'job=''[^'']+'''
11
+ GROUP BY job_class
12
+ ORDER BY total_exec_time_ms DESC
@@ -0,0 +1,13 @@
1
+ SELECT
2
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
3
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
5
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
6
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
+ query
8
+ FROM pg_stat_statements
9
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
10
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
11
+ AND query NOT LIKE '%/* pg_peek */%'
12
+ ORDER BY total_exec_time DESC
13
+ LIMIT {{limit}}
@@ -0,0 +1,14 @@
1
+ SELECT
2
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
3
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
5
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
6
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
+ query
8
+ FROM pg_stat_statements
9
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
10
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
11
+ AND query NOT LIKE '%/* pg_peek */%'
12
+ AND query LIKE {{job_pattern}}
13
+ ORDER BY total_exec_time DESC
14
+ LIMIT {{limit}}
@@ -0,0 +1,11 @@
1
+ -- Tables whose dead-tuple share is at or above the warning threshold.
2
+ SELECT
3
+ relname,
4
+ n_live_tup,
5
+ n_dead_tup,
6
+ round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
+ last_autovacuum
8
+ FROM pg_stat_user_tables
9
+ WHERE n_live_tup + n_dead_tup > 0
10
+ AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
11
+ ORDER BY dead_ratio DESC
@@ -0,0 +1,9 @@
1
+ SELECT
2
+ t.relname AS table_name,
3
+ s.heap_blks_read,
4
+ s.heap_blks_hit,
5
+ s.idx_blks_read,
6
+ s.idx_blks_hit
7
+ FROM pg_stat_user_tables t
8
+ LEFT JOIN pg_statio_user_tables s ON t.relid = s.relid
9
+ WHERE t.relname IN ({{placeholders:count}})