pgdexter 0.3.1 → 0.3.2

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
  SHA1:
3
- metadata.gz: 78c0747c456e498798accd4e4881a8041d246e07
4
- data.tar.gz: c4c9cffe3e04d07675c36abf0532db3634e356ea
3
+ metadata.gz: 714767248afe28ad9e354ebb6485b34a8d6aadd0
4
+ data.tar.gz: 545eaaea1df0312049f4cecff3fe9b03b80a6cd9
5
5
  SHA512:
6
- metadata.gz: df826f95f34809bd94cb7450cc43d4bd6beed81606e6e7079842dd4a05e983d4041bf956645c09a290cf8fa37140917da1a12fe332d6268969da5310f688e148
7
- data.tar.gz: f7015b408e7864bfe7e4e136cbb60acbe837cbbe640ec4f43dfb2d9631f2d4cb2b4c86826cb58051d68941bdfa6fd6625029babe7c1609e084db1a4f92a23652
6
+ metadata.gz: '096684887c5d5a48a2df74a2dae6cf70bc2b5e98dd5dc6d7d6d8b583bfb76193ce86166972d9553f89452f16629a8576a30f2627fd8d0b16a9672ce76a316835'
7
+ data.tar.gz: d128734845414672a3f470f138c6a422dc2b24024a66866e50524fe3bb3e949bd4a899305a93f71fb14af3b56820f66e20a00d5ba5788cd0c88bd0afaa064e32
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.3.2
2
+
3
+ - Fixed parsing issue with named prepared statements
4
+ - Fixed parsing issue with multiline queries in csv format
5
+ - Better explanations for indexing decisions
6
+
1
7
  ## 0.3.1
2
8
 
3
9
  - Added support for queries with bind variables
data/exe/dexter CHANGED
@@ -3,6 +3,8 @@
3
3
  require "dexter"
4
4
  begin
5
5
  Dexter::Client.new(ARGV).perform
6
+ rescue Dexter::Abort => e
7
+ abort e.message
6
8
  rescue Interrupt => e
7
9
  # do nothing
8
10
  end
data/lib/dexter/client.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Dexter
2
2
  class Client
3
+ include Logging
4
+
3
5
  attr_reader :arguments, :options
4
6
 
5
7
  def initialize(args)
@@ -14,6 +16,7 @@ module Dexter
14
16
  query = Query.new(options[:statement])
15
17
  Indexer.new(options).process_queries([query])
16
18
  elsif options[:pg_stat_statements]
19
+ # TODO support streaming option
17
20
  Indexer.new(options).process_stat_statements
18
21
  elsif arguments.any?
19
22
  ARGV.replace(arguments)
@@ -35,12 +38,12 @@ Options:)
35
38
  o.string "--include", "only include specific tables"
36
39
  o.string "--input-format", "input format", default: "stderr"
37
40
  o.integer "--interval", "time to wait between processing queries, in seconds", default: 60
38
- o.float "--min-calls", "only process queries that have been called a certain number of times", default: 0
39
- o.float "--min-time", "only process queries that have consumed a certain amount of DB time, in minutes", default: 0
40
- o.boolean "--pg-stat-statements", "use pg_stat_statements", default: false, help: false
41
41
  o.boolean "--log-explain", "log explain", default: false, help: false
42
42
  o.string "--log-level", "log level", default: "info"
43
43
  o.boolean "--log-sql", "log sql", default: false
44
+ o.float "--min-calls", "only process queries that have been called a certain number of times", default: 0
45
+ o.float "--min-time", "only process queries that have consumed a certain amount of DB time, in minutes", default: 0
46
+ o.boolean "--pg-stat-statements", "use pg_stat_statements", default: false, help: false
44
47
  o.string "-s", "--statement", "process a single statement"
45
48
  # separator must go here to show up correctly - slop bug?
46
49
  o.separator ""
@@ -72,9 +75,5 @@ Options:)
72
75
  rescue Slop::Error => e
73
76
  abort e.message
74
77
  end
