|
1
|
|
|
require 'forwardable' |
|
2
|
|
|
require 'ostruct' |
|
3
|
|
|
|
|
4
|
|
|
module NoSE |
|
5
|
|
|
module Plans |
|
6
|
|
|
# Ongoing state of a query throughout the execution plan |
|
7
|
|
|
class QueryState |
|
8
|
|
|
attr_accessor :fields, :eq, :range, :order_by, :graph, |
|
9
|
|
|
:joins, :cardinality, :hash_cardinality, :given_fields |
|
10
|
|
|
attr_reader :query, :model |
|
11
|
|
|
|
|
12
|
|
|
def initialize(query, model) |
|
13
|
|
|
@query = query |
|
14
|
|
|
@model = model |
|
15
|
|
|
@fields = query.select |
|
16
|
|
|
@eq = query.eq_fields.dup |
|
17
|
|
|
@range = query.range_field |
|
18
|
|
|
@graph = query.graph |
|
19
|
|
|
@joins = query.materialize_view.graph.join_order(@eq) |
|
20
|
|
|
@order_by = query.order.dup |
|
21
|
|
|
|
|
22
|
|
|
# We never need to order by fields listed in equality predicates |
|
23
|
|
|
# since we'll only ever have rows with a single value |
|
24
|
|
|
@order_by -= @eq.to_a |
|
25
|
|
|
|
|
26
|
|
|
@cardinality = 1 # this will be updated on the first index lookup |
|
27
|
|
|
@hash_cardinality = 1 |
|
28
|
|
|
@given_fields = @eq.dup |
|
29
|
|
|
end |
|
30
|
|
|
|
|
31
|
|
|
# All the fields referenced anywhere in the query |
|
32
|
|
|
def all_fields |
|
33
|
|
|
all_fields = @fields + @eq |
|
34
|
|
|
all_fields << @range unless @range.nil? |
|
35
|
|
|
all_fields |
|
36
|
|
|
end |
|
37
|
|
|
|
|
38
|
|
|
# :nocov: |
|
39
|
|
|
def to_color |
|
40
|
|
|
@query.text + |
|
41
|
|
|
"\n fields: " + @fields.map(&:to_color).to_a.to_color + |
|
42
|
|
|
"\n eq: " + @eq.map(&:to_color).to_a.to_color + |
|
43
|
|
|
"\n range: " + (@range.nil? ? '(nil)' : @range.name) + |
|
44
|
|
|
"\n order: " + @order_by.map(&:to_color).to_a.to_color + |
|
45
|
|
|
"\n graph: " + @graph.inspect |
|
46
|
|
|
end |
|
47
|
|
|
# :nocov: |
|
48
|
|
|
|
|
49
|
|
|
# Check if the query has been fully answered |
|
50
|
|
|
# @return [Boolean] |
|
51
|
|
|
def answered?(check_limit: true) |
|
52
|
|
|
done = @fields.empty? && @eq.empty? && @range.nil? && |
|
53
|
|
|
@order_by.empty? && @joins.empty? && @graph.empty? |
|
54
|
|
|
|
|
55
|
|
|
# Check if the limit has been applied |
|
56
|
|
|
done &&= @cardinality <= @query.limit unless @query.limit.nil? || |
|
57
|
|
|
!check_limit |
|
58
|
|
|
|
|
59
|
|
|
done |
|
60
|
|
|
end |
|
61
|
|
|
|
|
62
|
|
|
# Get all fields relevant for filtering in the given |
|
63
|
|
|
# graph, optionally including selected fields |
|
64
|
|
|
# @return [Array<Field>] |
|
65
|
|
|
def fields_for_graph(graph, include_entity, select: false) |
|
66
|
|
|
graph_fields = @eq + @order_by |
|
67
|
|
|
graph_fields << @range unless @range.nil? |
|
68
|
|
|
|
|
69
|
|
|
# If necessary, include ALL the fields which should be selected, |
|
70
|
|
|
# otherwise we can exclude fields from leaf entity sets since |
|
71
|
|
|
# we may end up selecting these with a separate index lookup |
|
72
|
|
|
entities = graph.entities |
|
73
|
|
|
graph_fields += @fields.select do |field| |
|
74
|
|
|
entities.include?(field.parent) && |
|
75
|
|
|
(select || !graph.leaf_entity?(field.parent) || |
|
76
|
|
|
(field.parent == include_entity && graph.size > 1)) |
|
77
|
|
|
end |
|
78
|
|
|
|
|
79
|
|
|
graph_fields.select { |field| entities.include? field.parent } |
|
80
|
|
|
end |
|
81
|
|
|
end |
|
82
|
|
|
|
|
83
|
|
|
# A tree of possible query plans |
|
84
|
|
|
class QueryPlanTree |
|
85
|
|
|
include Enumerable |
|
86
|
|
|
|
|
87
|
|
|
attr_reader :root |
|
88
|
|
|
attr_accessor :cost_model |
|
89
|
|
|
|
|
90
|
|
|
def initialize(state, cost_model) |
|
91
|
|
|
@root = RootPlanStep.new(state) |
|
92
|
|
|
@cost_model = cost_model |
|
93
|
|
|
end |
|
94
|
|
|
|
|
95
|
|
|
# Select all plans which use only a given set of indexes |
|
96
|
|
|
def select_using_indexes(indexes) |
|
97
|
|
|
select do |plan| |
|
98
|
|
|
plan.all? do |step| |
|
99
|
|
|
!step.is_a?(Plans::IndexLookupPlanStep) || |
|
100
|
|
|
indexes.include?(step.index) |
|
101
|
|
|
end |
|
102
|
|
|
end |
|
103
|
|
|
end |
|
104
|
|
|
|
|
105
|
|
|
# The query this tree of plans is generated for |
|
106
|
|
|
# @return [Query] |
|
107
|
|
|
def query |
|
108
|
|
|
@root.state.query |
|
109
|
|
|
end |
|
110
|
|
|
|
|
111
|
|
|
# Enumerate all plans in the tree |
|
112
|
|
|
def each |
|
113
|
|
|
nodes = [@root] |
|
114
|
|
|
|
|
115
|
|
|
while nodes.length > 0 |
|
116
|
|
|
node = nodes.pop |
|
117
|
|
|
if node.children.length > 0 |
|
118
|
|
|
nodes.concat node.children.to_a |
|
119
|
|
|
else |
|
120
|
|
|
# This is just an extra check to make absolutely |
|
121
|
|
|
# sure we never consider invalid statement plans |
|
122
|
|
|
fail unless node.state.answered? |
|
123
|
|
|
|
|
124
|
|
|
yield node.parent_steps @cost_model |
|
125
|
|
|
end |
|
126
|
|
|
end |
|
127
|
|
|
end |
|
128
|
|
|
|
|
129
|
|
|
# Return the total number of plans for this statement |
|
130
|
|
|
# @return [Integer] |
|
131
|
|
|
def size |
|
132
|
|
|
to_a.count |
|
133
|
|
|
end |
|
134
|
|
|
|
|
135
|
|
|
# :nocov: |
|
136
|
|
|
def to_color(step = nil, indent = 0) |
|
137
|
|
|
step = @root if step.nil? |
|
138
|
|
|
this_step = ' ' * indent + step.to_color |
|
139
|
|
|
this_step += " [yellow]$#{step.cost.round 5}[/]" \ |
|
140
|
|
|
unless step.is_a?(RootPlanStep) || step.cost.nil? |
|
141
|
|
|
this_step + "\n" + step.children.map do |child_step| |
|
142
|
|
|
to_color child_step, indent + 1 |
|
143
|
|
|
end.reduce('', &:+) |
|
144
|
|
|
end |
|
145
|
|
|
# :nocov: |
|
146
|
|
|
end |
|
147
|
|
|
|
|
148
|
|
|
# Thrown when it is not possible to construct a plan for a statement |
|
149
|
|
|
class NoPlanException < StandardError |
|
150
|
|
|
end |
|
151
|
|
|
|
|
152
|
|
|
# A single plan for a query |
|
153
|
|
|
class QueryPlan < AbstractPlan |
|
154
|
|
|
attr_reader :steps |
|
155
|
|
|
attr_accessor :query, :cost_model |
|
156
|
|
|
|
|
157
|
|
|
include Comparable |
|
158
|
|
|
include Enumerable |
|
159
|
|
|
|
|
160
|
|
|
# Most of the work is delegated to the array |
|
161
|
|
|
extend Forwardable |
|
162
|
|
|
def_delegators :@steps, :each, :<<, :[], :==, :===, :eql?, |
|
163
|
|
|
:inspect, :to_s, :to_a, :to_ary, :last, :length, :count |
|
164
|
|
|
|
|
165
|
|
|
def initialize(query, cost_model) |
|
166
|
|
|
@steps = [] |
|
167
|
|
|
@query = query |
|
168
|
|
|
@cost_model = cost_model |
|
169
|
|
|
end |
|
170
|
|
|
|
|
171
|
|
|
# The weight of this query for a given workload |
|
172
|
|
|
# @return [Fixnum] |
|
173
|
|
|
def weight |
|
174
|
|
|
return 1 if @workload.nil? |
|
175
|
|
|
|
|
176
|
|
|
@workload.statement_weights[@query] |
|
177
|
|
|
end |
|
178
|
|
|
|
|
179
|
|
|
# Groups for plans are stored in the query |
|
180
|
|
|
# @return [String] |
|
181
|
|
|
def group |
|
182
|
|
|
@query.group |
|
183
|
|
|
end |
|
184
|
|
|
|
|
185
|
|
|
# Name plans after the associated query |
|
186
|
|
|
# @return [String] |
|
187
|
|
|
def name |
|
188
|
|
|
@query.text |
|
189
|
|
|
end |
|
190
|
|
|
|
|
191
|
|
|
# Fields selected by this plan |
|
192
|
|
|
# @return [Array<Fields::Field>] |
|
193
|
|
|
def select_fields |
|
194
|
|
|
@query.select |
|
195
|
|
|
end |
|
196
|
|
|
|
|
197
|
|
|
# Parameters to this execution plan |
|
198
|
|
|
def params |
|
199
|
|
|
@query.conditions |
|
200
|
|
|
end |
|
201
|
|
|
|
|
202
|
|
|
# Two plans are compared by their execution cost |
|
203
|
|
|
# @return [Boolean] |
|
204
|
|
|
def <=>(other) |
|
205
|
|
|
cost <=> other.cost |
|
206
|
|
|
end |
|
207
|
|
|
|
|
208
|
|
|
# The estimated cost of executing the query using this plan |
|
209
|
|
|
# @return [Numeric] |
|
210
|
|
|
def cost |
|
211
|
|
|
costs = @steps.map(&:cost) |
|
212
|
|
|
costs.inject(0, &:+) unless costs.any?(&:nil?) |
|
213
|
|
|
end |
|
214
|
|
|
|
|
215
|
|
|
# Get the indexes used by this query plan |
|
216
|
|
|
# @return [Array<Index>] |
|
217
|
|
|
def indexes |
|
218
|
|
|
@steps.select { |step| step.is_a? IndexLookupPlanStep }.map(&:index) |
|
219
|
|
|
end |
|
220
|
|
|
end |
|
221
|
|
|
|
|
222
|
|
|
# A query planner which can construct a tree of query plans |
|
223
|
|
|
class QueryPlanner |
|
224
|
|
|
def initialize(model, indexes, cost_model) |
|
225
|
|
|
@logger = Logging.logger['nose::query_planner'] |
|
226
|
|
|
|
|
227
|
|
|
@model = model |
|
228
|
|
|
@indexes = indexes |
|
229
|
|
|
@cost_model = cost_model |
|
230
|
|
|
end |
|
231
|
|
|
|
|
232
|
|
|
# Find a tree of plans for the given query |
|
233
|
|
|
# @return [QueryPlanTree] |
|
234
|
|
|
# @raise [NoPlanException] |
|
235
|
|
|
def find_plans_for_query(query) |
|
236
|
|
|
state = QueryState.new query, @model |
|
237
|
|
|
state.freeze |
|
238
|
|
|
tree = QueryPlanTree.new state, @cost_model |
|
239
|
|
|
|
|
240
|
|
|
indexes_by_joins = indexes_for_query(query, state.joins) |
|
241
|
|
|
find_plans_for_step tree.root, indexes_by_joins |
|
242
|
|
|
|
|
243
|
|
|
if tree.root.children.empty? |
|
244
|
|
|
tree = QueryPlanTree.new state, @cost_model |
|
245
|
|
|
find_plans_for_step tree.root, indexes_by_joins, prune: false |
|
246
|
|
|
fail NoPlanException, "#{query.inspect} #{tree.inspect}" |
|
247
|
|
|
end |
|
248
|
|
|
|
|
249
|
|
|
@logger.debug { "Plans for #{query.inspect}: #{tree.inspect}" } |
|
250
|
|
|
|
|
251
|
|
|
tree |
|
252
|
|
|
end |
|
253
|
|
|
|
|
254
|
|
|
# Get the minimum cost plan for executing this query |
|
255
|
|
|
# @return [QueryPlan] |
|
256
|
|
|
def min_plan(query) |
|
257
|
|
|
find_plans_for_query(query).min |
|
258
|
|
|
end |
|
259
|
|
|
|
|
260
|
|
|
private |
|
261
|
|
|
|
|
262
|
|
|
# Produce indexes possibly useful for this query |
|
263
|
|
|
# grouped by the first entity they join on |
|
264
|
|
|
# @return [Hash] |
|
265
|
|
|
def indexes_for_query(query, joins) |
|
266
|
|
|
indexes_by_joins = Hash.new { |h, k| h[k] = Set.new } |
|
267
|
|
|
@indexes.each do |index| |
|
268
|
|
|
# Limit indices to those which cross the query path |
|
269
|
|
|
next unless index.graph.entities.to_set.subset? \ |
|
270
|
|
|
query.graph.entities.to_set |
|
271
|
|
|
|
|
272
|
|
|
first_entity = joins.find do |entity| |
|
273
|
|
|
index.graph.entities.include?(entity) |
|
274
|
|
|
end |
|
275
|
|
|
indexes_by_joins[first_entity].add index |
|
276
|
|
|
end |
|
277
|
|
|
|
|
278
|
|
|
indexes_by_joins |
|
279
|
|
|
end |
|
280
|
|
|
|
|
281
|
|
|
# Remove plans ending with this step in the tree |
|
282
|
|
|
# @return[Boolean] true if pruning resulted in an empty tree |
|
283
|
|
|
def prune_plan(prune_step) |
|
284
|
|
|
# Walk up the tree and remove the branch for the failed plan |
|
285
|
|
|
while prune_step.children.length <= 1 && |
|
286
|
|
|
!prune_step.is_a?(RootPlanStep) |
|
287
|
|
|
prune_step = prune_step.parent |
|
288
|
|
|
prev_step = prune_step |
|
289
|
|
|
end |
|
290
|
|
|
|
|
291
|
|
|
# If we reached the root, we have no plan |
|
292
|
|
|
return true if prune_step.is_a? RootPlanStep |
|
293
|
|
|
|
|
294
|
|
|
prune_step.children.delete prev_step |
|
295
|
|
|
|
|
296
|
|
|
false |
|
297
|
|
|
end |
|
298
|
|
|
|
|
299
|
|
|
# Find possible query plans for a query starting at the given step |
|
300
|
|
|
# @return [void] |
|
301
|
|
|
def find_plans_for_step(step, indexes_by_joins, prune: true) |
|
302
|
|
|
return if step.state.answered? |
|
303
|
|
|
|
|
304
|
|
|
steps = find_steps_for_state step, step.state, indexes_by_joins |
|
305
|
|
|
|
|
306
|
|
|
if !steps.empty? |
|
307
|
|
|
step.children = steps |
|
308
|
|
|
steps.each { |new_step| new_step.calculate_cost @cost_model } |
|
309
|
|
|
steps.each do |child_step| |
|
310
|
|
|
find_plans_for_step child_step, indexes_by_joins |
|
311
|
|
|
|
|
312
|
|
|
# Remove this step if finding a plan from here failed |
|
313
|
|
|
if child_step.children.empty? && !child_step.state.answered? |
|
314
|
|
|
step.children.delete child_step |
|
315
|
|
|
end |
|
316
|
|
|
end |
|
317
|
|
|
elsif prune |
|
318
|
|
|
return if step.is_a?(RootPlanStep) || prune_plan(step.parent) |
|
319
|
|
|
else |
|
320
|
|
|
step.children = [PrunedPlanStep.new] |
|
321
|
|
|
end |
|
322
|
|
|
end |
|
323
|
|
|
|
|
324
|
|
|
# Find all possible plan steps not using indexes |
|
325
|
|
|
# @return [Array<PlanStep>] |
|
326
|
|
|
def find_nonindexed_steps(parent, state) |
|
327
|
|
|
steps = [] |
|
328
|
|
|
return steps if parent.is_a? RootPlanStep |
|
329
|
|
|
|
|
330
|
|
|
[SortPlanStep, FilterPlanStep, LimitPlanStep].each \ |
|
331
|
|
|
{ |step| steps.push step.apply(parent, state) } |
|
332
|
|
|
steps.flatten! |
|
333
|
|
|
steps.compact! |
|
334
|
|
|
|
|
335
|
|
|
steps |
|
336
|
|
|
end |
|
337
|
|
|
|
|
338
|
|
|
# Get a list of possible next steps for a query in the given state |
|
339
|
|
|
# @return [Array<PlanStep>] |
|
340
|
|
|
def find_steps_for_state(parent, state, indexes_by_joins) |
|
341
|
|
|
steps = find_nonindexed_steps parent, state |
|
342
|
|
|
return steps unless steps.empty? |
|
343
|
|
|
|
|
344
|
|
|
# Don't allow indices to be used multiple times |
|
345
|
|
|
indexes = (indexes_by_joins[state.joins.first] || Set.new).to_set |
|
346
|
|
|
used_indexes = parent.parent_steps.indexes.to_set |
|
347
|
|
|
(indexes - used_indexes).each do |index| |
|
348
|
|
|
new_step = IndexLookupPlanStep.apply parent, index, state |
|
349
|
|
|
next if new_step.nil? |
|
350
|
|
|
|
|
351
|
|
|
new_step.add_fields_from_index index |
|
352
|
|
|
steps.push new_step |
|
353
|
|
|
end |
|
354
|
|
|
|
|
355
|
|
|
steps |
|
356
|
|
|
end |
|
357
|
|
|
end |
|
358
|
|
|
end |
|
359
|
|
|
end |
|
360
|
|
|
|