Keys and indexes
Indexes are crucial in improving database performance by allowing faster data retrieval. To understand the significance of indexes in Rell, let's start with the basics.
Imagine that you have a large dataset and need to find a specific entry within it. Without an index, searching through all the data can be time-consuming with a worst-case time complexity of O(n). However, when an attribute is indexed in Rell, it dramatically enhances query performance, reducing the time complexity to O(log(n)), thanks to PostgreSQL's underlying binary tree structure.
In Rell, you have two options to mark an attribute for indexing: key
and index
. While both improve query
performance, they serve different purposes:
Key
A key
is an index that enforces a unique constraint on the indexed attribute. This uniqueness constraint guarantees
that entities with the same attribute value will not exist in your database, making it less error-prone when creating
entries.
Index
An index
is used to improve query performance for non-unique attributes.
Note that both keys and indexes introduce extra time in creating entities, so be sure to mark your attributes cautiously.
Key and index example
Consider the scenario where multiple houses share the same street, each with its unique id. This could be modelled like:
entity house {
key id: integer;
index street: text;
}
Now, the following queries would be high performant:
val unique_house = house @ { .id == 10 };
val houses_on_main_street = house @* { .street == "main street" };
Composite index
To further optimize your database queries, you can add a composite index. This is particularly useful when finding entries based on a combination of attributes. For instance, if your app often queries all houses owned by a specific owner on a particular street, you can create a multi-column index.
index owner, street;
The order of the fields in a multi-column index is crucial for performance. This is how composite indexes work in SQL, where each column precedes the latter. To create the most efficient index, place the most significant attribute first. For instance, if you want to query all houses with exactly four rooms and a floor area greater than 100:
house @ { .number_of_rooms == 4, .floor_area > 100 };
In this case, an optimal index configuration would be:
index number_of_rooms, floor_area;
Composite keys work the same way. If a specific combination of entries can only exist once, adding a composite key will ensure this constraint in the database. Place the attribute yielding the fewest results first to achieve optimal performance.
Practical example
This example is focused on creating the house
records and requesting specific house
records.
Add the house
entity to the src/main/entities.rell
file:
entity house {
index street;
number: integer;
key street, number;
number_of_rooms: integer;
number_of_floors: integer;
floor_area: integer;
index number_of_rooms, floor_area;
}
To query the newly created house
records, add the code below to the src/main/queries.rell
file:
query get_specific_houses(number_of_rooms: integer, floor_area: integer) = house @* { .number_of_rooms == number_of_rooms, .floor_area >= floor_area } ( $.to_struct() );
To interact with the database, we must run the node locally. The --wipe
flag is ususlly set when there is a need to
delete all records from the database:
chr node start --wipe
Add the new street record:
chr tx create_street "Drottninggatan"
Create three houses using the create_house
operation:
chr tx create_house 1 30 4 1 200
chr tx create_house 1 31 4 1 100
chr tx create_house 1 32 4 1 109
At this point, we are ready to request specific houses that are available in the database:
chr query get_specific_houses
The result of the query above is as follows:
[
[
"floor_area": 200,
"number": 30,
"number_of_floors": 1,
"number_of_rooms": 4,
"street": 1
],
[
"floor_area": 109,
"number": 32,
"number_of_floors": 1,
"number_of_rooms": 4,
"street": 1
]
]