shiba 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b4f464acdc517169589f38206c835a77b160ae7fe5293f3c1c96bef2a736911
4
- data.tar.gz: ab4af7bcb0e55e042372c5579f8829f751c76efbb0ac3c0b17ae1fd97547362b
3
+ metadata.gz: 4c19ddf8bb56062725650ff02bede1ee4863ec0de0c6ab56ca420496eb35cc5b
4
+ data.tar.gz: 8ddfbc998e013cae6ecb5fc0b3ad05e7e76c2f086317cca14699099ffbdb7064
5
5
  SHA512:
6
- metadata.gz: 0c2809905f330b3e1e8874297e66d84c24a938fe4c532b29cfe87b88b8983a1d6700a03f60382e190fe171c5f7cc90ad81d0ca4aa2d1770cf2eb0afdd10621f5
7
- data.tar.gz: 36e6639d67a0b333c8faac40c45d092c7fe13b5acc0053066e1e68c0b3f9f70377fc19bb2d1fa0e724fd9163ea913b778bdf07ac8fbbd15d073678c31ac28b16
6
+ metadata.gz: e73819cb77cdc7efdaf521d76a64a2dc0ad3e27d97260cc45c2e4c59ac021fac6d12ad501fac2c299455963e6cd6c7650366e91ec6f067c873a1ff97e8d397c4
7
+ data.tar.gz: 302a916584abf9fd32c210498639ab63739d953b569d5b57313bcde0baa563d5fbf66bba1dcff28f16282c3fb8f9e18b56d6187d689f77a378bafc13323a7f49
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiba (0.4.0)
4
+ shiba (0.5.0)
5
5
  activesupport
6
6
  mysql2
7
7
  pg
@@ -10,8 +10,10 @@ parser = Shiba::Configure.make_options_parser(options, only_basics: true)
10
10
  parser.banner = "Dump database statistics into yaml file."
11
11
  parser.parse!
12
12
 
13
- Shiba.configure(options) do |errmsg|
14
- $stderr.puts(errmsg)
13
+ begin
14
+ Shiba.configure(options)
15
+ rescue Shiba::ConfigError => e
16
+ $stderr.puts(e.message)
15
17
  $stderr.puts(parser.help)
16
18
  exit 1
17
19
  end
@@ -25,8 +25,10 @@ end
25
25
  file = options.delete("file")
26
26
  file = File.open(file, "r") if file
27
27
 
28
- Shiba.configure(options) do |err_msg|
29
- $stderr.puts(err_msg)
28
+ begin
29
+ Shiba.configure(options)
30
+ rescue Shiba::ConfigError => e
31
+ $stderr.puts(e.message)
30
32
  $stderr.puts(parser)
31
33
  exit 2
32
34
  end
@@ -50,7 +52,7 @@ end
50
52
  table_stats = Shiba::TableStats.new(Shiba.index_config, Shiba.connection, {})
51
53
  queries = Shiba::Analyzer.analyze(file, json, table_stats, options)
52
54
 
53
- problems = queries.select { |q| q[:cost] && q[:cost] > 100 }
55
+ problems = queries.select { |q| q[:severity] && q[:severity] != 'none' }
54
56
 
55
57
  if problems.any?
56
58
  query_word = problems.size == 1 ? 'query' : 'queries'
@@ -7,16 +7,17 @@ require "byebug" if ENV['SHIBA_DEBUG']
7
7
 
8
8
  module Shiba
9
9
  class Error < StandardError; end
10
+ class ConfigError < StandardError; end
10
11
 
11
- def self.configure(options, &block)
12
- configure_mysql_defaults(options, &block)
12
+ def self.configure(options)
13
+ configure_mysql_defaults(options)
13
14
 
14
15
  @connection_hash = options.select { |k, v| [ 'default_file', 'default_group', 'server', 'username', 'database', 'host', 'password', 'port'].include?(k) }
15
16
  @main_config = Configure.read_config_file(options['config'], "config/shiba.yml")
16
17
  @index_config = Configure.read_config_file(options['index'], "config/shiba_index.yml")
17
18
  end
18
19
 
19
- def self.configure_mysql_defaults(options, &block)
20
+ def self.configure_mysql_defaults(options)
20
21
  option_path = Shiba::Configure.mysql_config_path
