Winsights

Vapor - Query Optimisations

The database used by a Vapor application in a production environment may not necessarily be on the same hardware as your application. It may be deployed on a separate database server, a different virtual machine/container or on a different disk in the connected storage area network (SAN). Hence each round trip to the database would have some additional latency and your network speed may be affected by other factors like network congestion etc. This becomes more prominent when you want to improve your application's performance especially as your application load grows.
With this in mind, we would like to keep our database calls to a minimum whenever possible.

Some such cases where we would like to exercise this optimisation are when:

Fetching Parent with Children

When fetching a collection of parent objects with all their children objects (one-to-many relationship) we want to avoid N+1 Selects (explained below).

Let's say we had Car objects (database rows) and each Car had a collection of Wheel objects (in a one-to-many relationship) and we wanted to get all the details of cars below a selected price along with the details of their wheels in a list

We could fetch each car and then its individual wheels as below

var carsWithWheels: [CarWithWheels] = []
return Car.query(on: database)
    .filter(\.$price < selectedPrice)
    .all()
    .map { cars in
        for car in cars {
            _ = car.$wheels.query(on: database)
                .all()
                .map { carWheels in
                    carsWithWheels.append(
                        CarWithWheels(car: car, wheels: carWheels))
                }
        }      
    }
    .map { _ in
        return carsWithWheels
    }

This would generate one database query for the Cars, and then N additional select queries in each iteration of the for loop to get the wheels of each car, where N is the total number of cars. This is known as the N+1 Selects issue

We can simply avoid these additional N roundtrips to the database by eager loading the wheels of each car along with the car using a join on it by adding with children at the time of fetching the data as below:

var carsWithWheels: [CarWithWheels] = []
return Car.query(on: database)
    .filter(\.$price < selectedPrice)
    .with(\.$wheels)    //The wheels child objects are eagerly loaded here
    .all()
    .flatMap { cars in
        for car in cars {
            carsWithWheels.append(CarWithWheels(car: car, wheels: car.wheels))
        }
        return req.eventLoop.future(carsWithWheels)
    }

This reduces the database calls to a single call with much cleaner looking code

Multiple Inserts and Updates

While performing multiple inserts and updates we would like to reduce the database calls by performing all of them together when possible.
Vapor allows up to do this using Fluent's Batching
If we wanted to create a list of cars in a single batch

let cars = [bmw, tesla, mercedes]
cars.create(on: database)

Large and Complex Data Operations

When dealing with large and complex data operations (like generating an annual report) we would like to perform data intensive operations as close to the database as possible. Stored procedures offer us this ability along other performance benefits and would be an ideal solution in such a case.
Using a stored procedure would also allow us to limit the number of database calls.
To call stored procedures with Fluent we would need to use the raw SQL calling capabilty by mapping the stored procedure's data to a Domain Transfer Object (DTO) and using the data from there.


References: The N+1 problem

Tagged with: