Table of Contents
Implementation of a lightweight database supporting core SQL operators using iterator model.
We can represent all conditions and joins using a graph. The edges correspond to the join predicates and nodes are the tables.
For a query
select *
from R,
S,
T
where R.a = S.a
and R.b = S.b
and S.b = T.b
and R.c = T.c
and R.a < 10
and 1 = 1We can construct a graph as such

This graph is constructed by the JoinGraph class. The predicates are grouped by the set of tables it references, and
then they are converted to a conjunction of predicates in an Expression. E.g. predicates
For predicates with a single column reference, we can treat is as a self-edge on a node. For literals such as
This graph sufficiently represents the entire join structure and selection structure of our query. Then constructing our join tree and applying selections can be done by resolving edges in this graph. We have to be careful in the order in which the edges are processed.
The first optimisation step is resolving all self-edges, also known as predicate pushdown. We further optimise to set self-edges of the literal predicates to the root node. If it is false, the left-deep leaf node on our join tree will be empty which reduces computation.
We then do our joins which uses a modified Breadth First Search (BFS). We start with our root node FROM clause. We explore the neighbours of
Next we explore the next neighbour of
As we explore each neighbour, find all edges that connects neighbour
This is akin to a growing cluster algorithm, where we explore the next node to add to the cluster using BFS. BFS allows us to find the set of nodes connected to the current cluster.
We run this algorithm for all nodes that are not visited, as we can have multiple disjoint clusters. Each disjoint cluster is then cross product to obtain the final cluster of all tables.
Ordering of tuples is not maintained in this join operation due to the order in which edges are resolved. Projection is
called at the end of the operation on the final cluster to maintain column ordering specified in FROM.
We need to handle group by, sum, and distinct and different combination of these which can be complicated. It is
best broken down into the different cases, then handled independently. Let's explore these cases.
group by&sum
Create a key based on group by columns. Within each group, sum based on equation. Return tuples for each group in the order they were created.group byonly
Remove all duplicates based on the group by columns.sumonly
Sum based on equation for entire relationdistinct
Distinct operates independently above group by and sum. It has the same effect asgroup byonly and it simply removes duplicates based on the columns in the select items instead of the group by items.
We have some opportunities for optimisation here by sorting the nodes based on table size so that the smallest table is always the outer relation in the join. But with selection, we also need to consider selectivity of predicates for truly optimal joins. All of which I did not implement.
Sorting stores a List of sorted tuples. We remove each element when the next tuple is requested instead of incrementing a pointer and dereferencing the element. Removing an element when requested reduces the memory required to store the entire list.
We make this operator non-blocking when it is called without aggregations. We can store a set of unique tuples. We only
return the next tuple if it does not exist in the set. Then we add the tuple to the set. We also implicitly project the
tuple to the key columns while doing duplicate elimination. E.g. select distinct a, b from Student, then we have the
key columns a and b which we can also project to in the same operator. The same can be done for group by.
Metadata for each tuple is required to specify the columns and table that the tuple data correspond to. This metadata is
stored as an instance variable within each operator to reduce memory consumption. We do not want to create a new
metadata object for every tuple. This also allows us to determine the output shape of the tuple before calling
getNextTuple.
The planner is important for composing the operators in the correct order for correctness and optimal performance. The general ordering is as such
- Scan base table
Base table as the left-most leaf relation in our operator tree. We will build the entire tree off this leaf. - Join & Selection
Apply selection and joins on all the tables as explained here. Predicate pushdown and join order ensure optimality. - Duplicate elimination with Aggregate
We process the group by statement and sum, which also does implicit projection - Duplicate elimination
For distinct which is independent of group by and sum. - Projection
Remove unwanted columns - Sort
Done last as this always blocking and expensive
We do duplicate elimination again for distinct because we can have
select distinct SUM(Student.A), Student.B
from Student
group by Student.A, Student.Bwhich means that we group by and sum first, which already does duplicate elimination for the key columns Student.A and
Student.B and aggregate the sum. But distinct does duplicate elimination again on the aggregate.