21
22
 
22
23
  if option_path
@@ -40,11 +41,11 @@ module Shiba
40
41
  end
41
42
 
42
43
  if !options["username"] && !option_file.include?('user')
43
- yield('Required: --username')
44
+ raise Shiba::ConfigError.new('Required: --username')
44
45
  end
45
46
 
46
47
  if !options["database"] && !option_file.include?('database')
47
- yield('Required: --database')
48
+ raise Shiba::ConfigError.new('Required: --database')
48
49
  end
49
50
  end
50
51
 
@@ -8,8 +8,6 @@ module Shiba
8
8
  # Given an explain log and a diff, returns any explain logs
9
9
  # that appear to be caused by the diff.
10
10
  class Checker
11
- MAGIC_COST = 100
12
-
13
11
  Result = Struct.new(:status, :message, :problems)
14
12
 
15
13
  attr_reader :options
@@ -35,7 +33,7 @@ module Shiba
35
33
  end
36
34
 
37
35
  explains = select_lines_with_changed_files(log)
38
- problems = explains.select { |explain| explain["cost"] && explain["cost"] > MAGIC_COST }
36
+ problems = explains.select { |explain| explain["severity"] && explain["severity"] != 'none' }
39
37
 
40
38
 
41
39
  if options["verbose"]
@@ -164,4 +162,4 @@ module Shiba
164
162
  end
165
163
  end
166
164
  end
167
- end
165
+ end
@@ -8,14 +8,21 @@ require 'shiba/explain/postgres_explain'
8
8
 
9
9
  module Shiba
10
10
  class Explain
11
+ COST_PER_ROW_READ = 2.5e-07 # TBD; data size would be better
12
+ COST_PER_ROW_SORT = 1.0e-07
13
+ COST_PER_ROW_RETURNED = 3.0e-05
14
+
11
15
  include CheckSupport
12
16
  extend CheckSupport::ClassMethods
13
- def initialize(sql, stats, backtrace, options = {})
14
- @sql = sql
15
- @backtrace = backtrace
17
+
18
+ def initialize(query, stats, options = {})
19
+ @query = query
20
+ @sql = query.sql
21
+
22
+ @backtrace = query.backtrace
16
23
 
17
24
  if options[:force_key]
18
- @sql = @sql.sub(/(FROM\s*\S+)/i, '\1' + " FORCE INDEX(`#{options[:force_key]}`)")
25
+ @sql = @sql.sub(/(FROM\s*\S+)/i, '\1' + " FORCE INDEX(`#{options[:force_key]}`)")
19
26
  end
20
27
 
21
28
  @options = options
@@ -35,7 +42,8 @@ module Shiba
35
42
  def as_json