75
-
76
- def log(message)
77
- $stderr.puts message
78
- end
79
78
  end
80
79
  end
@@ -2,10 +2,15 @@ require "csv"
2
2
 
3
3
  module Dexter
4
4
  class CsvLogParser < LogParser
5
+ FIRST_LINE_REGEX = /\A.+/
6
+
5
7
  def perform
6
- CSV.new(@logfile).each do |row|
8
+ CSV.new(@logfile.to_io).each do |row|
7
9
  if (m = REGEX.match(row[13]))
8
- active_line = m[3]
10
+ # replace first line with match
11
+ # needed for multiline queries
12
+ active_line = row[13].sub(FIRST_LINE_REGEX, m[3])
13
+
9
14
  add_parameters(active_line, row[14]) if row[14]
10
15
  process_entry(active_line, m[1].to_f)
11
16
  end
@@ -30,21 +30,11 @@ module Dexter
30
30
 
31
31
  tables = Set.new(database_tables)
32
32
 
33
- if @include_tables
34
- include_set = Set.new(@include_tables)
35
- tables.keep_if { |t| include_set.include?(t) || include_set.include?(t.split(".")[-1]) }
36
- end
37
-
38
- if @exclude_tables.any?
39
- exclude_set = Set.new(@exclude_tables)
40
- tables.delete_if { |t| exclude_set.include?(t) || exclude_set.include?(t.split(".")[-1]) }
41
- end
42
-
43
33
  # map tables without schema to schema
44
34
  no_schema_tables = {}
45
35
  search_path_index = Hash[search_path.map.with_index.to_a]
46
36
  tables.group_by { |t| t.split(".")[-1] }.each do |group, t2|
47
- no_schema_tables[group] = t2.sort_by { |t| search_path_index[t.split(".")[0]] || 1000000 }[0]
37
+ no_schema_tables[group] = t2.sort_by { |t| [search_path_index[t.split(".")[0]] || 1000000, t] }[0]
48
38
  end
49
39
 
50
40
  # filter queries from other databases and system tables
@@ -59,11 +49,29 @@ module Dexter
59
49
  # set tables
60
50
  tables = Set.new(queries.reject(&:missing_tables).flat_map(&:tables))
61
51
 
52
+ # must come after missing tables set
53
+ if @include_tables
54
+ include_set = Set.new(@include_tables)
55
+ tables.keep_if { |t| include_set.include?(t) || include_set.include?(t.split(".")[-1]) }
56
+ end
57
+
58
+ if @exclude_tables.any?
59
+ exclude_set = Set.new(@exclude_tables)
60
+ tables.delete_if { |t| exclude_set.include?(t) || exclude_set.include?(t.split(".")[-1]) }
61
+ end
62
+
63
+ # remove system tables
64
+ tables.delete_if { |t| t.start_with?("information_schema.") || t.start_with?("pg_catalog.") }
65
+
66
+ queries.each do |query|
67
+ query.candidate_tables = !query.missing_tables && query.tables.any? { |t| tables.include?(t) }
68
+ end
69
+
62
70
  # analyze tables if needed
63
71
  analyze_tables(tables) if tables.any? && (@analyze || @log_level == "debug2")
64
72
 
65
73
  # create hypothetical indexes and explain queries
66
- candidates = tables.any? ? create_hypothetical_indexes(queries.reject(&:missing_tables), tables) : {}
74
+ candidates = tables.any? ? create_hypothetical_indexes(queries.select(&:candidate_tables), tables) : {}
67
75
 
68
76
  # see if new indexes were used and meet bar
69
77
  new_indexes = determine_indexes(queries, candidates, tables)
@@ -344,7 +352,14 @@ module Dexter
344
352
  log "-" * 80
345
353
  log "Query #{query.fingerprint}"
346
354
  log "Total time: #{(query.total_time / 60000.0).round(1)} min, avg time: #{(query.total_time / query.calls.to_f).round} ms, calls: #{query.calls}" if query.total_time
