Expression Trees and Precedence
Turn a WHERE clause from one comparison into a tree of operators, and evaluate it row by row
Parse AND, OR, NOT, the six comparisons, arithmetic and parentheses into an expression tree with the right shape, check its types before reading a row, evaluate it for each row, and let the planner keep using the primary key for the bounds it can.
- An expression is a tree: operators are inner nodes, columns and literals are its leaves
- Precedence decides the shape of the tree, and the shape decides the answer
- Evaluation returns a typed value, never a bare boolean: an INT, a VARCHAR or a truth value
- Types are checked once, before any row is read; division by zero can only be found row by row
- AND and OR stop early when their left side already decides
- Conjuncts: the parts of a WHERE joined by AND, and the ones a key range can answer
Why this stage exists
Since the where-filter stage a WHERE clause has been one column, one of =, < or >, and one value. Real queries need more: where dept = 'eng' and salary > 100, where not done, where price * qty > 1000. Every later stage of this part leans on the same machinery: ORDER BY can sort by an expression, aggregates sum one, HAVING filters with one. So this stage replaces the single comparison with something general: an expression tree.
The tree is the important word. A line of text has no structure, and a = 1 or b = 2 and c = 3 can be read two ways. Read left to right, it keeps a row where a = 1 only if c = 3 as well. Read the way SQL means it, AND binds tighter, so a row with a = 1 is kept whatever c is. The two readings are two different trees, and they give different rows:
From text to tree
The lexer learns the new pieces: and, or and not become keywords, and <=, >=, !=, <>, +, -, / and % become symbols. A two-character symbol is one token only when its characters touch, so < = is two tokens and a syntax error. A - is always a token of its own: -1 is unary minus applied to the number 1, not a negative number. That keeps the lexer simple, and it means a-1 and a - 1 are the same thing.
The parser then reads tokens by precedence, from the loosest operator to the tightest:
| Level | Operators | Example |
|---|---|---|
| loosest | OR | a = 1 or b = 2 |
AND | a = 1 and b = 2 | |
NOT | not a = 1 is not (a = 1) | |
= != <> < > <= >=, one at a time | a + 1 = b | |
+ - | a - b - 1 is (a - b) - 1 | |
* / % | a + b * 2 is a + (b * 2) | |
unary - | -a * 2 is (-a) * 2 | |
| tightest | a column, a number, a string, ( ... ) |
The idea behind the parser is in that table. Each level only ever builds operators of its own level, and reads its operands from the level just below it. So the loosest operator ends up at the root, and the tightest ones sit just above the leaves. This is often called precedence climbing or recursive descent by levels. Parentheses work by sending the parser back to the loosest level for what is inside them; they are gone from the tree once it is built.
Comparisons do not chain. a = 1 = 2 has a second comparison where an operand belongs, and is 00302. A comparison gives a truth value, and the next section says why (a = 1) = (b = 2) would not get far either.
Values have types
The old WHERE check answered yes or no. An evaluator for expressions cannot: a + b is not yes or no, it is a number. So evaluation gives a value, and a value has a type: an INT, a VARCHAR, or a truth value, the result of a comparison, AND, OR or NOT. The next stage adds a third truth value, unknown, and is much easier if the evaluator already returns values rather than booleans.
| Operator | Takes | Gives |
|---|---|---|
+ - * / %, unary - | INTs | an INT |
| comparisons | two INTs, or two VARCHARs | a truth value |
AND OR NOT | truth values | a truth value |
The types of a tree do not depend on any row: a column's type is in the catalog, a literal's is written in the query. So they are checked once, before the first row is read, and a mistake is 00403 with nothing else printed. where name > 5 compares a VARCHAR with an INT; where a is an INT where a condition belongs. Both are caught even on an empty table.
What cannot be checked in advance is a value. a / b is fine until a row has b = 0, and then the statement stops with 01501. Arithmetic runs in signed 64-bit integers: a column holds 0 to 4294967295, but a - 5 may be negative and a * 2 may pass 4294967295 on the way to a comparison. A result outside 64 bits is 01502. Division drops the fraction towards zero, so -7 / 2 is -3, and the remainder takes the sign of the left side, so -7 % 2 is -1.
Evaluating, row by row
The scan hands up one row at a time, and the tree is evaluated from its root for each of them. A row is kept when the root gives true.
AND and OR evaluate their left operand first. When it already decides the answer, false for AND and true for OR, the right operand is not evaluated at all. That is what makes the guard in b != 0 and a / b = 2 work: on a row with b = 0, the division never happens.
A statement that stops with 01501 or 01502 prints its error line and nothing else, even if it had already found rows to print. Collect the rows, and print them only when the statement has finished. An UPDATE or DELETE that stops is undone by the statement rollback you built in Transactions and Recovery.
What the planner can still use
The planner of the index-scan stage turned where id > 25 into an IndexScan. It should keep doing that inside larger expressions, and it can, for one shape of tree: the parts of a WHERE joined by AND at the top. Each of those parts is a conjunct, and a row is kept only when every conjunct holds. So a conjunct that bounds the key can be answered by the B+Tree, and the rest checked by a Filter on the rows the tree finds:
An OR at the top is a single conjunct, so id = 4 or id = 5 is still a Filter over a SeqScan. That is correct, just slower; the Indexes and the Optimizer part comes back to it.
The algorithm in words
Parse by precedence
One function per level of the precedence table, from the loosest operator to the tightest. Each level reads the level below it for its operands, so tighter operators end up deeper in the tree.
- 1.The loosest level is OR. It reads one operand from the AND level, then, as long as the next token is OR, reads another and joins the two under an OR node. Joining as you go makes the operators of one level group from the left.
- 2.AND does the same with the NOT level. NOT, when the next token is NOT, consumes it and wraps whatever the NOT level gives next; otherwise it hands over to the comparison level.
- 3.The comparison level reads an operand from the additive level and, if a comparison operator follows, reads exactly one more. A second comparison operator right after is a syntax error.
- 4.The additive and multiplicative levels work like OR and AND, with + and -, then *, / and %.
- 5.Unary minus consumes a - and wraps the next unary operand. The tightest level is a column, a number, a string, or a parenthesised expression, which starts again from the loosest level and must end with ).
- 6.Invariant: each operator's operands are made only of operators that bind at least as tightly, or of parenthesised expressions. Printing the tree with every inner operand in parentheses shows it.
Check the tree, then evaluate it
A query reads many rows and the tree is the same for each of them. Everything that does not depend on a row is decided once, before the first row.
- 1.Walk the tree and look up every column in the table. An unknown one is 00402, before anything else is checked.
- 2.Walk it again, giving each node its type after its operands have theirs: arithmetic takes INTs and gives an INT, a comparison takes two INTs or two VARCHARs and gives a truth value, AND, OR and NOT take truth values. The first operator given anything else is 00403, and so is a WHERE whose root is not a truth value.
- 3.Then check each number as typed against 0 to 4294967295 (00404).
- 4.For each row the scan hands up, evaluate the tree from the root. A leaf gives the row's value or the literal. Arithmetic works in signed 64 bits and stops the statement with 01502 when a result does not fit, and with 01501 when it divides by zero.
- 5.AND evaluates its left operand and, when it is false, gives false without looking at the right one. OR does the same with true.
- 6.Keep the row when the root gives true. A statement stopped by an error prints that error and nothing else, so hold the rows until the end of the statement.
Find the key bounds
The planner from the index-scan stage looked at one comparison. Now it looks at the conjuncts and takes from them what the B+Tree can answer.
- 1.Collect the conjuncts: start at the root, and while a node is an AND, look into both of its operands. Every node that is not an AND is a conjunct.
- 2.A key bound is a conjunct of the form: the table's first column, then =, <, >, <= or >=, then a number. Anything else stays a conjunct for the Filter.
- 3.If there is an = bound, the first one is the scan's key. Otherwise take the first lower bound and the first upper bound as the scan's range.
- 4.Rebuild what is left: remove the bounds the scan uses, replacing each AND that lost an operand by its other operand. If something is left, it becomes a Filter above the IndexScan.
- 5.Invariant: the rows printed are the same with or without the IndexScan. Only rows_examined and the plan change.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, sections 3, 39 and 40.
| Input | Output |
|---|---|
tokenize a<=1 and b<>-1 | [IDENTIFIER - a], [SYMBOL - <=], [NUMBER - 1], [KEYWORD_AND - and], [IDENTIFIER - b], [SYMBOL - <>], [SYMBOL - -], [NUMBER - 1] |
ast select * from t where a = 1 or b = 2 and c = 3; | Statement: SELECT, Table: t, Columns: [*], Where: (a = 1) OR ((b = 2) AND (c = 3)) |
explain select id from t where id >= 3 and a = 40; | [PLAN] select id from t where id >= 3 and a = 40, [PLAN] └── Filter (a = 40), [PLAN] └── IndexScan (table=t, range: id >= 3) |
| an operator given the wrong types, or a WHERE that is not a condition | [ERROR:00403] <message> |
| a division or remainder by zero, met on a row | [ERROR:01501] <message>, and nothing else |
| a result outside the signed 64-bit range | [ERROR:01502] <message>, and nothing else |
a = 1 = 2, a missing ), a missing operand | [ERROR:00302] <message> |
How an expression is printed, in ast and in a Filter: a column as typed, a number as its digits, a string in single quotes; AND, OR and NOT in capitals; other operators as typed, with a space on each side; unary minus with no space. Every operand that is not a column or a literal goes between parentheses, and nothing else does. So a + b * 2 = 7 prints as (a + (b * 2)) = 7, and a single comparison prints as it always did: id = 5.
The plan of a WHERE: with no key bound, a Filter holding the whole WHERE over a SeqScan. Otherwise an IndexScan whose key is the first = bound, or whose range is the first lower and the first upper bound, lower first (range: id >= 3 AND id <= 5), with a Filter above it for whatever is left. rows_examined counts the rows the IndexScan hands up, as before.
What you should see when done
$droid --db shop.dbWelcome to droid!>create table t (id int, a int, b int, c int, name varchar(20));Create table statement executed successfully>insert into t (id, a, b, c, name) values (1, 1, 0, 0, 'ada');Insert statement executed successfully (1 rows)>insert into t (id, a, b, c, name) values (2, 0, 2, 3, 'bob');Insert statement executed successfully (1 rows)>insert into t (id, a, b, c, name) values (3, 0, 2, 0, 'cy');Insert statement executed successfully (1 rows)>select id from t where a = 1 or b = 2 and c = 3;id12(2 rows)[EXEC] rows_examined=3>select id from t where (a = 1 or b = 2) and c = 3;id2(1 rows)[EXEC] rows_examined=3>select name from t where a - 1 < 0 and id >= 2;namebobcy(2 rows)[EXEC] rows_examined=2>explain select name from t where a - 1 < 0 and id >= 2;[PLAN] select name from t where a - 1 < 0 and id >= 2[PLAN] └── Filter ((a - 1) < 0)[PLAN] └── IndexScan (table=t, range: id >= 2)>select id from t where b != 0 and c / b = 1;id2(1 rows)[EXEC] rows_examined=3>select id from t where c / b = 1;[ERROR:01501] Division by zero>select id from t where name > 5;[ERROR:00403] Cannot compare VARCHAR with INT>.exitexiting.. good bye!
Things to notice. Moving the parentheses changed which rows came back. a - 1 went below zero for rows 2 and 3, and that was fine. id >= 2 became the range of the scan, so only two rows were examined, while the rest of the WHERE became a Filter. The guarded division ran on every row without an error; the unguarded one stopped on row 1, where b is 0, and printed nothing but its error. The messages are yours to word; the tests check only the codes.
Common bugs in this stage
- Every binary operator at one level. The parser reads left to right and builds
((a = 1) or (b = 2)) and (c = 3). Write one level per row of the precedence table. - Right-grouping subtraction. A level written as "operand, then operator, then the whole level again" builds
a - (b - 1). Loop at each level, joining as you go. - NOT bound too tightly. If NOT reads only the next operand,
not a = 1becomes(not a) = 1, which is a type error. NOT takes a whole comparison. - The evaluator returns a boolean. It works today and breaks in the next stage. Return a value with a type.
- Unsigned arithmetic.
a - 5on a row witha = 3wraps to a huge number anda - 5 < 0keeps nothing. Work in signed 64 bits. - Division that rounds down.
-7 / 2is-3here. If your language's operator floors, or its remainder is always positive, fix the sign yourself. - Types checked per row.
where name > 5on an empty table must still be00403. Check types before the first row. - Printing rows before an error. A division by zero on the fifth row must not leave four rows and a header on the screen.
- No short-circuit.
b != 0 and a / b = 2fails with01501. AND must not evaluate its right side when the left is false. - Parentheses kept in the tree. A "group" node makes
astprint extra parentheses and the planner miss conjuncts written as(id >= 3) and a = 1. Parentheses only shape the tree. - A conjunct used twice. Taking
id >= 3as the range and leaving it in the Filter gives the right rows and the wrong plan line.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C and C++: an expression node is a tagged struct: a kind, an operator, a value, and pointers to its operands.
int64_tfrom<stdint.h>for arithmetic;__builtin_mul_overflowand its siblings (GCC and Clang) detect a result that does not fit. C's/and%already round towards zero. - Rust: an
enum Exprwith aBox<Expr>per operand is the natural tree.i64::checked_mul,checked_add,checked_subandchecked_divreturnNoneon overflow, andchecked_divalso on division by zero./and%round towards zero. - Zig: a tagged union for the node, with pointers to operands allocated from an arena freed after each statement.
@mulWithOverflowand friends detect overflow;@divTruncand@remare the rounding this page asks for.
Checklist
One line per thing the tests check. The tags name the test cases.
- AND binds tighter than OR, whichever side it is written onprec-and-over-or
- Parentheses change the tree and leave nothing in itparens-override
- NOT applies to one comparison, not to the AND after itnot-binds-tight
- *, / and % bind tighter than + and -arith-prec
- Operators of one level group from the left, and / drops the fractionarith-left-assoc
- Arithmetic is signed: it goes below zero, and / and % round towards zeroarith-signed
- Arithmetic is 64-bit, and a result past it is 01502 with nothing else printedint-overflow
- <= and >= include the value itself, on the key, on INT columns and on textlte-gte-rows
- != and <> are one operatorneq-both-spellings
- Strings compare as strings wherever they stand: '10' < '9'varchar-lexicographic
- An operator given the wrong types is 00403, and so is a WHERE that is not a conditiontype-mismatch
- Names are checked before types, and types before number rangescheck-order
- Two comparisons in a row, a missing ) or a missing operand are 00302syntax-errors
- Division by zero is 01501, prints nothing else, and an UPDATE or DELETE that meets it changes nothingdiv-zero
- AND and OR leave their right side alone when the left decidesshort-circuit
- UPDATE and DELETE select rows with the same expressionsupdate-delete-where
- and, or and not are keywords; the new operators are symbols, and -1 is two tokenstokenize-symbols
- ast prints the WHERE as a tree, every inner operand in parenthesesast-expressions
- The key bounds among the conjuncts make the IndexScan, the rest a Filter, with the counts to matchkey-range-plan