core/translate/optimizer/OPTIMIZER.md
Query optimization is obviously an important part of any SQL-based database engine. This document is an overview of what we currently do.
mod.rs
optimize_plan()access_method.rs
constraints.rs - Manages query constraints:
cost.rs
join.rs
order.rs
The goals of query optimization are at least the following:
The most important ways to achieve no. 1 and no. 2 are:
Limbo's optimizer is an implementation of an extremely traditional IBM System R style optimizer, i.e. straight from the 70s! The DP algorithm is explained below.
WHERE 1 is removed, WHERE 0 short-circuits the whole query because it is trivially false.WHERE t.x = 5, the expression 5 constrains table t to values of x that are exactly 5.Where t.x = u.x, the expression u.x constrains t, AND t.x constrains u.n = number of tables consideredn=1: find the lowest cost way to access each single table, given the constraints of the query. Memoize the result.n=2: for each table found in the n=1 step, find the best way to join that table with each other table. Memoize the result.n=3: for each 2-table subset found, find the best way to join that result to each other table. Memoize the result.n=m: for each m-1 table subset found, find the best way to join that result to the m'th tableSELECT * FROM a JOIN b JOIN c JOIN d. Compute JOIN(a,b,c,d) first. If JOIN (b,a) is already worse than JOIN(a,b,c,d), we don't have to even try JOIN(b,a,c).JOIN(b,a,c) is better than any other permutation of the same tables, e.g. JOIN(a,b,c), then we can discard ALL of the other permutations for that subset. For example, we don't need to consider JOIN(a,b,c,d) because we know it's worse than JOIN(b,a,c,d).join_order and Operations to match the computed best plan.Currently, in the absence of ANALYZE, sqlite_stat1 etc. we assume the following:
1,000,000 rows.=) filter will filter out some percentage of the result set.>) will filter out some smaller percentage of the result set.4096 byte database page holds 50 rows, i.e. roughly 80 bytes per rowFrom the above, we derive the following formula for estimating the cost of joining t1 with t2
JOIN_COST = PAGE_IO(t1.rows) + t1.rows * PAGE_IO(t2.rows)
For example, let's take the query SELECT * FROM t1 JOIN t2 USING(foo) WHERE t2.foo > 10. Let's assume the following:
t1 has 6400 rows and t2 has 8000 rowsThe best access method for both is a full table scan. The output cardinality of t1 is the full table, because nothing is filtering it. Hence, the cost of t1 JOIN t2 becomes:
JOIN_COST = PAGE_IO(t1.input_rows) + t1.output_rows * PAGE_IO(t2.input_rows)
// plugging in the values:
JOIN_COST = PAGE_IO(6400) + 6400 * PAGE_IO(8000)
JOIN_COST = 80 + 6400 * 100 = 640080
Now let's consider t2 JOIN t1. The best access method for both is still a full scan, but since we can filter on t2.foo > 10, its output cardinality decreases. Let's assume only 1/4 of the rows of t2 match the condition t2.foo > 10. Hence, the cost of t2 join t1 becomes:
JOIN_COST = PAGE_IO(t2.input_rows) + t2.output_rows * PAGE_IO(t1.input_rows)
// plugging in the values:
JOIN_COST = PAGE_IO(8000) + 1/4 * 8000 * PAGE_IO(6400)
JOIN_COST = 100 + 2000 * 80 = 160100
Even though t2 is a larger table, because we were able to reduce the input set to the join operation, it's dramatically cheaper.
Since we don't support ANALYZE, nor can we assume that users will call ANALYZE anyway, we use simple magic constants to estimate the selectivity of join predicates, row count of tables, and so on. When we have support for ANALYZE, we should plug the statistics from sqlite_stat1 and friends into the optimizer to make more informed decisions.
The output cardinality (output row count) of an operation is as follows:
OUTPUT_CARDINALITY_JOIN = INPUT_CARDINALITY_RHS * OUTPUT_CARDINALITY_RHS
where
INPUT_CARDINALITY_RHS = OUTPUT_CARDINALITY_LHS
example:
SELECT * FROM products p JOIN order_lines o ON p.id = o.product_id
Assuming there are 100 products, i.e. just selecting all products would yield 100 rows:
OUTPUT_CARDINALITY_LHS = 100
INPUT_CARDINALITY_RHS = 100
Assuming p.id = o.product_id will return three orders per each product:
OUTPUT_CARDINALITY_RHS = 3
OUTPUT_CARDINALITY_JOIN = 100 * 3 = 300
i.e. the join is estimated to return 300 rows, 3 for each product.
Again, in the absence of statistics, we use magic constants to estimate these cardinalities.
Estimating them is important because in multi-way joins the output cardinality of the previous join becomes the input cardinality of the next one.