36
43
  {
37
44
  sql: @sql,
38
- table: get_table,
45
+ table: @query.from_table,
46
+ md5: @query.md5,
39
47
  messages: @result.messages,
40
48
  cost: @result.cost,
41
49
  severity: severity,
@@ -52,17 +60,6 @@ module Shiba
52
60
  @result.cost
53
61
  end
54
62
 
55
- def get_table
56
- @sql =~ /\s+from\s*([^\s,]+)/i
57
- table = $1
58
- return nil unless table
59
-
60
- table = table.downcase
61
- table.gsub!('`', '')
62
- table.gsub!(/.*\.(.*)/, '\1')
63
- table
64
- end
65
-
66
63
  def first
67
64
  @rows.first
68
65
  end
@@ -77,30 +74,17 @@ module Shiba
77
74
 
78
75
  def severity
79
76
  case @result.cost
80
- when 0..100
77
+ when 0..0.01
78
+ "none"
79
+ when 0.01..0.10
81
80
  "low"
82
- when 100..1000
81
+ when 0.1..1.0
83
82
  "medium"
84
- when 1000..1_000_000_000
85
- "high"
86
- end
87
- end
88
-
89
- def limit
90
- if @sql =~ /limit\s*(\d+)\s*(offset \d+)?$/i
91
- $1.to_i
92
83
  else
93
- nil
84
+ "high"
94
85
  end
95
86
  end
96
87
 
97
- def aggregation?
98
- @sql =~ /select\s*(.*?)from/i
99
- select_fields = $1
100
- select_fields =~ /(min|max|avg|count|sum|group_concat)\s*\(.*?\)/i
101
- end
102
-
103
-
104
88
  def ignore?
105
89
  !!ignore_line_and_backtrace_line
106
90
  end
@@ -124,16 +108,16 @@ module Shiba
124
108
  def check_query_is_ignored
125
109
  if ignore?
126
110
  @result.messages << { tag: "ignored" }
127
- @cost = 0
111
+ @result.cost = 0
128
112
  end
129
113
  end
130
114
 
131
115
  check :check_no_matching_row_in_const_table
132
116
  def check_no_matching_row_in_const_table
133
117
  if no_matching_row_in_const_table?
134
- @result.messages << { tag: "access_type_const", table: get_table }
118
+ @result.messages << { tag: "access_type_const", table: @query.from_table }
135
119
  first['key'] = 'PRIMARY'
136
- @cost = 1
120
+ @result.cost = 0
137
121
  end
138
122
  end
139
123
 
@@ -147,25 +131,7 @@ module Shiba
147
131
  check :check_query_shortcircuits
148
132
  def check_query_shortcircuits
149
133
  if first_extra && IGNORE_PATTERNS.any? { |p| first_extra =~ p }
150
- @cost = 0
151
- end
152
- end
153
-
154
- # TODO: need to parse SQL here I think
155
- def simple_table_scan?
156
- @rows.size == 1 && (@sql !~ /order by/i) &&
157
- (@rows.first['using_index'] || !(@sql =~ /\s+WHERE\s+/i))
158
- end
159
-
160
- # TODO: we don't catch some cases like SELECT * from foo where index_col = 1 limit 1
161
- # bcs we really just need to parse the SQL.
162
- check :check_simple_table_scan
163
- def check_simple_table_scan
164
- if simple_table_scan?
165
- if limit
166
- @result.messages << { tag: 'limited_scan', cost: limit, table: @rows.first['table'] }
167
- @cost = limit
168
- end
134
+ @result.cost = 0
169
135
  end
170
136
  end
171
137
 
@@ -184,36 +150,32 @@ module Shiba
184
150
  end
185
151
 
186
152
  def check_return_size
187
- if limit
188
- return_size = limit
189
- elsif aggregation?
153
+ if @query.limit
154
+ return_size = [@query.limit, @result.result_size].min
155
+ elsif @query.aggregation?
190
156
  return_size = 1
191
157
  else
192
158
  return_size = @result.result_size
193
159
  end
194
160
 
195
- if return_size && return_size > 100
196
- @result.messages << { tag: "retsize_bad", result_size: return_size }
197
- else
198
- @result.messages << { tag: "retsize_good", result_size: return_size }
199
- end
161
+ cost = COST_PER_ROW_RETURNED * return_size
162
+ @result.cost += cost
163
+ @result.messages << { tag: "retsize", result_size: return_size, cost: cost }
200
164
  end
201
165
 
202
166
  def run_checks!
203
167
  # first run top-level checks
204
168
  _run_checks! do
205
- :stop if @cost
169
+ :stop if @result.cost
206
170
  end
207
171
 
208
- if @cost
209
- # we've decided to stop further analysis at the query level
210
- @result.cost = @cost
211
- else
212
- # run per-table checks
213
- 0.upto(@rows.size - 1) do |i|
214
- check = Checks.new(@rows, i, @stats, @options, @result)
215
- check.run_checks!
216
- end
172
+ return if @result.cost
173
+
174
+ @result.cost = 0
175
+ # run per-table checks
176
+ 0.upto(@rows.size - 1) do |i|
177
+ check = Checks.new(@rows, i, @stats, @options, @query, @result)
178
+ check.run_checks!
217
179
  end
218
180
 
219
181
  check_return_size
@@ -232,7 +194,7 @@ module Shiba
232
194
  next [] unless r['possible_keys'] && r['key'].nil?
233
195
  possible = r['possible_keys'] - [r['key']]
234
196
  possible.map do |p|
235
- Explain.new(@sql, @stats, @backtrace, force_key: p) rescue nil
197
+ Explain.new(@query, @stats, force_key: p) rescue nil
236
198
  end.compact
237
199
  end.flatten
238
200
  else
@@ -6,12 +6,13 @@ module Shiba
6
6
  include CheckSupport
7
7
  extend CheckSupport::ClassMethods
8
8
 
9
- def initialize(rows, index, stats, options, result)
9
+ def initialize(rows, index, stats, options, query, result)
10
10
  @rows = rows
11
11
  @row = rows[index]
12
12
  @index = index
13
13
  @stats = stats
14
14
  @options = options
15
+ @query = query
15
16
  @result = result
16
17
  @tbl_message = {}
17
18
  end
@@ -30,6 +31,26 @@ module Shiba
30
31
  @result.messages << { tag: tag, table_size: table_size, table: table }.merge(extra)
31
32
  end
32
33
 
34
+ # TODO: need to parse SQL here I think
35
+ def simple_table_scan?
36
+ @rows.size == 1 &&
37
+ (@row['using_index'] || !(@query.sql =~ /\s+WHERE\s+/i)) &&
38
+ (@row['access_type'] == "index" || (@query.sql !~ /order by/i)) &&
39
+ @query.limit
40
+ end
41
+
42
+ # TODO: we don't catch some cases like SELECT * from foo where index_col = 1 limit 1
43
+ # bcs we really just need to parse the SQL.
44
+ check :check_simple_table_scan
45
+ def check_simple_table_scan
46
+ if simple_table_scan?
47
+ rows_read = [@query.limit, table_size].min
48
+ @cost = @result.cost = rows_read * Shiba::Explain::COST_PER_ROW_READ
49
+ @result.messages << { tag: 'limited_scan', cost: @result.cost, table: table, rows_read: rows_read }
50
+ end
51
+ end
52
+
53
+
33
54
  check :check_derived
34
55
  def check_derived
35
56
  if table =~ /<derived.*?>/
@@ -75,54 +96,48 @@ module Shiba
75
96
 
76
97
  check :check_key_size
77
98
  def check_key_size
78
- if @row['key']
79
- rows_read = @stats.estimate_key(table, @row['key'], @row['used_key_parts'])
99
+ if @access_type == "access_type_index"
100
+ # access-type index means a table-scan as performed on an index... all rows.
101
+ key_size = table_size
102
+ elsif @row['key']
103
+ key_size = @stats.estimate_key(table, @row['key'], @row['used_key_parts'])
80
104
  else
81
- rows_read = table_size
105
+ key_size = table_size
82
106
  end
83
107
 
84
108
  # TBD: this appears to come from a couple of bugs.
85
109
  # one is we're not handling mysql index-merges, the other is that
86
110
  # we're not handling mysql table aliasing.
87
- if rows_read.nil?
88
- rows_read = 1
111
+ if key_size.nil?
112
+ key_size = 1
89
113
  end
90
114
 
91
115
  if @row['join_ref']
92
- # when joining, we'll say we read "@cost" rows -- but up to
116
+ # when joining, we'll say we read "key_size * (previous result size)" rows -- but up to
93
117
  # a max of the table size. I'm not sure this assumption is *exactly*
94
118
  # true but it feels good enough to start; a decent hash join should
95
119
  # nullify the cost of re-reading rows. I think.
96
- @cost = [@result.result_size * rows_read, table_size || 2**32].min
120
+ rows_read = [@result.result_size * key_size, table_size || 2**32].min
97
121
 
98
122
  # poke holes in this. Is this even remotely accurate?
99
123
  # We're saying that if we join to a a table with 100 rows per item
100
124
  # in the index, for each row we'll be joining in 100 more rows. Is that true?
101
- @result.result_size *= rows_read
125
+ @result.result_size *= key_size
102
126
  else
103
- @cost = rows_read
104
- @result.result_size += rows_read
127
+ rows_read = key_size
128
+ @result.result_size += key_size
105
129
  end
106
130
 
131
+ @cost = Shiba::Explain::COST_PER_ROW_READ * rows_read
107
132
  @result.cost += @cost
108
133
 
109
134
  @tbl_message['cost'] = @cost
135
+ @tbl_message['rows_read'] = rows_read
110
136
  @tbl_message['index'] = @row['key']
111
137
  @tbl_message['index_used'] = @row['used_key_parts']
112
138
  add_message(@access_type, @tbl_message)
113
139
  end
114
140
 
115
- def estimate_row_count_with_key(key)
116
- explain = Explain.new(@sql, @stats, @backtrace, force_key: key)
117
- explain.run_checks!
118
- rescue Mysql2::Error => e
119
- if /Key .+? doesn't exist in table/ =~ e.message
120
- return nil
121
- end
122
-
123
- raise e
124
- end
125
-
126
141
  def run_checks!
127
142
  _run_checks! do
128
143
  :stop if @cost
@@ -7,11 +7,12 @@ module Shiba
7
7
 
8
8
  def initialize
9
9
  @messages = []
10
- @cost = 0
10
+ @cost = nil
11
11
  @result_size = 0
12
+ @rows_read = 0
12
13
  end
13
14
 
14
- attr_accessor :messages, :cost, :result_size
15
+ attr_accessor :messages, :cost, :result_size, :rows_read
15
16
  end
16
17
  end
17
18
  end
@@ -40,8 +40,8 @@ module Shiba
40
40
 
41
41
  private
42
42
 
43
- BIG_FUZZ_SIZE = 5_000
44
- SMALL_FUZZ_SIZE = 100
43
+ BIG_FUZZ_SIZE = 100_000
44
+ SMALL_FUZZ_SIZE = 1000
45
45
 
46
46
 
47
47
  # Create fake table sizes based on the table's index count.
@@ -63,11 +63,9 @@ module Shiba
63
63
  end
64
64
 
65
65
  size = sizes[table_name]
66
- # Big
67
66
  if size >= large_table_index_count
68
67
  sizes[table_name] = BIG_FUZZ_SIZE
69
68
  else
70
- #small
71
69
  sizes[table_name] = SMALL_FUZZ_SIZE
72
70
  end
73
71
  end
@@ -13,7 +13,7 @@ module Shiba
13
13
  @tables.any?
14
14
  end
15
15
 
16
- Table = Struct.new(:name, :count, :indexes) do
16
+ Table = Struct.new(:name, :count, :indexes, :average_row_size) do
17
17
  def encode_with(coder)
18
18
  coder.map = self.to_h.stringify_keys
19
19
  coder.map.delete('name')
@@ -13,28 +13,23 @@ possible_key_check:
13
13
  MySQL reported that it had keys it could have used, but elected not to use them.
14
14
  Shiba checked each of the possible keys and returned the results from the best
15
15
  key possible. Sometimes "possible_keys" will be inaccurate and no keys were possible.
16
- level: info
17
16
  access_type_const:
18
17
  title: One row
19
18
  summary: The database only needs to read a single row from <b>{{table}}</b>.
20
19
  description: |
21
20
  This query selects at *most* one row, which is about as good as things get.
22
- level: success
23
21
  access_type_ref:
24
22
  title: Index Scan
25
23
  summary: The database reads {{ formatted_cost }} rows in <b>{{ table }}</b> via the <i>{{ index }}</i> index ({{ key_parts }}).
26
24
  description: |
27
25
  This query uses an index to find rows that match a single value. Often this
28
26
  has very good performance, but it depends on how many rows match that value.
29
- level: success
30
27
  join_type_eq_ref:
31
28
  title: Indexed Join
32
- summary: <b>{{ table }}</b> is joined to <b>{{ join_to }}</b> via the <i>{{ index }}</i> index, reading 1 row per joined item.
33
- level: success
29
+ summary: <b>{{ table }}</b> is joined to <b>{{ join_to }}</b> via <i>{{ index }}</i>, reading 1 row per joined item.
34
30
  join_type_ref:
35
31
  title: Indexed Join
36
- summary: <b>{{ table }}</b> is joined to <b>{{ join_to }}</b> via the <i>{{ index }}</i> index, reading {{ formatted_cost }} rows per joined item.
37
- level: success
32
+ summary: <b>{{ table }}</b> is joined to <b>{{ join_to }}</b> via <i>{{ index }}</i>, reading {{ formatted_cost }} rows per joined item.
38
33
  access_type_range:
39
34
  title: Indexed
40
35
  summary: The database uses a "range scan" to read more than {{ formatted_cost }} rows in {{ table }} via the <b>{{ index }}</b> index ({{ key_parts }})
@@ -43,7 +38,9 @@ access_type_range:
43
38
  `WHERE indexed_value in (1,2,5,6)` or `WHERE indexed_value >= 5 AND indexed_value <= 15`.
44
39
  It's very hard to estimate how many rows this query will consider in production, so we've
45
40
  upped the formatted_cost of this query.
46
- level: info
41
+ access_type_index:
42
+ title: Index Scan
43
+ summary: The database reads {{ formatted_cost }} of the rows in <b>{{ table }}</b> via <i>{{ index }}</i>
47
44
  access_type_tablescan:
48
45
  title: Table Scan
49
46
  summary: The database reads {{ formatted_cost }} of the rows in <b>{{ table }}</b>, skipping any indexes.
@@ -54,14 +51,12 @@ access_type_tablescan:
54
51
  This *can* be fine if, say, you're querying a tiny table (less than ~500 rows),
55
52
  but be aware that if this table is not effectively tiny or constant-sized you're entering
56
53
  a world of pain.
57
- level: danger
58
54
  limited_scan:
59
55
  title: Limited Scan
60
56
  summary: The database reads {{ formatted_cost }} rows from {{ table }}.
61
57
  description: |
62
58
  This query doesn't use any indexes to find data, but since it doesn't care about
63
59
  ordering and it doesn't have any conditions, it only ever reads {{ formatted_cost }} rows.
64
- level: info
65
60
  ignored:
66
61
  title: Ignored
67
62
  summary: This query matched an "ignore" rule in shiba.yml. Any further analysis was skipped.
@@ -74,11 +69,6 @@ index_walk:
74
69
  can read the index and simply pluck {LIMIT} rows out of the index. It's a very
75
70
  fast way to look up stuff by index.
76
71
  level: success
77
- retsize_bad:
78
- title: Big Results
79
- summary: The database returns {{ result_size }} rows to the client.
80
- level: danger
81
- retsize_good:
82
- title: Small Results
72
+ retsize:
73
+ title: Results
83
74
  summary: The database returns {{ result_size }} row(s) to the client.
84
- level: success
@@ -1,17 +1,29 @@
1
1
  require 'open3'
2
2
  require 'shiba/explain'
3
+ require 'timeout'
4
+ require 'thread'
5
+ require 'digest'
3
6
 
4
7
  module Shiba
5
8
  class Query
6
9
  @@index = 0
7
10
  FINGERPRINTER = Shiba.root + "/bin/fingerprint"
8
11
 
12
+ @@fingerprinter_mutex = Mutex.new
9
13
  def self.get_fingerprint(query)
10
- if !@stdin
11
- @stdin, @stdout, _ = Open3.popen2(FINGERPRINTER)
14
+ @@fingerprinter_mutex.synchronize do
15
+ if !@stdin
16
+ @stdin, @stdout, _ = Open3.popen2(FINGERPRINTER)
17
+ end
18
+ @stdin.puts(query.gsub(/\n/, ' '))
19
+ begin
20
+ Timeout.timeout(2) do
21
+ @stdout.readline.chomp
22
+ end
23
+ rescue StandardError => e
24
+ $stderr.puts("shiba: timed out waiting for fingerprinter on #{query}...")
25
+ end
12
26
  end
13
- @stdin.puts(query.gsub(/\n/, ' '))
14
- @stdout.readline.chomp
15
27
  end
16
28
 
17
29
  def initialize(sql, stats)
@@ -29,19 +41,43 @@ module Shiba
29
41
  @index = @@index
30
42
  end
31
43
 
32
- attr_reader :sql, :index
33
-
44
+ attr_reader :sql, :index, :backtrace
34
45
 
35
46
  def fingerprint
36
47
  @fingerprint ||= self.class.get_fingerprint(@sql)
37
48
  end
38
49
 
50
+ def md5
51
+ Digest::MD5.hexdigest(fingerprint)
52
+ end
53
+
39
54
  def explain
40
- Explain.new(@sql, @stats, @backtrace)
55
+ Explain.new(self, @stats)
56
+ end
57
+
58
+ def from_table
59
+ @sql =~ /\s+from\s*([^\s,]+)/i
60
+ table = $1
61
+ return nil unless table
62
+
63
+ table = table.downcase
64
+ table.gsub!('`', '')
65
+ table.gsub!(/.*\.(.*)/, '\1')
66
+ table
67
+ end
68
+
69
+ def limit
70
+ if @sql =~ /limit\s*(\d+)\s*(offset \d+)?$/i
71
+ $1.to_i
72
+ else
73
+ nil
74
+ end
41
75
  end
42
76
 
43
- def backtrace
44
- @backtrace
77
+ def aggregation?
78
+ @sql =~ /select\s*(.*?)from/i
79
+ select_fields = $1
80
+ select_fields =~ /(min|max|avg|count|sum|group_concat)\s*\(.*?\)/i
45
81
  end
46
82
  end
47
83
  end
@@ -22,6 +22,7 @@ module Shiba
22
22
  body << "\n"
23
23
  end
24
24
 
25
+ body << "Estimated query time: %.2fs" % explain['cost']
25
26
  body
26
27
  end
27
28
 
@@ -44,6 +45,7 @@ module Shiba
44
45
  "table_size" => message["table_size"],
45
46
  "result_size" => message["result_size"],
46
47
  "index" => message["index"],
48
+ "join_to" => message["join_to"],
47
49
  "key_parts" => (message["index_used"] || []).join(','),
48
50
  "size" => message["size"],
49
51
  "formatted_cost" => formatted_cost(message)
@@ -51,13 +53,13 @@ module Shiba
51
53
  end
52
54
 
53
55
  def formatted_cost(explain)
54
- return nil unless explain["cost"] && explain["table_size"]
55
- percentage = (explain["cost"] / explain["table_size"]) * 100.0;
56
+ return nil unless explain["rows_read"] && explain["table_size"]
57
+ percentage = (explain["rows_read"] / explain["table_size"]) * 100.0;
56
58
 
57
- if explain["cost"] > 100 && percentage > 1
58
- "#{percentage.floor}% (#{explain["cost"]}) of the"
59
+ if explain["rows_read"] > 100 && percentage > 1
60
+ "#{percentage.floor}% (#{explain["rows_read"]}) of the"
59
61
  else
60
- explain["cost"]
62
+ explain["rows_read"]
61
63
  end
62
64
  end
63
65
 
@@ -11,6 +11,7 @@ module Shiba
11
11
  # is semi-corrected but still a problem
12
12
  class Reviewer
13
13
  TEMPLATE_FILE = File.join(Shiba.root, 'lib/shiba/output/tags.yaml')
14
+ MESSAGE_FILTER_THRESHOLD = 0.005
14
15
 
15
16
  attr_reader :repo_url, :problems, :options
16
17
 
@@ -34,9 +35,7 @@ module Shiba
34
35
 
35
36
  position = diff.find_position(file, line_number.to_i)
36
37
 
37
- if options["submit"]
38
- explain = keep_only_dangerous_messages(explain)
39
- end
38
+ explain = keep_only_dangerous_messages(explain)
40
39
 
41
40
  { body: renderer.render(explain),
42
41
  commit_id: @commit_id,
@@ -93,8 +92,7 @@ module Shiba
93
92
  def keep_only_dangerous_messages(explain)
94
93
  explain_b = explain.dup
95
94
  explain_b["messages"] = explain_b["messages"].select do |message|
96
- tag = message['tag']
97
- tags[tag]["level"] == "danger"
95
+ message['cost'] && message['cost'] > MESSAGE_FILTER_THRESHOLD
98
96
  end
99
97
  explain_b
100
98
  end
@@ -1,3 +1,3 @@
1
1
  module Shiba
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -25,7 +25,7 @@
25
25
  var queriesByTable = [];
26
26
  var queriesByTableLow = [];
27
27
  var queriesHaveFuzzed = false;
28
- var severityIndexes = { high: 1, medium: 2, low: 3 };
28
+ var severityIndexes = { high: 1, medium: 2, low: 3, none: 4 };
29
29
 
30
30
  function sortByFunc(fields) {
31
31
  return function(a, b) {
@@ -76,7 +76,7 @@
76
76
  data.queries.forEach(function(query) {
77
77
  var q = new Query(query);
78
78
 
79
- if ( q.cost < 100 ) {
79
+ if ( q.severity == "none" ) {
80
80
  queriesByTableLow.push(q);
81
81
  } else {
82
82
  queriesByTable.push(q);
@@ -89,11 +89,11 @@
89
89
 
90
90
  var rCost = 0;
91
91
  q.messages.forEach(function(m) {
92
- if ( m.cost ) {
92
+ if ( m.cost && m.cost != 0) {
93
93
  rCost += m.cost;
94
94
  m.running_cost = rCost;
95
95
  } else {
96
- m.running_cost = '';
96
+ m.running_cost = undefined;
97
97
  }
98
98
  });
99
99
  });
@@ -129,6 +129,9 @@
129
129
  <table class="shiba-messages">
130
130
  <component v-for="message in query.messages" v-bind:is="'tag-' + message.tag" v-bind="message"></component>
131
131
  </table>
132
+ <% if ENV['SHIBA_DEBUG'] %>
133
+ <div style="font-size: 10px">md5: {{ query.md5 }}</div>
134
+ <% end %>
132
135
  <div v-if="!rawExpanded">
133
136
  <a href="#" v-on:click.prevent="rawExpanded = !rawExpanded">See full EXPLAIN</a>
134
137
  </div>
@@ -178,24 +181,31 @@
178
181
  return str;
179
182
  },
180
183
  formatted_cost: function() {
181
- var costPercentage = (this.cost / this.table_size) * 100.0;
182
- if ( this.cost > 100 && costPercentage > 1 ) // todo: make better
183
- return `${costPercentage.toFixed()}% (${this.cost.toLocaleString()}) of the`;
184
+ var readPercentage = (this.rows_read / this.table_size) * 100.0;
185
+ if ( this.rows_read > 100 && readPercentage > 1 ) // todo: make better
186
+ return `${readPercentage.toFixed()}% (${this.rows_read.toLocaleString()}) of the`;
184
187
  else
185
- return this.cost.toLocaleString();
188
+ return this.rows_read.toLocaleString();
186
189
  },
187
190
  costToColor: function() {
188
191
  var goodColor = [34, 160, 60];
189
192
  var endColor = [255, 0, 0];
190
- var costScale = this.cost ? this.cost / 5000 : 0;
193
+ var costScale = this.cost ? this.cost / 0.5 : 0;
191
194
 
192
195
  if ( costScale > 1 )
193
196
  costScale = 1;
194
197
 
195
198
  var pos = (costScale * (greenToRedGradient.length - 1)).toFixed();
196
199
 
197
- debugger;
198
200
  return "border-color: " + greenToRedGradient[pos];
201
+ },
202
+ formattedRunningCost: function() {
203
+ if ( this.running_cost === undefined )
204
+ return "-";
205
+ else if ( this.running_cost < 1.0 )
206
+ return (this.running_cost * 100).toFixed() + "ms";
207
+ else
208
+ return this.running_cost.toFixed(1) + "s";
199
209
  }
200
210
  }
201
211
  </script>
@@ -209,15 +219,18 @@
209
219
  <%= h['summary'] %>
210
220
  </td>
211
221
  <td class="running-totals">
212
- {{ running_cost.toLocaleString() }}
222
+ {{ formattedRunningCost }}
213
223
  </td>
214
224
  </tr>
215
225
  </script>
216
226
  <script>
217
227
  Vue.component('tag-<%= tag %>', {
218
228
  template: '#tag-<%= tag %>-template',
219
- props: [ 'table_size', 'result_size', 'table', 'cost', 'index', 'join_to', 'index_used', 'running_cost', 'tables' ],
220
- computed: templateComputedFunctions
229
+ props: [ 'table_size', 'result_size', 'table', 'cost', 'index', 'join_to', 'index_used', 'running_cost', 'tables', 'rows_read' ],
230
+ computed: templateComputedFunctions,
231
+ data: function () {
232
+ return { lastRunnningCost: undefined };
233
+ }
221
234
  });
222
235
  </script>
223
236
  <% end %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shiba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Osheroff
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-02-27 00:00:00.000000000 Z
12
+ date: 2019-03-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -177,8 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
177
  - !ruby/object:Gem::Version
178
178
  version: '0'
179
179
  requirements: []
180
- rubyforge_project:
181
- rubygems_version: 2.7.6
180
+ rubygems_version: 3.0.1
182
181
  signing_key:
183
182
  specification_version: 4
184
183
  summary: A gem that attempts to find bad queries before you shoot self in foot