pgdexter 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +18 -0
- data/CHANGELOG.md +5 -0
- data/README.md +3 -2
- data/lib/dexter/client.rb +1 -0
- data/lib/dexter/indexer.rb +90 -27
- data/lib/dexter/log_parser.rb +4 -6
- data/lib/dexter/processor.rb +7 -3
- data/lib/dexter/query.rb +18 -1
- data/lib/dexter/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 496685bbacfac7387182be3ebda3515a492723cc
|
4
|
+
data.tar.gz: b99548fccab8337b9ff5765a6182472abba9bb06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f9b30e7355c26cf7084b8deee9735206da484cd9f4725a5c1f751db5808dc32d66954341c5be3ba46cd783fa2e360e2b9c32ab0667aa2372213ebca695b29ff
|
7
|
+
data.tar.gz: 41346b8ed9906e24c68847c26bc9ffe54176eb0e74dc771138ec266ebccfca3e0d18ebd37223b73f1f2b8a9f512320502e4ad9597982bc253846881ea7e93e8d
|
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
+
- wget https://github.com/dalibo/hypopg/archive/1.0.0.tar.gz
|
10
|
+
- tar xf 1.0.0.tar.gz
|
11
|
+
- cd hypopg-1.0.0
|
12
|
+
- make
|
13
|
+
- sudo make install
|
14
|
+
- psql -c 'create database dexter_test;' -U postgres
|
15
|
+
notifications:
|
16
|
+
email:
|
17
|
+
on_success: never
|
18
|
+
on_failure: change
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,13 +4,14 @@ The automatic indexer for Postgres
|
|
4
4
|
|
5
5
|
[Read about how it works](https://medium.com/@ankane/introducing-dexter-the-automatic-indexer-for-postgres-5f8fa8b28f27)
|
6
6
|
|
7
|
+
[![Build Status](https://travis-ci.org/ankane/dexter.svg?branch=master)](https://travis-ci.org/ankane/dexter)
|
8
|
+
|
7
9
|
## Installation
|
8
10
|
|
9
11
|
First, install [HypoPG](https://github.com/dalibo/hypopg) on your database server. This doesn’t require a restart.
|
10
12
|
|
11
13
|
```sh
|
12
|
-
|
13
|
-
tar xf 1.0.0.tar.gz
|
14
|
+
curl -L https://github.com/dalibo/hypopg/archive/1.0.0.tar.gz | tar -x
|
14
15
|
cd hypopg-1.0.0
|
15
16
|
make
|
16
17
|
make install # may need sudo
|
data/lib/dexter/client.rb
CHANGED
@@ -30,6 +30,7 @@ module Dexter
|
|
30
30
|
Options:)
|
31
31
|
o.boolean "--create", "create indexes", default: false
|
32
32
|
o.array "--exclude", "prevent specific tables from being indexed"
|
33
|
+
o.string "--include", "only include specific tables"
|
33
34
|
o.integer "--interval", "time to wait between processing queries, in seconds", default: 60
|
34
35
|
o.float "--min-time", "only process queries that have consumed a certain amount of DB time, in minutes", default: 0
|
35
36
|
o.boolean "--pg-stat-statements", "use pg_stat_statements", default: false, help: false
|
data/lib/dexter/indexer.rb
CHANGED
@@ -7,11 +7,13 @@ module Dexter
|
|
7
7
|
@create = options[:create]
|
8
8
|
@log_level = options[:log_level]
|
9
9
|
@exclude_tables = options[:exclude]
|
10
|
+
@include_tables = Array(options[:include].split(",")) if options[:include]
|
10
11
|
@log_sql = options[:log_sql]
|
11
12
|
@log_explain = options[:log_explain]
|
12
13
|
@min_time = options[:min_time] || 0
|
13
14
|
|
14
15
|
create_extension unless extension_exists?
|
16
|
+
execute("SET lock_timeout = '5s'")
|
15
17
|
end
|
16
18
|
|
17
19
|
def process_stat_statements
|
@@ -30,6 +32,10 @@ module Dexter
|
|
30
32
|
query.missing_tables = !query.tables.all? { |t| tables.include?(t) }
|
31
33
|
end
|
32
34
|
|
35
|
+
if @include_tables
|
36
|
+
tables = Set.new(tables.to_a & @include_tables)
|
37
|
+
end
|
38
|
+
|
33
39
|
# exclude user specified tables
|
34
40
|
# TODO exclude write-heavy tables
|
35
41
|
@exclude_tables.each do |table|
|
@@ -43,7 +49,7 @@ module Dexter
|
|
43
49
|
candidates = tables.any? ? create_hypothetical_indexes(queries.reject(&:missing_tables), tables) : {}
|
44
50
|
|
45
51
|
# see if new indexes were used and meet bar
|
46
|
-
new_indexes = determine_indexes(queries, candidates)
|
52
|
+
new_indexes = determine_indexes(queries, candidates, tables)
|
47
53
|
|
48
54
|
# display and create new indexes
|
49
55
|
show_and_create_indexes(new_indexes)
|
@@ -55,6 +61,8 @@ module Dexter
|
|
55
61
|
execute("SET client_min_messages = warning")
|
56
62
|
begin
|
57
63
|
execute("CREATE EXTENSION IF NOT EXISTS hypopg")
|
64
|
+
rescue PG::UndefinedFile
|
65
|
+
abort "Install HypoPG first: https://github.com/ankane/dexter#installation"
|
58
66
|
rescue PG::InsufficientPrivilege
|
59
67
|
abort "Use a superuser to run: CREATE EXTENSION hypopg"
|
60
68
|
end
|
@@ -115,41 +123,85 @@ module Dexter
|
|
115
123
|
end
|
116
124
|
|
117
125
|
def create_hypothetical_indexes(queries, tables)
|
126
|
+
candidates = {}
|
127
|
+
|
118
128
|
# get initial costs for queries
|
119
129
|
calculate_plan(queries)
|
120
|
-
explainable_queries = queries.select
|
130
|
+
explainable_queries = queries.select { |q| q.explainable? && q.high_cost? }
|
121
131
|
|
122
|
-
#
|
123
|
-
|
124
|
-
indexes(tables).each do |index|
|
125
|
-
# TODO make sure btree
|
126
|
-
index_set << [index["table"], index["columns"]]
|
127
|
-
end
|
132
|
+
# filter tables for performance
|
133
|
+
tables = Set.new(explainable_queries.flat_map(&:tables))
|
128
134
|
|
129
|
-
|
130
|
-
|
131
|
-
|
135
|
+
if tables.any?
|
136
|
+
# get existing indexes
|
137
|
+
index_set = Set.new
|
138
|
+
indexes(tables).each do |index|
|
139
|
+
# TODO make sure btree
|
140
|
+
index_set << [index["table"], index["columns"]]
|
141
|
+
end
|
142
|
+
|
143
|
+
# since every set of multi-column indexes are expensive
|
144
|
+
# try to parse out columns
|
145
|
+
possible_columns = Set.new
|
146
|
+
explainable_queries.each do |query|
|
147
|
+
find_columns(query.tree).each do |col|
|
148
|
+
last_col = col["fields"].last
|
149
|
+
if last_col["String"]
|
150
|
+
possible_columns << last_col["String"]["str"]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# create hypothetical indexes
|
156
|
+
columns_by_table = columns(tables).select { |c| possible_columns.include?(c[:column]) }.group_by { |c| c[:table] }
|
132
157
|
|
133
|
-
|
134
|
-
|
158
|
+
# create single column indexes
|
159
|
+
create_hypothetical_indexes_helper(columns_by_table, 1, index_set, candidates)
|
135
160
|
|
136
|
-
|
137
|
-
|
161
|
+
# get next round of costs
|
162
|
+
calculate_plan(explainable_queries)
|
138
163
|
|
139
|
-
|
140
|
-
|
164
|
+
# create multicolumn indexes
|
165
|
+
create_hypothetical_indexes_helper(columns_by_table, 2, index_set, candidates)
|
141
166
|
|
142
|
-
|
143
|
-
|
167
|
+
# get next round of costs
|
168
|
+
calculate_plan(explainable_queries)
|
169
|
+
end
|
144
170
|
|
145
171
|
candidates
|
146
172
|
end
|
147
173
|
|
148
|
-
def
|
174
|
+
def find_columns(plan)
|
175
|
+
find_by_key(plan, "ColumnRef")
|
176
|
+
end
|
177
|
+
|
178
|
+
def find_indexes(plan)
|
179
|
+
find_by_key(plan, "Index Name")
|
180
|
+
end
|
181
|
+
|
182
|
+
def find_by_key(plan, key)
|
183
|
+
indexes = []
|
184
|
+
case plan
|
185
|
+
when Hash
|
186
|
+
plan.each do |k, v|
|
187
|
+
if k == key
|
188
|
+
indexes << v
|
189
|
+
else
|
190
|
+
indexes.concat(find_by_key(v, key))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
when Array
|
194
|
+
indexes.concat(plan.flat_map { |v| find_by_key(v, key) })
|
195
|
+
end
|
196
|
+
indexes
|
197
|
+
end
|
198
|
+
|
199
|
+
def determine_indexes(queries, candidates, tables)
|
149
200
|
new_indexes = {}
|
201
|
+
index_name_to_columns = candidates.invert
|
150
202
|
|
151
203
|
queries.each do |query|
|
152
|
-
if query.explainable?
|
204
|
+
if query.explainable? && query.high_cost?
|
153
205
|
new_cost, new_cost2 = query.costs[1..2]
|
154
206
|
|
155
207
|
cost_savings = new_cost < query.initial_cost * 0.5
|
@@ -159,10 +211,13 @@ module Dexter
|
|
159
211
|
query.new_cost = cost_savings2 ? new_cost2 : new_cost
|
160
212
|
|
161
213
|
query_indexes = []
|
162
|
-
|
163
|
-
|
214
|
+
key = cost_savings2 ? 2 : 1
|
215
|
+
indexes = find_indexes(query.plans[key]).uniq.sort
|
216
|
+
|
217
|
+
indexes.each do |index_name|
|
218
|
+
col_set = index_name_to_columns[index_name]
|
164
219
|
|
165
|
-
if
|
220
|
+
if col_set
|
166
221
|
index = {
|
167
222
|
table: col_set[0][:table],
|
168
223
|
columns: col_set.map { |c| c[:column] }
|
@@ -179,7 +234,11 @@ module Dexter
|
|
179
234
|
|
180
235
|
if @log_level == "debug2"
|
181
236
|
log "Processed #{query.fingerprint}"
|
182
|
-
if
|
237
|
+
if tables.empty?
|
238
|
+
log "No candidate tables for indexes"
|
239
|
+
elsif query.explainable? && !query.high_cost?
|
240
|
+
log "Low initial cost: #{query.initial_cost}"
|
241
|
+
elsif query.explainable?
|
183
242
|
log "Cost: #{query.initial_cost} -> #{query.new_cost}"
|
184
243
|
|
185
244
|
if query_indexes.any?
|
@@ -233,8 +292,12 @@ module Dexter
|
|
233
292
|
statement = "CREATE INDEX CONCURRENTLY ON #{quote_ident(index[:table])} (#{index[:columns].map { |c| quote_ident(c) }.join(", ")})"
|
234
293
|
log "Creating index: #{statement}"
|
235
294
|
started_at = Time.now
|
236
|
-
|
237
|
-
|
295
|
+
begin
|
296
|
+
execute(statement)
|
297
|
+
log "Index created: #{((Time.now - started_at) * 1000).to_i} ms"
|
298
|
+
rescue PG::LockNotAvailable => e
|
299
|
+
log "Could not acquire lock: #{index[:table]}"
|
300
|
+
end
|
238
301
|
end
|
239
302
|
end
|
240
303
|
end
|
data/lib/dexter/log_parser.rb
CHANGED
@@ -6,6 +6,8 @@ module Dexter
|
|
6
6
|
def initialize(logfile, collector)
|
7
7
|
@logfile = logfile
|
8
8
|
@collector = collector
|
9
|
+
|
10
|
+
abort "Log file not found" unless File.exist?(logfile)
|
9
11
|
end
|
10
12
|
|
11
13
|
def perform
|
@@ -38,12 +40,8 @@ module Dexter
|
|
38
40
|
yield line
|
39
41
|
end
|
40
42
|
else
|
41
|
-
|
42
|
-
|
43
|
-
yield line
|
44
|
-
end
|
45
|
-
rescue Errno::ENOENT
|
46
|
-
abort "Log file not found"
|
43
|
+
File.foreach(@logfile) do |line|
|
44
|
+
yield line
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
data/lib/dexter/processor.rb
CHANGED
@@ -3,8 +3,6 @@ module Dexter
|
|
3
3
|
include Logging
|
4
4
|
|
5
5
|
def initialize(database_url, logfile, options)
|
6
|
-
log "Started"
|
7
|
-
|
8
6
|
@logfile = logfile
|
9
7
|
|
10
8
|
@collector = Collector.new(min_time: options[:min_time])
|
@@ -16,6 +14,8 @@ module Dexter
|
|
16
14
|
|
17
15
|
@mutex = Mutex.new
|
18
16
|
@last_checked_at = {}
|
17
|
+
|
18
|
+
log "Started"
|
19
19
|
end
|
20
20
|
|
21
21
|
def perform
|
@@ -24,7 +24,11 @@ module Dexter
|
|
24
24
|
Thread.new do
|
25
25
|
sleep(@starting_interval)
|
26
26
|
loop do
|
27
|
-
|
27
|
+
begin
|
28
|
+
process_queries
|
29
|
+
rescue PG::ServerError => e
|
30
|
+
log "ERROR: #{e.class.name}: #{e.message}"
|
31
|
+
end
|
28
32
|
sleep(@interval)
|
29
33
|
end
|
30
34
|
end
|
data/lib/dexter/query.rb
CHANGED
@@ -13,7 +13,11 @@ module Dexter
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def tables
|
16
|
-
@tables ||=
|
16
|
+
@tables ||= parse ? parse.tables : []
|
17
|
+
end
|
18
|
+
|
19
|
+
def tree
|
20
|
+
parse.tree
|
17
21
|
end
|
18
22
|
|
19
23
|
def explainable?
|
@@ -27,5 +31,18 @@ module Dexter
|
|
27
31
|
def initial_cost
|
28
32
|
costs[0]
|
29
33
|
end
|
34
|
+
|
35
|
+
def high_cost?
|
36
|
+
initial_cost && initial_cost >= 100
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def parse
|
42
|
+
unless defined?(@parse)
|
43
|
+
@parse = PgQuery.parse(statement) rescue nil
|
44
|
+
end
|
45
|
+
@parse
|
46
|
+
end
|
30
47
|
end
|
31
48
|
end
|
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.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: slop
|
@@ -103,6 +103,7 @@ extensions: []
|
|
103
103
|
extra_rdoc_files: []
|
104
104
|
files:
|
105
105
|
- ".gitignore"
|
106
|
+
- ".travis.yml"
|
106
107
|
- CHANGELOG.md
|
107
108
|
- Gemfile
|
108
109
|
- LICENSE.txt
|