ruby-pg-extras 2.2.0 → 3.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: 67ee92c7a3fefa174bf843ee90a6d0247968726dbddfa57d83e02bec80614b3c
4
- data.tar.gz: '0893af856168f741cfe505c39415211b079202684e4472f38614f76bc6bfc7e2'
3
+ metadata.gz: 3fd26a79ff47d4681d4e39888c8f1b362ff2c585c526c06b779e0fd84c3bdd7b
4
+ data.tar.gz: c516bc6586e939a6bdf9c815823094a1e1b92ef30c3dcf7064eb9a2b3394a845
5
5
  SHA512:
6
- metadata.gz: b63de82209cb9b52710c29360025058e88987ec14d48c5d59ab16f677512fab8d11f7042cd168358971b30421edf0910d59a69d082dc024440991cac5820ddf7
7
- data.tar.gz: 4c9bd4431a96091ba6e5ba581ae7b7493a3b49d5329e0cd85903f90dbc13477e2d519a719ff078117b9e03a0b7dc89efb263eaafc3e458a4ff5ff6beb5e6fe23
6
+ metadata.gz: ecc8790295246456eb2ff3bd7025a08ed0a44b435bcf2af7070b35e2f081eeb302061e976a96d0ef5ef21422168637053fed1931687ec96d44e64f457ec21f51
7
+ data.tar.gz: 4a87d50af4289473bbf40bfe1dfae20bb214feca969ff56b34b3f27ba9321b28c3415443c3374970747877e3d5b3c14c0780e4e3108cdbf757eb1e0426c088d8
data/.circleci/config.yml CHANGED
@@ -31,7 +31,8 @@ jobs:
31
31
  - checkout
32
32
  - run: gem update --system
33
33
  - run: gem install bundler
34
- - run: bundle install --path vendor/bundle
34
+ - run: bundle config set --local path 'vendor/bundle'
35
+ - run: bundle install
35
36
  - run: sudo apt-get update --allow-releaseinfo-change
36
37
  - run: sudo apt install postgresql-client-11
37
38
  - run: dockerize -wait tcp://postgres11:5432 -timeout 1m
data/README.md CHANGED
@@ -26,7 +26,7 @@ In your Gemfile
26
26
  gem "ruby-pg-extras"
27
27
  ```
28
28
 
29
- Some of the queries (e.g., `calls` and `outliers`) require [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) extension enabled.
29
+ `calls` and `outliers` queries require [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) extension.
30
30
 
31
31
  You can check if it is enabled in your database by running:
32
32
 
@@ -39,6 +39,12 @@ You should see the similar line in the output:
39
39
  | pg_stat_statements | 1.7 | 1.7 | track execution statistics of all SQL statements executed |
40
40
  ```
41
41
 
42
+ `ssl_used` requires `sslinfo` extension, and `buffercache_usage`/`buffercache_usage` queries need `pg_buffercache`. You can enable them all by running:
43
+
44
+ ```ruby
45
+ RubyPGExtras.add_extensions
46
+ ```
47
+
42
48
  ## Usage
43
49
 
44
50
  Gem expects the `ENV['DATABASE_URL']` value in the following format:
@@ -92,6 +98,18 @@ RubyPGExtras.long_running_queries(args: { threshold: "200 milliseconds" })
92
98
 
93
99
  ```
94
100
 
101
+ ## Diagnose report
102
+
103
+ The simplest way to start using pg-extras is to execute a `diagnose` method. It runs a set of checks and prints out a report highlighting areas that may require additional investigation:
104
+
105
+ ```ruby
106
+ RubyPGExtras.diagnose
107
+ ```
108
+
109
+ ![Diagnose report](https://github.com/pawurb/ruby-pg-extras/raw/master/ruby-pg-extras-diagnose.png)
110
+
111
+ Keep reading to learn about methods that `diagnose` uses under the hood.
112
+
95
113
  ## Available methods
96
114
 
97
115
  ### `cache_hit`
@@ -167,7 +185,7 @@ This method displays values for selected PostgreSQL settings. You can compare th
167
185
 
168
186
  [More info](https://pawelurbanek.com/postgresql-fix-performance#cache-hit)
169
187
 
170
- ### 'ssl_used'
188
+ ### `ssl_used`
171
189
 
172
190
  ```ruby
173
191
 
@@ -236,7 +254,7 @@ This command displays all the current locks, regardless of their type.
236
254
 
237
255
  RubyPGExtras.outliers(args: { limit: 20 })
238
256
 
239
- qry | exec_time | prop_exec_time | ncalls | sync_io_time
257
+ query | exec_time | prop_exec_time | ncalls | sync_io_time
240
258
  -----------------------------------------+------------------+----------------+-------------+--------------
241
259
  SELECT * FROM archivable_usage_events.. | 154:39:26.431466 | 72.2% | 34,211,877 | 00:00:00
