pgdexter 0.3.3 → 0.3.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +38 -13
- data/LICENSE.txt +1 -1
- data/README.md +22 -11
- data/exe/dexter +4 -7
- data/lib/dexter.rb +10 -4
- data/lib/dexter/client.rb +21 -12
- data/lib/dexter/csv_log_parser.rb +1 -1
- data/lib/dexter/indexer.rb +52 -21
- data/lib/dexter/logging.rb +18 -3
- data/lib/dexter/pg_stat_activity_parser.rb +25 -0
- data/lib/dexter/processor.rb +10 -6
- data/lib/dexter/sql_log_parser.rb +10 -0
- data/lib/dexter/version.rb +1 -1
- metadata +13 -19
- data/.gitignore +0 -9
- data/.travis.yml +0 -17
- data/Gemfile +0 -4
- data/Rakefile +0 -11
- data/guides/Hosted-Postgres.md +0 -102
- data/guides/Linux.md +0 -70
- data/pgdexter.gemspec +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3940bf892014f2337cc5cd95c85b66f245b3725ad7ce84397c4eb4f9e3751c3c
|
4
|
+
data.tar.gz: b8fc834cda0161c8ef0b44853c14f5f608c00f373e6a95481321e45611d2d5b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 463430e69b2a1f08ca411db0a4938c7243839e78f81db72455d508ff1e9de1a752889bff8a3e42486cf4859042065b0847f0b6c0216c80c9b9a36a10cf360753
|
7
|
+
data.tar.gz: d31949fc719e863aa56672fd807141dfbeb521d2ce2525ca551109e09840115c3091e7130cc47617a6b836ff31853dbf12a1bb9fb4dea492b17cbf7c9bfd5c8e
|
data/CHANGELOG.md
CHANGED
@@ -1,22 +1,47 @@
|
|
1
|
-
## 0.3.
|
1
|
+
## 0.3.8 (2020-08-17)
|
2
|
+
|
3
|
+
- Colorize output
|
4
|
+
- Fixed error when unable to parse view definitions
|
5
|
+
|
6
|
+
## 0.3.7 (2020-07-10)
|
7
|
+
|
8
|
+
- Fixed help output
|
9
|
+
|
10
|
+
## 0.3.6 (2020-03-30)
|
11
|
+
|
12
|
+
- Fixed warning with Ruby 2.7
|
13
|
+
|
14
|
+
## 0.3.5 (2018-04-30)
|
15
|
+
|
16
|
+
- Added `sql` input format
|
17
|
+
- Fixed error for queries with double dash comments
|
18
|
+
- Fixed connection threading issue with `--pg-stat-activity` option
|
19
|
+
|
20
|
+
## 0.3.4 (2018-04-09)
|
21
|
+
|
22
|
+
- Fixed `--username` option
|
23
|
+
- Fixed `JSON::NestingError`
|
24
|
+
- Added `--pg-stat-activity` option
|
25
|
+
|
26
|
+
## 0.3.3 (2018-02-22)
|
2
27
|
|
3
28
|
- Added support for views and materialized views
|
4
29
|
- Better handle case when multiple indexes are found for a query
|
5
30
|
- Added `--min-cost-savings-pct` option
|
6
31
|
|
7
|
-
## 0.3.2
|
32
|
+
## 0.3.2 (2018-01-04)
|
8
33
|
|
9
34
|
- Fixed parsing issue with named prepared statements
|
10
35
|
- Fixed parsing issue with multiline queries in csv format
|
11
36
|
- Better explanations for indexing decisions
|
12
37
|
|
13
|
-
## 0.3.1
|
38
|
+
## 0.3.1 (2017-12-28)
|
14
39
|
|
15
40
|
- Added support for queries with bind variables
|
16
41
|
- Fixed error with streaming logs as csv format
|
17
42
|
- Handle malformed CSV gracefully
|
18
43
|
|
19
|
-
## 0.3.0
|
44
|
+
## 0.3.0 (2017-12-22)
|
20
45
|
|
21
46
|
- Added support for schemas
|
22
47
|
- Added support for csv format
|
@@ -24,12 +49,12 @@
|
|
24
49
|
- Added `--min-calls` option
|
25
50
|
- Fixed debug output when indexes not found
|
26
51
|
|
27
|
-
## 0.2.1
|
52
|
+
## 0.2.1 (2017-09-02)
|
28
53
|
|
29
54
|
- Fixed bad suggestions
|
30
55
|
- Improved debugging output
|
31
56
|
|
32
|
-
## 0.2.0
|
57
|
+
## 0.2.0 (2017-08-27)
|
33
58
|
|
34
59
|
- Added same connection options as `psql`
|
35
60
|
- Added support for multiple files
|
@@ -40,38 +65,38 @@ Breaking
|
|
40
65
|
|
41
66
|
- `-h` option changed to `--host` instead of `--help` for consistency with `psql`
|
42
67
|
|
43
|
-
## 0.1.6
|
68
|
+
## 0.1.6 (2017-08-26)
|
44
69
|
|
45
70
|
- Significant performance improvements
|
46
71
|
- Added `--include` option
|
47
72
|
|
48
|
-
## 0.1.5
|
73
|
+
## 0.1.5 (2017-08-14)
|
49
74
|
|
50
75
|
- Added support for non-`SELECT` queries
|
51
76
|
- Added `--pg-stat-statements` option
|
52
77
|
- Added advisory locks
|
53
78
|
- Added support for running as a non-superuser
|
54
79
|
|
55
|
-
## 0.1.4
|
80
|
+
## 0.1.4 (2017-07-02)
|
56
81
|
|
57
82
|
- Added support for multicolumn indexes
|
58
83
|
|
59
|
-
## 0.1.3
|
84
|
+
## 0.1.3 (2017-06-30)
|
60
85
|
|
61
86
|
- Fixed error with non-lowercase columns
|
62
87
|
- Fixed error with `json` columns
|
63
88
|
|
64
|
-
## 0.1.2
|
89
|
+
## 0.1.2 (2017-06-26)
|
65
90
|
|
66
91
|
- Added `--exclude` option
|
67
92
|
- Added `--log-sql` option
|
68
93
|
|
69
|
-
## 0.1.1
|
94
|
+
## 0.1.1 (2017-06-25)
|
70
95
|
|
71
96
|
- Added `--interval` option
|
72
97
|
- Added `--min-time` option
|
73
98
|
- Added `--log-level` option
|
74
99
|
|
75
|
-
## 0.1.0
|
100
|
+
## 0.1.0 (2017-06-24)
|
76
101
|
|
77
102
|
- Launched
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -2,18 +2,18 @@
|
|
2
2
|
|
3
3
|
The automatic indexer for Postgres
|
4
4
|
|
5
|
-
[Read about how it works](https://
|
5
|
+
[Read about how it works](https://ankane.org/introducing-dexter)
|
6
6
|
|
7
7
|
[![Build Status](https://travis-ci.org/ankane/dexter.svg?branch=master)](https://travis-ci.org/ankane/dexter)
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
|
-
First, install [HypoPG](https://github.com/
|
11
|
+
First, install [HypoPG](https://github.com/HypoPG/hypopg) on your database server. This doesn’t require a restart.
|
12
12
|
|
13
13
|
```sh
|
14
14
|
cd /tmp
|
15
|
-
curl -L https://github.com/
|
16
|
-
cd hypopg-1.1.
|
15
|
+
curl -L https://github.com/HypoPG/hypopg/archive/1.1.4.tar.gz | tar xz
|
16
|
+
cd hypopg-1.1.4
|
17
17
|
make
|
18
18
|
make install # may need sudo
|
19
19
|
```
|
@@ -104,13 +104,19 @@ or pass files:
|
|
104
104
|
dexter <connection-options> <file1> <file2>
|
105
105
|
```
|
106
106
|
|
107
|
+
or collect running queries with:
|
108
|
+
|
109
|
+
```sh
|
110
|
+
dexter <connection-options> --pg-stat-activity
|
111
|
+
```
|
112
|
+
|
107
113
|
or use the [pg_stat_statements](https://www.postgresql.org/docs/current/static/pgstatstatements.html) extension:
|
108
114
|
|
109
115
|
```sh
|
110
116
|
dexter <connection-options> --pg-stat-statements
|
111
117
|
```
|
112
118
|
|
113
|
-
> Note: Logs are highly preferred over pg_stat_statements, as pg_stat_statements often doesn’t store enough information to optimize queries.
|
119
|
+
> Note: Logs or running queries are highly preferred over pg_stat_statements, as pg_stat_statements often doesn’t store enough information to optimize queries.
|
114
120
|
|
115
121
|
### Collection Options
|
116
122
|
|
@@ -140,10 +146,10 @@ dexter --interval 60 # seconds
|
|
140
146
|
|
141
147
|
## Examples
|
142
148
|
|
143
|
-
Ubuntu with PostgreSQL
|
149
|
+
Ubuntu with PostgreSQL 12
|
144
150
|
|
145
151
|
```sh
|
146
|
-
tail -F -n +1 /var/log/postgresql/postgresql-
|
152
|
+
tail -F -n +1 /var/log/postgresql/postgresql-12-main.log | sudo -u postgres dexter dbname
|
147
153
|
```
|
148
154
|
|
149
155
|
Homebrew on Mac
|
@@ -209,6 +215,10 @@ gem specific_install https://github.com/ankane/dexter.git
|
|
209
215
|
|
210
216
|
This software wouldn’t be possible without [HypoPG](https://github.com/dalibo/hypopg), which allows you to create hypothetical indexes, and [pg_query](https://github.com/lfittl/pg_query), which allows you to parse and fingerprint queries. A big thanks to Dalibo and Lukas Fittl respectively.
|
211
217
|
|
218
|
+
## Research
|
219
|
+
|
220
|
+
This is known as the Index Selection Problem (ISP).
|
221
|
+
|
212
222
|
## Contributing
|
213
223
|
|
214
224
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
@@ -218,17 +228,18 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
|
|
218
228
|
- Write, clarify, or fix documentation
|
219
229
|
- Suggest or add new features
|
220
230
|
|
221
|
-
To get started, run:
|
231
|
+
To get started with development, run:
|
222
232
|
|
223
233
|
```sh
|
224
234
|
git clone https://github.com/ankane/dexter.git
|
225
235
|
cd dexter
|
226
|
-
bundle
|
227
|
-
rake install
|
236
|
+
bundle install
|
237
|
+
bundle exec rake install
|
228
238
|
```
|
229
239
|
|
230
240
|
To run tests, use:
|
231
241
|
|
232
242
|
```sh
|
233
|
-
|
243
|
+
createdb dexter_test
|
244
|
+
bundle exec rake test
|
234
245
|
```
|
data/exe/dexter
CHANGED
data/lib/dexter.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
|
-
|
2
|
-
require "slop"
|
1
|
+
# dependencies
|
3
2
|
require "pg"
|
4
3
|
require "pg_query"
|
5
|
-
require "
|
4
|
+
require "slop"
|
5
|
+
|
6
|
+
# stdlib
|
6
7
|
require "set"
|
7
|
-
require "
|
8
|
+
require "time"
|
9
|
+
|
10
|
+
# modules
|
11
|
+
require "dexter/version"
|
8
12
|
require "dexter/logging"
|
9
13
|
require "dexter/client"
|
10
14
|
require "dexter/collector"
|
11
15
|
require "dexter/indexer"
|
12
16
|
require "dexter/log_parser"
|
13
17
|
require "dexter/csv_log_parser"
|
18
|
+
require "dexter/pg_stat_activity_parser"
|
19
|
+
require "dexter/sql_log_parser"
|
14
20
|
require "dexter/processor"
|
15
21
|
require "dexter/query"
|
16
22
|
|
data/lib/dexter/client.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
module Dexter
|
2
2
|
class Client
|
3
|
+
extend Logging
|
3
4
|
include Logging
|
4
5
|
|
5
6
|
attr_reader :arguments, :options
|
6
7
|
|
8
|
+
def self.start
|
9
|
+
Dexter::Client.new(ARGV).perform
|
10
|
+
rescue Dexter::Abort, PG::UndefinedFile => e
|
11
|
+
abort colorize(e.message.strip, :red)
|
12
|
+
end
|
13
|
+
|
7
14
|
def initialize(args)
|
8
15
|
@arguments, @options = parse_args(args)
|
9
16
|
end
|
@@ -18,6 +25,8 @@ module Dexter
|
|
18
25
|
elsif options[:pg_stat_statements]
|
19
26
|
# TODO support streaming option
|
20
27
|
Indexer.new(options).process_stat_statements
|
28
|
+
elsif options[:pg_stat_activity]
|
29
|
+
Processor.new(:pg_stat_activity, options).perform
|
21
30
|
elsif arguments.any?
|
22
31
|
ARGV.replace(arguments)
|
23
32
|
Processor.new(ARGF, options).perform
|
@@ -29,9 +38,9 @@ module Dexter
|
|
29
38
|
def parse_args(args)
|
30
39
|
opts = Slop.parse(args) do |o|
|
31
40
|
o.banner = %(Usage:
|
32
|
-
dexter [options]
|
33
|
-
|
34
|
-
Options:
|
41
|
+
dexter [options])
|
42
|
+
o.separator ""
|
43
|
+
o.separator "Options:"
|
35
44
|
o.boolean "--analyze", "analyze tables that haven't been analyzed in the past hour", default: false
|
36
45
|
o.boolean "--create", "create indexes", default: false
|
37
46
|
o.array "--exclude", "prevent specific tables from being indexed"
|
@@ -44,11 +53,9 @@ Options:)
|
|
44
53
|
o.float "--min-calls", "only process queries that have been called a certain number of times", default: 0
|
45
54
|
o.float "--min-time", "only process queries that have consumed a certain amount of DB time, in minutes", default: 0
|
46
55
|
o.integer "--min-cost-savings-pct", default: 50, help: false
|
56
|
+
o.boolean "--pg-stat-activity", "use pg_stat_activity", default: false, help: false
|
47
57
|
o.boolean "--pg-stat-statements", "use pg_stat_statements", default: false, help: false
|
48
58
|
o.string "-s", "--statement", "process a single statement"
|
49
|
-
# separator must go here to show up correctly - slop bug?
|
50
|
-
o.separator ""
|
51
|
-
o.separator "Connection options:"
|
52
59
|
o.on "-v", "--version", "print the version" do
|
53
60
|
log Dexter::VERSION
|
54
61
|
exit
|
@@ -57,10 +64,12 @@ Options:)
|
|
57
64
|
log o
|
58
65
|
exit
|
59
66
|
end
|
60
|
-
o.
|
61
|
-
o.
|
62
|
-
o.string "-
|
63
|
-
o.
|
67
|
+
o.separator ""
|
68
|
+
o.separator "Connection options:"
|
69
|
+
o.string "-d", "--dbname", "database name"
|
70
|
+
o.string "-h", "--host", "database host"
|
71
|
+
o.integer "-p", "--port", "database port"
|
72
|
+
o.string "-U", "--username", "database user"
|
64
73
|
end
|
65
74
|
|
66
75
|
arguments = opts.arguments
|
@@ -70,11 +79,11 @@ Options:)
|
|
70
79
|
|
71
80
|
# TODO don't use global var
|
72
81
|
$log_level = options[:log_level].to_s.downcase
|
73
|
-
|
82
|
+
raise Dexter::Abort, "Unknown log level" unless ["error", "info", "debug", "debug2", "debug3"].include?($log_level)
|
74
83
|
|
75
84
|
[arguments, options]
|
76
85
|
rescue Slop::Error => e
|
77
|
-
|
86
|
+
raise Dexter::Abort, e.message
|
78
87
|
end
|
79
88
|
end
|
80
89
|
end
|
data/lib/dexter/indexer.rb
CHANGED
@@ -14,6 +14,7 @@ module Dexter
|
|
14
14
|
@analyze = options[:analyze]
|
15
15
|
@min_cost_savings_pct = options[:min_cost_savings_pct].to_i
|
16
16
|
@options = options
|
17
|
+
@mutex = Mutex.new
|
17
18
|
|
18
19
|
create_extension unless extension_exists?
|
19
20
|
execute("SET lock_timeout = '5s'")
|
@@ -25,6 +26,23 @@ module Dexter
|
|
25
26
|
process_queries(queries)
|
26
27
|
end
|
27
28
|
|
29
|
+
def stat_activity
|
30
|
+
execute <<-SQL
|
31
|
+
SELECT
|
32
|
+
pid || ':' || COALESCE(query_start, xact_start) AS id,
|
33
|
+
query,
|
34
|
+
EXTRACT(EPOCH FROM NOW() - COALESCE(query_start, xact_start)) * 1000.0 AS duration_ms
|
35
|
+
FROM
|
36
|
+
pg_stat_activity
|
37
|
+
WHERE
|
38
|
+
datname = current_database()
|
39
|
+
AND state = 'active'
|
40
|
+
AND pid != pg_backend_pid()
|
41
|
+
ORDER BY
|
42
|
+
1
|
43
|
+
SQL
|
44
|
+
end
|
45
|
+
|
28
46
|
def process_queries(queries)
|
29
47
|
# reset hypothetical indexes
|
30
48
|
reset_hypothetical_indexes
|
@@ -89,13 +107,13 @@ module Dexter
|
|
89
107
|
analyze_tables(tables) if tables.any? && (@analyze || @log_level == "debug2")
|
90
108
|
|
91
109
|
# create hypothetical indexes and explain queries
|
92
|
-
candidates = tables.any? ? create_hypothetical_indexes(queries.select(&:candidate_tables)
|
110
|
+
candidates = tables.any? ? create_hypothetical_indexes(queries.select(&:candidate_tables)) : {}
|
93
111
|
|
94
112
|
# see if new indexes were used and meet bar
|
95
113
|
new_indexes = determine_indexes(queries, candidates, tables)
|
96
114
|
|
97
115
|
# display and create new indexes
|
98
|
-
show_and_create_indexes(new_indexes, queries
|
116
|
+
show_and_create_indexes(new_indexes, queries)
|
99
117
|
end
|
100
118
|
|
101
119
|
private
|
@@ -105,9 +123,9 @@ module Dexter
|
|
105
123
|
begin
|
106
124
|
execute("CREATE EXTENSION IF NOT EXISTS hypopg")
|
107
125
|
rescue PG::UndefinedFile
|
108
|
-
|
126
|
+
raise Dexter::Abort, "Install HypoPG first: https://github.com/ankane/dexter#installation"
|
109
127
|
rescue PG::InsufficientPrivilege
|
110
|
-
|
128
|
+
raise Dexter::Abort, "Use a superuser to run: CREATE EXTENSION hypopg"
|
111
129
|
end
|
112
130
|
end
|
113
131
|
|
@@ -164,9 +182,9 @@ module Dexter
|
|
164
182
|
query.plans << plan(query.statement)
|
165
183
|
if @log_explain
|
166
184
|
# Pass format to prevent ANALYZE
|
167
|
-
puts execute("EXPLAIN (FORMAT TEXT) #{safe_statement(query.statement)}").map { |r| r["QUERY PLAN"] }.join("\n")
|
185
|
+
puts execute("EXPLAIN (FORMAT TEXT) #{safe_statement(query.statement)}", pretty: false).map { |r| r["QUERY PLAN"] }.join("\n")
|
168
186
|
end
|
169
|
-
rescue PG::Error => e
|
187
|
+
rescue PG::Error, JSON::NestingError => e
|
170
188
|
if @log_explain
|
171
189
|
log e.message
|
172
190
|
end
|
@@ -175,7 +193,7 @@ module Dexter
|
|
175
193
|
end
|
176
194
|
end
|
177
195
|
|
178
|
-
def create_hypothetical_indexes(queries
|
196
|
+
def create_hypothetical_indexes(queries)
|
179
197
|
candidates = {}
|
180
198
|
|
181
199
|
# get initial costs for queries
|
@@ -345,11 +363,12 @@ module Dexter
|
|
345
363
|
winning_cost < query.initial_cost * savings_ratio
|
346
364
|
end
|
347
365
|
|
366
|
+
query_indexes = [winning_index]
|
367
|
+
new_cost3 = winning_cost
|
368
|
+
query.pass3_indexes = query_indexes
|
369
|
+
|
348
370
|
if use_winning
|
349
|
-
query_indexes = [winning_index]
|
350
371
|
cost_savings3 = true
|
351
|
-
new_cost3 = winning_cost
|
352
|
-
query.pass3_indexes = query_indexes
|
353
372
|
else
|
354
373
|
suggest_index = false
|
355
374
|
end
|
@@ -398,11 +417,11 @@ module Dexter
|
|
398
417
|
end
|
399
418
|
end
|
400
419
|
|
401
|
-
def show_and_create_indexes(new_indexes, queries
|
420
|
+
def show_and_create_indexes(new_indexes, queries)
|
402
421
|
# print summary
|
403
422
|
if new_indexes.any?
|
404
423
|
new_indexes.each do |index|
|
405
|
-
log "Index found: #{index[:table]} (#{index[:columns].join(", ")})"
|
424
|
+
log colorize("Index found: #{index[:table]} (#{index[:columns].join(", ")})", :green)
|
406
425
|
end
|
407
426
|
else
|
408
427
|
log "No new indexes found"
|
@@ -439,7 +458,7 @@ module Dexter
|
|
439
458
|
log "Pass3: #{query.costs[3]} : #{log_indexes(query.pass3_indexes || [])}"
|
440
459
|
end
|
441
460
|
log "Final: #{query.new_cost} : #{log_indexes(query.suggest_index ? query_indexes : [])}"
|
442
|
-
if
|
461
|
+
if (query.pass1_indexes.any? || query.pass2_indexes.any?) && !query.suggest_index
|
443
462
|
log "Need #{@min_cost_savings_pct}% cost savings to suggest index"
|
444
463
|
end
|
445
464
|
else
|
@@ -479,6 +498,9 @@ module Dexter
|
|
479
498
|
|
480
499
|
def conn
|
481
500
|
@conn ||= begin
|
501
|
+
# set connect timeout if none set
|
502
|
+
ENV["PGCONNECT_TIMEOUT"] ||= "2"
|
503
|
+
|
482
504
|
if @options[:dbname] =~ /\Apostgres(ql)?:\/\//
|
483
505
|
config = @options[:dbname]
|
484
506
|
else
|
@@ -486,17 +508,17 @@ module Dexter
|
|
486
508
|
host: @options[:host],
|
487
509
|
port: @options[:port],
|
488
510
|
dbname: @options[:dbname],
|
489
|
-
user: @options[:
|
511
|
+
user: @options[:username]
|
490
512
|
}.reject { |_, value| value.to_s.empty? }
|
491
513
|
config = config[:dbname] if config.keys == [:dbname] && config[:dbname].include?("=")
|
492
514
|
end
|
493
515
|
PG::Connection.new(config)
|
494
516
|
end
|
495
517
|
rescue PG::ConnectionBad => e
|
496
|
-
|
518
|
+
raise Dexter::Abort, e.message
|
497
519
|
end
|
498
520
|
|
499
|
-
def execute(query)
|
521
|
+
def execute(query, pretty: true)
|
500
522
|
# use exec_params instead of exec for security
|
501
523
|
#
|
502
524
|
# Unlike PQexec, PQexecParams allows at most one SQL command in the given string.
|
@@ -504,14 +526,17 @@ module Dexter
|
|
504
526
|
# This is a limitation of the underlying protocol, but has some usefulness
|
505
527
|
# as an extra defense against SQL-injection attacks.
|
506
528
|
# https://www.postgresql.org/docs/current/static/libpq-exec.html
|
507
|
-
query = squish(query)
|
508
|
-
log "
|
509
|
-
|
529
|
+
query = squish(query) if pretty
|
530
|
+
log colorize("[sql] #{query}", :cyan) if @log_sql
|
531
|
+
|
532
|
+
@mutex.synchronize do
|
533
|
+
conn.exec_params(query, []).to_a
|
534
|
+
end
|
510
535
|
end
|
511
536
|
|
512
537
|
def plan(query)
|
513
538
|
# strip semi-colons as another measure of defense
|
514
|
-
JSON.parse(execute("EXPLAIN (FORMAT JSON) #{safe_statement(query)}").first["QUERY PLAN"]).first["Plan"]
|
539
|
+
JSON.parse(execute("EXPLAIN (FORMAT JSON) #{safe_statement(query)}", pretty: false).first["QUERY PLAN"], max_nesting: 1000).first["Plan"]
|
515
540
|
end
|
516
541
|
|
517
542
|
# TODO for multicolumn indexes, use ordering
|
@@ -571,7 +596,13 @@ module Dexter
|
|
571
596
|
|
572
597
|
view_tables = {}
|
573
598
|
result.each do |row|
|
574
|
-
|
599
|
+
begin
|
600
|
+
view_tables[row["table_name"]] = PgQuery.parse(row["definition"]).tables
|
601
|
+
rescue PgQuery::ParseError
|
602
|
+
if @log_level.start_with?("debug")
|
603
|
+
log colorize("ERROR: Cannot parse view definition: #{row["table_name"]}", :red)
|
604
|
+
end
|
605
|
+
end
|
575
606
|
end
|
576
607
|
|
577
608
|
view_tables
|
data/lib/dexter/logging.rb
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
module Dexter
|
2
2
|
module Logging
|
3
|
+
COLOR_CODES = {
|
4
|
+
red: 31,
|
5
|
+
green: 32,
|
6
|
+
yellow: 33,
|
7
|
+
cyan: 36
|
8
|
+
}
|
9
|
+
|
10
|
+
def output
|
11
|
+
$stdout
|
12
|
+
end
|
13
|
+
|
3
14
|
def log(message = "")
|
4
|
-
puts
|
15
|
+
output.puts(message) unless $log_level == "error"
|
5
16
|
end
|
6
17
|
|
7
|
-
def
|
8
|
-
|
18
|
+
def colorize(message, color)
|
19
|
+
if output.tty?
|
20
|
+
"\e[#{COLOR_CODES[color]}m#{message}\e[0m"
|
21
|
+
else
|
22
|
+
message
|
23
|
+
end
|
9
24
|
end
|
10
25
|
end
|
11
26
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dexter
|
2
|
+
class PgStatActivityParser < LogParser
|
3
|
+
def perform
|
4
|
+
queries = {}
|
5
|
+
|
6
|
+
loop do
|
7
|
+
new_queries = {}
|
8
|
+
@logfile.stat_activity.each do |row|
|
9
|
+
new_queries[row["id"]] = row
|
10
|
+
end
|
11
|
+
|
12
|
+
# store queries after they complete
|
13
|
+
queries.each do |id, row|
|
14
|
+
unless new_queries[id]
|
15
|
+
process_entry(row["query"], row["duration_ms"].to_f)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
queries = new_queries
|
20
|
+
|
21
|
+
sleep(1)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/dexter/processor.rb
CHANGED
@@ -6,15 +6,19 @@ module Dexter
|
|
6
6
|
@logfile = logfile
|
7
7
|
|
8
8
|
@collector = Collector.new(min_time: options[:min_time], min_calls: options[:min_calls])
|
9
|
+
@indexer = Indexer.new(options)
|
10
|
+
|
9
11
|
@log_parser =
|
10
|
-
if
|
12
|
+
if @logfile == :pg_stat_activity
|
13
|
+
PgStatActivityParser.new(@indexer, @collector)
|
14
|
+
elsif options[:input_format] == "csv"
|
11
15
|
CsvLogParser.new(logfile, @collector)
|
16
|
+
elsif options[:input_format] == "sql"
|
17
|
+
SqlLogParser.new(logfile, @collector)
|
12
18
|
else
|
13
19
|
LogParser.new(logfile, @collector)
|
14
20
|
end
|
15
21
|
|
16
|
-
@indexer = Indexer.new(options)
|
17
|
-
|
18
22
|
@starting_interval = 3
|
19
23
|
@interval = options[:interval]
|
20
24
|
|
@@ -25,7 +29,7 @@ module Dexter
|
|
25
29
|
end
|
26
30
|
|
27
31
|
def perform
|
28
|
-
if @logfile
|
32
|
+
if [STDIN, :pg_stat_activity].include?(@logfile)
|
29
33
|
Thread.abort_on_exception = true
|
30
34
|
Thread.new do
|
31
35
|
sleep(@starting_interval)
|
@@ -33,7 +37,7 @@ module Dexter
|
|
33
37
|
begin
|
34
38
|
process_queries
|
35
39
|
rescue PG::ServerError => e
|
36
|
-
log "ERROR: #{e.class.name}: #{e.message}"
|
40
|
+
log colorize("ERROR: #{e.class.name}: #{e.message}", :red)
|
37
41
|
end
|
38
42
|
sleep(@interval)
|
39
43
|
end
|
@@ -43,7 +47,7 @@ module Dexter
|
|
43
47
|
begin
|
44
48
|
@log_parser.perform
|
45
49
|
rescue Errno::ENOENT => e
|
46
|
-
|
50
|
+
raise Dexter::Abort, "ERROR: #{e.message}"
|
47
51
|
end
|
48
52
|
|
49
53
|
process_queries
|
data/lib/dexter/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgdexter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: slop
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 4.2
|
19
|
+
version: 4.8.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 4.2
|
26
|
+
version: 4.8.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: pg
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.18.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.18.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pg_query
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,23 +95,16 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
description:
|
98
|
-
email:
|
99
|
-
- andrew@chartkick.com
|
98
|
+
email: andrew@chartkick.com
|
100
99
|
executables:
|
101
100
|
- dexter
|
102
101
|
extensions: []
|
103
102
|
extra_rdoc_files: []
|
104
103
|
files:
|
105
|
-
- ".gitignore"
|
106
|
-
- ".travis.yml"
|
107
104
|
- CHANGELOG.md
|
108
|
-
- Gemfile
|
109
105
|
- LICENSE.txt
|
110
106
|
- README.md
|
111
|
-
- Rakefile
|
112
107
|
- exe/dexter
|
113
|
-
- guides/Hosted-Postgres.md
|
114
|
-
- guides/Linux.md
|
115
108
|
- lib/dexter.rb
|
116
109
|
- lib/dexter/client.rb
|
117
110
|
- lib/dexter/collector.rb
|
@@ -119,12 +112,14 @@ files:
|
|
119
112
|
- lib/dexter/indexer.rb
|
120
113
|
- lib/dexter/log_parser.rb
|
121
114
|
- lib/dexter/logging.rb
|
115
|
+
- lib/dexter/pg_stat_activity_parser.rb
|
122
116
|
- lib/dexter/processor.rb
|
123
117
|
- lib/dexter/query.rb
|
118
|
+
- lib/dexter/sql_log_parser.rb
|
124
119
|
- lib/dexter/version.rb
|
125
|
-
- pgdexter.gemspec
|
126
120
|
homepage: https://github.com/ankane/dexter
|
127
|
-
licenses:
|
121
|
+
licenses:
|
122
|
+
- MIT
|
128
123
|
metadata: {}
|
129
124
|
post_install_message:
|
130
125
|
rdoc_options: []
|
@@ -134,15 +129,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
129
|
requirements:
|
135
130
|
- - ">="
|
136
131
|
- !ruby/object:Gem::Version
|
137
|
-
version: '
|
132
|
+
version: '2.2'
|
138
133
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
134
|
requirements:
|
140
135
|
- - ">="
|
141
136
|
- !ruby/object:Gem::Version
|
142
137
|
version: '0'
|
143
138
|
requirements: []
|
144
|
-
|
145
|
-
rubygems_version: 2.6.13
|
139
|
+
rubygems_version: 3.1.2
|
146
140
|
signing_key:
|
147
141
|
specification_version: 4
|
148
142
|
summary: The automatic indexer for Postgres
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
rvm: 2.4.1
|
3
|
-
cache: bundler
|
4
|
-
script: bundle exec rake test
|
5
|
-
addons:
|
6
|
-
postgresql: "9.6"
|
7
|
-
before_script:
|
8
|
-
- sudo apt-get install postgresql-server-dev-9.6
|
9
|
-
- git clone https://github.com/dalibo/hypopg.git
|
10
|
-
- cd hypopg
|
11
|
-
- make
|
12
|
-
- sudo make install
|
13
|
-
- psql -c 'create database dexter_test;' -U postgres
|
14
|
-
notifications:
|
15
|
-
email:
|
16
|
-
on_success: never
|
17
|
-
on_failure: change
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/guides/Hosted-Postgres.md
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
# Hosted Postgres
|
2
|
-
|
3
|
-
Some hosted providers like Amazon RDS and Heroku do not support the HypoPG extension, which Dexter needs to run. Hopefully this will change with time. For now, we can spin up a separate database instance to run Dexter. It’s not super convenient, but can be useful to do from time to time.
|
4
|
-
|
5
|
-
### Install Postgres and Ruby
|
6
|
-
|
7
|
-
Linux
|
8
|
-
|
9
|
-
```sh
|
10
|
-
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
11
|
-
sudo apt-get install -y wget ca-certificates
|
12
|
-
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
13
|
-
sudo apt-get update
|
14
|
-
sudo apt-get install -y postgresql-9.6 postgresql-server-dev-9.6
|
15
|
-
sudo -u postgres createuser $(whoami) -s
|
16
|
-
sudo apt-get install -y ruby2.2 ruby2.2-dev
|
17
|
-
```
|
18
|
-
|
19
|
-
Mac
|
20
|
-
|
21
|
-
```sh
|
22
|
-
brew install postgresql
|
23
|
-
brew install ruby
|
24
|
-
```
|
25
|
-
|
26
|
-
### Install HypoPG and Dexter
|
27
|
-
|
28
|
-
HypoPG
|
29
|
-
|
30
|
-
```sh
|
31
|
-
cd /tmp
|
32
|
-
curl -L https://github.com/dalibo/hypopg/archive/1.0.0.tar.gz | tar xz
|
33
|
-
cd hypopg-1.0.0
|
34
|
-
make
|
35
|
-
make install # may need sudo
|
36
|
-
```
|
37
|
-
|
38
|
-
Dexter
|
39
|
-
|
40
|
-
```sh
|
41
|
-
gem install pgdexter # may need sudo
|
42
|
-
```
|
43
|
-
|
44
|
-
### Download logs
|
45
|
-
|
46
|
-
#### Amazon RDS
|
47
|
-
|
48
|
-
Create an IAM user with the policy below:
|
49
|
-
|
50
|
-
```
|
51
|
-
{
|
52
|
-
"Statement": [
|
53
|
-
{
|
54
|
-
"Action": [
|
55
|
-
"rds:DescribeDBLogFiles",
|
56
|
-
"rds:DownloadDBLogFilePortion"
|
57
|
-
],
|
58
|
-
"Effect": "Allow",
|
59
|
-
"Resource": "*"
|
60
|
-
}
|
61
|
-
]
|
62
|
-
}
|
63
|
-
```
|
64
|
-
|
65
|
-
And run:
|
66
|
-
|
67
|
-
```sh
|
68
|
-
aws configure
|
69
|
-
gem install pghero_logs # may need sudo
|
70
|
-
pghero_logs download <instance-id>
|
71
|
-
```
|
72
|
-
|
73
|
-
#### Heroku
|
74
|
-
|
75
|
-
Production-tier databases only
|
76
|
-
|
77
|
-
```sh
|
78
|
-
heroku logs -p postgres > postgresql.log
|
79
|
-
```
|
80
|
-
|
81
|
-
### Dump and restore
|
82
|
-
|
83
|
-
We recommend creating a new instance from a snapshot for the dump to avoid affecting customers.
|
84
|
-
|
85
|
-
```sh
|
86
|
-
pg_dump -v -j 8 -Fd -f /tmp/newout.dir <connection-options>
|
87
|
-
```
|
88
|
-
|
89
|
-
Then shutdown the dump instance. Restore with:
|
90
|
-
|
91
|
-
```sh
|
92
|
-
createdb dexter_restore
|
93
|
-
pg_restore -v -j 8 -x -O --format=d -d dexter_restore /tmp/newout.dir/
|
94
|
-
```
|
95
|
-
|
96
|
-
### Run Dexter
|
97
|
-
|
98
|
-
```sh
|
99
|
-
dexter dexter_restore postgresql.log* --analyze
|
100
|
-
```
|
101
|
-
|
102
|
-
:tada:
|
data/guides/Linux.md
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
# Linux Packages
|
2
|
-
|
3
|
-
Distributions
|
4
|
-
|
5
|
-
- [Ubuntu 16.04 (Xenial)](#ubuntu-1604-xenial)
|
6
|
-
- [Ubuntu 14.04 (Trusty)](#ubuntu-1404-trusty)
|
7
|
-
- [Debian 9 (Stretch)](#debian-9-stretch)
|
8
|
-
- [Debian 8 (Jesse)](#debian-8-jesse)
|
9
|
-
- [CentOS / RHEL 7](#centos--rhel-7)
|
10
|
-
- [SUSE Linux Enterprise Server 12](#suse-linux-enterprise-server-12)
|
11
|
-
|
12
|
-
### Ubuntu 16.04 (Xenial)
|
13
|
-
|
14
|
-
```sh
|
15
|
-
wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
|
16
|
-
sudo wget -O /etc/apt/sources.list.d/dexter.list \
|
17
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/ubuntu/16.04.repo
|
18
|
-
sudo apt-get update
|
19
|
-
sudo apt-get -y install dexter
|
20
|
-
```
|
21
|
-
|
22
|
-
### Ubuntu 14.04 (Trusty)
|
23
|
-
|
24
|
-
```sh
|
25
|
-
wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
|
26
|
-
sudo wget -O /etc/apt/sources.list.d/dexter.list \
|
27
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/ubuntu/14.04.repo
|
28
|
-
sudo apt-get update
|
29
|
-
sudo apt-get install dexter
|
30
|
-
```
|
31
|
-
|
32
|
-
### Debian 9 (Stretch)
|
33
|
-
|
34
|
-
```sh
|
35
|
-
wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
|
36
|
-
sudo wget -O /etc/apt/sources.list.d/dexter.list \
|
37
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/debian/9.repo
|
38
|
-
sudo apt-get update
|
39
|
-
sudo apt-get install dexter
|
40
|
-
```
|
41
|
-
|
42
|
-
### Debian 8 (Jesse)
|
43
|
-
|
44
|
-
```sh
|
45
|
-
wget -qO- https://dl.packager.io/srv/pghero/dexter/key | sudo apt-key add -
|
46
|
-
sudo wget -O /etc/apt/sources.list.d/dexter.list \
|
47
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/debian/8.repo
|
48
|
-
sudo apt-get update
|
49
|
-
sudo apt-get install dexter
|
50
|
-
```
|
51
|
-
|
52
|
-
### CentOS / RHEL 7
|
53
|
-
|
54
|
-
```sh
|
55
|
-
sudo wget -O /etc/yum.repos.d/dexter.repo \
|
56
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/el/7.repo
|
57
|
-
sudo yum install dexter
|
58
|
-
```
|
59
|
-
|
60
|
-
### SUSE Linux Enterprise Server 12
|
61
|
-
|
62
|
-
```sh
|
63
|
-
sudo wget -O /etc/zypp/repos.d/dexter.repo \
|
64
|
-
https://dl.packager.io/srv/pghero/dexter/master/installer/sles/12.repo
|
65
|
-
sudo zypper install dexter
|
66
|
-
```
|
67
|
-
|
68
|
-
## Credits
|
69
|
-
|
70
|
-
:heart: Made possible by [Packager](https://packager.io/)
|
data/pgdexter.gemspec
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
lib = File.expand_path("../lib", __FILE__)
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require "dexter/version"
|
6
|
-
|
7
|
-
Gem::Specification.new do |spec|
|
8
|
-
spec.name = "pgdexter"
|
9
|
-
spec.version = Dexter::VERSION
|
10
|
-
spec.authors = ["Andrew Kane"]
|
11
|
-
spec.email = ["andrew@chartkick.com"]
|
12
|
-
|
13
|
-
spec.summary = "The automatic indexer for Postgres"
|
14
|
-
spec.homepage = "https://github.com/ankane/dexter"
|
15
|
-
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
-
f.match(%r{^(test|spec|features)/})
|
18
|
-
end
|
19
|
-
spec.bindir = "exe"
|
20
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
-
spec.require_paths = ["lib"]
|
22
|
-
|
23
|
-
spec.add_dependency "slop", ">= 4.2.0"
|
24
|
-
spec.add_dependency "pg"
|
25
|
-
spec.add_dependency "pg_query"
|
26
|
-
|
27
|
-
spec.add_development_dependency "bundler"
|
28
|
-
spec.add_development_dependency "rake"
|
29
|
-
spec.add_development_dependency "minitest"
|
30
|
-
end
|