ruby-pg-extras 2.3.0 → 3.2.1

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: a93ebb6cb69e2591e7e9c327b326edf9353105b985f0023ec00deceec631e398
4
- data.tar.gz: 0e6b50933c072c9c04fba24875d71c0c561ecf4b5a0cb60f8cb66df9cb000075
3
+ metadata.gz: 05d2e5841e02ca2a6ea167296d565a0df5e9a516c9e975589db3bd1b7856a5f7
4
+ data.tar.gz: 21a2d1b015d0558f4875f5cf1862b5e63a6e1ecbface59d88439712894716d17
5
5
  SHA512:
6
- metadata.gz: b855303347b32b4e20e9a9061f058843f8a337968344d3517a14a70000849691f378e077163720f487c2798df441c1a59f7f53e80b6d45ef2fe7796854cf79f6
7
- data.tar.gz: 4429a4634e5b154dca182bdb232c64009178f287a00b1c58594dd089221ab2cc1520522ba2112153dd645905685d7751b7c51f9d9a87968d0ab416af93797d26
6
+ metadata.gz: 0bf1ddceb1d24b718b979abc9dc03c04b94d6500487318e32805c8885f8a244bb548dd7c9dd7f738be7de0fa73f1212493a3a22c431ace69478aed2a38901dc7
7
+ data.tar.gz: 628eeea090e2839bc38d512a801f13ab7ef7413eb2c45719171dd9946800e9a98444f81a96d14102da749de85776931a616aee5e06f46a4d4f0469cca0e6cb1c
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`
@@ -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
  ---------------------+--------------------------------------------+------------+-------------
@@ -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,46 @@
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
+ el.fetch(:ok) ? 1 : -1
14
+ end.map do |el|
15
+ symbol = el.fetch(:ok) ? "√" : "x"
16
+ color = el.fetch(:ok) ? :green : :red
17
+
18
+ [
19
+ colorize("[#{symbol}] - #{el.fetch(:check_name)}", color),
20
+ colorize(el.fetch(:message), color)
21
+ ]
22
+ end
23
+
24
+ puts Terminal::Table.new(
25
+ title: title,
26
+ rows: rows
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def title
33
+ "ruby-pg-extras - diagnose report"
34
+ end
35
+
36
+ def colorize(string, color)
37
+ if color == :red
38
+ "\e[0;31;49m#{string}\e[0m"
39
+ elsif color == :green
40
+ "\e[0;32;49m#{string}\e[0m"
41
+ else
42
+ raise "Unsupported color: #{color}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
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.sort_by do |index_data|
15
+ index_data.fetch("tablename")
16
+ end.map do |index_data|
17
+ index_name = index_data.fetch("indexname")
18
+
19
+ {
20
+ index_name: index_name,
21
+ table_name: index_data.fetch("tablename"),
22
+ columns: index_data.fetch("columns").split(',').map(&:strip),
23
+ index_size: index_size_data.find do |el|
24
+ el.fetch("name") == index_name
25
+ end.fetch("size", "N/A"),
26
+ index_scans: index_scans_data.find do |el|
27
+ el.fetch("index") == index_name
28
+ end.fetch("index_scans", "N/A"),
29
+ null_frac: null_indexes_data.find do |el|
30
+ el.fetch("index") == index_name
31
+ end&.fetch("null_frac", "N/A") || "0.00%"
32
+ }
33
+ end
34
+ end
35
+
36
+ def index_size_data
37
+ @_index_size_data ||= query_module.index_size(in_format: :hash)
38
+ end
39
+
40
+ def null_indexes_data
41
+ @_null_indexes_data ||= query_module.null_indexes(
42
+ in_format: :hash,
43
+ args: { min_relation_size_mb: 0 }
44
+ )
45
+ end
46
+
47
+ def index_scans_data
48
+ @_index_scans_data ||= query_module.index_scans(in_format: :hash)
49
+ end
50
+
51
+ def indexes_data
52
+ @_indexes_data ||= query_module.indexes(in_format: :hash)
53
+ end
54
+
55
+ private
56
+
57
+ def query_module
58
+ RubyPGExtras
59
+ end
60
+ end
61
+ 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);
@@ -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,77 @@
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.sort_by do |table_data|
15
+ table_data.fetch("tablename")
16
+ end.map do |table_data|
17
+ table_name = table_data.fetch("tablename")
18
+
19
+ {
20
+ table_name: table_name,
21
+ table_size: table_size_data.find do |el|
22
+ el.fetch("name") == table_name
23
+ end.fetch("size", "N/A"),
24
+ table_cache_hit: table_cache_hit_data.find do |el|
25
+ el.fetch("name") == table_name
26
+ end.fetch("ratio", "N/A"),
27
+ indexes_cache_hit: index_cache_hit_data.find do |el|
28
+ el.fetch("name") == table_name
29
+ end.fetch("ratio", "N/A"),
30
+ estimated_rows: records_rank_data.find do |el|
31
+ el.fetch("name") == table_name
32
+ end.fetch("estimated_count", "N/A"),
33
+ sequential_scans: seq_scans_data.find do |el|
34
+ el.fetch("name") == table_name
35
+ end.fetch("count", "N/A"),
36
+ indexes_scans: table_index_scans_data.find do |el|
37
+ el.fetch("name") == table_name
38
+ end.fetch("count", "N/A")
39
+ }
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def index_cache_hit_data
46
+ @_index_cache_hit_data ||= query_module.index_cache_hit(in_format: :hash)
47
+ end
48
+
49
+ def table_cache_hit_data
50
+ @_table_cache_hit_data ||= query_module.table_cache_hit(in_format: :hash)
51
+ end
52
+
53
+ def table_size_data
54
+ @_table_size_data ||= query_module.table_size(in_format: :hash)
55
+ end
56
+
57
+ def records_rank_data
58
+ @_records_rank_data ||= query_module.records_rank(in_format: :hash)
59
+ end
60
+
61
+ def tables_data
62
+ @_tables_data ||= query_module.tables(in_format: :hash)
63
+ end
64
+
65
+ def seq_scans_data
66
+ @_seq_scans_data ||= query_module.seq_scans(in_format: :hash)
67
+ end
68
+
69
+ def table_index_scans_data
70
+ @_table_index_scans_data ||= query_module.table_index_scans(in_format: :hash)
71
+ end
72
+
73
+ def query_module
74
+ RubyPGExtras
75
+ end
76
+ end
77
+ 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.3.0"
4
+ VERSION = "3.2.1"
5
5
  end
@@ -3,17 +3,23 @@
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
25
  pg_stat_statements_reset buffercache_stats
@@ -28,7 +34,7 @@ module RubyPGExtras
28
34
  outliers_legacy: { limit: 10 },
29
35
  buffercache_stats: { limit: 10 },
30
36
  buffercache_usage: { limit: 20 },
31
- unused_indexes: { min_scans: 50 },
37
+ unused_indexes: { max_scans: 50 },
32
38
  null_indexes: { min_relation_size_mb: 10 }
33
39
  })
34
40
 
@@ -67,6 +73,48 @@ module RubyPGExtras
67
73
  )
68
74
  end
69
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
+
70
118
  def self.display_result(result, title:, in_format:)
71
119
  case in_format
72
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.3.0
4
+ version: 3.2.1
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-27 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
@@ -109,16 +130,25 @@ files:
109
130
  - lib/ruby-pg-extras/queries/seq_scans.sql
110
131
  - lib/ruby-pg-extras/queries/ssl_used.sql
111
132
  - lib/ruby-pg-extras/queries/table_cache_hit.sql
133
+ - lib/ruby-pg-extras/queries/table_index_scans.sql
112
134
  - lib/ruby-pg-extras/queries/table_indexes_size.sql
113
135
  - lib/ruby-pg-extras/queries/table_size.sql
136
+ - lib/ruby-pg-extras/queries/tables.sql
114
137
  - lib/ruby-pg-extras/queries/total_index_size.sql
115
138
  - lib/ruby-pg-extras/queries/total_table_size.sql
116
139
  - lib/ruby-pg-extras/queries/unused_indexes.sql
117
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
118
143
  - lib/ruby-pg-extras/version.rb
144
+ - ruby-pg-extras-diagnose.png
119
145
  - ruby-pg-extras.gemspec
146
+ - spec/diagnose_data_spec.rb
147
+ - spec/diagnose_print_spec.rb
148
+ - spec/index_info_spec.rb
120
149
  - spec/smoke_spec.rb
121
150
  - spec/spec_helper.rb
151
+ - spec/table_info_spec.rb
122
152
  homepage: http://github.com/pawurb/ruby-pg-extras
123
153
  licenses:
124
154
  - MIT
@@ -143,5 +173,9 @@ signing_key:
143
173
  specification_version: 4
144
174
  summary: Ruby PostgreSQL performance database insights
145
175
  test_files:
176
+ - spec/diagnose_data_spec.rb
177
+ - spec/diagnose_print_spec.rb
178
+ - spec/index_info_spec.rb
146
179
  - spec/smoke_spec.rb
147
180
  - spec/spec_helper.rb
181
+ - spec/table_info_spec.rb