Completed
Push — master ( 377b48...31c010 )
by Michael
07:41
created

QueryPlanner.indexes_for_query()   A

Complexity

Conditions 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
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