242
260
  COPY public.archivable_usage_events (.. | 50:38:33.198418 | 23.6% | 13 | 13:34:21.00108
@@ -387,7 +405,7 @@ This command displays the total size of each table and materialized view in the
387
405
 
388
406
  ```ruby
389
407
 
390
- RubyPGExtras.unused_indexes(args: { min_scans: 20 })
408
+ RubyPGExtras.unused_indexes(args: { max_scans: 20 })
391
409
 
392
410
  table | index | index_size | index_scans
393
411
  ---------------------+--------------------------------------------+------------+-------------
@@ -541,6 +559,14 @@ RubyPGExtras.kill_all
541
559
 
542
560
  This commands kills all the currently active connections to the database. It can be useful as a last resort when your database is stuck in a deadlock.
543
561
 
562
+ ### `pg_stat_statements_reset`
563
+
564
+ ```ruby
565
+ RubyPGExtras.pg_stat_statements_reset
566
+ ```
567
+
568
+ This command discards all statistics gathered so far by pg_stat_statements.
569
+
544
570
  ### `buffercache_stats`
545
571
 
546
572
  ```ruby
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'filesize'
4
+
5
+ module RubyPGExtras
6
+ class DiagnoseData
7
+ PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED = "0.985"
8
+ PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED = "0.985"
9
+ PG_EXTRAS_UNUSED_INDEXES_MAX_SCANS = 20
10
+ PG_EXTRAS_UNUSED_INDEXES_MIN_SIZE_BYTES = Filesize.from("1 MB").to_i # 1000000 bytes
11
+ PG_EXTRAS_NULL_INDEXES_MIN_SIZE_MB = 1 # 1 MB
12
+ PG_EXTRAS_NULL_MIN_NULL_FRAC_PERCENT = 50 # 50%
13
+ PG_EXTRAS_BLOAT_MIN_VALUE = 10
14
+ PG_EXTRAS_OUTLIERS_MIN_EXEC_RATIO = 33 # 33%
15
+
16
+ def self.call
17
+ new.call
18
+ end
19
+
20
+ def call
21
+ [
22
+ :table_cache_hit,
23
+ :index_cache_hit,
24
+ :unused_indexes,
25
+ :null_indexes,
26
+ :bloat,
27
+ :duplicate_indexes
28
+ ].yield_self do |checks|
29
+ extensions_data = query_module.extensions(in_format: :hash)
30
+ pg_stats_enabled = extensions_data.find do |el|
31
+ el.fetch("name") == "pg_stat_statements"
32
+ end.fetch("installed_version", false)
33
+
34
+ ssl_info_enabled = extensions_data.find do |el|
35
+ el.fetch("name") == "sslinfo"
36
+ end.fetch("installed_version", false)
37
+
38
+ if pg_stats_enabled
39
+ checks = checks.concat([:outliers])
40
+ end
41
+
42
+ if ssl_info_enabled
43
+ checks = checks.concat([:ssl_used])
44
+ end
45
+
46
+ checks
47
+ end.map do |check|
48
+ send(check).merge(check_name: check)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def query_module
55
+ RubyPGExtras
56
+ end
57
+
58
+ def table_cache_hit
59
+ min_expected = ENV.fetch(
60
+ "PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED",
61
+ PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED
62
+ ).to_f
63
+
64
+ table_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[1].fetch("ratio").to_f.round(6)
65
+
66
+ if table_cache_hit_ratio > min_expected
67
+ {
68
+ ok: true,
69
+ message: "Table cache hit ratio is correct: #{table_cache_hit_ratio}."
70
+ }
71
+ else
72
+ {
73
+ ok: false,
74
+ message: "Table hit ratio is too low: #{table_cache_hit_ratio}."
75
+ }
76
+ end
77
+ end
78
+
79
+ def index_cache_hit
80
+ min_expected = ENV.fetch(
81
+ "PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED",
82
+ PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED
83
+ ).to_f
84
+
85
+ index_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[0].fetch("ratio").to_f.round(6)
86
+
87
+ if index_cache_hit_ratio > min_expected
88
+ {
89
+ ok: true,
90
+ message: "Index hit ratio is correct: #{index_cache_hit_ratio}."
91
+ }
92
+ else
93
+ {
94
+ ok: false,
95
+ message: "Index hit ratio is too low: #{index_cache_hit_ratio}."
96
+ }
97
+ end
98
+ end
99
+
100
+ def ssl_used
101
+ ssl_connection = query_module.ssl_used(in_format: :hash)[0].fetch("ssl_is_used")
102
+
103
+ if ssl_connection
104
+ {
105
+ ok: true,
106
+ message: "Database client is using a secure SSL connection."
107
+ }
108
+ else
109
+ {
110
+ ok: false,
111
+ message: "Database client is using an unencrypted connection."
112
+ }
113
+ end
114
+ end
115
+
116
+ def unused_indexes
117
+ indexes = query_module.unused_indexes(
118
+ in_format: :hash,
119
+ args: { min_scans: PG_EXTRAS_UNUSED_INDEXES_MAX_SCANS }
120
+ ).select do |i|
121
+ Filesize.from(i.fetch("index_size")).to_i >= PG_EXTRAS_UNUSED_INDEXES_MIN_SIZE_BYTES
122
+ end
123
+
124
+ if indexes.count == 0
125
+ {
126
+ ok: true,
127
+ message: "No unused indexes detected."
128
+ }
129
+ else
130
+ print_indexes = indexes.map do |i|
131
+ "'#{i.fetch('index')}' on '#{i.fetch('table')}' size #{i.fetch('index_size')}"
132
+ end.join(",\n")
133
+ {
134
+ ok: false,
135
+ message: "Unused indexes detected:\n#{print_indexes}"
136
+ }
137
+ end
138
+ end
139
+
140
+ def null_indexes
141
+ indexes = query_module.null_indexes(
142
+ in_format: :hash,
143
+ args: { min_relation_size_mb: PG_EXTRAS_NULL_INDEXES_MIN_SIZE_MB }
144
+ ).select do |i|
145
+ i.fetch("null_frac").gsub("%", "").to_f >= PG_EXTRAS_NULL_MIN_NULL_FRAC_PERCENT
146
+ end
147
+
148
+ if indexes.count == 0
149
+ {
150
+ ok: true,
151
+ message: "No null indexes detected."
152
+ }
153
+ else
154
+ print_indexes = indexes.map do |i|
155
+ "'#{i.fetch('index')}' size #{i.fetch('index_size')} null values fraction #{i.fetch('null_frac')}"
156
+ end.join(",\n")
157
+ {
158
+ ok: false,
159
+ message: "Null indexes detected:\n#{print_indexes}"
160
+ }
161
+ end
162
+ end
163
+
164
+ def bloat
165
+ bloat_data = query_module.bloat(in_format: :hash).select do |b|
166
+ b.fetch("bloat").to_f >= PG_EXTRAS_BLOAT_MIN_VALUE
167
+ end
168
+
169
+ if bloat_data.count == 0
170
+ {
171
+ ok: true,
172
+ message: "No bloat detected."
173
+ }
174
+ else
175
+ print_bloat = bloat_data.map do |b|
176
+ "'#{b.fetch('object_name')}' bloat #{b.fetch('bloat')} waste #{b.fetch('waste')}"
177
+ end.join(",\n")
178
+
179
+ {
180
+ ok: false,
181
+ message: "Bloat detected:\n#{print_bloat}"
182
+ }
183
+ end
184
+ end
185
+
186
+ def duplicate_indexes
187
+ indexes = query_module.duplicate_indexes(in_format: :hash)
188
+
189
+ if indexes.count == 0
190
+ {
191
+ ok: true,
192
+ message: "No duplicate indexes detected."
193
+ }
194
+ else
195
+ print_indexes = indexes.map do |i|
196
+ "'#{i.fetch('idx1')}' of size #{i.fetch('size')} is identical to '#{i.fetch('idx2')}'"
197
+ end.join(",\n")
198
+
199
+ {
200
+ ok: false,
201
+ message: "Duplicate indexes detected:\n#{print_indexes}"
202
+ }
203
+ end
204
+ end
205
+
206
+ def outliers
207
+ queries = query_module.outliers(in_format: :hash).select do |q|
208
+ q.fetch("prop_exec_time").gsub("%", "").to_f >= PG_EXTRAS_OUTLIERS_MIN_EXEC_RATIO
209
+ end
210
+
211
+ if queries.count == 0
212
+ {
213
+ ok: true,
214
+ message: "No queries using significant execution ratio detected."
215
+ }
216
+ else
217
+ print_queries = queries.map do |q|
218
+ "'#{q.fetch('query').slice(0, 30)}...' called #{q.fetch('ncalls')} times, using #{q.fetch('prop_exec_time')} of total exec time."
219
+ end.join(",\n")
220
+
221
+ {
222
+ ok: false,
223
+ message: "Queries using significant execution ratio detected:\n#{print_queries}"
224
+ }
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module RubyPGExtras
6
+ class DiagnosePrint
7
+ def self.call(data)
8
+ new.call(data)
9
+ end
10
+
11
+ def call(data)
12
+ rows = data.sort do |el|
13
+ p el
14
+ el.fetch(:ok) ? 1 : -1
15
+ end.map do |el|
16
+ symbol = el.fetch(:ok) ? "√" : "x"
17
+ color = el.fetch(:ok) ? :green : :red
18
+
19
+ [
20
+ colorize("[#{symbol}] - #{el.fetch(:check_name)}", color),
21
+ colorize(el.fetch(:message), color)
22
+ ]
23
+ end
24
+
25
+ puts Terminal::Table.new(
26
+ title: title,
27
+ rows: rows
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def title
34
+ "ruby-pg-extras - diagnose report"
35
+ end
36
+
37
+ def colorize(string, color)
38
+ if color == :red
39
+ "\e[0;31;49m#{string}\e[0m"
40
+ elsif color == :green
41
+ "\e[0;32;49m#{string}\e[0m"
42
+ else
43
+ raise "Unsupported color: #{color}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ module RubyPGExtras
2
+ class IndexInfo
3
+ def self.call(table_name = nil)
4
+ new.call(table_name)
5
+ end
6
+
7
+ def call(table_name = nil)
8
+ indexes_data.select do |index_data|
9
+ if table_name == nil
10
+ true
11
+ else
12
+ index_data.fetch("tablename") == table_name
13
+ end
14
+ end.map do |index_data|
15
+ index_name = index_data.fetch("indexname")
16
+ {
17
+ index_name: index_data.fetch("indexname"),
18
+ table_name: index_data.fetch("tablename"),
19
+ columns: index_data.fetch("columns").split(',').map(&:strip),
20
+ index_size: index_size_data.find do |el|
21
+ el.fetch("name") == index_name
22
+ end.fetch("size", "N/A"),
23
+ index_scans: index_scans_data.find do |el|
24
+ el.fetch("index") == index_name
25
+ end.fetch("index_scans", "N/A"),
26
+ null_frac: null_indexes_data.find do |el|
27
+ el.fetch("index") == index_name
28
+ end&.fetch("null_frac", "N/A") || "0.00%"
29
+ }
30
+ end
31
+ end
32
+
33
+ def index_size_data
34
+ @_index_size_data ||= query_module.index_size(in_format: :hash)
35
+ end
36
+
37
+ def null_indexes_data
38
+ @_null_indexes_data ||= query_module.null_indexes(
39
+ in_format: :hash,
40
+ args: { min_relation_size_mb: 0 }
41
+ )
42
+ end
43
+
44
+ def index_scans_data
45
+ @_index_scans_data ||= query_module.index_scans(in_format: :hash)
46
+ end
47
+
48
+ def indexes_data
49
+ @_indexes_data ||= query_module.indexes(in_format: :hash)
50
+ end
51
+
52
+ private
53
+
54
+ def query_module
55
+ RubyPGExtras
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module RubyPGExtras
6
+ class IndexInfoPrint
7
+ def self.call(data)
8
+ new.call(data)
9
+ end
10
+
11
+ def call(data)
12
+ rows = data.map do |el|
13
+ [
14
+ el.fetch(:index_name),
15
+ el.fetch(:table_name),
16
+ el.fetch(:columns).join(', '),
17
+ el.fetch(:index_size),
18
+ el.fetch(:index_scans),
19
+ el.fetch(:null_frac)
20
+ ]
21
+ end
22
+
23
+ puts Terminal::Table.new(
24
+ headings: [
25
+ "Index name",
26
+ "Table name",
27
+ "Columns",
28
+ "Index size",
29
+ "Index scans",
30
+ "Null frac"
31
+ ],
32
+ title: title,
33
+ rows: rows
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def title
40
+ "Index info"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ /* Configure extensions necessary for other queries to work */
2
+
3
+ CREATE EXTENSION IF NOT EXISTS sslinfo;
4
+ CREATE EXTENSION IF NOT EXISTS pg_buffercache;
5
+ CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
@@ -0,0 +1,12 @@
1
+ /* Number of scans performed on indexes */
2
+
3
+ SELECT
4
+ schemaname,
5
+ relname AS table,
6
+ indexrelname AS index,
7
+ pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
8
+ idx_scan as index_scans
9
+ FROM pg_stat_user_indexes ui
10
+ JOIN pg_index i ON ui.indexrelid = i.indexrelid
11
+ ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
12
+ pg_relation_size(i.indexrelid) DESC;
@@ -0,0 +1,9 @@
1
+ /* List all the indexes with their corresponding tables and columns. */
2
+
3
+ SELECT
4
+ schemaname,
5
+ indexname,
6
+ tablename,
7
+ rtrim(split_part(indexdef, '(', 2), ')') as columns
8
+ FROM pg_indexes
9
+ where tablename in (select relname from pg_statio_user_tables);
@@ -0,0 +1,3 @@
1
+ /* pg_stat_statements_reset discards statistics gathered so far by pg_stat_statements */
2
+
3
+ SELECT pg_stat_statements_reset();
@@ -1,4 +1,3 @@
1
1
  /* Check if SSL connection is used */
2
2
 
3
- CREATE EXTENSION IF NOT EXISTS sslinfo;
4
3
  SELECT ssl_is_used();
@@ -0,0 +1,7 @@
1
+ /* Count of index scans by table descending by order */
2
+
3
+ SELECT relname AS name,
4
+ idx_scan as count
5
+ FROM
6
+ pg_stat_user_tables
7
+ ORDER BY idx_scan DESC;
@@ -0,0 +1,3 @@
1
+ /* List all the tables. */
2
+
3
+ select relname as tablename, schemaname from pg_statio_user_tables;
@@ -11,6 +11,6 @@ SELECT
11
11
  idx_scan as index_scans
12
12
  FROM pg_stat_user_indexes ui
13
13
  JOIN pg_index i ON ui.indexrelid = i.indexrelid
14
- WHERE NOT indisunique AND idx_scan < %{min_scans} AND pg_relation_size(relid) > 5 * 8192
14
+ WHERE NOT indisunique AND idx_scan < %{max_scans} AND pg_relation_size(relid) > 5 * 8192
15
15
  ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
16
16
  pg_relation_size(i.indexrelid) DESC;
@@ -0,0 +1,75 @@
1
+ module RubyPGExtras
2
+ class TableInfo
3
+ def self.call(table_name = nil)
4
+ new.call(table_name)
5
+ end
6
+
7
+ def call(table_name)
8
+ tables_data.select do |table_data|
9
+ if table_name == nil
10
+ true
11
+ else
12
+ table_data.fetch("tablename") == table_name
13
+ end
14
+ end.map do |table_data|
15
+ table_name = table_data.fetch("tablename")
16
+
17
+ {
18
+ table_name: table_name,
19
+ table_size: table_size_data.find do |el|
20
+ el.fetch("name") == table_name
21
+ end.fetch("size", "N/A"),
22
+ table_cache_hit: table_cache_hit_data.find do |el|
23
+ el.fetch("name") == table_name
24
+ end.fetch("ratio", "N/A"),
25
+ indexes_cache_hit: index_cache_hit_data.find do |el|
26
+ el.fetch("name") == table_name
27
+ end.fetch("ratio", "N/A"),
28
+ estimated_rows: records_rank_data.find do |el|
29
+ el.fetch("name") == table_name
30
+ end.fetch("estimated_count", "N/A"),
31
+ sequential_scans: seq_scans_data.find do |el|
32
+ el.fetch("name") == table_name
33
+ end.fetch("count", "N/A"),
34
+ indexes_scans: table_index_scans_data.find do |el|
35
+ el.fetch("name") == table_name
36
+ end.fetch("count", "N/A")
37
+ }
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def index_cache_hit_data
44
+ @_index_cache_hit_data ||= query_module.index_cache_hit(in_format: :hash)
45
+ end
46
+
47
+ def table_cache_hit_data
48
+ @_table_cache_hit_data ||= query_module.table_cache_hit(in_format: :hash)
49
+ end
50
+
51
+ def table_size_data
52
+ @_table_size_data ||= query_module.table_size(in_format: :hash)
53
+ end
54
+
55
+ def records_rank_data
56
+ @_records_rank_data ||= query_module.records_rank(in_format: :hash)
57
+ end
58
+
59
+ def tables_data
60
+ @_tables_data ||= query_module.tables(in_format: :hash)
61
+ end
62
+
63
+ def seq_scans_data
64
+ @_seq_scans_data ||= query_module.seq_scans(in_format: :hash)
65
+ end
66
+
67
+ def table_index_scans_data
68
+ @_table_index_scans_data ||= query_module.table_index_scans(in_format: :hash)
69
+ end
70
+
71
+ def query_module
72
+ RubyPGExtras
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module RubyPGExtras
6
+ class TableInfoPrint
7
+ def self.call(data)
8
+ new.call(data)
9
+ end
10
+
11
+ def call(data)
12
+ rows = data.map do |el|
13
+ [
14
+ el.fetch(:table_name),
15
+ el.fetch(:table_size),
16
+ el.fetch(:table_cache_hit),
17
+ el.fetch(:indexes_cache_hit),
18
+ el.fetch(:estimated_rows),
19
+ el.fetch(:sequential_scans),
20
+ el.fetch(:indexes_scans)
21
+ ]
22
+ end
23
+
24
+ puts Terminal::Table.new(
25
+ headings: [
26
+ "Table name",
27
+ "Table size",
28
+ "Table cache hit",
29
+ "Indexes cache hit",
30
+ "Estimated rows",
31
+ "Sequentail scans",
32
+ "Indexes scans"
33
+ ],
34
+ title: title,
35
+ rows: rows
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def title
42
+ "Table info"
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyPGExtras
4
- VERSION = "2.2.0"
4
+ VERSION = "3.2.0"
5
5
  end
@@ -3,20 +3,27 @@
3
3
  require 'terminal-table'
4
4
  require 'uri'
5
5
  require 'pg'
6
+ require 'ruby-pg-extras/diagnose_data'
7
+ require 'ruby-pg-extras/diagnose_print'
8
+ require 'ruby-pg-extras/index_info'
9
+ require 'ruby-pg-extras/index_info_print'
10
+ require 'ruby-pg-extras/table_info'
11
+ require 'ruby-pg-extras/table_info_print'
6
12
 
7
13
  module RubyPGExtras
8
14
  @@database_url = nil
9
15
  NEW_PG_STAT_STATEMENTS = "1.8"
10
16
 
11
17
  QUERIES = %i(
12
- bloat blocking cache_hit db_settings
13
- calls extensions table_cache_hit index_cache_hit
14
- index_size index_usage null_indexes locks all_locks
18
+ add_extensions bloat blocking cache_hit db_settings
19
+ calls extensions table_cache_hit tables index_cache_hit
20
+ indexes index_size index_usage index_scans null_indexes locks all_locks
15
21
  long_running_queries mandelbrot outliers
16
- records_rank seq_scans table_indexes_size
22
+ records_rank seq_scans table_index_scans table_indexes_size
17
23
  table_size total_index_size total_table_size
18
24
  unused_indexes duplicate_indexes vacuum_stats kill_all
19
- buffercache_stats buffercache_usage ssl_used
25
+ pg_stat_statements_reset buffercache_stats
26
+ buffercache_usage ssl_used
20
27
  )
21
28
 
22
29
  DEFAULT_ARGS = Hash.new({}).merge({
@@ -27,7 +34,7 @@ module RubyPGExtras
27
34
  outliers_legacy: { limit: 10 },
28
35
  buffercache_stats: { limit: 10 },
29
36
  buffercache_usage: { limit: 20 },
30
- unused_indexes: { min_scans: 50 },
37
+ unused_indexes: { max_scans: 50 },
31
38
  null_indexes: { min_relation_size_mb: 10 }
32
39
  })
33
40
 
@@ -66,6 +73,48 @@ module RubyPGExtras
66
73
  )
67
74
  end
68
75
 
76
+ def self.diagnose(in_format: :display_table)
77
+ data = RubyPGExtras::DiagnoseData.call
78
+
79
+ if in_format == :display_table
80
+ RubyPGExtras::DiagnosePrint.call(data)
81
+ elsif in_format == :hash
82
+ data
83
+ elsif in_format == :array
84
+ data.map(&:values)
85
+ else
86
+ raise "Invalid 'in_format' argument!"
87
+ end
88
+ end
89
+
90
+ def self.index_info(args:, in_format: :display_table)
91
+ data = RubyPGExtras::IndexInfo.call(args[:table_name])
92
+
93
+ if in_format == :display_table
94
+ RubyPGExtras::IndexInfoPrint.call(data)
95
+ elsif in_format == :hash
96
+ data
97
+ elsif in_format == :array
98
+ data.map(&:values)
99
+ else
100
+ raise "Invalid 'in_format' argument!"
101
+ end
102
+ end
103
+
104
+ def self.table_info(args:, in_format: :display_table)
105
+ data = RubyPGExtras::TableInfo.call(args[:table_name])
106
+
107
+ if in_format == :display_table
108
+ RubyPGExtras::TableInfoPrint.call(data)
109
+ elsif in_format == :hash
110
+ data
111
+ elsif in_format == :array
112
+ data.map(&:values)
113
+ else
114
+ raise "Invalid 'in_format' argument!"
115
+ end
116
+ end
117
+
69
118
  def self.display_result(result, title:, in_format:)
70
119
  case in_format
71
120
  when :array
Binary file
@@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.license = "MIT"
18
18
  gem.add_dependency "pg"
19
+ gem.add_dependency "filesize"
19
20
  gem.add_dependency "terminal-table"
20
21
  gem.add_development_dependency "rake"
21
22
  gem.add_development_dependency "rspec"
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RubyPGExtras::DiagnoseData do
6
+ subject(:result) do
7
+ RubyPGExtras::DiagnoseData.call
8
+ end
9
+
10
+ describe "call" do
11
+ context "stubbed cases" do
12
+ before do
13
+ expect(RubyPGExtras).to receive(:unused_indexes) {
14
+ [
15
+ { "table" => "public.plans", "index" => "index_plans_on_payer_id", "index_size" => "16 MB", "index_scans" => 0 },
16
+ { "table" => "public.feedbacks", "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "index_scans" => 1 },
17
+ { "table" => "public.channels", "index" => "index_channels_on_slack_id", "index_size" => "56 MB", "index_scans" => 7}
18
+ ]
19
+ }
20
+
21
+ expect(RubyPGExtras).to receive(:null_indexes) {
22
+ [
23
+ { "oid" => 123, "index" => "index_plans_on_payer_id", "index_size" => "16 MB", "unique" => true, "null_frac" => "00.00%", "expected_saving" => "0 kb" },
24
+ { "oid" => 321, "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "unique" => true, "null_frac" => "97.00%", "expected_saving" => "77 kb" },
25
+ { "oid" => 231, "index" => "index_channels_on_slack_id", "index_size" => "56 MB", "unique" => true, "null_frac" => "49.99%", "expected_saving" => "28 MB" }
26
+ ]
27
+ }
28
+
29
+ expect(RubyPGExtras).to receive(:bloat) {
30
+ [
31
+ { "type" => "table", "schemaname" => "public", "object_name" => "bloated_table_1", "bloat" => 8, "waste" => "0 kb" },
32
+ { "type" => "table", "schemaname" => "public", "object_name" => "bloated_table_2", "bloat" => 8, "waste" => "77 kb" },
33
+ { "type" => "schemaname", "public" => "index_channels_on_slack_id", "object_name" => "bloated_index", "bloat" => 11, "waste" => "28 MB" }
34
+ ]
35
+ }
36
+
37
+ expect(RubyPGExtras).to receive(:duplicate_indexes) {
38
+ [
39
+ { "size" => "128 kb", "idx1" => "users_pkey", "idx2" => "index_users_id" }
40
+ ]
41
+ }
42
+
43
+ expect(RubyPGExtras).to receive(:outliers) {
44
+ [
45
+ { "query" => "SELECT * FROM users WHERE users.age > 20 AND users.height > 160", "exec_time" => "154:39:26.431466", "prop_exec_time" => "72.2%", "ncalls" => "34,211,877", "sync_io_time" => "00:34:19.784318" }
46
+ ]
47
+ }
48
+ end
49
+
50
+ it "works" do
51
+ expect {
52
+ RubyPGExtras::DiagnosePrint.call(result)
53
+ }.not_to raise_error
54
+ end
55
+ end
56
+
57
+ context "real database data" do
58
+ it "works" do
59
+ expect {
60
+ RubyPGExtras::DiagnosePrint.call(result)
61
+ }.not_to raise_error
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RubyPGExtras::DiagnosePrint do
6
+ subject(:print_result) do
7
+ RubyPGExtras::DiagnosePrint.call(data)
8
+ end
9
+
10
+ let(:data) do
11
+ [
12
+ {
13
+ :check_name => :table_cache_hit,
14
+ :ok => false,
15
+ :message => "Table hit ratio too low: 0.906977."
16
+ },
17
+ {
18
+ :check_name => :index_cache_hit,
19
+ :ok => false,
20
+ :message => "Index hit ratio is too low: 0.818182."
21
+ },
22
+ {
23
+ :check_name => :ssl_used,
24
+ :ok => true,
25
+ :message => "Database client is using a secure SSL connection."
26
+ }
27
+ ]
28
+ end
29
+
30
+ describe "call" do
31
+ it "works" do
32
+ expect {
33
+ print_result
34
+ }.not_to raise_error
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RubyPGExtras::IndexInfo do
6
+ subject(:result) do
7
+ RubyPGExtras::IndexInfo.call
8
+ end
9
+
10
+ describe "call" do
11
+ context "stubbed cases" do
12
+ before do
13
+ expect(RubyPGExtras).to receive(:indexes) {
14
+ [
15
+ { "schemaname" => "public", "indexname" => "index_users_on_api_auth_token", "tablename" => "users", "columns" => "api_auth_token, column2" },
16
+ {"schemaname" => "public", "indexname" => "index_teams_on_slack_id", "tablename" => "teams", "columns" => "slack_id" },
17
+ ]
18
+ }
19
+
20
+ expect(RubyPGExtras).to receive(:index_size) {
21
+ [
22
+ { "name" => "index_users_on_api_auth_token", "size" => "1744 kB" },
23
+ {"name" => "index_teams_on_slack_id", "size" => "500 kB"},
24
+ ]
25
+ }
26
+
27
+ expect(RubyPGExtras).to receive(:null_indexes) {
28
+ [
29
+ { "oid" => 16803, "index" => "index_users_on_api_auth_token", "index_size" => "1744 kB", "unique"=>true, "indexed_column" => "api_auth_token", "null_frac" => "25.00%", "expected_saving" => "300 kB" }
30
+ ]
31
+ }
32
+
33
+ expect(RubyPGExtras).to receive(:index_scans) {
34
+ [
35
+ { "schemaname" => "public", "table" => "users", "index" => "index_users_on_api_auth_token", "index_size" => "1744 kB", "index_scans"=> 0 },
36
+ { "schemaname" => "public", "table" => "teams", "index" => "index_teams_on_slack_id", "index_size" => "500 kB", "index_scans"=> 0 }
37
+ ]
38
+ }
39
+ end
40
+
41
+ it "works" do
42
+ expect {
43
+ RubyPGExtras::IndexInfoPrint.call(result)
44
+ }.not_to raise_error
45
+ end
46
+ end
47
+
48
+ context "real data" do
49
+ it "works" do
50
+ expect {
51
+ RubyPGExtras::IndexInfoPrint.call(result)
52
+ }.not_to raise_error
53
+ end
54
+ end
55
+ end
56
+ end
data/spec/smoke_spec.rb CHANGED
@@ -3,11 +3,6 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe RubyPGExtras do
6
- before(:all) do
7
- RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
8
- RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
9
- end
10
-
11
6
  RubyPGExtras::QUERIES.each do |query_name|
12
7
  it "#{query_name} description can be read" do
13
8
  expect do
data/spec/spec_helper.rb CHANGED
@@ -17,3 +17,11 @@ else
17
17
  end
18
18
 
19
19
  ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:#{port}/ruby-pg-extras-test"
20
+
21
+ RSpec.configure do |config|
22
+ config.before(:suite) do
23
+ RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;")
24
+ RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS pg_buffercache;")
25
+ RubyPGExtras.connection.exec("CREATE EXTENSION IF NOT EXISTS sslinfo;")
26
+ end
27
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RubyPGExtras::TableInfo do
6
+ subject(:result) do
7
+ RubyPGExtras::TableInfo.call
8
+ end
9
+
10
+ describe "call" do
11
+ context "stubbed cases" do
12
+ before do
13
+ expect(RubyPGExtras).to receive(:tables) {
14
+ [
15
+ { "schemaname" => "public", "tablename" => "users" },
16
+ { "schemaname" => "public", "tablename" => "teams" }
17
+ ]
18
+ }
19
+
20
+ expect(RubyPGExtras).to receive(:table_size) {
21
+ [
22
+ { "name" => "teams", "size" => "25 MB" },
23
+ {"name" => "users", "size" => "250 MB"},
24
+ ]
25
+ }
26
+
27
+ expect(RubyPGExtras).to receive(:index_cache_hit) {
28
+ [
29
+ { "name" => "teams", "ratio" => "0.98" },
30
+ { "name" => "users", "ratio" => "0.999" },
31
+ ]
32
+ }
33
+
34
+ expect(RubyPGExtras).to receive(:table_cache_hit) {
35
+ [
36
+ { "name" => "teams", "ratio" => "0.88" },
37
+ { "name" => "users", "ratio" => "0.899" },
38
+ ]
39
+ }
40
+
41
+ expect(RubyPGExtras).to receive(:records_rank) {
42
+ [
43
+ { "name" => "teams", "estimated_count" => "358" },
44
+ { "name" => "users", "estimated_count" => "8973" },
45
+ ]
46
+ }
47
+
48
+ expect(RubyPGExtras).to receive(:seq_scans) {
49
+ [
50
+ { "name" => "teams", "count" => "0" },
51
+ { "name" => "users", "count" => "409328" },
52
+ ]
53
+ }
54
+
55
+ expect(RubyPGExtras).to receive(:table_index_scans) {
56
+ [
57
+ { "name" => "teams", "count" => "8579" },
58
+ { "name" => "users", "count" => "0" },
59
+ ]
60
+ }
61
+ end
62
+
63
+ it "works" do
64
+ expect {
65
+ RubyPGExtras::TableInfoPrint.call(result)
66
+ }.not_to raise_error
67
+ end
68
+ end
69
+
70
+ context "real data" do
71
+ it "works" do
72
+ expect {
73
+ RubyPGExtras::TableInfoPrint.call(result)
74
+ }.not_to raise_error
75
+ end
76
+ end
77
+ end
78
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-pg-extras
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pawurb
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-26 00:00:00.000000000 Z
11
+ date: 2021-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: filesize
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
  - !ruby/object:Gem::Dependency
28
42
  name: terminal-table
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -83,6 +97,11 @@ files:
83
97
  - Rakefile
84
98
  - docker-compose.yml.sample
85
99
  - lib/ruby-pg-extras.rb
100
+ - lib/ruby-pg-extras/diagnose_data.rb
101
+ - lib/ruby-pg-extras/diagnose_print.rb
102
+ - lib/ruby-pg-extras/index_info.rb
103
+ - lib/ruby-pg-extras/index_info_print.rb
104
+ - lib/ruby-pg-extras/queries/add_extensions.sql
86
105
  - lib/ruby-pg-extras/queries/all_locks.sql
87
106
  - lib/ruby-pg-extras/queries/bloat.sql
88
107
  - lib/ruby-pg-extras/queries/blocking.sql
@@ -95,8 +114,10 @@ files:
95
114
  - lib/ruby-pg-extras/queries/duplicate_indexes.sql
96
115
  - lib/ruby-pg-extras/queries/extensions.sql
97
116
  - lib/ruby-pg-extras/queries/index_cache_hit.sql
117
+ - lib/ruby-pg-extras/queries/index_scans.sql
98
118
  - lib/ruby-pg-extras/queries/index_size.sql
99
119
  - lib/ruby-pg-extras/queries/index_usage.sql
120
+ - lib/ruby-pg-extras/queries/indexes.sql
100
121
  - lib/ruby-pg-extras/queries/kill_all.sql
101
122
  - lib/ruby-pg-extras/queries/locks.sql
102
123
  - lib/ruby-pg-extras/queries/long_running_queries.sql
@@ -104,20 +125,30 @@ files:
104
125
  - lib/ruby-pg-extras/queries/null_indexes.sql
105
126
  - lib/ruby-pg-extras/queries/outliers.sql
106
127
  - lib/ruby-pg-extras/queries/outliers_legacy.sql
128
+ - lib/ruby-pg-extras/queries/pg_stat_statements_reset.sql
107
129
  - lib/ruby-pg-extras/queries/records_rank.sql
108
130
  - lib/ruby-pg-extras/queries/seq_scans.sql
109
131
  - lib/ruby-pg-extras/queries/ssl_used.sql
110
132
  - lib/ruby-pg-extras/queries/table_cache_hit.sql
133
+ - lib/ruby-pg-extras/queries/table_index_scans.sql
111
134
  - lib/ruby-pg-extras/queries/table_indexes_size.sql
112
135
  - lib/ruby-pg-extras/queries/table_size.sql
136
+ - lib/ruby-pg-extras/queries/tables.sql
113
137
  - lib/ruby-pg-extras/queries/total_index_size.sql
114
138
  - lib/ruby-pg-extras/queries/total_table_size.sql
115
139
  - lib/ruby-pg-extras/queries/unused_indexes.sql
116
140
  - lib/ruby-pg-extras/queries/vacuum_stats.sql
141
+ - lib/ruby-pg-extras/table_info.rb
142
+ - lib/ruby-pg-extras/table_info_print.rb
117
143
  - lib/ruby-pg-extras/version.rb
144
+ - ruby-pg-extras-diagnose.png
118
145
  - ruby-pg-extras.gemspec
146
+ - spec/diagnose_data_spec.rb
147
+ - spec/diagnose_print_spec.rb
148
+ - spec/index_info_spec.rb
119
149
  - spec/smoke_spec.rb
120
150
  - spec/spec_helper.rb
151
+ - spec/table_info_spec.rb
121
152
  homepage: http://github.com/pawurb/ruby-pg-extras
122
153
  licenses:
123
154
  - MIT
@@ -142,5 +173,9 @@ signing_key:
142
173
  specification_version: 4
143
174
  summary: Ruby PostgreSQL performance database insights
144
175
  test_files:
176
+ - spec/diagnose_data_spec.rb
177
+ - spec/diagnose_print_spec.rb
178
+ - spec/index_info_spec.rb
145
179
  - spec/smoke_spec.rb
146
180
  - spec/spec_helper.rb
181
+ - spec/table_info_spec.rb