nose-cli 0.1.0pre
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 +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>
|