nose-cli 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/nose +26 -0
- data/bin/random_rubis +105 -0
- data/bin/restart-cassandra.sh +20 -0
- data/bin/run-experiments.sh +61 -0
- data/data/nose-cli/nose.yml.example +32 -0
- data/lib/nose_cli.rb +364 -0
- data/lib/nose_cli/analyze.rb +94 -0
- data/lib/nose_cli/benchmark.rb +145 -0
- data/lib/nose_cli/collect_results.rb +55 -0
- data/lib/nose_cli/console.rb +50 -0
- data/lib/nose_cli/create.rb +35 -0
- data/lib/nose_cli/diff_plans.rb +39 -0
- data/lib/nose_cli/dump.rb +67 -0
- data/lib/nose_cli/execute.rb +241 -0
- data/lib/nose_cli/export.rb +39 -0
- data/lib/nose_cli/genworkload.rb +24 -0
- data/lib/nose_cli/graph.rb +24 -0
- data/lib/nose_cli/load.rb +44 -0
- data/lib/nose_cli/measurements.rb +36 -0
- data/lib/nose_cli/plan_schema.rb +84 -0
- data/lib/nose_cli/proxy.rb +32 -0
- data/lib/nose_cli/random_plans.rb +82 -0
- data/lib/nose_cli/recost.rb +45 -0
- data/lib/nose_cli/reformat.rb +22 -0
- data/lib/nose_cli/repl.rb +144 -0
- data/lib/nose_cli/search.rb +77 -0
- data/lib/nose_cli/search_all.rb +120 -0
- data/lib/nose_cli/search_bench.rb +52 -0
- data/lib/nose_cli/shared_options.rb +30 -0
- data/lib/nose_cli/texify.rb +141 -0
- data/lib/nose_cli/why.rb +70 -0
- data/templates/completions.erb +56 -0
- data/templates/man.erb +33 -0
- data/templates/report.erb +138 -0
- data/templates/subman.erb +19 -0
- metadata +345 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module CLI
|
5
|
+
# Add a command to run the advisor and benchmarks for a given workload
|
6
|
+
class NoSECLI < Thor
|
7
|
+
desc 'search-bench NAME', 'run the workload NAME and benchmarks'
|
8
|
+
|
9
|
+
long_desc <<-LONGDESC
|
10
|
+
`nose search-bench` is a convenient way to quickly test many different
|
11
|
+
schemas. It will run `nose search`, `nose create`, `nose load`, and
|
12
|
+
`nose benchmark` to produce final benchmark results. It also accepts
|
13
|
+
all of the command-line options for each of those commands.
|
14
|
+
LONGDESC
|
15
|
+
|
16
|
+
def search_bench(name)
|
17
|
+
# Open a tempfile which will be used for advisor output
|
18
|
+
filename = Tempfile.new('workload').path
|
19
|
+
|
20
|
+
# Set some default options for various commands
|
21
|
+
opts = options.to_h
|
22
|
+
opts[:output] = filename
|
23
|
+
opts[:format] = 'json'
|
24
|
+
opts[:skip_existing] = true
|
25
|
+
|
26
|
+
o = filter_command_options opts, 'search'
|
27
|
+
$stderr.puts "Running advisor #{o}..."
|
28
|
+
invoke self.class, :search, [name], o
|
29
|
+
|
30
|
+
invoke self.class, :reformat, [filename], {}
|
31
|
+
|
32
|
+
o = filter_command_options opts, 'create'
|
33
|
+
$stderr.puts "Creating indexes #{o}..."
|
34
|
+
invoke self.class, :create, [filename], o
|
35
|
+
|
36
|
+
o = filter_command_options opts, 'load'
|
37
|
+
$stderr.puts "Loading data #{o}..."
|
38
|
+
invoke self.class, :load, [filename], o
|
39
|
+
|
40
|
+
o = filter_command_options opts, 'benchmark'
|
41
|
+
$stderr.puts "Running benchmark #{o}..."
|
42
|
+
invoke self.class, :benchmark, [filename], o
|
43
|
+
end
|
44
|
+
|
45
|
+
# Allow this command to accept the options for all commands it calls
|
46
|
+
commands['search_bench'].options.merge! commands['create'].options
|
47
|
+
commands['search_bench'].options.merge! commands['benchmark'].options
|
48
|
+
commands['search_bench'].options.merge! commands['load'].options
|
49
|
+
commands['search_bench'].options.merge! commands['search'].options
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module CLI
|
5
|
+
# Add the use of shared options
|
6
|
+
class NoSECLI < Thor
|
7
|
+
# Add a new option to those which can be potentially shared
|
8
|
+
def self.share_option(name, options = {})
|
9
|
+
@options ||= {}
|
10
|
+
@options[name] = options
|
11
|
+
end
|
12
|
+
|
13
|
+
# Use a shared option for the current command
|
14
|
+
# @return [void]
|
15
|
+
def self.shared_option(name)
|
16
|
+
method_option name, @options[name]
|
17
|
+
end
|
18
|
+
|
19
|
+
share_option :mix, type: :string, default: 'default',
|
20
|
+
desc: 'the name of the mix for weighting queries'
|
21
|
+
share_option :format, type: :string, default: 'txt',
|
22
|
+
enum: %w(txt json yml html), aliases: '-f',
|
23
|
+
desc: 'the format of the produced plans'
|
24
|
+
share_option :output, type: :string, default: nil, aliases: '-o',
|
25
|
+
banner: 'FILE',
|
26
|
+
desc: 'a file where produced plans ' \
|
27
|
+
'should be stored'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module CLI
|
5
|
+
# Add a command to reformat a plan file
|
6
|
+
class NoSECLI < Thor
|
7
|
+
desc 'texify PLAN_FILE',
|
8
|
+
'print the results from PLAN_FILE in LaTeX format'
|
9
|
+
|
10
|
+
long_desc <<-LONGDESC
|
11
|
+
`nose texify` loads the generated schema from the given file and prints
|
12
|
+
the generated schema in LaTeX format.
|
13
|
+
LONGDESC
|
14
|
+
|
15
|
+
shared_option :mix
|
16
|
+
|
17
|
+
def texify(plan_file)
|
18
|
+
# Load the indexes from the file
|
19
|
+
result, = load_plans plan_file, options
|
20
|
+
|
21
|
+
# If these are manually generated plans, load them separately
|
22
|
+
if result.plans.nil?
|
23
|
+
plans = Plans::ExecutionPlans.load(plan_file) \
|
24
|
+
.groups.values.flatten(1)
|
25
|
+
result.plans = plans.select { |p| p.update_steps.empty? }
|
26
|
+
result.update_plans = plans.reject { |p| p.update_steps.empty? }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Print document header
|
30
|
+
puts "\\documentclass{article}\n\\begin{document}\n\\begin{flushleft}"
|
31
|
+
|
32
|
+
# Print the LaTeX for all indexes and plans
|
33
|
+
texify_indexes result.indexes
|
34
|
+
texify_plans result.plans + result.update_plans
|
35
|
+
|
36
|
+
# End the document
|
37
|
+
puts "\\end{flushleft}\n\\end{document}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Escape values for latex output
|
43
|
+
# @return [String]
|
44
|
+
def tex_escape(str)
|
45
|
+
str.gsub '_', '\\_'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Print the LaTeX for all query plans
|
49
|
+
# @return [void]
|
50
|
+
def texify_plans(plans)
|
51
|
+
puts '\\bigskip\\textbf{Plans} \\\\\\bigskip'
|
52
|
+
|
53
|
+
plans.group_by(&:group).each do |group, grouped_plans|
|
54
|
+
group = group.nil? ? '' : tex_escape(group)
|
55
|
+
texify_plan_group group, grouped_plans
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Print the LaTeX from a group of query plans
|
60
|
+
# @return [void]
|
61
|
+
def texify_plan_group(group, grouped_plans)
|
62
|
+
puts "\\textbf{#{group}} \\\\" unless group.empty?
|
63
|
+
|
64
|
+
grouped_plans.each do |plan|
|
65
|
+
if plan.is_a?(Plans::QueryPlan) ||
|
66
|
+
(plan.is_a?(Plans::QueryExecutionPlan) &&
|
67
|
+
plan.update_steps.empty?)
|
68
|
+
puts texify_plan_steps plan.steps
|
69
|
+
else
|
70
|
+
puts texify_plan_steps plan.query_plans.flat_map(&:to_a) + \
|
71
|
+
plan.update_steps
|
72
|
+
end
|
73
|
+
|
74
|
+
puts ' \\\\'
|
75
|
+
end
|
76
|
+
|
77
|
+
puts '\\medskip'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Print the LaTeX from a set of plan steps
|
81
|
+
# @return [void]
|
82
|
+
def texify_plan_steps(steps)
|
83
|
+
steps.map do |step|
|
84
|
+
case step
|
85
|
+
when Plans::IndexLookupPlanStep
|
86
|
+
"Request \\textbf{#{tex_escape step.index.key}}"
|
87
|
+
when Plans::FilterPlanStep
|
88
|
+
"Filter by #{texify_fields((step.eq + [step.range]).compact)}"
|
89
|
+
when Plans::SortPlanStep
|
90
|
+
"Sort by #{texify_fields step.sort_fields}"
|
91
|
+
when Plans::LimitPlanStep
|
92
|
+
"Limit #{step.limit}"
|
93
|
+
when Plans::DeletePlanStep
|
94
|
+
"Delete from \\textbf{#{tex_escape step.index.key}}"
|
95
|
+
when Plans::InsertPlanStep
|
96
|
+
"Insert into \\textbf{#{tex_escape step.index.key}}"
|
97
|
+
end
|
98
|
+
end.join(', ')
|
99
|
+
end
|
100
|
+
|
101
|
+
# Print all LaTeX for a given index
|
102
|
+
# @return [void]
|
103
|
+
def texify_indexes(indexes)
|
104
|
+
puts '\\bigskip\\textbf{Indexes} \\\\\\bigskip'
|
105
|
+
|
106
|
+
indexes.each do |index|
|
107
|
+
# Print the key of the index
|
108
|
+
puts "\\textbf{#{tex_escape index.key}} \\\\"
|
109
|
+
|
110
|
+
fields = index.hash_fields.map do |field|
|
111
|
+
texify_field(field, true)
|
112
|
+
end
|
113
|
+
|
114
|
+
fields += index.order_fields.map do |field|
|
115
|
+
texify_field(field, true, true)
|
116
|
+
end
|
117
|
+
|
118
|
+
fields += index.extra.map { |field| texify_field(field) }
|
119
|
+
|
120
|
+
puts fields.join(', ') + ' \\\\\\medskip'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Produce the LaTex for an array of fields
|
125
|
+
# @return [String]
|
126
|
+
def texify_fields(fields)
|
127
|
+
fields.map { |field| texify_field field }.join ', '
|
128
|
+
end
|
129
|
+
|
130
|
+
# Produce the LaTeX for a given index field
|
131
|
+
# @return [String]
|
132
|
+
def texify_field(field, underline = false, italic = false)
|
133
|
+
tex = tex_escape field.to_s
|
134
|
+
tex = "\\textit{#{tex}}" if italic
|
135
|
+
tex = "\\underline{#{tex}}" if underline
|
136
|
+
|
137
|
+
tex
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/nose_cli/why.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module CLI
|
5
|
+
# Add a command to generate a graphic of the schema from a workload
|
6
|
+
class NoSECLI < Thor
|
7
|
+
desc 'why PLAN_FILE',
|
8
|
+
'output the reason for including each index in PLAN_FILE'
|
9
|
+
|
10
|
+
long_desc <<-LONGDESC
|
11
|
+
`nose why` is used to better understand why NoSE included a particular
|
12
|
+
index in a schema. This is especially helpful when comparing with
|
13
|
+
manually-defined execution plans.
|
14
|
+
LONGDESC
|
15
|
+
|
16
|
+
def why(plan_file)
|
17
|
+
result = load_results plan_file
|
18
|
+
indexes_usage = Hash.new { |h, k| h[k] = [] }
|
19
|
+
|
20
|
+
# Count the indexes used in queries
|
21
|
+
query_count = Set.new
|
22
|
+
update_index_usage result.plans, indexes_usage, query_count
|
23
|
+
|
24
|
+
# Count the indexes used in support queries
|
25
|
+
# (ignoring those used in queries)
|
26
|
+
support_count = Set.new
|
27
|
+
result.update_plans.each do |plan|
|
28
|
+
update_index_usage plan.query_plans, indexes_usage,
|
29
|
+
support_count, query_count
|
30
|
+
end
|
31
|
+
|
32
|
+
# Produce the final output of index usage
|
33
|
+
print_index_usage indexes_usage, query_count, support_count
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Track usage of indexes in the set of query plans updating both
|
39
|
+
# a dictionary of statements relevant to each index and a set
|
40
|
+
# of unique statements used (optionally ignoring some)
|
41
|
+
# @return [void]
|
42
|
+
def update_index_usage(plans, indexes_usage, statement_usage,
|
43
|
+
ignore = Set.new)
|
44
|
+
plans.each do |plan|
|
45
|
+
plan.indexes.each do |index|
|
46
|
+
indexes_usage[index] << if plan.respond_to?(:statement)
|
47
|
+
plan.statement
|
48
|
+
else
|
49
|
+
plan.query
|
50
|
+
end
|
51
|
+
statement_usage.add index unless ignore.include? index
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Print out the statements each index is used for
|
57
|
+
# @return [void]
|
58
|
+
def print_index_usage(indexes_usage, query_count, support_count)
|
59
|
+
indexes_usage.each do |index, statements|
|
60
|
+
p index
|
61
|
+
statements.each { |s| p s }
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
|
65
|
+
puts " Queries: #{query_count.length}"
|
66
|
+
puts "Support queries: #{support_count.length}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
_nose_complete() {
|
2
|
+
case $1 in
|
3
|
+
1) _nose_commands;;
|
4
|
+
2)
|
5
|
+
case "$2" in
|
6
|
+
help) _nose_commands;;
|
7
|
+
*) _nose_option;;
|
8
|
+
esac;;
|
9
|
+
*) _nose_option;;
|
10
|
+
esac
|
11
|
+
}
|
12
|
+
|
13
|
+
if type compdef 1>/dev/null 2>/dev/null; then
|
14
|
+
compdef _nose nose
|
15
|
+
_nose() { _nose_complete $((${#words} - 1)) "${words[2]}"; }
|
16
|
+
_nose_commands() { list=(<%= commands.map { |name, c| "#{name}:\"#{Shellwords.escape(c.description)}\"" }.join(' ') %>) _describe -t common-commands 'common commands' list; }
|
17
|
+
_nose_option() {
|
18
|
+
case "${words[2]}" in
|
19
|
+
<%= commands.map do |name, c|
|
20
|
+
" #{name}) _arguments -s -S " + c.options.values.flat_map do |opt|
|
21
|
+
(opt.aliases << opt.switch_name).map do |switch|
|
22
|
+
"\"#{switch}[#{Shellwords.escape(opt.description)}]\""
|
23
|
+
end
|
24
|
+
end.uniq.join(' ') + ' \'*:file:_files\' && return 0;;'
|
25
|
+
end.join("\n") %>
|
26
|
+
esac
|
27
|
+
}
|
28
|
+
elif type compctl 1>/dev/null 2>/dev/null; then
|
29
|
+
compctl -K _nose nose
|
30
|
+
_nose() { read -cA words && _nose_complete $((${#words} - 1)) "${words[2]}"; }
|
31
|
+
_nose_commands() { reply=(<%= commands.map { |name, _| "\"#{name}\"" }.join(' ') %>); }
|
32
|
+
_nose_option() {
|
33
|
+
case "${words[2]}" in
|
34
|
+
<%= commands.map do |name, c|
|
35
|
+
" #{name}) reply=(" + c.options.values.flat_map do |opt|
|
36
|
+
(opt.aliases << opt.switch_name).map { |s| "\"#{s}\"" }
|
37
|
+
end.uniq.join(' ') + ');;'
|
38
|
+
end.join("\n") %>
|
39
|
+
esac
|
40
|
+
}
|
41
|
+
elif type complete 1>/dev/null 2>/dev/null; then
|
42
|
+
complete -F _nose nose
|
43
|
+
_nose() { _nose_complete "$COMP_CWORD" "${COMP_WORDS[1]}"; }
|
44
|
+
_nose_commands() { COMPREPLY=( $(compgen -W "<%= commands.map(&:first).join(' ') %>" -- "${COMP_WORDS[COMP_CWORD]}") ); }
|
45
|
+
_nose_option() {
|
46
|
+
local options
|
47
|
+
case "${COMP_WORDS[1]}" in
|
48
|
+
<%= commands.map do |name, c|
|
49
|
+
" #{name}) options=\"" + c.options.values.flat_map do |opt|
|
50
|
+
(opt.aliases << opt.switch_name)
|
51
|
+
end.uniq.join(' ') + '";;'
|
52
|
+
end.join("\n") %>
|
53
|
+
esac
|
54
|
+
COMPREPLY=( $(compgen -W "$options" -- "${COMP_WORDS[COMP_CWORD]}") )
|
55
|
+
}
|
56
|
+
fi
|
data/templates/man.erb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
nose(1) -- NoSQL Schema Evaluator
|
2
|
+
========================================
|
3
|
+
|
4
|
+
## SYNOPSIS
|
5
|
+
|
6
|
+
`nose` <%= options.each_value.map do |option|
|
7
|
+
'[' + (option.aliases + ["--#{option.name}"]).join('|') + ']'
|
8
|
+
end.join ', ' %>
|
9
|
+
|
10
|
+
## DESCRIPTION
|
11
|
+
|
12
|
+
NoSE is a tool for schema design in NoSQL databases.
|
13
|
+
|
14
|
+
## OPTIONS
|
15
|
+
|
16
|
+
<% options.each do |name, option| %>
|
17
|
+
* <%= option.usage %>
|
18
|
+
<%= option.description %>
|
19
|
+
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
## SUBCOMMANDS
|
23
|
+
|
24
|
+
<% commands.each do |name, command| %>
|
25
|
+
`nose-<%= name %>`(1)
|
26
|
+
<%= command.description %>
|
27
|
+
|
28
|
+
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
## SEE ALSO
|
32
|
+
|
33
|
+
<%= commands.map { |name, _| "`nose-#{name}`(1)" }.join ', ' %>
|
@@ -0,0 +1,138 @@
|
|
1
|
+
<% require 'ansi-to-html' %>
|
2
|
+
|
3
|
+
<!DOCTYPE html>
|
4
|
+
<html lang="en">
|
5
|
+
<head>
|
6
|
+
<meta charset="utf-8">
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
9
|
+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
|
10
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
11
|
+
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
12
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/default.min.css">
|
13
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
|
14
|
+
<script>hljs.initHighlightingOnLoad();</script>
|
15
|
+
<title>NoSE Schema Recommendation</title>
|
16
|
+
</head>
|
17
|
+
<body style="margin: 1em">
|
18
|
+
<h1>NoSE Schema Recommendation</h1>
|
19
|
+
|
20
|
+
<h2>Model</h2>
|
21
|
+
<%= svg %>
|
22
|
+
|
23
|
+
<% unless workload.model.source_code.nil? %>
|
24
|
+
<a class="btn btn-primary" style="display: block" data-toggle="collapse" data-target="#model-code">Model code
|
25
|
+
<span class="glyphicon glyphicon-menu-hamburger" style="float:right"></span></a>
|
26
|
+
<pre id="model-code" class="collapse"><code class="lang-ruby"><%= workload.model.source_code %></pre></code>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<h2>Queries</h2>
|
30
|
+
<% workload.queries.each do |query| %>
|
31
|
+
<% unless query.label.nil? %><strong><%= query.label %></strong><% end %>
|
32
|
+
<pre><code class="lang-sql"><%= query.text %></code></pre>
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<% unless workload.updates.empty? %><h2>Updates</h2><% end %>
|
36
|
+
<% workload.updates.each do |update| %>
|
37
|
+
<% unless update.label.nil? %><strong><%= update.label %></strong><% end %>
|
38
|
+
<pre><code class="lang-sql"><%= update.text %></code></pre>
|
39
|
+
<% end %>
|
40
|
+
|
41
|
+
<% unless workload.source_code.nil? %>
|
42
|
+
<a class="btn btn-primary" style="display: block" data-toggle="collapse" data-target="#workload-code">Workload code
|
43
|
+
<span class="glyphicon glyphicon-menu-hamburger" style="float:right"></span></a>
|
44
|
+
<pre id="workload-code" class="collapse"><code class="lang-ruby"><%= workload.source_code %></pre></code>
|
45
|
+
<% end %>
|
46
|
+
|
47
|
+
<h2>Indexes</h2>
|
48
|
+
<% ddl = backend ? backend.indexes_ddl.to_a : nil %>
|
49
|
+
<% indexes.each_with_index do |index, i| %>
|
50
|
+
<h3><%= index.key %></h3>
|
51
|
+
<% if ddl %>
|
52
|
+
<p><tt><%= ddl[i] %></tt></p>
|
53
|
+
<% end %>
|
54
|
+
<p><%= Ansi::To::Html.new(index.inspect).to_html %></p>
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
<h2>Query plans</h2>
|
58
|
+
<% plans.each do |plan| %>
|
59
|
+
<% unless plan.query.label.nil? %><strong><%= plan.query.label %></strong><% end %>
|
60
|
+
<pre><code class="lang-sql"><%= Ansi::To::Html.new(plan.query.inspect).to_html %></code></pre>
|
61
|
+
<strong>Cost: <%= plan.cost %></strong>
|
62
|
+
|
63
|
+
<% if plan.steps.length > 1 %>
|
64
|
+
<ol>
|
65
|
+
<% plan.each do |step| %>
|
66
|
+
<li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
|
67
|
+
<% end %>
|
68
|
+
</ol>
|
69
|
+
<% else %>
|
70
|
+
<p><%= Ansi::To::Html.new(plan.steps.first.inspect).to_html %></p>
|
71
|
+
<% end %>
|
72
|
+
<hr>
|
73
|
+
<% end %>
|
74
|
+
|
75
|
+
<h2>Update plans</h2>
|
76
|
+
<% update_plans.group_by(&:statement).each do |statement, plans| %>
|
77
|
+
<% unless statement.label.nil? %><strong><%= statement.label %></strong><% end %>
|
78
|
+
<pre><code class="lang-sql"><%= statement.text %></code></pre>
|
79
|
+
<strong>Total cost: <%= plans.map(&:cost).inject(0, &:+) %></strong>
|
80
|
+
|
81
|
+
<% plans.each do |plan| %>
|
82
|
+
<h4><%= plan.index.key %></h4>
|
83
|
+
<strong>Cost: <%= plan.cost %></strong>
|
84
|
+
|
85
|
+
<% unless plan.query_plans.empty? %>
|
86
|
+
<h5>Support queries</h5>
|
87
|
+
<% end %>
|
88
|
+
|
89
|
+
<% plan.query_plans.each do |query_plan| %>
|
90
|
+
<% unless query_plan.query.label.nil? %><strong><%= query_plan.query.label %></strong><% end %>
|
91
|
+
<pre><code class="lang-sql"><%= query_plan.query.text %></code></pre>
|
92
|
+
<% if query_plan.steps.length > 1 %>
|
93
|
+
<ol>
|
94
|
+
<% query_plan.each do |step| %>
|
95
|
+
<li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
|
96
|
+
<% end %>
|
97
|
+
</ol>
|
98
|
+
<% else %>
|
99
|
+
<p><%= Ansi::To::Html.new(query_plan.steps.first.inspect).to_html %></p>
|
100
|
+
<% end %>
|
101
|
+
<% end %>
|
102
|
+
|
103
|
+
<h5>Updates</h5>
|
104
|
+
|
105
|
+
<% if plan.update_steps.length > 1 %>
|
106
|
+
<ol>
|
107
|
+
<% plan.update_steps.each do |step| %>
|
108
|
+
<li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
|
109
|
+
<% end %>
|
110
|
+
</ol>
|
111
|
+
<% else %>
|
112
|
+
<p><%= Ansi::To::Html.new(plan.update_steps.first.inspect).to_html %></p>
|
113
|
+
<% end %>
|
114
|
+
<% end %>
|
115
|
+
<hr>
|
116
|
+
<% end %>
|
117
|
+
|
118
|
+
<% if enumerated_indexes %>
|
119
|
+
<h2>Enumerated Indexes</h2>
|
120
|
+
<% enumerated_indexes.each_with_index do |index, i| %>
|
121
|
+
<h3><%= index.key %></h3>
|
122
|
+
<% if ddl %>
|
123
|
+
<p><tt><%= ddl[i] %></tt></p>
|
124
|
+
<% end %>
|
125
|
+
<p><%= Ansi::To::Html.new(index.inspect).to_html %></p>
|
126
|
+
<% end %>
|
127
|
+
<% end %>
|
128
|
+
|
129
|
+
<h2>Summary</h2>
|
130
|
+
<dl>
|
131
|
+
<dt>Total size</dt>
|
132
|
+
<dd><%= total_size %></dd>
|
133
|
+
|
134
|
+
<dt>Total cost</dt>
|
135
|
+
<dd><%= total_cost %></dd>
|
136
|
+
</dl>
|
137
|
+
</body>
|
138
|
+
</html>
|