ruby-pg-extras 2.1.0 → 3.0.6
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 +4 -4
- data/.circleci/config.yml +2 -1
- data/README.md +43 -3
- data/lib/ruby-pg-extras/diagnose_data.rb +230 -0
- data/lib/ruby-pg-extras/diagnose_print.rb +48 -0
- data/lib/ruby-pg-extras/queries/add_extensions.sql +5 -0
- data/lib/ruby-pg-extras/queries/duplicate_indexes.sql +1 -1
- data/lib/ruby-pg-extras/queries/pg_stat_statements_reset.sql +3 -0
- data/lib/ruby-pg-extras/queries/ssl_used.sql +3 -0
- data/lib/ruby-pg-extras/queries/unused_indexes.sql +1 -1
- data/lib/ruby-pg-extras/version.rb +1 -1
- data/lib/ruby-pg-extras.rb +18 -3
- data/ruby-pg-extras-diagnose.png +0 -0
- data/ruby-pg-extras.gemspec +1 -0
- data/spec/diagnose_data_spec.rb +76 -0
- data/spec/diagnose_print_spec.rb +34 -0
- data/spec/smoke_spec.rb +0 -5
- data/spec/spec_helper.rb +8 -0
- metadata +26 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed716b44ced4653bf0c794ea1690b99cecf460f30dc767a56f328748790d4c0f
|
4
|
+
data.tar.gz: 3652a5add3cfd162959578af4d840ee494f99311d4b2e48ffbd2f1ca5c078b1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3078b1e69e4a36686cc76a2c5326a35b8a61b4751cfe1031df74cda6306ae62fcd07026be2218ca40ac83dd535d0748e608bf00e5e3571aae9de0f1d794d43ff
|
7
|
+
data.tar.gz: 116e9de1f4a0dc927eef92e6bc88f354c54a5929831d68d7a19c58efbd8085329b0abff6cde1d482757c41d27225191e50bf2852acd9d57637d2306f61568fa7
|
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
|
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
|
-
|
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
|
+

