Оптимізація GraphQL: усуваємо N+1 за допомогою DataLoader

Клієнт скаржиться: GraphQL-ендпоінт «зависає» на списку з 100 постів. Причина — класична N+1 проблема: для кожного поста виконується окремий запит автора, разом 101 SQL замість одного-двох. Таймаути, падіння сервера, незадоволені користувачі — це типовий сценарій. Ми за 5 років оптимізували понад 20

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Оптимізація GraphQL: усуваємо N+1 за допомогою DataLoader
Середній
~2-3 дні

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1418
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1286
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1243
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    998

Клієнт скаржиться: GraphQL-ендпоінт «зависає» на списку з 100 постів. Причина — класична N+1 проблема: для кожного поста виконується окремий запит автора, разом 101 SQL замість одного-двох. Таймаути, падіння сервера, незадоволені користувачі — це типовий сценарій. Ми за 5 років оптимізували понад 20 GraphQL-проектів, скорочуючи кількість запитів у 20–30 разів. Результат — зниження затримки на 40–60% та економія на хмарних ресурсах до $400–600 на місяць. DataLoader — основний інструмент для боротьби з N+1. Типовий проект окупається за 2–3 місяці завдяки зниженню витрат на інфраструктуру.

Чому проблема N+1 критична для GraphQL?

GraphQL-резолвери викликаються незалежно для кожного батьківського об'єкта. Якщо схема містить вкладене поле, що потребує окремого SQL-запиту, то для N елементів виконується N+1 запит. Наприклад:

query { posts { id title author { name } } } 

Без DataLoader цей запит генерує SELECT * FROM posts, а потім для кожного з 100 постів — SELECT * FROM users WHERE id = ? — разом 101 запит до БД. Навантаження на базу зростає лінійно, а час відповіді — квадратично. Позбавлення від N+1 — ключ до стабільної продуктивності.

Як DataLoader батчить запити?

DataLoader — бібліотека (Node.js DataLoader — реалізація для JavaScript), яка батчить виклики. Вона збирає всі .load() за один такт event loop і виконує один груповий запит. Крім того, вона кешує результати на час запиту, уникаючи повторного завантаження тих самих даних. Ось приклад реалізації:

import DataLoader from 'dataloader' async function batchUsers(userIds) { const users = await db.query( 'SELECT * FROM users WHERE id = ANY($1)', [userIds] ) const userMap = new Map(users.map(u => [u.id, u])) return userIds.map(id => userMap.get(id) || null) } const userLoader = new DataLoader(batchUsers) const resolvers = { Post: { author: async (post, args, context) => { return context.loaders.userById.load(post.author_id) } } } 

Важно: DataLoader створюється на кожен HTTP-запит, щоб уникнути витоку даних між різними користувачами.

Реєстр завантажувачів (per-request)

Ми рекомендуємо збирати всі DataLoader'и в єдиний клас, який ініціалізується в контексті запиту. Це спрощує підтримку і гарантує, що кожен завантажувач живе рівно один запит.

export class DataLoaderRegistry { constructor(db) { this.db = db this.userById = new DataLoader(async (ids) => { const rows = await db.query( 'SELECT * FROM users WHERE id = ANY($1::int[])', [ids] ) const map = new Map(rows.map(r => [r.id, r])) return ids.map(id => map.get(id) ?? null) }) this.postsByAuthorId = new DataLoader(async (authorIds) => { const rows = await db.query( 'SELECT * FROM posts WHERE author_id = ANY($1::int[])', [authorIds] ) const map = new Map() for (const row of rows) { if (!map.has(row.author_id)) map.set(row.author_id, []) map.get(row.author_id).push(row) } return authorIds.map(id => map.get(id) ?? []) }) this.commentsByPostId = new DataLoader(async (postIds) => { const rows = await db.query( 'SELECT * FROM comments WHERE post_id = ANY($1::int[]) ORDER BY created_at', [postIds] ) const map = new Map() for (const row of rows) { if (!map.has(row.post_id)) map.set(row.post_id, []) map.get(row.post_id).push(row) } return postIds.map(id => map.get(id) ?? []) }) } } // В context factory context: async ({ req }) => { const user = await authenticate(req) const loaders = new DataLoaderRegistry(db) return { user, db, loaders } } 

