Summary
An RDBMS executes a SQL query by translating it into a sequence of operations on tables (query planning) and then executing that sequence (query processing). The lecture defines three basic operations mathematically: projection Π \Pi Π , selection σ \sigma σ and the Cartesian product × \times × , and shows that every SELECT … FROM … WHERE … query can be written with them, as a sequence, a nested expression or a tree. Equivalent sequences can differ greatly in running time, so each operation gets simple estimates of its cost and of the size of its result. With these estimates, sequences that filter early and avoid large Cartesian products turn out to be much cheaper.
Organisation# Note Organisation
ES3 (database design) feedback was positive; the second ES3 question was about Richard Hipp, the creator of SQLite3 (Slides 3–4 ). ES4 (DBIS Sheet 04 (not published) ): implement a very simple database engine with the three operations, give three different operation sequences for a given SQL query, measure their running times and check the results against SQLite3. Each operation is a class, and the objects implicitly form a tree like the one on Slide 18; the provided code template is extensive (Slide 31 ).
Basic Operations# From SQL queries to operations# Lecture 3 explained by example what SELECT … FROM … WHERE … computes. An RDBMS computes it in three steps (Slide 5 ):
Internally, the RDBMS implements operations on tables (three today, more in Lecture 5). A SQL query is translated into a sequence of these operations: query planning (done manually for ES4). The sequence is executed: query processing (implemented in ES4). The three basic operations are projection , selection and Cartesian product (“cross-product” is used synonymously). Each takes one or two tables as input and always outputs a single table (Slide 6 ).
Notation# Recap from Lecture 3 (Relational Model , Slide 7 ): a table is a triple T = ( C , D , R ) T = (C, D, R) T = ( C , D , R ) with column names C = ( c 1 , … , c k ) C = (c_1, \dots, c_k) C = ( c 1 , … , c k ) , domains D = ( D 1 , … , D k ) D = (D_1, \dots, D_k) D = ( D 1 , … , D k ) and a multiset R R R of tuples ( r 1 , … , r k ) (r_1, \dots, r_k) ( r 1 , … , r k ) with r j ∈ D j r_j \in D_j r j ∈ D j . A tuple from R R R is called a row, hence the letter R R R .
Projection# Definition Projection Π c o l s , d i s t i n c t ( T ) \Pi_{\mathit{cols},\,\mathit{distinct}}(T) Π cols , distinct ( T ) (Relational Algebra , Slide 9 )
Signature: project(T, cols, distinct) or Π c o l s , d i s t i n c t ( T ) \Pi_{\mathit{cols},\,\mathit{distinct}}(T) Π cols , distinct ( T ) , where T = ( C , D , R ) T = (C, D, R) T = ( C , D , R ) is a table with k k k columns, c o l s = ( j 1 , … , j m ) \mathit{cols} = (j_1, \dots, j_m) cols = ( j 1 , … , j m ) is a sequence of indices from { 1 , … , k } \{1, \dots, k\} { 1 , … , k } , and d i s t i n c t ∈ { true , false } \mathit{distinct} \in \{\text{true}, \text{false}\} distinct ∈ { true , false } .
Result: the table T ′ = ( C ′ , D ′ , R ′ ) T' = (C', D', R') T ′ = ( C ′ , D ′ , R ′ ) with
C ′ = ( c j 1 , … , c j m ) , D ′ = ( D j 1 , … , D j m ) , R ′ = { ( r j 1 , … , r j m ) : r = ( r 1 , … , r k ) ∈ R } . C' = (c_{j_1}, \dots, c_{j_m}), \quad D' = (D_{j_1}, \dots, D_{j_m}), \quad R' = \{(r_{j_1}, \dots, r_{j_m}) : r = (r_1, \dots, r_k) \in R\}. C ′ = ( c j 1 , … , c j m ) , D ′ = ( D j 1 , … , D j m ) , R ′ = {( r j 1 , … , r j m ) : r = ( r 1 , … , r k ) ∈ R } . If d i s t i n c t \mathit{distinct} distinct is true, all duplicates are removed from R ′ R' R ′ (the multiset becomes the corresponding set).
Intuitively: keep a subset of the columns of a table.
Example Projection (Slide 10 )
CREATE TABLE people(id TEXT, first TEXT, last TEXT);
SELECT DISTINCT first , last FROM people;
Operation: project(people, (2, 3), true).
id first last Q36301 Anne Hathaway Q237251 Anne Hathaway Q106514 River Phoenix Q185140 Joaquin Phoenix
Result: (Anne, Hathaway), (River, Phoenix), (Joaquin, Phoenix). The two Anne Hathaways collapse into one row because d i s t i n c t \mathit{distinct} distinct is true.
Selection# Definition Selection σ φ ( T ) \sigma_\varphi(T) σ φ ( T ) (Slide 11 )
Signature: select(T, φ) or σ φ ( T ) \sigma_\varphi(T) σ φ ( T ) , where T = ( C , D , R ) T = (C, D, R) T = ( C , D , R ) is a table and φ : D 1 × ⋯ × D k → { true , false } \varphi: D_1 \times \dots \times D_k \to \{\text{true}, \text{false}\} φ : D 1 × ⋯ × D k → { true , false } is a predicate on rows.
Result: T ′ = ( C , D , R ′ ) T' = (C, D, R') T ′ = ( C , D , R ′ ) with R ′ = { r ∈ R : φ ( r ) = true } R' = \{r \in R : \varphi(r) = \text{true}\} R ′ = { r ∈ R : φ ( r ) = true } .
Intuitively: filter the rows of a table by the predicate φ \varphi φ .
! “Select” is the WHERE clause
The operation is traditionally called select , but in SQL it is expressed in the WHERE clause, not in the SELECT clause. The SELECT clause corresponds to projection .
Example Selection (Slide 12 )
SELECT * FROM people WHERE first = 'Anne' ;
Operation: select(people, r → r[2] = "Anne"). Result: the rows Q36301 and Q237251 (both Anne Hathaway).
Cartesian product# Definition Cartesian product T 1 × T 2 T_1 \times T_2 T 1 × T 2 (Slide 13 )
Signature: cartesian-product(T1, T2) or T 1 × T 2 T_1 \times T_2 T 1 × T 2 , where T 1 = ( C 1 , D 1 , R 1 ) T_1 = (C_1, D_1, R_1) T 1 = ( C 1 , D 1 , R 1 ) and T 2 = ( C 2 , D 2 , R 2 ) T_2 = (C_2, D_2, R_2) T 2 = ( C 2 , D 2 , R 2 ) .
Result: the table T = ( C , D , R ) T = (C, D, R) T = ( C , D , R ) with
C = ( C 1 , C 2 ) , D = ( D 1 , D 2 ) , R = R 1 × R 2 = { ( r 1 , r 2 ) : r 1 ∈ R 1 , r 2 ∈ R 2 } , C = (C_1, C_2), \quad D = (D_1, D_2), \quad R = R_1 \times R_2 = \{(r_1, r_2) : r_1 \in R_1,\ r_2 \in R_2\}, C = ( C 1 , C 2 ) , D = ( D 1 , D 2 ) , R = R 1 × R 2 = {( r 1 , r 2 ) : r 1 ∈ R 1 , r 2 ∈ R 2 } , hence ∣ C ∣ = ∣ C 1 ∣ + ∣ C 2 ∣ |C| = |C_1| + |C_2| ∣ C ∣ = ∣ C 1 ∣ + ∣ C 2 ∣ and ∣ R ∣ = ∣ R 1 ∣ ⋅ ∣ R 2 ∣ |R| = |R_1| \cdot |R_2| ∣ R ∣ = ∣ R 1 ∣ ⋅ ∣ R 2 ∣ .
Intuitively: all combinations of a row from T 1 T_1 T 1 with a row from T 2 T_2 T 2 .
Example Cartesian product (Slide 14 )
CREATE TABLE gender(id INT, what TEXT);
CREATE TABLE handed(id INT, what TEXT);
SELECT * FROM gender, handed;
gender has 3 rows (female, male, other) and handed has 2 rows (lefty, righty), so cartesian-product(gender, handed) has 2 + 2 = 4 2 + 2 = 4 2 + 2 = 4 columns and 3 ⋅ 2 = 6 3 \cdot 2 = 6 3 ⋅ 2 = 6 rows, one per (gender, handedness) pair.
Translating SQL into operations# With project, select and cartesian-product, every query of the form (Slide 15 )
SELECT [comma- separated list of column names ]
FROM [comma- separated list of tables]
WHERE [Boolean expression on column names ];
can be translated into a sequence of operations that computes its result: Cartesian product of the FROM tables, then selection with the WHERE expression, then projection onto the SELECT columns. This sequence can be written in three equivalent ways.
Example Running example (Slides 16–18 )
CREATE TABLE movies(id, title, year , score);
CREATE TABLE persons(id, name, birth_date);
CREATE TABLE roles(movie_id, person_id);
SELECT movies.title, persons.name
FROM movies, persons, roles
WHERE roles.person_id = persons.id AND roles.movie_id = movies.id;
Column indices after movies × persons × roles: 1 movies.id, 2 title, 3 year, 4 score, 5 persons.id, 6 name, 7 birth_date, 8 movie_id, 9 person_id.
As a sequence of operations:
T1 = cartesian-product(movies, persons)
T2 = cartesian-product(T1, roles)
T3 = select(T2, r → r[1] = r[8] ∧ r[5] = r[9])
T4 = project(T3, (2, 6), false)
As a single nested expression:
project(select(cartesian-product(cartesian-product(movies, persons), roles),
r → r[1] = r[8] ∧ r[5] = r[9]), (2, 6))
This looks scary but is no harder to parse and evaluate than an arithmetic expression like ( ( ( 3 + 4 ) ⋅ 4 ) − 3 + 8 ⋅ 4 ) − 30 / 2 (((3 + 4) \cdot 4) - 3 + 8 \cdot 4) - 30 / 2 ((( 3 + 4 ) ⋅ 4 ) − 3 + 8 ⋅ 4 ) − 30/2 .
In relational-algebra notation: Π ( 2 , 6 ) , false ( σ r [ 1 ] = r [ 8 ] ∧ r [ 5 ] = r [ 9 ] ( ( movies × persons ) × roles ) ) \Pi_{(2,6),\,\text{false}}\big(\sigma_{r[1] = r[8] \,\wedge\, r[5] = r[9]}((\text{movies} \times \text{persons}) \times \text{roles})\big) Π ( 2 , 6 ) , false ( σ r [ 1 ] = r [ 8 ] ∧ r [ 5 ] = r [ 9 ] (( movies × persons ) × roles ) ) .
As a tree (leaves are the input tables, inner nodes the operations, the root produces the result):
lecture-04, PDF page 18 Query Planning# Motivation# A single SQL query has many sequences of operations that compute its result. Regarding the result they are all equivalent (up to a permutation of the columns), but their processing time can differ greatly (Slide 19 ). Choosing a good sequence, a Query Plan , is query planning .
Example Three pairs of equivalent sequences (Slides 20–22 )
Schemas: movies(id, title, year, score), persons(person_id, name, birth_date), roles(movie_id, person_id).
Example 1: SELECT * FROM movies, persons, roles;
Sequence 1: T1 = cartesian-product(movies, persons); T2 = cartesian-product(T1, roles)
Sequence 2: T1 = cartesian-product(movies, roles); T2 = cartesian-product(T1, persons)
Example 2: SELECT * FROM movies, roles WHERE score >= 8.0;
Sequence 1: T1 = cartesian-product(movies, roles); T2 = select(T1, r → r[4] ≥ 8.0)
Sequence 2: T1 = select(movies, r → r[4] ≥ 8.0); T2 = cartesian-product(T1, roles)
Example 3: SELECT DISTINCT title FROM movies, roles WHERE score >= 8.0;
Sequence 1: T1 = cartesian-product(movies, roles); T2 = select(T1, r → r[4] ≥ 8.0); T3 = project(T2, (2), true)
Sequence 2: T1 = select(movies, r → r[4] ≥ 8.0); T2 = project(T1, (2), true)
Which sequence is better, assuming real data for the tables?
! Slide bug: column index of score
Slides 21 and 28 write the predicate of Example 2 as r → r[3] ≥ 8.0. With movies(id, title, year, score), column 3 is year; score is column 4, as used (and annotated) on Slide 22 for Example 3. The correct predicate is r → r[4] ≥ 8.0. Columns are numbered from 1.
Note Beyond the lecture: when is Example 3’s Sequence 2 equivalent?
Sequence 2 of Example 3 does not touch roles at all. Because of DISTINCT, it gives the same result as Sequence 1 only if roles is non-empty: if roles has 0 rows, the Cartesian product (and thus the query) is empty, while Sequence 2 still returns the titles of all movies with score ≥ 8.0 \geq 8.0 ≥ 8.0 .
Cost and size estimates# To pick a sequence (Slide 23 ):
estimate the cost of each operation in each sequence, sum these to estimate the total cost of each sequence, pick the sequence with the lowest total cost. The cost of an operation depends on the size of its input, but input sizes are only known for the operations at the bottom of the tree (the actual tables). Each operation therefore also needs an estimate of the size of its result (rows and columns). Real query planners use even more statistics.
Let T T T have n n n rows and k k k columns, T i T_i T i have n i n_i n i rows and k i k_i k i columns, and let k ′ = ∣ c o l s ∣ k' = |\mathit{cols}| k ′ = ∣ cols ∣ be the number of columns kept by a projection.
Operation cost rows cols Π c o l s , d i s t i n c t ( T ) \Pi_{\mathit{cols},\,\mathit{distinct}}(T) Π cols , distinct ( T ) ∼ n ⋅ k ′ \sim n \cdot k' ∼ n ⋅ k ′ ∼ n \sim n ∼ n k ′ k' k ′ σ φ ( T ) \sigma_\varphi(T) σ φ ( T ) ∼ n ⋅ k \sim n \cdot k ∼ n ⋅ k ∼ n / 2 \sim n/2 ∼ n /2 k k k T 1 × T 2 T_1 \times T_2 T 1 × T 2 ∼ n 1 ⋅ n 2 ⋅ ( k 1 + k 2 ) \sim n_1 \cdot n_2 \cdot (k_1 + k_2) ∼ n 1 ⋅ n 2 ⋅ ( k 1 + k 2 ) n 1 ⋅ n 2 n_1 \cdot n_2 n 1 ⋅ n 2 k 1 + k 2 k_1 + k_2 k 1 + k 2
Assumptions and reality check:
Projection (Slide 24 ): iterate over every row and copy the k ′ k' k ′ selected columns. Reasonable if tables are stored in row order ; in column order one could just copy “pointers” to the columns. The row estimate ignores duplicate removal for d i s t i n c t = true \mathit{distinct} = \text{true} distinct = true .Selection (Slide 25 ): iterate over every row, φ \varphi φ is evaluated in constant time and (rather arbitrarily) is true for half of the rows. Better: evaluate φ \varphi φ on sample rows to estimate its cost and selectivity. If φ \varphi φ has a special form (e.g. score ≥ 8.0) and the table is sorted by that column, not all rows need to be visited.Cartesian product (Slide 26 ): these estimates are accurate, since the result of size n 1 ⋅ n 2 ⋅ ( k 1 + k 2 ) n_1 \cdot n_2 \cdot (k_1 + k_2) n 1 ⋅ n 2 ⋅ ( k 1 + k 2 ) has to be produced anyway. Good query plans therefore avoid Cartesian products as far as possible; this leads to the join operations of Lecture 5.! Total cost of a sequence
The cost of a sequence is the sum of the costs of all its operations, including those that only produce intermediate results. Each cost is computed from the estimated size of the operation’s input, so the row and column estimates must be propagated from the leaves up the tree.
Cost estimates for the examples# Let movies have n 1 n_1 n 1 rows and 4 columns. In Example 1, persons has n 2 n_2 n 2 rows and 3 columns and roles has n 3 n_3 n 3 rows and 2 columns. In Examples 2 and 3, roles has n 2 n_2 n 2 rows and 2 columns. Handwritten annotations on Slides 27–29 :
Example Example 1: order of Cartesian products (Slide 27 )
Reasonable assumption: n 1 , n 2 ≪ n 3 n_1, n_2 \ll n_3 n 1 , n 2 ≪ n 3 (a movie has many actors, an actor plays in many movies).
Operation cost rows cols Seq. 1 T 1 = movies × persons T_1 = \text{movies} \times \text{persons} T 1 = movies × persons 7 n 1 n 2 7 n_1 n_2 7 n 1 n 2 n 1 n 2 n_1 n_2 n 1 n 2 7 T 2 = T 1 × roles T_2 = T_1 \times \text{roles} T 2 = T 1 × roles 9 n 1 n 2 n 3 9 n_1 n_2 n_3 9 n 1 n 2 n 3 n 1 n 2 n 3 n_1 n_2 n_3 n 1 n 2 n 3 9 Seq. 2 T 1 = movies × roles T_1 = \text{movies} \times \text{roles} T 1 = movies × roles 6 n 1 n 3 6 n_1 n_3 6 n 1 n 3 n 1 n 3 n_1 n_3 n 1 n 3 6 T 2 = T 1 × persons T_2 = T_1 \times \text{persons} T 2 = T 1 × persons 9 n 1 n 2 n 3 9 n_1 n_2 n_3 9 n 1 n 2 n 3 (same as above)n 1 n 2 n 3 n_1 n_2 n_3 n 1 n 2 n 3 9
The final product costs the same; Sequence 1 is better because 7 n 1 n 2 ≪ 6 n 1 n 3 7 n_1 n_2 \ll 6 n_1 n_3 7 n 1 n 2 ≪ 6 n 1 n 3 . Combine the small tables first.
Example Example 2: select before product (Slide 28 )
Assumption: n 1 ≪ n 2 n_1 \ll n_2 n 1 ≪ n 2 .
Operation cost rows cols Seq. 1 T 1 = movies × roles T_1 = \text{movies} \times \text{roles} T 1 = movies × roles 6 n 1 n 2 6 n_1 n_2 6 n 1 n 2 n 1 n 2 n_1 n_2 n 1 n 2 6 T 2 = σ φ ( T 1 ) T_2 = \sigma_\varphi(T_1) T 2 = σ φ ( T 1 ) 6 n 1 n 2 6 n_1 n_2 6 n 1 n 2 n 1 n 2 / 2 n_1 n_2 / 2 n 1 n 2 /2 6 Seq. 2 T 1 = σ φ ( movies ) T_1 = \sigma_\varphi(\text{movies}) T 1 = σ φ ( movies ) 4 n 1 4 n_1 4 n 1 n 1 / 2 n_1 / 2 n 1 /2 4 T 2 = T 1 × roles T_2 = T_1 \times \text{roles} T 2 = T 1 × roles n 1 2 ⋅ n 2 ⋅ 6 = 3 n 1 n 2 \tfrac{n_1}{2} \cdot n_2 \cdot 6 = 3 n_1 n_2 2 n 1 ⋅ n 2 ⋅ 6 = 3 n 1 n 2 n 1 n 2 / 2 n_1 n_2 / 2 n 1 n 2 /2 6
Totals: Sequence 1 = 12 n 1 n 2 = 12\, n_1 n_2 = 12 n 1 n 2 , Sequence 2 = 3 n 1 n 2 + 4 n 1 = 3\, n_1 n_2 + 4 n_1 = 3 n 1 n 2 + 4 n 1 , so Sequence 2 is better (push the selection below the product).
Example Example 3: select and project without the product (Slide 29 )
Assumption: n 1 ≪ n 2 n_1 \ll n_2 n 1 ≪ n 2 .
Operation cost rows cols Seq. 1 T 1 = movies × roles T_1 = \text{movies} \times \text{roles} T 1 = movies × roles 6 n 1 n 2 6 n_1 n_2 6 n 1 n 2 n 1 n 2 n_1 n_2 n 1 n 2 6 T 2 = σ φ ( T 1 ) T_2 = \sigma_\varphi(T_1) T 2 = σ φ ( T 1 ) 6 n 1 n 2 6 n_1 n_2 6 n 1 n 2 n 1 n 2 / 2 n_1 n_2 / 2 n 1 n 2 /2 6 T 3 = Π ( 2 ) , true ( T 2 ) T_3 = \Pi_{(2),\,\text{true}}(T_2) T 3 = Π ( 2 ) , true ( T 2 ) n 1 n 2 / 2 n_1 n_2 / 2 n 1 n 2 /2 n 1 n 2 / 2 n_1 n_2 / 2 n 1 n 2 /2 1 Seq. 2 T 1 = σ φ ( movies ) T_1 = \sigma_\varphi(\text{movies}) T 1 = σ φ ( movies ) 4 n 1 4 n_1 4 n 1 n 1 / 2 n_1 / 2 n 1 /2 4 T 2 = Π ( 2 ) , true ( T 1 ) T_2 = \Pi_{(2),\,\text{true}}(T_1) T 2 = Π ( 2 ) , true ( T 1 ) n 1 / 2 n_1 / 2 n 1 /2 n 1 / 2 n_1 / 2 n 1 /2 1
Totals: Sequence 1 = 12.5 n 1 n 2 = 12.5\, n_1 n_2 = 12.5 n 1 n 2 , Sequence 2 = 4.5 n 1 = 4.5\, n_1 = 4.5 n 1 , so Sequence 2 is better by a factor of order n 2 n_2 n 2 .
Query plans in SQLite3# SQLite3 shows the plan it will use (Slide 30 ):
-- high-level description of the query plan
EXPLAIN QUERY PLAN
SELECT DISTINCT title FROM movies, roles WHERE score >= 8 .0 ;
-- low-level execution plan
EXPLAIN
SELECT DISTINCT title FROM movies, roles WHERE score >= 8 .0 ;
Note Scratchpad: optimal join order (material of Lecture 6)
The scratchpad links the Query Plan note under “Query planning”. Its content goes beyond this lecture and is covered in Lecture 6 (see Query Optimization ):
A plan for N N N tables combined by binary operations is a binary tree over them; the number of such trees grows like the Catalan numbers. Join graph: one node per table, with an edge between T 1 T_1 T 1 and T 2 T_2 T 2 if they can be joined.Cost model for joins: s i z e ( A ⋈ B ) = min { s i z e ( A ) , s i z e ( B ) } \mathrm{size}(A \bowtie B) = \min\{\mathrm{size}(A), \mathrm{size}(B)\} size ( A ⋈ B ) = min { size ( A ) , size ( B )} and c o s t ( A ⋈ B ) = c o s t ( A ) + c o s t ( B ) + s i z e ( A ⋈ B ) \mathrm{cost}(A \bowtie B) = \mathrm{cost}(A) + \mathrm{cost}(B) + \mathrm{size}(A \bowtie B) cost ( A ⋈ B ) = cost ( A ) + cost ( B ) + size ( A ⋈ B ) . The optimal cost of a set of tables S S S is c o s t ∗ ( S ) = min S 1 , S 2 { c o s t ∗ ( S 1 ) + c o s t ∗ ( S 2 ) + s i z e ( S 1 ⋈ S 2 ) } \mathrm{cost}^*(S) = \min_{S_1, S_2} \{\mathrm{cost}^*(S_1) + \mathrm{cost}^*(S_2) + \mathrm{size}(S_1 \bowtie S_2)\} cost ∗ ( S ) = min S 1 , S 2 { cost ∗ ( S 1 ) + cost ∗ ( S 2 ) + size ( S 1 ⋈ S 2 )} over all S 1 , S 2 S_1, S_2 S 1 , S 2 with S 1 ∪ S 2 = S S_1 \cup S_2 = S S 1 ∪ S 2 = S , S 1 ∩ S 2 = ∅ S_1 \cap S_2 = \emptyset S 1 ∩ S 2 = ∅ , S 1 , S 2 ≠ ∅ S_1, S_2 \neq \emptyset S 1 , S 2 = ∅ , and c o s t ∗ = 0 \mathrm{cost}^* = 0 cost ∗ = 0 for a single table. (The Lecture 6 slide writes the last term as c o s t ∗ ( S 1 ⋈ S 2 ) \mathrm{cost}^*(S_1 \bowtie S_2) cost ∗ ( S 1 ⋈ S 2 ) ; by the cost function above, the join adds its result size.) The recursion is computed efficiently with dynamic programming, in rounds over subsets of increasing size. Concepts# Relational Algebra — formal definitions of projection Π \Pi Π , selection σ \sigma σ and Cartesian product × \times × , with their cost and size estimatesCartesian Product — all row combinations of two tables; # c o l s \#\mathrm{cols} # cols add up, # r o w s \#\mathrm{rows} # rows multiply, so plans avoid itQuery Plan — a sequence / nested expression / tree of operations computing a SQL query; choosing the cheapest one is query planningRelational Model — the table definition ( C , D , R ) (C, D, R) ( C , D , R ) on which the operations are definedCost model (query planning) — cost, rows and columns estimates per operation; total cost = sum over the sequence Query processing — executing the chosen sequence of operations EXPLAIN / EXPLAIN QUERY PLAN — inspecting SQLite3’s query plan