rails-pg-extras 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00bbc417cc97e72d3815a72a4411d3ce44216259011f27f661d92bd433673731
4
- data.tar.gz: 9fc774e5852ce88293dd0350783ac17f4c426777e9a1a87b31cd350eb3368c9e
3
+ metadata.gz: 7d015c34f17329bb7986f95030416c916c6fbbe017d417f41ef5989f2c6d3540
4
+ data.tar.gz: fdd19bc750196bf2d1943ec2c4136c5075736fb21d3e4bed1e13c3a6aa0156bb
5
5
  SHA512:
6
- metadata.gz: 00a4ae363c93f55fed8d03fab9f8f558cff3967d010434fd20e712f4145a9d56feb6afdf4269b3cb567673592daaf60daf22e6ea929198a78bf44a4f6844a6ab
7
- data.tar.gz: a64be50d53b081ff70db4512f4ea14ffe09bc0c73c18b62f38af5d9909d030c577c2176686bcb24d4ffa694431fd8ccfb9189b770a1cc547990c1d83483f038d
6
+ metadata.gz: e77d66aa4817bafc1abfa034327ba4463df0fce00199ea7728acd4284ecace4fbdef8d0b3754ff5136c2f083cec18cec2a13b4c95547ea959ca37d5e86a34bde
7
+ data.tar.gz: abe72eeffe9ea727d91e01581129ff66664b40aa5448630a50b31edf06c2114c4904f8bfdc7149ea37b51bc234f950f849585c31f9cab1a62195bda97ab8ddd3
data/README.md CHANGED
@@ -1 +1,324 @@
1
1
  # Rails PG Extras