DataLoader зі складеними ключами

Зауважте: коли потрібна фільтрація за додатковими аргументами, використовуйте складений ключ:

this.productsByCategoryAndStatus = new DataLoader( async (keys) => { const categoryIds = [...new Set(keys.map(k => k.categoryId))] const statuses = [...new Set(keys.map(k => k.status))] const rows = await db.query(` SELECT * FROM products WHERE category_id = ANY($1::int[]) AND status = ANY($2::text[]) `, [categoryIds, statuses]) const map = new Map() for (const row of rows) { const key = `${row.category_id}:${row.status}` if (!map.has(key)) map.set(key, []) map.get(key).push(row) } return keys.map(k => map.get(`${k.categoryId}:${k.status}`) ?? []) }, { cacheKeyFn: (key) => `${key.categoryId}:${key.status}` } ) 

Праймінг кешу DataLoader

Якщо ви вже завантажили дані (наприклад, авторів у запиті постів), можна підказати DataLoader'у їх значення — це запобіжить повторному батчу:

const resolvers = { Query: { posts: async (parent, { limit }, context) => { const posts = await context.db.posts.findAll({ limit }) for (const post of posts) { if (post.author) { context.loaders.userById.prime(post.author.id, post.author) } } return posts } } } 
Кейс: каталог товарів з 50 категоріями

На одному з проектів ми оптимізували каталог, де на сторінці відображалися товари з 50 категорій. Без DataLoader кожен резолвер категорії робив окремий запит — разом 51 SQL. Після впровадження DataLoader з батчингом по author_id запитів стало 2: один на список товарів, один на всіх авторів разом. Час відповіді впав з 2.5 секунд до 300 мс.

Порівняння підходів: наївний резолвер проти DataLoader

Підхід Кількість SQL (100 постів + автор) Складність впровадження
Наївний резолвер 101 Низька
DataLoader 3 Середня
Ручна оптимізація запитів Залежить від реалізації Висока

DataLoader кращий за наївний резолвер у 20-30 разів за кількістю запитів до БД при тій самій складності підтримки. З точки зору GraphQL performance, це одне з найефективніших рішень.

Вигода від впровадження DataLoader

Сценарій Без DataLoader З DataLoader
100 постів + автор 101 SQL 3 SQL (posts + users batch + comments batch)
50 постів + теги 51 SQL 2 SQL
20 категорій + товари 21 SQL 2 SQL

DataLoader скорочує кількість запитів до БД у 20–30 разів порівняно з наївною реалізацією. На проектах з навантаженням від 1000 RPS це дає зниження затримки на 40–60% та економію хмарних ресурсів до $400–600 на місяць. Типовий проект окупається за 2–3 місяці завдяки зниженню витрат на інфраструктуру.

Покрокове впровадження DataLoader

  1. Визначте всі точки з N+1: запишіть поточні SQL-запити на кожен резолвер.
  2. Для кожної сутності створіть DataLoader з функцією батчингу.
  3. Впровадьте реєстр завантажувачів у контекст запиту.
  4. Замініть прямі виклики БД у резолверах на .load().
  5. Протестуйте: порівняйте кількість SQL-запитів до та після.

Що входить в реалізацію DataLoader

  • Аудит поточної GraphQL-схеми: виявляємо всі N+1 точки.
  • Написання DataLoader'ів для кожної сутності (до 10 відношень).
  • Інтеграція реєстру завантажувачів у контекст запиту.
  • Навантажувальне тестування: порівнюємо час відповіді до та після.
  • Документація з підтримки та розширення рішення.
  • Гарантія: наші інженери з 5-річним досвідом забезпечують стабільну роботу навіть під високим навантаженням.

Зв'яжіться з нами — ми оцінимо ваш проект і дамо точний кошторис. Отримайте консультацію прямо зараз: напишіть нам, і ми розберемо ваш випадок безкоштовно. Замовте впровадження DataLoader у вашому проекті — напишіть нам.

Посилання на офіційну документацію: DataLoader (GraphQL Foundation) DataLoader official repository.