To efficiently process a query, a RDBMS needs to be able optimise certain relational operators which are the backbone of every query. The main operations it needs to be able to perform and optimise are selection, projection and joins.
We use the same basic operation to
Also note that:
- = Number of records in the table
- = Number of pages in the table
Selection
Recall what a selection is:
In order to optimise a selection, a DBMS needs to know:
- All available indexes/pointers to records (to actually find the files)
- The expected size of the result i.e. number of pages (to know when to stop looking)
Reducing Factor
A DBMS uses it’s optimiser to estimate the expected size using reducing factors, which is a fraction per predicate, which is a basic relational statement (i.e. involves only one relational comparators):
- : Reducing factor (also called selectivity), a fraction that states how many records satisfy the associated predicate
- : Number of predicates/ basic relational statements
Once the estimated size is obtained, the DBMS can search through the database file structure to find records matching the selection criteria:
Searching: Heap Files
For heap files (unsorted files), it needs to linear search through every single record so it doesn’t miss anything.
%%🖋 Edit in Excalidraw, and the dark exported image%%
Searching: Sorted Files
For sorted files, the DBMS can simply binary search to find the first record matching condition, then iterate till until the estimate size of records have been scanned
%%🖋 Edit in Excalidraw, and the dark exported image%%
Searching Through Indexed Files
First the DBMS needs to traverse through the B+ Tree (or use the hash table if the index is hash-based) to find indexes that satisfy the criteria.
Hash-based Indexes Only Work On Equality Checks!
Then, linearly iterate through the indexes (note that the indexes would be stored in their own pages, which need to be brought to RAM). Let be the number of pages occupied by indexes.#todo B-tree has leaf nodes doubly linked.
If the indexes are clustered, then the total cost is:
Why? Every index points to a record, and so if only records satisfy the criteria, then we only need to iterate through indexes. For each index, we need to load up the page it points to, but because the indexes are clustered, many of the indexes point to the same page.
If the indexes are unclustered, then the total cost is:
This is because the index lookup does not guarantee indexes pointing to records in the same page (due to the unclustered nature), so we need do an I/O operation for each record (total records).
Composed Selection Criteria
In most cases, the selection criteria contains multiple predicates, involving multiple attributes/columns.
A tree-based index can only match predicates that involve attributes in the prefix of the search key fields . What this means is that if we have a composed selection criteria of multiple predicates, an indexed database is only useful if at least one predicate has any of the prefixes of the search key field(s)
Predicates must involve search key field to be optimised
If we have a index on column , we can only optimise selections if they have predicates involving .
On the fly checking is checking predicates that are not part of the search key fields. It does not contribute to the I/O cost, because the page is already loaded when checking from key fields.
In terms of reduction factors, we only have them if the predicates involve search keys:
Example: Three different selections
Assume we have a database containing columns with a unclustered index on column (i.e. our indexes are ) Look at the following selection criteria:
The size estimations would be as follows:
- : Reduction factor involving , because is a prefix of
- : Reduction factors involving both and , because is a prefix of
- : No reduction factors, because is not a prefix of . Would need to perform a full table scan
- : Reduction factors only involve and and needs to be checked on the fly
- : No reduction factors because is not in
Projection
Recall projection is:
Notice the Duplicate elimination warning? That’s what the DBMS aims to optimise: elimination of duplicates
Projecting Factor
Like with the reducing factor for selection, the projecting factor is a ratio of projected columns to total columns, which lets us know what percentage of attributes are being projected. The DBMS uses this to estimate how much space the final output will take.
This can be done by either sorting the data using sorting algorithms (which guarantees duplicates are next to each other) or use hash collisions
The main complication with sorting is that the data stored in disk is mostly much larger than the maximum space in RAM (where it can be processed). To solve this, we use an external sorting algorithm.
External Merge
The main method for this is with external merge sort:
In terms of O operations, external merge sort over has a runtime of:
There is a factor of two because we need to both read & write data when sorting.
Hash-based
Operation Cost
The total cost to remove duplicates with external merge sort is:
- Reading the entire table, so we only keep the attributes that need to be projected:
- Write pages with projected attributes to disk:
- Sort pages with projected attributes using external merge sort:
- Read sorted projected pages to discard adjacent duplicates:
Joins
Joins are generally very expensive to perform, and in the worst case result in the cross product. Out of all the possible joins, the natural join is the most commonly queried one, and hence need to be the most optimised. A RDBMS can implement a join in many ways:
- Nested-loop joins
- Simple nested-loop join
- Page based nested-loop join
- Block nested-loop join
- Sort-merge join
- Hash join
Simple Nested-Loop Join
The most straight-forward method of performing a natural join over unsorted files is using a nested for-loop:
Algorithm SimpleNestedLoopJoin(A: Table, B: Table):
For each tuple a in A do:
For each tuple b in B do:
If a.attribute == b.attribute then:
Add (a,b) to the Join
End If
End do
End do
Return Join
End Algorithm
The final join is unsorted.
Total cost:
Page-based Nested-Loop Join
Page-based nested-loop joins exploit the fact that multiple tuples are stored in a single page, so we can start by loading a page and then comparing every tuple in that page in order to minimise the number of I/O operations.
Algorithm PageBasedNestedLoopJoin(A: Table, B: Table):
For each page a_page in A do:
For each page b_page in B do:
For each tuple a in A do:
For each tuple b in B do:
If a.attribute == b.attribute then:
Add (a,b) to the Join
End If
End do
End do
End do
End do
Return Join
End Algorithm
The final join is unsorted.
Because we measure our total runtime in terms of page accesses, once we access a page, any tuples in it do not add extra time. Hence the final cost is:
Block Nested-Loop Join
A further optimisation is to exploit as much main memory (RAM) available. The block-based nested-loop join first reserves two pages in the RAM for an input and output buffer to reduce the number of I/O operations.
Assume we are treating as the outer loop table, and as the inner loop table
If the maximum capacity of the RAM (in pages) is , then the block-based approach reads in sized ‘blocks’ of pages of , comparing it with the input buffer for . If the join conditions are satisfied, then writes to the output buffer page.
- Whenever the block is fully read, read in a new block (of ) from disk
- Whenever the input buffer is read, read in a new page (of ) from disk
- Whenever the output buffer is full, flush it (write to disk)
%%🖋 Edit in Excalidraw, and the dark exported image%%
The total I/O operations are:
Sort-Merge Join
The sort-merge join is an improvement over nested-loop joins, because once and are sorted, there is no need to check all of for a single value of , since if we have a value , every value of after fails to satisfy the condition, so we can stop checking.
The ‘merging’ operation is simply taking the column values of and and merging them to form the join .
is always only scanned once, and (approximately), is scanned once. Technically, there would be multiple scans when has duplicates, but the amortised cost is that is only scanned once.
The total I/O cost is:
Assuming we use external merge sort:
Hash Join
General Join Conditions
For composed join conditions that consists of only equality checks, i.e. equi-joins, we can use any of the join techniques, with sort-merge join and hash join giving the best runtimes:
- For sort-merge join, sort over the combination of columns in and . For example, if we have the join: we sort with the combination of (this can be done by string concatenation, for example), and sort over .
- For hash join, we partition over the combination of columns
For joins containing inequalities in their join conditions, i.e. theta-joins, we cannot* use hash join or sort-merge join! The best option is to use block nested-loop join.