2
+
3
+ Inspired by (a shameless ripoff of) [Heroku PG Extras](https://github.com/heroku/heroku-pg-extras). The goal of this project is to provide a powerful insights into PostgreSQL database for Ruby on Rails apps that are not using the default Heroku PostgreSQL plugin.
4
+
5
+ Included rake tasks and Ruby methods can be used to obtain information about a Postgres instance, that may be useful when analyzing performance issues. This includes information about locks, index usage, buffer cache hit ratios and vacuum statistics. Ruby API enables developers to easily integrate the tool into e.g. automatic monitoring tasks.
6
+
7
+ ### Installation
8
+
9
+ In your Gemfile
10
+
11
+ ```ruby
12
+ gem 'rails-pg-extras'
13
+ ```
14
+
15
+ ### Usage
16
+
17
+ Each command can be used as a rake task, or a directly from the Ruby code.
18
+
19
+ #### `cache_hit`
20
+
21
+ ```bash
22
+ $ rake pg_extras:cache_hit
23
+ name | ratio
24
+ ----------------+------------------------
25
+ index hit rate | 0.99957765013541945832
26
+ table hit rate | 1.00
27
+ (2 rows)
28
+ ```
29
+
30
+ This command provides information on the efficiency of the buffer cache, for both index reads (`index hit rate`) as well as table reads (`table hit rate`). A low buffer cache hit ratio can be a sign that the Postgres instance is too small for the workload.
31
+
32
+ #### `index_usage`
33
+
34
+ ```
35
+ $ rake pg_extras:index_usage
36
+ relname | percent_of_times_index_used | rows_in_table
37
+ ---------------------+-----------------------------+---------------
38
+ events | 65 | 1217347
39
+ app_infos | 74 | 314057
40
+ app_infos_user_info | 0 | 198848
41
+ user_info | 5 | 94545
42
+ delayed_jobs | 27 | 0
43
+ (5 rows)
44
+ ```
45
+
46
+ This command provides information on the efficiency of indexes, represented as what percentage of total scans were index scans. A low percentage can indicate under indexing, or wrong data being indexed.
47
+
48
+ ### `locks`
49
+
50
+ ```
51
+ $ rake pg_extras:locks
52
+ procpid | relname | transactionid | granted | query_snippet | age
53
+ ---------+---------+---------------+---------+-----------------------+-----------------
54
+ 31776 | | | t | <IDLE> in transaction | 00:19:29.837898
55
+ 31776 | | 1294 | t | <IDLE> in transaction | 00:19:29.837898
56
+ 31912 | | | t | select * from hello; | 00:19:17.94259
57
+ 3443 | | | t | +| 00:00:00
58
+ | | | | select +|
59
+ | | | | pg_stat_activi |
60
+ (4 rows)
61
+ ```
62
+
63
+ This command displays queries that have taken out an exlusive lock on a relation. Exclusive locks typically prevent other operations on that relation from taking place, and can be a cause of "hung" queries that are waiting for a lock to be granted.
64
+
65
+ ### `pg:outliers`
66
+
67
+ ```
68
+ $ rake pg_extras:outliers
69
+ qry | exec_time | prop_exec_time | ncalls | sync_io_time
70
+ -----------------------------------------+------------------+----------------+-------------+--------------
71
+ SELECT * FROM archivable_usage_events.. | 154:39:26.431466 | 72.2% | 34,211,877 | 00:00:00
72
+ COPY public.archivable_usage_events (.. | 50:38:33.198418 | 23.6% | 13 | 13:34:21.00108
73
+ COPY public.usage_events (id, reporte.. | 02:32:16.335233 | 1.2% | 13 | 00:34:19.784318
74
+ INSERT INTO usage_events (id, retaine.. | 01:42:59.436532 | 0.8% | 12,328,187 | 00:00:00
75
+ SELECT * FROM usage_events WHERE (alp.. | 01:18:10.754354 | 0.6% | 102,114,301 | 00:00:00
76
+ UPDATE usage_events SET reporter_id =.. | 00:52:35.683254 | 0.4% | 23,786,348 | 00:00:00
77
+ INSERT INTO usage_events (id, retaine.. | 00:49:24.952561 | 0.4% | 21,988,201 | 00:00:00
78
+ COPY public.app_ownership_events (id,.. | 00:37:14.31082 | 0.3% | 13 | 00:12:32.584754
79
+ INSERT INTO app_ownership_events (id,.. | 00:26:59.808212 | 0.2% | 383,109 | 00:00:00
80
+ SELECT * FROM app_ownership_events .. | 00:19:06.021846 | 0.1% | 744,879 | 00:00:00
81
+ (10 rows)
82
+ ```
83
+
84
+ This command displays statements, obtained from `pg_stat_statements`, ordered by the amount of time to execute in aggregate. This includes the statement itself, the total execution time for that statement, the proportion of total execution time for all statements that statement has taken up, the number of times that statement has been called, and the amount of time that statement spent on synchronous I/O (reading/writing from the filesystem).
85
+
86
+ Typically, an efficient query will have an appropriate ratio of calls to total execution time, with as little time spent on I/O as possible. Queries that have a high total execution time but low call count should be investigated to improve their performance. Queries that have a high proportion of execution time being spent on synchronous I/O should also be investigated.
87
+
88
+ ### `calls`
89
+
90
+ ```
91
+ $ rake pg_extras:calls
92
+ qry | exec_time | prop_exec_time | ncalls | sync_io_time
93
+ -----------------------------------------+------------------+----------------+-------------+--------------
94
+ SELECT * FROM usage_events WHERE (alp.. | 01:18:11.073333 | 0.6% | 102,120,780 | 00:00:00
95
+ BEGIN | 00:00:51.285988 | 0.0% | 47,288,662 | 00:00:00
96
+ COMMIT | 00:00:52.31724 | 0.0% | 47,288,615 | 00:00:00
97
+ SELECT * FROM archivable_usage_event.. | 154:39:26.431466 | 72.2% | 34,211,877 | 00:00:00
98
+ UPDATE usage_events SET reporter_id =.. | 00:52:35.986167 | 0.4% | 23,788,388 | 00:00:00
99
+ INSERT INTO usage_events (id, retaine.. | 00:49:25.260245 | 0.4% | 21,990,326 | 00:00:00
100
+ INSERT INTO usage_events (id, retaine.. | 01:42:59.436532 | 0.8% | 12,328,187 | 00:00:00
101
+ SELECT * FROM app_ownership_events .. | 00:19:06.289521 | 0.1% | 744,976 | 00:00:00
102
+ INSERT INTO app_ownership_events(id, .. | 00:26:59.885631 | 0.2% | 383,153 | 00:00:00
103
+ UPDATE app_ownership_events SET app_i.. | 00:01:22.282337 | 0.0% | 359,741 | 00:00:00
104
+ (10 rows)
105
+ ```
106
+
107
+ This command is much like `pg:outliers`, but ordered by the number of times a statement has been called.
108
+
109
+ ### `blocking`
110
+
111
+ ```
112
+ $ rake pg_extras:blocking
113
+ blocked_pid | blocking_statement | blocking_duration | blocking_pid | blocked_statement | blocked_duration
114
+ -------------+--------------------------+-------------------+--------------+------------------------------------------------------------------------------------+------------------
115
+ 461 | select count(*) from app | 00:00:03.838314 | 15682 | UPDATE "app" SET "updated_at" = '2013-03-04 15:07:04.746688' WHERE "id" = 12823149 | 00:00:03.821826
116
+ (1 row)
117
+ ```
118
+
119
+ This command displays statements that are currently holding locks that other statements are waiting to be released. This can be used in conjunction with `pg:locks` to determine which statements need to be terminated in order to resolve lock contention.
120
+
121
+ #### `total_index_size`
122
+
123
+ ```
124
+ $ rake pg_extras:total_index_size
125
+ size
126
+ -------
127
+ 28194 MB
128
+ (1 row)
129
+ ```
130
+
131
+ This command displays the total size of all indexes on the database, in MB. It is calculated by taking the number of pages (reported in `relpages`) and multiplying it by the page size (8192 bytes).
132
+
133
+ ### `index_size`
134
+
135
+ ```
136
+ $ rake pg_extras:index_size
137
+ name | size
138
+ ---------------------------------------------------------------+---------
139
+ idx_activity_attemptable_and_type_lesson_enrollment | 5196 MB
140
+ index_enrollment_attemptables_by_attempt_and_last_in_group | 4045 MB
141
+ index_attempts_on_student_id | 2611 MB
142
+ enrollment_activity_attemptables_pkey | 2513 MB
143
+ index_attempts_on_student_id_final_attemptable_type | 2466 MB
144
+ attempts_pkey | 2466 MB
145
+ index_attempts_on_response_id | 2404 MB
146
+ index_attempts_on_enrollment_id | 1957 MB
147
+ index_enrollment_attemptables_by_enrollment_activity_id | 1789 MB
148
+ enrollment_activities_pkey | 458 MB
149
+ index_enrollment_activities_by_lesson_enrollment_and_activity | 402 MB
150
+ index_placement_attempts_on_response_id | 109 MB
151
+ index_placement_attempts_on_placement_test_id | 108 MB
152
+ index_placement_attempts_on_grade_level_id | 97 MB
153
+ index_lesson_enrollments_on_lesson_id | 93 MB
154
+ (truncated results for brevity)
155
+ ```
156
+
157
+ This command displays the size of each each index in the database, in MB. It is calculated by taking the number of pages (reported in `relpages`) and multiplying it by the page size (8192 bytes).
158
+
159
+ ### `table_size`
160
+
161
+ ```
162
+ $ rake pg_extras:table_size
163
+ name | size
164
+ ---------------------------------------------------------------+---------
165
+ learning_coaches | 196 MB
166
+ states | 145 MB
167
+ grade_levels | 111 MB
168
+ charities_customers | 73 MB
169
+ charities | 66 MB
170
+ (truncated results for brevity)
171
+ ```
172
+
173
+ This command displays the size of each table in the database, in MB. It is calculated by using the system administration function `pg_table_size()`, which includes the size of the main data fork, free space map, visibility map and TOAST data.
174
+
175
+ ### `table_indexes_size`
176
+
177
+ ```
178
+ $ rake pg_extras:table-indexes-size
179
+ table | indexes_size
180
+ ---------------------------------------------------------------+--------------
181
+ learning_coaches | 153 MB
182
+ states | 125 MB
183
+ charities_customers | 93 MB
184
+ charities | 16 MB
185
+ grade_levels | 11 MB
186
+ (truncated results for brevity)
187
+ ```
188
+
189
+ This command displays the total size of indexes for each table, in MB. It is calcualtes by using the system administration function `pg_indexes_size()`.
190
+
191
+ ### `total_table_size`
192
+
193
+ ```
194
+ $ rake pg_extras:total_table_size
195
+ name | size
196
+ ---------------------------------------------------------------+---------
197
+ learning_coaches | 349 MB
198
+ states | 270 MB
199
+ charities_customers | 166 MB
200
+ grade_levels | 122 MB
201
+ charities | 82 MB
202
+ (truncated results for brevity)
203
+ ```
204
+
205
+ This command displays the total size of each table in the database, in MB. It is calculated by using the system administration function `pg_total_relation_size()`, which includes table size, total index size and TOAST data.
206
+
207
+ ### `unused_indexes`
208
+
209
+ ```
210
+ $ rake pg_extras:unused_indexes
211
+ table | index | index_size | index_scans
212
+ ---------------------+--------------------------------------------+------------+-------------
213
+ public.grade_levels | index_placement_attempts_on_grade_level_id | 97 MB | 0
214
+ public.observations | observations_attrs_grade_resources | 33 MB | 0
215
+ public.messages | user_resource_id_idx | 12 MB | 0
216
+ (3 rows)
217
+ ```
218
+
219
+ This command displays indexes that have < 50 scans recorded against them, and are greater than 5 pages in size, ordered by size relative to the number of index scans. This command is generally useful for eliminating indexes that are unused, which can impact write performance, as well as read performance should they occupy space in memory.
220
+
221
+ ### `seq_scans`
222
+
223
+ ```
224
+ $ rake pg_extras:seq_scans
225
+
226
+ name | count
227
+ -----------------------------------+----------
228
+ learning_coaches | 44820063
229
+ states | 36794975
230
+ grade_levels | 13972293
231
+ charities_customers | 8615277
232
+ charities | 4316276
233
+ messages | 3922247
234
+ contests_customers | 2915972
235
+ classroom_goals | 2142014
236
+ contests | 1370267
237
+ goals | 1112659
238
+ districts | 158995
239
+ rollup_reports | 115942
240
+ customers | 93847
241
+ schools | 92984
242
+ classrooms | 92982
243
+ customer_settings | 91226
244
+ (truncated results for brevity)
245
+ ```
246
+
247
+ This command displays the number of sequential scans recorded against all tables, descending by count of sequential scans. Tables that have very high numbers of sequential scans may be underindexed, and it may be worth investigating queries that read from these tables.
248
+
249
+ ### long_running_queries
250
+
251
+ ```
252
+ $ rake pg_extras:long_running_queries
253
+
254
+ pid | duration | query
255
+ -------+-----------------+---------------------------------------------------------------------------------------
256
+ 19578 | 02:29:11.200129 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1450645 LIMIT 1
257
+ 19465 | 02:26:05.542653 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1889881 LIMIT 1
258
+ 19632 | 02:24:46.962818 | EXPLAIN SELECT "students".* FROM "students" WHERE "students"."id" = 1581884 LIMIT 1
259
+ (truncated results for brevity)
260
+ ```
261
+
262
+ This command displays currently running queries, that have been running for longer than 5 minutes, descending by duration. Very long running queries can be a source of multiple issues, such as preventing DDL statements completing or vacuum being unable to update `relfrozenxid`.
263
+
264
+ ### records_rank
265
+
266
+ ```
267
+ $ rake pg_extras:records_rank
268
+ name | estimated_count
269
+ -----------------------------------+-----------------
270
+ tastypie_apiaccess | 568891
271
+ notifications_event | 381227
272
+ core_todo | 178614
273
+ core_comment | 123969
274
+ notifications_notification | 102101
275
+ django_session | 68078
276
+ (truncated results for brevity)
277
+ ```
278
+
279
+ This command displays an estimated count of rows per table, descending by estimated count. The estimated count is derived from `n_live_tup`, which is updated by vacuum operations. Due to the way `n_live_tup` is populated, sparse vs. dense pages can result in estimations that are significantly out from the real count of rows.
280
+
281
+ ### bloat
282
+
283
+ ```
284
+ $ rake pg_extras:bloat
285
+
286
+ type | schemaname | object_name | bloat | waste
287
+ -------+------------+-------------------------------+-------+----------
288
+ table | public | bloated_table | 1.1 | 98 MB
289
+ table | public | other_bloated_table | 1.1 | 58 MB
290
+ index | public | bloated_table::bloated_index | 3.7 | 34 MB
291
+ table | public | clean_table | 0.2 | 3808 kB
292
+ table | public | other_clean_table | 0.3 | 1576 kB
293
+ ```
294
+
295
+ This command displays an estimation of table "bloat" – space allocated to a relation that is full of dead tuples, that has yet to be reclaimed. Tables that have a high bloat ratio, typically 10 or greater, should be investigated to see if vacuuming is aggressive enough, and can be a sign of high table churn.
296
+
297
+ ### vacuum_stats
298
+
299
+ ```
300
+ $ rake pg_extras:vacuum_stats
301
+ schema | table | last_vacuum | last_autovacuum | rowcount | dead_rowcount | autovacuum_threshold | expect_autovacuum
302
+ --------+-----------------------+-------------+------------------+----------------+----------------+----------------------+-------------------
303
+ public | log_table | | 2013-04-26 17:37 | 18,030 | 0 | 3,656 |
304
+ public | data_table | | 2013-04-26 13:09 | 79 | 28 | 66 |
305
+ public | other_table | | 2013-04-26 11:41 | 41 | 47 | 58 |
306
+ public | queue_table | | 2013-04-26 17:39 | 12 | 8,228 | 52 | yes
307
+ public | picnic_table | | | 13 | 0 | 53 |
308
+ ```
309
+
310
+ This command displays statistics related to vacuum operations for each table, including an estiamtion of dead rows, last autovacuum and the current autovacuum threshold. This command can be useful when determining if current vacuum thresholds require adjustments, and to determine when the table was last vacuumed.
311
+
312
+ ### pg:mandelbrot
313
+
314
+ ```
315
+ $ rake pg_extras:mandelbrot
316
+ ```
317
+
318
+ This command outputs the Mandelbrot set, calculated through SQL.
319
+
320
+ ## FAQ
321
+
322
+ * Does is not violate the Heroku PG Extras license?
323
+
324
+ The original plugin is [MIT based](https://github.com/heroku/heroku-pg-extras/blob/master/LICENSE) so it means that copying and redistribution in any format is permitted.
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.bloat_description
5
+ "Table and index bloat in your database ordered by most wasteful"
6
+ end
7
+
8
+ def self.bloat_sql
9
+ <<-EOS
10
+ WITH constants AS (
11
+ SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 4 AS ma
12
+ ), bloat_info AS (
13
+ SELECT
14
+ ma,bs,schemaname,tablename,
15
+ (datawidth+(hdr+ma-(case when hdr%ma=0 THEN ma ELSE hdr%ma END)))::numeric AS datahdr,
16
+ (maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma ELSE nullhdr%ma END))) AS nullhdr2
17
+ FROM (
18
+ SELECT
19
+ schemaname, tablename, hdr, ma, bs,
20
+ SUM((1-null_frac)*avg_width) AS datawidth,
21
+ MAX(null_frac) AS maxfracsum,
22
+ hdr+(
23
+ SELECT 1+count(*)/8
24
+ FROM pg_stats s2
25
+ WHERE null_frac<>0 AND s2.schemaname = s.schemaname AND s2.tablename = s.tablename
26
+ ) AS nullhdr
27
+ FROM pg_stats s, constants
28
+ GROUP BY 1,2,3,4,5
29
+ ) AS foo
30
+ ), table_bloat AS (
31
+ SELECT
32
+ schemaname, tablename, cc.relpages, bs,
33
+ CEIL((cc.reltuples*((datahdr+ma-
34
+ (CASE WHEN datahdr%ma=0 THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)) AS otta
35
+ FROM bloat_info
36
+ JOIN pg_class cc ON cc.relname = bloat_info.tablename
37
+ JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname AND nn.nspname <> 'information_schema'
38
+ ), index_bloat AS (
39
+ SELECT
40
+ schemaname, tablename, bs,
41
+ COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) AS ituples, COALESCE(c2.relpages,0) AS ipages,
42
+ COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) AS iotta -- very rough approximation, assumes all cols
43
+ FROM bloat_info
44
+ JOIN pg_class cc ON cc.relname = bloat_info.tablename
45
+ JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname AND nn.nspname <> 'information_schema'
46
+ JOIN pg_index i ON indrelid = cc.oid
47
+ JOIN pg_class c2 ON c2.oid = i.indexrelid
48
+ )
49
+ SELECT
50
+ type, schemaname, object_name, bloat, pg_size_pretty(raw_waste) as waste
51
+ FROM
52
+ (SELECT
53
+ 'table' as type,
54
+ schemaname,
55
+ tablename as object_name,
56
+ ROUND(CASE WHEN otta=0 THEN 0.0 ELSE table_bloat.relpages/otta::numeric END,1) AS bloat,
57
+ CASE WHEN relpages < otta THEN '0' ELSE (bs*(table_bloat.relpages-otta)::bigint)::bigint END AS raw_waste
58
+ FROM
59
+ table_bloat
60
+ UNION
61
+ SELECT
62
+ 'index' as type,
63
+ schemaname,
64
+ tablename || '::' || iname as object_name,
65
+ ROUND(CASE WHEN iotta=0 OR ipages=0 THEN 0.0 ELSE ipages/iotta::numeric END,1) AS bloat,
66
+ CASE WHEN ipages < iotta THEN '0' ELSE (bs*(ipages-iotta))::bigint END AS raw_waste
67
+ FROM
68
+ index_bloat) bloat_summary
69
+ ORDER BY raw_waste DESC, bloat DESC
70
+ EOS
71
+ end
72
+ end
73
+
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.blocking_description
5
+ "Queries holding locks other queries are waiting to be released"
6
+ end
7
+
8
+ def self.blocking_sql
9
+ <<-EOS
10
+ SELECT bl.pid AS blocked_pid,
11
+ ka.query AS blocking_statement,
12
+ now() - ka.query_start AS blocking_duration,
13
+ kl.pid AS blocking_pid,
14
+ a.query AS blocked_statement,
15
+ now() - a.query_start AS blocked_duration
16
+ FROM pg_catalog.pg_locks bl
17
+ JOIN pg_catalog.pg_stat_activity a
18
+ ON bl.pid = a.pid
19
+ JOIN pg_catalog.pg_locks kl
20
+ JOIN pg_catalog.pg_stat_activity ka
21
+ ON kl.pid = ka.pid
22
+ ON bl.transactionid = kl.transactionid AND bl.pid != kl.pid
23
+ WHERE NOT bl.granted
24
+ EOS
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.cache_hit_description
5
+ "Index and table hit rate"
6
+ end
7
+
8
+ def self.cache_hit_sql
9
+ <<-EOS
10
+ SELECT
11
+ 'index hit rate' AS name,
12
+ (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) AS ratio
13
+ FROM pg_statio_user_indexes
14
+ UNION ALL
15
+ SELECT
16
+ 'table hit rate' AS name,
17
+ sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) AS ratio
18
+ FROM pg_statio_user_tables;
19
+ EOS
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.calls_description
5
+ "10 queries that have longest execution time in aggregate"
6
+ end
7
+
8
+ def self.calls_sql
9
+ <<-EOS
10
+ SELECT query AS qry,
11
+ interval '1 millisecond' * total_time AS exec_time,
12
+ to_char((total_time/sum(total_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time,
13
+ to_char(calls, 'FM999G999G990') AS ncalls,
14
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time
15
+ FROM pg_stat_statements WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
16
+ ORDER BY calls DESC LIMIT 10
17
+ EOS
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.extensions_description
5
+ "Available and installed extensions"
6
+ end
7
+
8
+ def self.extensions_sql
9
+ <<-EOS
10
+ SELECT * FROM pg_available_extensions ORDER BY installed_version;
11
+ EOS
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.index_size_description
5
+ "The size of indexes, descending by size"
6
+ end
7
+
8
+ def self.index_size_sql
9
+ <<-EOS
10
+ SELECT c.relname AS name,
11
+ pg_size_pretty(sum(c.relpages::bigint*8192)::bigint) AS size
12
+ FROM pg_class c
13
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ AND n.nspname !~ '^pg_toast'
16
+ AND c.relkind='i'
17
+ GROUP BY c.relname
18
+ ORDER BY sum(c.relpages) DESC;
19
+ EOS
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.index_usage_description
5
+ "Index hit rate (effective databases are at 99% and up)"
6
+ end
7
+
8
+ def self.index_usage_sql
9
+ <<-EOS
10
+ SELECT relname,
11
+ CASE idx_scan
12
+ WHEN 0 THEN 'Insufficient data'
13
+ ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
14
+ END percent_of_times_index_used,
15
+ n_live_tup rows_in_table
16
+ FROM
17
+ pg_stat_user_tables
18
+ ORDER BY
19
+ n_live_tup DESC;
20
+ EOS
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.locks_description
5
+ "Queries with active locks"
6
+ end
7
+
8
+ def self.locks_sql
9
+ <<-EOS
10
+ SELECT
11
+ pg_stat_activity.pid,
12
+ pg_class.relname,
13
+ pg_locks.transactionid,
14
+ pg_locks.granted,
15
+ pg_stat_activity.query AS query_snippet,
16
+ age(now(),pg_stat_activity.query_start) AS "age"
17
+ FROM pg_stat_activity,pg_locks left
18
+ OUTER JOIN pg_class
19
+ ON (pg_locks.relation = pg_class.oid)
20
+ WHERE pg_stat_activity.query <> '<insufficient privilege>'
21
+ AND pg_locks.pid = pg_stat_activity.pid
22
+ AND pg_locks.mode = 'ExclusiveLock'
23
+ AND pg_stat_activity.pid <> pg_backend_pid() order by query_start;
24
+ EOS
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.long_running_queries_description
5
+ "All queries longer than five minutes by descending duration"
6
+ end
7
+
8
+ def self.long_running_queries_sql
9
+ <<-EOS
10
+ SELECT
11
+ pid,
12
+ now() - pg_stat_activity.query_start AS duration,
13
+ query AS query
14
+ FROM
15
+ pg_stat_activity
16
+ WHERE
17
+ pg_stat_activity.query <> ''::text
18
+ AND state <> 'idle'
19
+ AND now() - pg_stat_activity.query_start > interval '5 minutes'
20
+ ORDER BY
21
+ now() - pg_stat_activity.query_start DESC;
22
+ EOS
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.mandelbrot_description
5
+ "The mandelbrot set"
6
+ end
7
+
8
+ def self.mandelbrot_sql
9
+ <<-EOS
10
+ WITH RECURSIVE Z(IX, IY, CX, CY, X, Y, I) AS (
11
+ SELECT IX, IY, X::float, Y::float, X::float, Y::float, 0
12
+ FROM (select -2.2 + 0.031 * i, i from generate_series(0,101) as i) as xgen(x,ix),
13
+ (select -1.5 + 0.031 * i, i from generate_series(0,101) as i) as ygen(y,iy)
14
+ UNION ALL
15
+ SELECT IX, IY, CX, CY, X * X - Y * Y + CX AS X, Y * X * 2 + CY, I + 1
16
+ FROM Z
17
+ WHERE X * X + Y * Y < 16::float
18
+ AND I < 100
19
+ )
20
+ SELECT array_to_string(array_agg(SUBSTRING(' .,,,-----++++%%%%@@@@#### ', LEAST(GREATEST(I,1),27), 1)),'')
21
+ FROM (
22
+ SELECT IX, IY, MAX(I) AS I
23
+ FROM Z
24
+ GROUP BY IY, IX
25
+ ORDER BY IY, IX
26
+ ) AS ZT
27
+ GROUP BY IY
28
+ ORDER BY IY
29
+ EOS
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.outliers_description
5
+ "10 queries that have longest execution time in aggregate"
6
+ end
7
+
8
+ def self.outliers_sql
9
+ <<-EOS
10
+ SELECT interval '1 millisecond' * total_time AS total_exec_time,
11
+ to_char((total_time/sum(total_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time,
12
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
13
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
14
+ query AS query
15
+ FROM pg_stat_statements WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
16
+ ORDER BY total_time DESC
17
+ LIMIT 10
18
+ EOS
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.records_rank_description
5
+ "All tables and the number of rows in each ordered by number of rows descending"
6
+ end
7
+
8
+ def self.records_rank_sql
9
+ <<-EOS
10
+ SELECT
11
+ relname AS name,
12
+ n_live_tup AS estimated_count
13
+ FROM
14
+ pg_stat_user_tables
15
+ ORDER BY
16
+ n_live_tup DESC;
17
+ EOS
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.seq_scans_description
5
+ "Count of sequential scans by table descending by order"
6
+ end
7
+
8
+ def self.seq_scans_sql
9
+ <<-EOS
10
+ SELECT relname AS name,
11
+ seq_scan as count
12
+ FROM
13
+ pg_stat_user_tables
14
+ ORDER BY seq_scan DESC;
15
+ EOS
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.table_indexes_size_description
5
+ "Total size of all the indexes on each table, descending by size"
6
+ end
7
+
8
+ def self.table_indexes_size_sql
9
+ <<-EOS
10
+ SELECT c.relname AS table,
11
+ pg_size_pretty(pg_indexes_size(c.oid)) AS index_size
12
+ FROM pg_class c
13
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ AND n.nspname !~ '^pg_toast'
16
+ AND c.relkind='r'
17
+ ORDER BY pg_indexes_size(c.oid) DESC;
18
+ EOS
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.table_size_description
5
+ "Size of the tables (excluding indexes), descending by size"
6
+ end
7
+
8
+ def self.table_size_sql
9
+ <<-EOS
10
+ SELECT c.relname AS name,
11
+ pg_size_pretty(pg_table_size(c.oid)) AS size
12
+ FROM pg_class c
13
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ AND n.nspname !~ '^pg_toast'
16
+ AND c.relkind='r'
17
+ ORDER BY pg_table_size(c.oid) DESC;
18
+ EOS
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.total_index_size_description
5
+ "Total size of all indexes in MB"
6
+ end
7
+
8
+ def self.total_index_size_sql
9
+ <<-EOS
10
+ SELECT pg_size_pretty(sum(c.relpages::bigint*8192)::bigint) AS size
11
+ FROM pg_class c
12
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
13
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
14
+ AND n.nspname !~ '^pg_toast'
15
+ AND c.relkind='i';
16
+ EOS
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.total_table_size_description
5
+ "Size of the tables (including indexes), descending by size"
6
+ end
7
+
8
+ def self.total_table_size_sql
9
+ <<-EOS
10
+ SELECT c.relname AS name,
11
+ pg_size_pretty(pg_total_relation_size(c.oid)) AS size
12
+ FROM pg_class c
13
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ AND n.nspname !~ '^pg_toast'
16
+ AND c.relkind='r'
17
+ ORDER BY pg_total_relation_size(c.oid) DESC;
18
+ EOS
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.unused_indexes_description
5
+ "Unused and almost unused indexes"
6
+ end
7
+ # Ordered by their size relative to the number of index scans.
8
+ # Exclude indexes of very small tables (less than 5 pages),
9
+ # where the planner will almost invariably select a sequential scan,
10
+ # but may not in the future as the table grows
11
+
12
+ def self.unused_indexes_sql
13
+ <<-EOS
14
+ SELECT
15
+ schemaname || '.' || relname AS table,
16
+ indexrelname AS index,
17
+ pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
18
+ idx_scan as index_scans
19
+ FROM pg_stat_user_indexes ui
20
+ JOIN pg_index i ON ui.indexrelid = i.indexrelid
21
+ WHERE NOT indisunique AND idx_scan < 50 AND pg_relation_size(relid) > 5 * 8192
22
+ ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
23
+ pg_relation_size(i.indexrelid) DESC;
24
+ EOS
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPGExtras
4
+ def self.vacuum_stats_description
5
+ "Dead rows and whether an automatic vacuum is expected to be triggered"
6
+ end
7
+
8
+ def self.vacuum_stats_sql
9
+ <<-EOS
10
+ WITH table_opts AS (
11
+ SELECT
12
+ pg_class.oid, relname, nspname, array_to_string(reloptions, '') AS relopts
13
+ FROM
14
+ pg_class INNER JOIN pg_namespace ns ON relnamespace = ns.oid
15
+ ), vacuum_settings AS (
16
+ SELECT
17
+ oid, relname, nspname,
18
+ CASE
19
+ WHEN relopts LIKE '%autovacuum_vacuum_threshold%'
20
+ THEN substring(relopts, '.*autovacuum_vacuum_threshold=([0-9.]+).*')::integer
21
+ ELSE current_setting('autovacuum_vacuum_threshold')::integer
22
+ END AS autovacuum_vacuum_threshold,
23
+ CASE
24
+ WHEN relopts LIKE '%autovacuum_vacuum_scale_factor%'
25
+ THEN substring(relopts, '.*autovacuum_vacuum_scale_factor=([0-9.]+).*')::real
26
+ ELSE current_setting('autovacuum_vacuum_scale_factor')::real
27
+ END AS autovacuum_vacuum_scale_factor
28
+ FROM
29
+ table_opts
30
+ )
31
+ SELECT
32
+ vacuum_settings.nspname AS schema,
33
+ vacuum_settings.relname AS table,
34
+ to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI') AS last_vacuum,
35
+ to_char(psut.last_autovacuum, 'YYYY-MM-DD HH24:MI') AS last_autovacuum,
36
+ to_char(pg_class.reltuples, '9G999G999G999') AS rowcount,
37
+ to_char(psut.n_dead_tup, '9G999G999G999') AS dead_rowcount,
38
+ to_char(autovacuum_vacuum_threshold
39
+ + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples), '9G999G999G999') AS autovacuum_threshold,
40
+ CASE
41
+ WHEN autovacuum_vacuum_threshold + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples) < psut.n_dead_tup
42
+ THEN 'yes'
43
+ END AS expect_autovacuum
44
+ FROM
45
+ pg_stat_user_tables psut INNER JOIN pg_class ON psut.relid = pg_class.oid
46
+ INNER JOIN vacuum_settings ON pg_class.oid = vacuum_settings.oid
47
+ ORDER BY 1
48
+ EOS
49
+ end
50
+ end
51
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RailsPGExtras::Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load 'rails-pg-extras/tasks/all.rake'
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails-pg-extras'
4
+
5
+ namespace :pg_extras do
6
+ RailsPGExtras::QUERIES.each do |query_name|
7
+ desc RailsPGExtras.public_send("#{query_name}_description")
8
+ task query_name.to_sym => :environment do
9
+ RailsPGExtras.public_send(query_name)
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsPGExtras
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,2 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+ require 'rails-pg-extras/railtie' if defined?(Rails)
5
+
1
6
  module RailsPGExtras
7
+ QUERIES = %i(
8
+ bloat blocking cache_hit
9
+ calls extensions
10
+ index_size index_usage locks
11
+ long_running_queries mandelbrot outliers
12
+ records_rank seq_scans table_indexes_size
13
+ table_size total_index_size total_table_size
14
+ unused_indexes vacuum_stats
15
+ )
16
+
17
+ QUERIES.each do |query_name|
18
+ require "rails-pg-extras/queries/#{query_name}"
19
+
20
+ define_singleton_method query_name do |options = { in_format: :display_table }|
21
+ run_query(
22
+ query_name: query_name,
23
+ in_format: options.fetch(:in_format)
24
+ )
25
+ end
26
+ end
27
+
28
+ def self.run_query(query_name:, in_format:)
29
+ result = connection.execute(self.public_send("#{query_name}_sql"))
30
+ title = self.public_send("#{query_name}_description")
31
+ display_result(result, title: title, in_format: in_format)
32
+ end
33
+
34
+ def self.display_result(result, title:, in_format:)
35
+ case in_format
36
+ when :array
37
+ result.values
38
+ when :hash
39
+ result.to_a
40
+ when :raw
41
+ result
42
+ when :display_table
43
+ headings = if result.count > 0
44
+ result[0].keys
45
+ else
46
+ ["No results"]
47
+ end
48
+
49
+ puts Terminal::Table.new(
50
+ title: title,
51
+ headings: headings,
52
+ rows: result.values
53
+ )
54
+ else
55
+ raise "Invalid in_format option"
56
+ end
57
+ end
58
+
59
+ def self.connection
60
+ ActiveRecord::Base.connection
61
+ end
62
+
63
+ private_class_method :connection
64
+ private_class_method :display_result
2
65
  end
@@ -15,4 +15,5 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.license = "MIT"
17
17
  gem.add_dependency "activerecord"
18
+ gem.add_dependency "terminal-table"
18
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-pg-extras
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pawurb
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: terminal-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: " A bunch of rake tasks for showing what's going on inside your Rails
28
42
  PostgreSQL database "
29
43
  email:
@@ -41,7 +55,27 @@ files:
41
55
  - coverage/.resultset.json
42
56
  - coverage/.resultset.json.lock
43
57
  - lib/rails-pg-extras.rb
44
- - lib/rails-pg-extras/main.rb
58
+ - lib/rails-pg-extras/queries/bloat.rb
59
+ - lib/rails-pg-extras/queries/blocking.rb
60
+ - lib/rails-pg-extras/queries/cache_hit.rb
61
+ - lib/rails-pg-extras/queries/calls.rb
62
+ - lib/rails-pg-extras/queries/extensions.rb
63
+ - lib/rails-pg-extras/queries/index_size.rb
64
+ - lib/rails-pg-extras/queries/index_usage.rb
65
+ - lib/rails-pg-extras/queries/locks.rb
66
+ - lib/rails-pg-extras/queries/long_running_queries.rb
67
+ - lib/rails-pg-extras/queries/mandelbrot.rb
68
+ - lib/rails-pg-extras/queries/outliers.rb
69
+ - lib/rails-pg-extras/queries/records_rank.rb
70
+ - lib/rails-pg-extras/queries/seq_scans.rb
71
+ - lib/rails-pg-extras/queries/table_indexes_size.rb
72
+ - lib/rails-pg-extras/queries/table_size.rb
73
+ - lib/rails-pg-extras/queries/total_index_size.rb
74
+ - lib/rails-pg-extras/queries/total_table_size.rb
75
+ - lib/rails-pg-extras/queries/unused_indexes.rb
76
+ - lib/rails-pg-extras/queries/vacuum_stats.rb
77
+ - lib/rails-pg-extras/railtie.rb
78
+ - lib/rails-pg-extras/tasks/all.rake
45
79
  - lib/rails-pg-extras/version.rb
46
80
  - rails-pg-extras.gemspec
47
81
  homepage: http://github.com/pawurb/rails-pg-extras
File without changes