Your first GraphQL API - Solving N+1 query problem

A well-known problem with GraphQL is the N+1 query problem. It is not necessarily caused by the API itself. But when we develop the resolver, we can introduce the N+1 query problem easily because of its query flexibility.

In this tutorial, we will show you how to solve the N+1 query problem when working with webonyx/graphql-php library.

Use Case

Let's assume we have a list of events and each event has a venue. To get all the events and its associated venues. Our GraphQL looks like this:

{
  events {
    venue {
      name
    }
  }
}

Typical Resolver

Typically to build the resolver, our resolver function will take the event ID from the parent and run a SQL query using it. Similar to the code as shown below:

$venueType = new ObjectType([
    'name' => 'Venue',
    'fields' => [
        'name' => [
            'type' => Type::string(),
            'resolve' => function($event) {
                $sql = 'select * from venues where event_id = '. $event['id'];
                return MySQLOrm::runQuery($sql);
            }
        ],
    ]
]);

When we run the GraphQL query above, suppose we have 10 events in total through first query(1), and the $sql query for getting venues in code above will run10(N) times, this is known as the N+1 query problem.

This is apparently inefficient.

Delayed Resolver

To solve the N+1 problem, we can use a delayed resolver, also known as dataloader. Its basic idea is that we load the required records once and cache them in a buffer. When it comes to resolving time, we pick it up from the buffer using its identification key.

When using a delayed resolver, our Type class looks like this:

$venueType = new ObjectType([
    'name' => 'Venue',
    'fields' => [
        'name' => [
            'type' => Type::string(),
            'resolve' => function ($event) {

                VenueBuffer::getInstance()->add($event['id']);

                return new GraphQL\Deferred(function () use ($event) {
                    VenueBuffer::getInstance()->loadBuffered();
                    return VenueBuffer::getInstance()->get($event['id']);
                });

            }
        ],
    ]
]);

Nothing changed besides the resolve section. We will explain VenueBuffer in detail later on. For now, the interesting part is the GraphQL\Deferred class. This class will be called at last when all the resolvable fields are returned. It gives us a chance to do some interesting stuff as such loading data from a buffer.

The VenueBuffer class is a singleton class, meaning it has only one instance. This pattern helps us build the buffer when GraphQL resolves the GraphQL\Deferred class.

class VenueBuffer
{
    private static $instance;

    private $result = [];

    private $keys = [];

    public static function getInstance()
    {
        if (null == self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function add($key)
    {
        $this->keys[] = $key;
    }

    public function get($key)
    {
        if (isset($this->result[$key])) {
            return $this->result[$key];
        }
        return null;
    }

    public function loadBuffered()
    {
        if (!empty($this->result)) {
            return;
        }
        $sql = 'select * from venues where event_id = IN () ' . implode(',', $this->keys);
        $records = MySQLOrm::runQuery($sql);
        $this->result = $this->groupRecordsUsingKey($records);
    }


}

The interesting part of this class is the loadBuffered() method, in this method, we make sure the $sql query only run once by checking whether the $result variable has been set with data.

End

This is how we solve the N+1 query problem when working with the webonyx/graphql-php library.