ruby-pg-extras 2.3.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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