347
- if tables.empty?
355
+
356
+ if query.fingerprint == "unknown"
357
+ log "Could not parse query"
358
+ elsif query.tables.empty?
359
+ log "No tables"
360
+ elsif query.missing_tables
361
+ log "Tables not present in current database"
362
+ elsif !query.candidate_tables
348
363
  log "No candidate tables for indexes"
349
364
  elsif query.explainable? && !query.high_cost?
350
365
  log "Low initial cost: #{query.initial_cost}"
@@ -354,15 +369,9 @@ module Dexter
354
369
  log "Pass1: #{query.costs[1]} : #{log_indexes(query.pass1_indexes || [])}"
355
370
  log "Pass2: #{query.costs[2]} : #{log_indexes(query.pass2_indexes || [])}"
356
371
  log "Final: #{query.new_cost} : #{log_indexes(query.suggest_index ? query_indexes : [])}"
357
- if query_indexes.any? && !query.suggest_index
372
+ if query_indexes.size == 1 && !query.suggest_index
358
373
  log "Need 50% cost savings to suggest index"
359
374
  end
360
- elsif query.fingerprint == "unknown"
361
- log "Could not parse query"
362
- elsif query.tables.empty?
363
- log "No tables"
364
- elsif query.missing_tables
365
- log "Tables not present in current database"
366
375
  else
367
376
  log "Could not run explain"
368
377
  end
@@ -452,9 +461,7 @@ module Dexter
452
461
  FROM
453
462
  information_schema.tables
454
463
  WHERE
455
- table_catalog = current_database() AND
456
- table_schema NOT IN ('pg_catalog', 'information_schema')
457
- AND table_type = 'BASE TABLE'
464
+ table_catalog = current_database()
458
465
  SQL
459
466
  result.map { |r| r["table_name"] }
460
467
  end
@@ -549,7 +556,7 @@ module Dexter
549
556
  end
550
557
 
551
558
  def search_path
552
- execute("SHOW search_path")[0]["search_path"].split(",").map(&:strip)
559
+ execute("SELECT current_schemas(true)")[0]["current_schemas"][1..-2].split(",")
553
560
  end
554
561
 
555
562
  def unquote(part)
@@ -1,6 +1,8 @@
1
1
  module Dexter
2
2
  class LogParser
3
- REGEX = /duration: (\d+\.\d+) ms (statement|execute <unnamed>): (.+)/
3
+ include Logging
4
+
5
+ REGEX = /duration: (\d+\.\d+) ms (statement|execute [^:]+): (.+)/
4
6
  LINE_SEPERATOR = ": ".freeze
5
7
  DETAIL_LINE = "DETAIL: ".freeze
6
8
 
@@ -3,5 +3,9 @@ module Dexter
3
3
  def log(message = "")
4
4
  puts message unless $log_level == "error"
5
5
  end
6
+
7
+ def abort(message)
8
+ raise Dexter::Abort, message
9
+ end
6
10
  end
7
11
  end
data/lib/dexter/query.rb CHANGED
@@ -2,7 +2,7 @@ module Dexter
2
2
  class Query
3
3
  attr_reader :statement, :fingerprint, :plans
4
4
  attr_writer :tables
5
- attr_accessor :missing_tables, :new_cost, :total_time, :calls, :indexes, :suggest_index, :pass1_indexes, :pass2_indexes
5
+ attr_accessor :missing_tables, :new_cost, :total_time, :calls, :indexes, :suggest_index, :pass1_indexes, :pass2_indexes, :candidate_tables
6
6
 
7
7
  def initialize(statement, fingerprint = nil)
8
8
  @statement = statement
@@ -1,3 +1,3 @@
1
1
  module Dexter
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
data/lib/dexter.rb CHANGED
@@ -13,3 +13,7 @@ require "dexter/log_parser"
13
13
  require "dexter/csv_log_parser"
14
14
  require "dexter/processor"
15
15
  require "dexter/query"
16
+
17
+ module Dexter
18
+ class Abort < StandardError; end
19
+ end
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.1
4
+ version: 0.3.2
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-12-29 00:00:00.000000000 Z
11
+ date: 2018-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slop