Lesson 15 / 25

Solving N+1 With DataLoader

Batching and per-request caching.

Collect keys, load once

Naive resolvers cause the N+1 problem: fetching 50 orders, then calling the product resolver for each item, triggers dozens of separate database queries. DataLoader (and equivalents in other languages) collects all keys requested during one tick of execution, calls a batch function once with every key (for example WHERE id IN (...)), and caches results for the rest of the request. Create new loaders per request so caches never leak between users.

A product loader

JavaScript using the dataloader package (a sketch).

import DataLoader from "dataloader";

function createLoaders(db) {
  return {
    productById: new DataLoader(async (ids) => {
      const rows = await db.query("SELECT * FROM products WHERE id = ANY($1)", [ids]);
      const byId = new Map(rows.map((r) => [String(r.id), r]));
      return ids.map((id) => byId.get(String(id)) ?? null);   // same order as keys
    }),
  };
}

// context: ({ req }) => ({ loaders: createLoaders(db), ... })
const resolvers = {
  OrderItem: {
    product: (item, _args, { loaders }) => loaders.productById.load(item.productId),
  },
};

Return results in key order

The batch function must return one result per key, in the same order, using null for missing ones.

Quick check: What does DataLoader do?

  • Batches many individual loads into one call and caches them per request
  • Encrypts queries
  • Generates the schema
  • Replaces the database
Answer

Batches many individual loads into one call and caches them per request — Fixes N+1 queries.