shiba 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +13 -0
- data/.travis/my.cnf +3 -0
- data/Gemfile.lock +14 -1
- data/README.md +93 -30
- data/Rakefile +9 -1
- data/TODO +25 -7
- data/bin/check +0 -0
- data/bin/dump_stats +38 -0
- data/bin/explain +67 -28
- data/bin/shiba +4 -4
- data/lib/shiba.rb +3 -1
- data/lib/shiba/analyzer.rb +6 -5
- data/lib/shiba/backtrace.rb +56 -0
- data/lib/shiba/checker.rb +103 -0
- data/lib/shiba/configure.rb +28 -8
- data/lib/shiba/diff.rb +119 -0
- data/lib/shiba/explain.rb +149 -49
- data/lib/shiba/fuzzer.rb +77 -0
- data/lib/shiba/index.rb +8 -129
- data/lib/shiba/index_stats.rb +210 -0
- data/lib/shiba/output.rb +24 -18
- data/lib/shiba/output/tags.yaml +34 -13
- data/lib/shiba/query_watcher.rb +3 -46
- data/lib/shiba/railtie.rb +31 -8
- data/lib/shiba/table_stats.rb +34 -0
- data/lib/shiba/version.rb +1 -1
- data/shiba.gemspec +1 -0
- data/shiba.yml.example +4 -0
- data/web/main.css +32 -2
- data/web/results.html.erb +132 -58
- metadata +26 -6
- data/bin/analyze +0 -77
- data/bin/inspect +0 -0
- data/bin/parse +0 -0
- data/bin/watch.rb +0 -19
data/lib/shiba.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
require "shiba/version"
|
2
2
|
require "shiba/configure"
|
3
3
|
require "mysql2"
|
4
|
+
require "pp"
|
5
|
+
require "byebug" if ENV['SHIBA_DEBUG']
|
4
6
|
|
5
7
|
module Shiba
|
6
8
|
class Error < StandardError; end
|
7
9
|
|
8
10
|
def self.configure(options)
|
9
|
-
@connection_hash = options.select { |k, v| ['username', 'database', 'host', 'password'].include?(k) }
|
11
|
+
@connection_hash = options.select { |k, v| [ 'default_file', 'default_group', 'username', 'database', 'host', 'password'].include?(k) }
|
10
12
|
@main_config = Configure.read_config_file(options['config'], "config/shiba.yml")
|
11
13
|
@index_config = Configure.read_config_file(options['index'], "config/shiba_index.yml")
|
12
14
|
end
|
data/lib/shiba/analyzer.rb
CHANGED
@@ -69,7 +69,7 @@ module Shiba
|
|
69
69
|
protected
|
70
70
|
|
71
71
|
def dump_error(e, query)
|
72
|
-
$stderr.puts "got exception trying to explain: #{e.message}"
|
72
|
+
$stderr.puts "got #{e.class.name} exception trying to explain: #{e.message}"
|
73
73
|
$stderr.puts "query: #{query.sql} (index #{query.index})"
|
74
74
|
$stderr.puts e.backtrace.join("\n")
|
75
75
|
end
|
@@ -79,10 +79,7 @@ module Shiba
|
|
79
79
|
begin
|
80
80
|
explain = query.explain
|
81
81
|
rescue Mysql2::Error => e
|
82
|
-
|
83
|
-
if !(e.message =~ /You have an error in your SQL syntax/)
|
84
|
-
dump_error(e, query)
|
85
|
-
end
|
82
|
+
dump_error(e, query) if verbose?
|
86
83
|
rescue StandardError => e
|
87
84
|
dump_error(e, query)
|
88
85
|
end
|
@@ -96,5 +93,9 @@ module Shiba
|
|
96
93
|
def write(line)
|
97
94
|
@output.puts(line)
|
98
95
|
end
|
96
|
+
|
97
|
+
def verbose?
|
98
|
+
@options['verbose'] == true
|
99
|
+
end
|
99
100
|
end
|
100
101
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Shiba
|
4
|
+
module Backtrace
|
5
|
+
IGNORE = /\.rvm|gem|vendor\/|rbenv|seed|db|shiba|test|spec/
|
6
|
+
|
7
|
+
# 8 backtrace lines starting from the app caller, cleaned of app/project cruft.
|
8
|
+
def self.from_app
|
9
|
+
app_line_idx = caller_locations.index { |line| line.to_s !~ IGNORE }
|
10
|
+
if app_line_idx == nil
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
caller_locations(app_line_idx+1, 8).map do |loc|
|
15
|
+
clean!(loc.to_s)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.clean!(line)
|
20
|
+
line.sub!(backtrace_ignore_pattern, '')
|
21
|
+
line
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def self.backtrace_ignore_pattern
|
27
|
+
@roots ||= begin
|
28
|
+
paths = Gem.path
|
29
|
+
paths << Rails.root.to_s if defined?(Rails.root)
|
30
|
+
paths << repo_root
|
31
|
+
paths << ENV['HOME']
|
32
|
+
paths.uniq!
|
33
|
+
paths.compact!
|
34
|
+
# match and replace longest path first
|
35
|
+
paths.sort_by!(&:size).reverse!
|
36
|
+
|
37
|
+
r = Regexp.new(paths.map {|r| Regexp.escape(r) }.join("|"))
|
38
|
+
# kill leading slash
|
39
|
+
/(#{r})\/?/
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# /user/git_repo => "/user/git_repo"
|
44
|
+
# /user/not_a_repo => nil
|
45
|
+
def self.repo_root
|
46
|
+
root = nil
|
47
|
+
Open3.popen3('git rev-parse --show-toplevel') {|_,o,_,_|
|
48
|
+
if root = o.gets
|
49
|
+
root = root.chomp
|
50
|
+
end
|
51
|
+
}
|
52
|
+
root
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
require 'shiba/diff'
|
5
|
+
require 'shiba/backtrace'
|
6
|
+
|
7
|
+
module Shiba
|
8
|
+
class Checker
|
9
|
+
Result = Struct.new(:status, :message, :problems)
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(log)
|
18
|
+
msg = nil
|
19
|
+
|
20
|
+
if options['verbose']
|
21
|
+
puts cmd
|
22
|
+
end
|
23
|
+
|
24
|
+
if changes.empty?
|
25
|
+
if options['verbose']
|
26
|
+
msg = "No changes found in git"
|
27
|
+
end
|
28
|
+
return Result.new(:pass, msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
explains = select_lines_with_changed_files(log)
|
32
|
+
problems = explains.select { |explain| explain["cost"] && explain["cost"] > MAGIC_COST }
|
33
|
+
|
34
|
+
problems.select! do |problem|
|
35
|
+
backtrace_has_updated_line?(problem["backtrace"], updated_lines)
|
36
|
+
end
|
37
|
+
|
38
|
+
if problems.empty?
|
39
|
+
if options['verbose']
|
40
|
+
msg = "No problems found"
|
41
|
+
end
|
42
|
+
|
43
|
+
return Result.new(:pass, msg)
|
44
|
+
end
|
45
|
+
|
46
|
+
return Result.new(:fail, "Potential problems", problems)
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def backtrace_has_updated_line?(backtrace, updates)
|
52
|
+
backtrace.any? do |bl|
|
53
|
+
updates.any? do |path, lines|
|
54
|
+
next if !bl.start_with?(path)
|
55
|
+
bl =~ /:(\d+):/
|
56
|
+
lines.include?($1.to_i)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def select_lines_with_changed_files(log)
|
62
|
+
patterns = changes.split("\n").map { |path| "-e #{path}" }.join(" ")
|
63
|
+
json_lines = `grep #{log} #{patterns}`
|
64
|
+
json_lines.each_line.map { |line| JSON.parse(line) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def changes
|
68
|
+
@changes ||= begin
|
69
|
+
result = `git diff#{cmd} --name-only --diff-filter=d`
|
70
|
+
if $?.exitstatus != 0
|
71
|
+
error("Failed to read changes", $?.exitstatus)
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def updated_lines
|
79
|
+
return @updated_lines if @updated_lines
|
80
|
+
|
81
|
+
Open3.popen3("git diff#{cmd} --unified=0 --diff-filter=d") {|_,o,_,_|
|
82
|
+
@updated_lines = Shiba::Diff.new(o).updated_lines
|
83
|
+
}
|
84
|
+
|
85
|
+
@updated_lines.map! do |path, lines|
|
86
|
+
[ Shiba::Backtrace.clean!(path), lines ]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def cmd
|
91
|
+
cmd = case
|
92
|
+
when options["staged"]
|
93
|
+
" --staged"
|
94
|
+
when options["unstaged"]
|
95
|
+
""
|
96
|
+
else
|
97
|
+
commit = " HEAD"
|
98
|
+
commit << "...#{options["branch"]}" if options["branch"]
|
99
|
+
commit
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/shiba/configure.rb
CHANGED
@@ -27,6 +27,13 @@ module Shiba
|
|
27
27
|
raise e, "Cannot load `#{path}`:\n#{e.message}", e.backtrace
|
28
28
|
end
|
29
29
|
|
30
|
+
# loosely based on https://dev.mysql.com/doc/refman/8.0/en/option-files.html
|
31
|
+
def self.mysql_config_path
|
32
|
+
paths = [ File.join(Dir.home, '.mylogin.cnf'), File.join(Dir.home, '.my.cnf') ]
|
33
|
+
|
34
|
+
paths.detect { |p| File.exist?(p) }
|
35
|
+
end
|
36
|
+
|
30
37
|
def self.read_config_file(option_file, default)
|
31
38
|
file_to_read = nil
|
32
39
|
if option_file
|
@@ -78,25 +85,38 @@ module Shiba
|
|
78
85
|
options["limit"] = l.to_i
|
79
86
|
end
|
80
87
|
|
81
|
-
opts.on("-s","--stats FILES", "location of index statistics tsv file") do |f|
|
82
|
-
options["stats"] = f
|
83
|
-
end
|
84
|
-
|
85
88
|
opts.on("-f", "--file FILE", "location of file containing queries") do |f|
|
86
89
|
options["file"] = f
|
87
90
|
end
|
88
91
|
|
89
|
-
opts.on("-
|
90
|
-
|
92
|
+
opts.on("-j", "--json [FILE]", "write JSON report here. default: to stdout") do |f|
|
93
|
+
if f
|
94
|
+
options["json"] = File.open(f, 'w')
|
95
|
+
else
|
96
|
+
options["json"] = $stdout
|
97
|
+
end
|
91
98
|
end
|
92
99
|
|
93
|
-
opts.on("-
|
94
|
-
options["
|
100
|
+
opts.on("-h", "--html FILE", "write html report here. Default to /tmp/explain.html") do |h|
|
101
|
+
options["html"] = h
|
95
102
|
end
|
96
103
|
|
97
104
|
opts.on("-t", "--test", "analyze queries at --file instead of analyzing a process") do |f|
|
98
105
|
options["test"] = true
|
99
106
|
end
|
107
|
+
|
108
|
+
opts.on("-v", "--verbose", "print internal runtime information") do
|
109
|
+
options["verbose"] = true
|
110
|
+
end
|
111
|
+
|
112
|
+
# This naming seems to be mysql convention, maybe we should just do our own thing though.
|
113
|
+
opts.on("--login-path", "The option group from the mysql config file to read from") do |f|
|
114
|
+
options["default_group"] = f
|
115
|
+
end
|
116
|
+
|
117
|
+
opts.on("--default-extras-file", "The option file to read mysql configuration from") do |f|
|
118
|
+
options["default_file"] = f
|
119
|
+
end
|
100
120
|
end
|
101
121
|
end
|
102
122
|
end
|
data/lib/shiba/diff.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
module Shiba
|
2
|
+
class Diff
|
3
|
+
# +++ b/config/environments/test.rb
|
4
|
+
FILE_PATTERN = /\A\+\+\+ b\/(.*?)\Z/
|
5
|
+
|
6
|
+
# @@ -177,0 +178 @@ ...
|
7
|
+
# @@ -177,0 +178,5 @@ ...
|
8
|
+
# @@ -21 +24 @@ ...
|
9
|
+
LINE_PATTERN = /\A@@ \-\d+,?\d+? \+(\d+),?(\d+)? @@/
|
10
|
+
|
11
|
+
# via https://developer.github.com/v3/pulls/comments/#create-a-comment
|
12
|
+
# The position value equals the number of lines down from the first "@@" hunk header
|
13
|
+
# in the file you want to add a comment.
|
14
|
+
|
15
|
+
# diff = `git diff --unified=0`
|
16
|
+
# parse_diff(StringIO.new(diff))
|
17
|
+
# => "hello.rb:1"
|
18
|
+
# => "hello.rb:2"
|
19
|
+
# => "test.rb:5"
|
20
|
+
|
21
|
+
# For simplicity, the default output of git diff is not supported.
|
22
|
+
# The expected format is from 'git diff unified=0'
|
23
|
+
|
24
|
+
attr_reader :status
|
25
|
+
|
26
|
+
def initialize(file)
|
27
|
+
@diff = file
|
28
|
+
@status = :new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the file and line numbers that contain inserts. Deletions are ignored.
|
32
|
+
def updated_lines
|
33
|
+
io = @diff.each_line
|
34
|
+
path = nil
|
35
|
+
|
36
|
+
found = []
|
37
|
+
|
38
|
+
while true
|
39
|
+
line = io.next
|
40
|
+
if line =~ FILE_PATTERN
|
41
|
+
path = $1
|
42
|
+
end
|
43
|
+
|
44
|
+
if hunk_header?(line)
|
45
|
+
line_numbers = line_numbers_for_destination(line)
|
46
|
+
found << [ path, line_numbers ]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue StopIteration
|
50
|
+
return found
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the position in the diff, after the relevant file header,
|
54
|
+
# that contains the specified file/lineno modification.
|
55
|
+
# Only supports finding the position in the destination / newest version of the file.
|
56
|
+
def find_position(path, line_number)
|
57
|
+
io = @diff.each_line # maybe redundant?
|
58
|
+
|
59
|
+
file_header = "+++ b/#{path}\n" # fixme
|
60
|
+
if !io.find_index(file_header)
|
61
|
+
@status = :file_not_found
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
line = io.peek
|
66
|
+
if !hunk_header?(line)
|
67
|
+
raise StandardError.new("Expected hunk header to be after file header, but got '#{line}'")
|
68
|
+
end
|
69
|
+
|
70
|
+
pos = 0
|
71
|
+
|
72
|
+
while true
|
73
|
+
line = io.next
|
74
|
+
pos += 1
|
75
|
+
|
76
|
+
if file_header?(line)
|
77
|
+
@status = :line_not_found
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
if !hunk_header?(line)
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
line_numbers = line_numbers_for_destination(line)
|
86
|
+
|
87
|
+
if destination_position = line_numbers.find_index(line_number)
|
88
|
+
@status = :found_position
|
89
|
+
return pos + find_hunk_index(io, destination_position)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
rescue StopIteration
|
93
|
+
@status = :line_not_found
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def find_hunk_index(hunk, pos)
|
99
|
+
line, idx = hunk.with_index.select { |l,idx| !l.start_with?('-') }.take(pos+1).last
|
100
|
+
idx
|
101
|
+
end
|
102
|
+
|
103
|
+
def file_header?(line)
|
104
|
+
line.match?(FILE_PATTERN)
|
105
|
+
end
|
106
|
+
|
107
|
+
def hunk_header?(line)
|
108
|
+
line.match?(LINE_PATTERN)
|
109
|
+
end
|
110
|
+
|
111
|
+
def line_numbers_for_destination(diff_line)
|
112
|
+
diff_line =~ LINE_PATTERN
|
113
|
+
line = $1.to_i
|
114
|
+
line_count = ($2 && $2.to_i) || 0
|
115
|
+
line..line+line_count
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
data/lib/shiba/explain.rb
CHANGED
@@ -13,8 +13,8 @@ module Shiba
|
|
13
13
|
|
14
14
|
@options = options
|
15
15
|
ex = Shiba.connection.query("EXPLAIN FORMAT=JSON #{@sql}").to_a
|
16
|
-
|
17
|
-
@rows = self.class.transform_json(
|
16
|
+
@explain_json = JSON.parse(ex.first['EXPLAIN'])
|
17
|
+
@rows = self.class.transform_json(@explain_json['query_block'])
|
18
18
|
@stats = stats
|
19
19
|
run_checks!
|
20
20
|
end
|
@@ -23,11 +23,15 @@ module Shiba
|
|
23
23
|
{
|
24
24
|
sql: @sql,
|
25
25
|
table: get_table,
|
26
|
+
table_size: table_size,
|
26
27
|
key: first_key,
|
27
28
|
tags: messages,
|
28
29
|
cost: @cost,
|
30
|
+
return_size: @return_size,
|
31
|
+
severity: severity,
|
29
32
|
used_key_parts: first['used_key_parts'],
|
30
33
|
possible_keys: first['possible_keys'],
|
34
|
+
raw_explain: humanized_explain,
|
31
35
|
backtrace: @backtrace
|
32
36
|
}
|
33
37
|
end
|
@@ -43,7 +47,7 @@ module Shiba
|
|
43
47
|
table
|
44
48
|
end
|
45
49
|
|
46
|
-
def self.transform_table(table)
|
50
|
+
def self.transform_table(table, extra = {})
|
47
51
|
t = table
|
48
52
|
res = {}
|
49
53
|
res['table'] = t['table_name']
|
@@ -57,24 +61,30 @@ module Shiba
|
|
57
61
|
res['possible_keys'] = t['possible_keys']
|
58
62
|
end
|
59
63
|
res['using_index'] = t['using_index'] if t['using_index']
|
64
|
+
|
65
|
+
res.merge!(extra)
|
66
|
+
|
60
67
|
res
|
61
68
|
end
|
62
69
|
|
63
|
-
def self.transform_json(json, res = [])
|
70
|
+
def self.transform_json(json, res = [], extra = {})
|
64
71
|
rows = []
|
65
72
|
|
66
|
-
if json['ordering_operation']
|
67
|
-
|
73
|
+
if (ordering = json['ordering_operation'])
|
74
|
+
index_walk = (ordering['using_filesort'] == false)
|
75
|
+
return transform_json(json['ordering_operation'], res, { "index_walk" => index_walk } )
|
68
76
|
elsif json['duplicates_removal']
|
69
|
-
return transform_json(json['duplicates_removal'])
|
77
|
+
return transform_json(json['duplicates_removal'], res, extra)
|
78
|
+
elsif json['grouping_operation']
|
79
|
+
return transform_json(json['grouping_operation'], res, extra)
|
70
80
|
elsif !json['nested_loop'] && !json['table']
|
71
81
|
return [{'Extra' => json['message']}]
|
72
82
|
elsif json['nested_loop']
|
73
83
|
json['nested_loop'].map do |nested|
|
74
|
-
transform_json(nested, res)
|
84
|
+
transform_json(nested, res, extra)
|
75
85
|
end
|
76
86
|
elsif json['table']
|
77
|
-
res << transform_table(json['table'])
|
87
|
+
res << transform_table(json['table'], extra)
|
78
88
|
end
|
79
89
|
res
|
80
90
|
end
|
@@ -112,15 +122,12 @@ module Shiba
|
|
112
122
|
first.merge(cost: cost, messages: messages)
|
113
123
|
end
|
114
124
|
|
115
|
-
IGNORE_PATTERNS = [
|
116
|
-
/No tables used/,
|
117
|
-
/Impossible WHERE/,
|
118
|
-
/Select tables optimized away/,
|
119
|
-
/No matching min\/max row/
|
120
|
-
]
|
121
|
-
|
122
125
|
def table_size
|
123
|
-
|
126
|
+
@stats.table_count(first['table'])
|
127
|
+
end
|
128
|
+
|
129
|
+
def fuzzed?(table)
|
130
|
+
@stats.fuzzed?(first['table'])
|
124
131
|
end
|
125
132
|
|
126
133
|
def no_matching_row_in_const_table?
|
@@ -128,7 +135,6 @@ module Shiba
|
|
128
135
|
end
|
129
136
|
|
130
137
|
def ignore_explain?
|
131
|
-
first_extra && IGNORE_PATTERNS.any? { |p| first_extra =~ p }
|
132
138
|
end
|
133
139
|
|
134
140
|
def derived?
|
@@ -137,7 +143,18 @@ module Shiba
|
|
137
143
|
|
138
144
|
# TODO: need to parse SQL here I think
|
139
145
|
def simple_table_scan?
|
140
|
-
@rows.size == 1 &&
|
146
|
+
@rows.size == 1 && first['using_index'] && (@sql !~ /order by/i)
|
147
|
+
end
|
148
|
+
|
149
|
+
def severity
|
150
|
+
case @cost
|
151
|
+
when 0..100
|
152
|
+
"low"
|
153
|
+
when 100..1000
|
154
|
+
"medium"
|
155
|
+
when 1000..1_000_000_000
|
156
|
+
"high"
|
157
|
+
end
|
141
158
|
end
|
142
159
|
|
143
160
|
def limit
|
@@ -148,53 +165,115 @@ module Shiba
|
|
148
165
|
end
|
149
166
|
end
|
150
167
|
|
151
|
-
def
|
152
|
-
|
168
|
+
def aggregation?
|
169
|
+
@sql =~ /select\s*(.*?)from/i
|
170
|
+
select_fields = $1
|
171
|
+
select_fields =~ /min|max|avg|count|sum|group_concat\s*\(.*?\)/i
|
172
|
+
end
|
153
173
|
|
154
|
-
|
155
|
-
|
156
|
-
|
174
|
+
def self.check(c)
|
175
|
+
@checks ||= []
|
176
|
+
@checks << c
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.get_checks
|
180
|
+
@checks
|
181
|
+
end
|
182
|
+
|
183
|
+
check :check_query_is_ignored
|
184
|
+
def check_query_is_ignored
|
185
|
+
if ignore?
|
186
|
+
messages << "ignored"
|
187
|
+
@cost = 0
|
188
|
+
end
|
157
189
|
end
|
158
190
|
|
159
|
-
|
191
|
+
check :check_no_matching_row_in_const_table
|
192
|
+
def check_no_matching_row_in_const_table
|
160
193
|
if no_matching_row_in_const_table?
|
161
194
|
messages << "access_type_const"
|
162
195
|
first['key'] = 'PRIMARY'
|
163
|
-
|
196
|
+
@cost = 1
|
164
197
|
end
|
198
|
+
end
|
165
199
|
|
166
|
-
|
200
|
+
IGNORE_PATTERNS = [
|
201
|
+
/No tables used/,
|
202
|
+
/Impossible WHERE/,
|
203
|
+
/Select tables optimized away/,
|
204
|
+
/No matching min\/max row/
|
205
|
+
]
|
167
206
|
|
168
|
-
|
207
|
+
check :check_query_shortcircuits
|
208
|
+
def check_query_shortcircuits
|
209
|
+
if first_extra && IGNORE_PATTERNS.any? { |p| first_extra =~ p }
|
210
|
+
@cost = 0
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
check :check_fuzzed
|
215
|
+
def check_fuzzed
|
216
|
+
messages << "fuzzed_data" if fuzzed?(first_table)
|
217
|
+
end
|
169
218
|
|
219
|
+
check :check_simple_table_scan
|
220
|
+
def check_simple_table_scan
|
170
221
|
if simple_table_scan?
|
171
222
|
if limit
|
172
223
|
messages << 'limited_tablescan'
|
224
|
+
@cost = limit
|
173
225
|
else
|
174
|
-
|
226
|
+
tag_query_type
|
227
|
+
@cost = @stats.estimate_key(first_table, first_key, first['used_key_parts'])
|
175
228
|
end
|
176
|
-
|
177
|
-
return limit || table_size
|
178
229
|
end
|
230
|
+
end
|
179
231
|
|
232
|
+
check :check_derived
|
233
|
+
def check_derived
|
180
234
|
if derived?
|
181
235
|
# select count(*) from ( select 1 from foo where blah )
|
182
236
|
@rows.shift
|
183
|
-
return
|
237
|
+
return run_checks!
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
check :tag_query_type
|
243
|
+
def tag_query_type
|
244
|
+
access_type = first['access_type']
|
245
|
+
|
246
|
+
if access_type.nil?
|
247
|
+
@cost = 0
|
248
|
+
return
|
184
249
|
end
|
185
250
|
|
186
|
-
|
251
|
+
access_type = 'tablescan' if access_type == 'ALL'
|
252
|
+
messages << "access_type_" + access_type
|
253
|
+
end
|
187
254
|
|
255
|
+
#check :check_index_walk
|
256
|
+
# disabling this one for now, it's not quite good enough and has a high
|
257
|
+
# false-negative rate.
|
258
|
+
def check_index_walk
|
259
|
+
if first['index_walk']
|
260
|
+
@cost = limit
|
261
|
+
messages << 'index_walk'
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
check :check_key_size
|
266
|
+
def check_key_size
|
188
267
|
# TODO: if possible_keys but mysql chooses NULL, this could be a test-data issue,
|
189
268
|
# pick the best key from the list of possibilities.
|
190
269
|
#
|
191
270
|
if first_key
|
192
|
-
|
271
|
+
@cost = @stats.estimate_key(first_table, first_key, first['used_key_parts'])
|
193
272
|
else
|
194
273
|
if first['possible_keys'].nil?
|
195
274
|
# if no possibile we're table scanning, use PRIMARY to indicate that cost.
|
196
275
|
# note that this can be wildly inaccurate bcs of WHERE + LIMIT stuff.
|
197
|
-
|
276
|
+
@cost = table_size
|
198
277
|
else
|
199
278
|
if @options[:force_key]
|
200
279
|
# we were asked to force a key, but mysql still told us to fuck ourselves.
|
@@ -202,20 +281,37 @@ module Shiba
|
|
202
281
|
#
|
203
282
|
# there seems to be cases where mysql lists `possible_key` values
|
204
283
|
# that it then cannot use, seen this in OR queries.
|
205
|
-
|
284
|
+
@cost = table_size
|
285
|
+
else
|
286
|
+
possibilities = [table_size]
|
287
|
+
possibilities += first['possible_keys'].map do |key|
|
288
|
+
estimate_row_count_with_key(key)
|
289
|
+
end
|
290
|
+
@cost = possibilities.compact.min
|
206
291
|
end
|
207
|
-
|
208
|
-
possibilities = [Shiba::Index.count(first_table, @stats)]
|
209
|
-
possibilities += first['possible_keys'].map do |key|
|
210
|
-
estimate_row_count_with_key(key)
|
211
|
-
end
|
212
|
-
possibilities.compact.min
|
213
292
|
end
|
214
293
|
end
|
215
294
|
end
|
216
295
|
|
296
|
+
def check_return_size
|
297
|
+
if limit
|
298
|
+
@return_size = limit
|
299
|
+
elsif aggregation?
|
300
|
+
@return_size = 1
|
301
|
+
else
|
302
|
+
@return_size = @cost
|
303
|
+
end
|
304
|
+
|
305
|
+
if @return_size && @return_size > 100
|
306
|
+
messages << "retsize_bad"
|
307
|
+
else
|
308
|
+
messages << "retsize_good"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
217
312
|
def estimate_row_count_with_key(key)
|
218
|
-
Explain.new(@sql, @stats, @backtrace, force_key: key)
|
313
|
+
explain = Explain.new(@sql, @stats, @backtrace, force_key: key)
|
314
|
+
explain.run_checks!
|
219
315
|
rescue Mysql2::Error => e
|
220
316
|
if /Key .+? doesn't exist in table/ =~ e.message
|
221
317
|
return nil
|
@@ -244,14 +340,18 @@ module Shiba
|
|
244
340
|
end
|
245
341
|
|
246
342
|
def run_checks!
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
return
|
343
|
+
self.class.get_checks.each do |check|
|
344
|
+
res = send(check)
|
345
|
+
break if @cost
|
251
346
|
end
|
347
|
+
check_return_size
|
348
|
+
@cost
|
349
|
+
end
|
252
350
|
|
253
|
-
|
351
|
+
def humanized_explain
|
352
|
+
h = @explain_json['query_block'].dup
|
353
|
+
%w(select_id cost_info).each { |i| h.delete(i) }
|
354
|
+
h
|
254
355
|
end
|
255
356
|
end
|
256
357
|
end
|
257
|
-
|