Query Optimization
Query optimization finds the join order of minimal total cost for a set of tables by computing the optimal cost cost*(S) of every joinable subset S bottom-up with dynamic programming over partitions into two smaller subsets.
Formula#
Join graph. Undirected graph with one node per input table and an edge between two tables iff they can and should be joined according to the query. Two plans with table sets are joinable iff and the join graph has an edge between a table of and a table of .
Cost model (total cost; input tables have cost 0):
Optimal cost (recursion over partitions):
The recursion terminates because and are strict subsets of .
Dynamic programming in rounds. Plain recursion recomputes the same subsets over and over and takes exponential time, like the naive recursion for Fibonacci numbers (, while computing bottom-up takes linear time). Instead compute bottom-up:
- Round 1: one singleton plan per input table, cost 0.
- Round 2: all joinable pairs of input tables.
- Round : for and , combine every optimal plan of size with every joinable optimal plan of size ; keep the cheapest plan per table set.
- Round : yields for the set of all input tables.
Properties#
- Only reachable subsets: in round , do not enumerate all subsets of size ; only combine plans from earlier rounds whose union has size . Subsets that are not connected in the join graph never appear.
- Joinability check: the lecture code uses a nested loop in ; in practice one uses bit masks for the table sets.
- Size is plan-independent: with this cost model, is the minimum size of the tables in , whatever the split. The plans differ only in the cost of their intermediate results, so it pays to join the smallest table first.
- Scope: only binary joins are optimised; selections, projections and the choice of join algorithm (hash join, Merge Join, index lookup via B+ Tree) are not part of this model.
# public/code/lecture-06/query-planning.py (excerpt)
for k in range(2, len(self.input_plans) + 1):
optimal_plans[k] = {}
for k1 in range(1, k // 2 + 1):
k2 = k - k1
for plan1 in optimal_plans[k1].values():
for plan2 in optimal_plans[k2].values():
if self.join_graph.joinable(plan1, plan2):
plan = QueryPlan.join(plan1, plan2) # size = min, cost = c1 + c2 + size
if (plan.tables not in optimal_plans[k]
or plan.cost < optimal_plans[k][plan.tables].cost):
optimal_plans[k][plan.tables] = plan
Example#
Sources#
- Slides 06, p. 13–14 (recap, plans as parenthesised expressions), p. 15 (join graph), p. 16 (cost function), p. 17 (optimal cost), p. 18 (Fibonacci), p. 19–21 (rounds, example, implementation)
- DBIS_VL06 > Computing the optimal query plan
- DBIS Sheet 06 (not published) (optimisation by hand and in the own DB system)