|
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,6 +185,20 @@ 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
|
|
188
|
+
### `ssl_used`
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
|
192
|
+
RubyPGExtras.ssl_used
|
193
|
+
|
194
|
+
| ssl_is_used |
|
195
|
+
+---------------------------------+
|
196
|
+
| t |
|
197
|
+
|
198
|
+
```
|
199
|
+
|
200
|
+
Returns boolean indicating if an encrypted SSL is currently used. Connecting to the database via an unencrypted connection is a critical security risk.
|
201
|
+
|
170
202
|
### `index_usage`
|
171
203
|
|
172
204
|
```ruby
|
@@ -222,7 +254,7 @@ This command displays all the current locks, regardless of their type.
|
|
222
254
|
|
223
255
|
RubyPGExtras.outliers(args: { limit: 20 })
|
224
256
|
|
225
|
-
|
257
|
+
query | exec_time | prop_exec_time | ncalls | sync_io_time
|
226
258
|
-----------------------------------------+------------------+----------------+-------------+--------------
|
227
259
|
SELECT * FROM archivable_usage_events.. | 154:39:26.431466 | 72.2% | 34,211,877 | 00:00:00
|
228
260
|
COPY public.archivable_usage_events (.. | 50:38:33.198418 | 23.6% | 13 | 13:34:21.00108
|
@@ -373,7 +405,7 @@ This command displays the total size of each table and materialized view in the
|
|
373
405
|
|
374
406
|
```ruby
|
375
407
|
|
376
|
-
RubyPGExtras.unused_indexes(args: {
|
408
|
+
RubyPGExtras.unused_indexes(args: { max_scans: 20 })
|
377
409
|
|
378
410
|
table | index | index_size | index_scans
|
379
411
|
---------------------+--------------------------------------------+------------+-------------
|
@@ -527,6 +559,14 @@ RubyPGExtras.kill_all
|
|
527
559
|
|
528
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.
|
529
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
|
+
|
530
570
|
### `buffercache_stats`
|
531
571
|
|
532
572
|
```ruby
|
@@ -0,0 +1,230 @@
|
|
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: table_cache_hit,
|
23
|
+
index_cache_hit: index_cache_hit,
|
24
|
+
unused_indexes: unused_indexes,
|
25
|
+
null_indexes: null_indexes,
|
26
|
+
bloat: bloat,
|
27
|
+
duplicate_indexes: 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.merge!({
|
40
|
+
outliers: outliers
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
if ssl_info_enabled
|
45
|
+
checks.merge!({
|
46
|
+
ssl_used: ssl_used
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
checks
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def query_module
|
57
|
+
RubyPGExtras
|
58
|
+
end
|
59
|
+
|
60
|
+
def table_cache_hit
|
61
|
+
min_expected = ENV.fetch(
|
62
|
+
"PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED",
|
63
|
+
PG_EXTRAS_TABLE_CACHE_HIT_MIN_EXPECTED
|
64
|
+
).to_f
|
65
|
+
|
66
|
+
table_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[1].fetch("ratio").to_f.round(6)
|
67
|
+
|
68
|
+
if table_cache_hit_ratio > min_expected
|
69
|
+
{
|
70
|
+
ok: true,
|
71
|
+
message: "Table cache hit ratio is correct: #{table_cache_hit_ratio}."
|
72
|
+
}
|
73
|
+
else
|
74
|
+
{
|
75
|
+
ok: false,
|
76
|
+
message: "Table hit ratio is too low: #{table_cache_hit_ratio}."
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def index_cache_hit
|
82
|
+
min_expected = ENV.fetch(
|
83
|
+
"PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED",
|
84
|
+
PG_EXTRAS_INDEX_CACHE_HIT_MIN_EXPECTED
|
85
|
+
).to_f
|
86
|
+
|
87
|
+
index_cache_hit_ratio = query_module.cache_hit(in_format: :hash)[0].fetch("ratio").to_f.round(6)
|
88
|
+
|
89
|
+
if index_cache_hit_ratio > min_expected
|
90
|
+
{
|
91
|
+
ok: true,
|
92
|
+
message: "Index hit ratio is correct: #{index_cache_hit_ratio}."
|
93
|
+
}
|
94
|
+
else
|
95
|
+
{
|
96
|
+
ok: false,
|
97
|
+
message: "Index hit ratio is too low: #{index_cache_hit_ratio}."
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def ssl_used
|
103
|
+
ssl_connection = query_module.ssl_used(in_format: :hash)[0].fetch("ssl_is_used")
|
104
|
+
|
105
|
+
if ssl_connection
|
106
|
+
{
|
107
|
+
ok: true,
|
108
|
+
message: "Database client is using a secure SSL connection."
|
109
|
+
}
|
110
|
+
else
|
111
|
+
{
|
112
|
+
ok: false,
|
113
|
+
message: "Database client is using an unencrypted connection."
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def unused_indexes
|
119
|
+
indexes = query_module.unused_indexes(
|
120
|
+
in_format: :hash,
|
121
|
+
args: { min_scans: PG_EXTRAS_UNUSED_INDEXES_MAX_SCANS }
|
122
|
+
).select do |i|
|
123
|
+
Filesize.from(i.fetch("index_size")).to_i >= PG_EXTRAS_UNUSED_INDEXES_MIN_SIZE_BYTES
|
124
|
+
end
|
125
|
+
|
126
|
+
if indexes.count == 0
|
127
|
+
{
|
128
|
+
ok: true,
|
129
|
+
message: "No unused indexes detected."
|
130
|
+
}
|
131
|
+
else
|
132
|
+
print_indexes = indexes.map do |i|
|
133
|
+
"'#{i.fetch('index')}' on '#{i.fetch('table')}' size #{i.fetch('index_size')}"
|
134
|
+
end.join(",\n")
|
135
|
+
{
|
136
|
+
ok: false,
|
137
|
+
message: "Unused indexes detected:\n#{print_indexes}"
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def null_indexes
|
143
|
+
indexes = query_module.null_indexes(
|
144
|
+
in_format: :hash,
|
145
|
+
args: { min_relation_size_mb: PG_EXTRAS_NULL_INDEXES_MIN_SIZE_MB }
|
146
|
+
).select do |i|
|
147
|
+
i.fetch("null_frac").gsub("%", "").to_f >= PG_EXTRAS_NULL_MIN_NULL_FRAC_PERCENT
|
148
|
+
end
|
149
|
+
|
150
|
+
if indexes.count == 0
|
151
|
+
{
|
152
|
+
ok: true,
|
153
|
+
message: "No null indexes detected."
|
154
|
+
}
|
155
|
+
else
|
156
|
+
print_indexes = indexes.map do |i|
|
157
|
+
"'#{i.fetch('index')}' size #{i.fetch('index_size')} null values fraction #{i.fetch('null_frac')}"
|
158
|
+
end.join(",\n")
|
159
|
+
{
|
160
|
+
ok: false,
|
161
|
+
message: "Null indexes detected:\n#{print_indexes}"
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def bloat
|
167
|
+
bloat_data = query_module.bloat(in_format: :hash).select do |b|
|
168
|
+
b.fetch("bloat").to_f >= PG_EXTRAS_BLOAT_MIN_VALUE
|
169
|
+
end
|
170
|
+
|
171
|
+
if bloat_data.count == 0
|
172
|
+
{
|
173
|
+
ok: true,
|
174
|
+
message: "No bloat detected."
|
175
|
+
}
|
176
|
+
else
|
177
|
+
print_bloat = bloat_data.map do |b|
|
178
|
+
"'#{b.fetch('object_name')}' bloat #{b.fetch('bloat')} waste #{b.fetch('waste')}"
|
179
|
+
end.join(",\n")
|
180
|
+
|
181
|
+
{
|
182
|
+
ok: false,
|
183
|
+
message: "Bloat detected:\n#{print_bloat}"
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def duplicate_indexes
|
189
|
+
indexes = query_module.duplicate_indexes(in_format: :hash)
|
190
|
+
|
191
|
+
if indexes.count == 0
|
192
|
+
{
|
193
|
+
ok: true,
|
194
|
+
message: "No duplicate indexes detected."
|
195
|
+
}
|
196
|
+
else
|
197
|
+
print_indexes = indexes.map do |i|
|
198
|
+
"'#{i.fetch('idx1')}' of size #{i.fetch('size')} is identical to '#{i.fetch('idx2')}'"
|
199
|
+
end.join(",\n")
|
200
|
+
|
201
|
+
{
|
202
|
+
ok: false,
|
203
|
+
message: "Duplicate indexes detected:\n#{print_indexes}"
|
204
|
+
}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def outliers
|
209
|
+
queries = query_module.outliers(in_format: :hash).select do |q|
|
210
|
+
q.fetch("prop_exec_time").gsub("%", "").to_f >= PG_EXTRAS_OUTLIERS_MIN_EXEC_RATIO
|
211
|
+
end
|
212
|
+
|
213
|
+
if queries.count == 0
|
214
|
+
{
|
215
|
+
ok: true,
|
216
|
+
message: "No queries using significant execution ratio detected."
|
217
|
+
}
|
218
|
+
else
|
219
|
+
print_queries = queries.map do |q|
|
220
|
+
"'#{q.fetch('query').slice(0, 30)}...' called #{q.fetch('ncalls')} times, using #{q.fetch('prop_exec_time')} of total exec time."
|
221
|
+
end.join(",\n")
|
222
|
+
|
223
|
+
{
|
224
|
+
ok: false,
|
225
|
+
message: "Queries using significant execution ratio detected:\n#{print_queries}"
|
226
|
+
}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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 |key|
|
13
|
+
key[1].fetch(:ok) ? 1 : -1
|
14
|
+
end.map do |el|
|
15
|
+
key = el[0]
|
16
|
+
val = el[1]
|
17
|
+
symbol = val.fetch(:ok) ? "√" : "x"
|
18
|
+
color = val.fetch(:ok) ? :green : :red
|
19
|
+
|
20
|
+
[
|
21
|
+
colorize("[#{symbol}] - #{key}", color),
|
22
|
+
colorize(val.fetch(:message), color)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
puts Terminal::Table.new(
|
27
|
+
title: title,
|
28
|
+
rows: rows
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def title
|
35
|
+
"ruby-pg-extras - diagnose report"
|
36
|
+
end
|
37
|
+
|
38
|
+
def colorize(string, color)
|
39
|
+
if color == :red
|
40
|
+
"\e[0;31;49m#{string}\e[0m"
|
41
|
+
elsif color == :green
|
42
|
+
"\e[0;32;49m#{string}\e[0m"
|
43
|
+
else
|
44
|
+
raise "Unsupported color: #{color}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
/* Multiple indexes that have the same set of columns, same opclass, expression and predicate
|
1
|
+
/* Multiple indexes that have the same set of columns, same opclass, expression and predicate. */
|
2
2
|
|
3
3
|
SELECT pg_size_pretty(sum(pg_relation_size(idx))::bigint) as size,
|
4
4
|
(array_agg(idx))[1] as idx1, (array_agg(idx))[2] as idx2,
|
@@ -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 < %{
|
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;
|
data/lib/ruby-pg-extras.rb
CHANGED
@@ -3,20 +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'
|
6
8
|
|
7
9
|
module RubyPGExtras
|
8
10
|
@@database_url = nil
|
9
11
|
NEW_PG_STAT_STATEMENTS = "1.8"
|
10
12
|
|
11
13
|
QUERIES = %i(
|
12
|
-
bloat blocking cache_hit db_settings
|
14
|
+
add_extensions bloat blocking cache_hit db_settings
|
13
15
|
calls extensions table_cache_hit index_cache_hit
|
14
16
|
index_size index_usage null_indexes locks all_locks
|
15
17
|
long_running_queries mandelbrot outliers
|
16
18
|
records_rank seq_scans table_indexes_size
|
17
19
|
table_size total_index_size total_table_size
|
18
20
|
unused_indexes duplicate_indexes vacuum_stats kill_all
|
19
|
-
buffercache_stats
|
21
|
+
pg_stat_statements_reset buffercache_stats
|
22
|
+
buffercache_usage ssl_used
|
20
23
|
)
|
21
24
|
|
22
25
|
DEFAULT_ARGS = Hash.new({}).merge({
|
@@ -27,7 +30,7 @@ module RubyPGExtras
|
|
27
30
|
outliers_legacy: { limit: 10 },
|
28
31
|
buffercache_stats: { limit: 10 },
|
29
32
|
buffercache_usage: { limit: 20 },
|
30
|
-
unused_indexes: {
|
33
|
+
unused_indexes: { max_scans: 50 },
|
31
34
|
null_indexes: { min_relation_size_mb: 10 }
|
32
35
|
})
|
33
36
|
|
@@ -66,6 +69,18 @@ module RubyPGExtras
|
|
66
69
|
)
|
67
70
|
end
|
68
71
|
|
72
|
+
def self.diagnose(in_format: :display_table)
|
73
|
+
data = RubyPGExtras::DiagnoseData.call
|
74
|
+
|
75
|
+
if in_format == :display_table
|
76
|
+
RubyPGExtras::DiagnosePrint.call(data)
|
77
|
+
elsif in_format == :hash
|
78
|
+
data
|
79
|
+
else
|
80
|
+
raise "Invalid 'in_format' argument!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
69
84
|
def self.display_result(result, title:, in_format:)
|
70
85
|
case in_format
|
71
86
|
when :array
|
Binary file
|
data/ruby-pg-extras.gemspec
CHANGED
@@ -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,76 @@
|
|
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" => "t", "null_frac" => "00.00%", "expected_saving" => "0 kb" },
|
24
|
+
{ "oid" => 321, "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "unique" => "f", "null_frac" => "97.00%", "expected_saving" => "77 kb" },
|
25
|
+
{ "oid" => 231, "index" => "index_channels_on_slack_id", "index_size" => "56 MB", "unique" => "t", "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
|
+
before do
|
59
|
+
expect(RubyPGExtras).to receive(:unused_indexes) {
|
60
|
+
[
|
61
|
+
{ "table" => "public.plans", "index" => "index_plans_on_payer_id", "index_size" => "2 MB", "index_scans" => 0 },
|
62
|
+
{ "table" => "public.feedbacks", "index" => "index_feedbacks_on_target_id", "index_size" => "80 kB", "index_scans" => 1 },
|
63
|
+
{ "table" => "public.channels", "index" => "index_channels_on_slack_id", "index_size" => "1.1 MB", "index_scans" => 7}
|
64
|
+
]
|
65
|
+
}
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it "works" do
|
70
|
+
expect {
|
71
|
+
RubyPGExtras::DiagnosePrint.call(result)
|
72
|
+
}.not_to raise_error
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,34 @@
|
|
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
|
+
:table_cache_hit => {
|
13
|
+
:ok => false,
|
14
|
+
:message => "Table hit ratio too low: 0.906977."
|
15
|
+
},
|
16
|
+
:index_cache_hit => {
|
17
|
+
:ok => false,
|
18
|
+
:message => "Index hit ratio is too low: 0.818182."
|
19
|
+
},
|
20
|
+
:ssl_used => {
|
21
|
+
:ok => true,
|
22
|
+
:message => "Database client is using a secure SSL connection."
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "call" do
|
28
|
+
it "works" do
|
29
|
+
expect {
|
30
|
+
print_result
|
31
|
+
}.not_to raise_error
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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
|
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:
|
4
|
+
version: 3.0.6
|
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-
|
11
|
+
date: 2021-10-28 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,9 @@ 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/queries/add_extensions.sql
|
86
103
|
- lib/ruby-pg-extras/queries/all_locks.sql
|
87
104
|
- lib/ruby-pg-extras/queries/bloat.sql
|
88
105
|
- lib/ruby-pg-extras/queries/blocking.sql
|
@@ -104,8 +121,10 @@ files:
|
|
104
121
|
- lib/ruby-pg-extras/queries/null_indexes.sql
|
105
122
|
- lib/ruby-pg-extras/queries/outliers.sql
|
106
123
|
- lib/ruby-pg-extras/queries/outliers_legacy.sql
|
124
|
+
- lib/ruby-pg-extras/queries/pg_stat_statements_reset.sql
|
107
125
|
- lib/ruby-pg-extras/queries/records_rank.sql
|
108
126
|
- lib/ruby-pg-extras/queries/seq_scans.sql
|
127
|
+
- lib/ruby-pg-extras/queries/ssl_used.sql
|
109
128
|
- lib/ruby-pg-extras/queries/table_cache_hit.sql
|
110
129
|
- lib/ruby-pg-extras/queries/table_indexes_size.sql
|
111
130
|
- lib/ruby-pg-extras/queries/table_size.sql
|
@@ -114,7 +133,10 @@ files:
|
|
114
133
|
- lib/ruby-pg-extras/queries/unused_indexes.sql
|
115
134
|
- lib/ruby-pg-extras/queries/vacuum_stats.sql
|
116
135
|
- lib/ruby-pg-extras/version.rb
|
136
|
+
- ruby-pg-extras-diagnose.png
|
117
137
|
- ruby-pg-extras.gemspec
|
138
|
+
- spec/diagnose_data_spec.rb
|
139
|
+
- spec/diagnose_print_spec.rb
|
118
140
|
- spec/smoke_spec.rb
|
119
141
|
- spec/spec_helper.rb
|
120
142
|
homepage: http://github.com/pawurb/ruby-pg-extras
|
@@ -141,5 +163,7 @@ signing_key:
|
|
141
163
|
specification_version: 4
|
142
164
|
summary: Ruby PostgreSQL performance database insights
|
143
165
|
test_files:
|
166
|
+
- spec/diagnose_data_spec.rb
|
167
|
+
- spec/diagnose_print_spec.rb
|
144
168
|
- spec/smoke_spec.rb
|
145
169
|
- spec/spec_helper.rb
|