MeWrite Docs

GraphQL N+1 Query Problem

GraphQLでN+1クエリ問題が発生した場合の対処法

概要

GraphQLのリゾルバーがネストしたデータを取得する際、親レコードごとにデータベースクエリが発行されてしまうN+1問題です。パフォーマンスに深刻な影響を与えます。

問題の例

1
2
3
4
5
6
7
8
# このクエリで
query {
  users {      # 1クエリ
    posts {    # ユーザーごとに1クエリ = N クエリ
      title
    }
  }
}
# 発行されるSQL(100ユーザーの場合)
SELECT * FROM users;                    -- 1回
SELECT * FROM posts WHERE user_id = 1;  -- 100回
SELECT * FROM posts WHERE user_id = 2;
...
SELECT * FROM posts WHERE user_id = 100;
# 合計101クエリ

解決策

1. DataLoaderを使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import DataLoader from 'dataloader'

// バッチ関数を定義
const postsByUserLoader = new DataLoader(async (userIds: string[]) => {
  const posts = await db.posts.findMany({
    where: { userId: { in: userIds } }
  })

  // userIdでグループ化して返す
  const postsByUserId = new Map()
  posts.forEach(post => {
    const existing = postsByUserId.get(post.userId) || []
    postsByUserId.set(post.userId, [...existing, post])
  })

  return userIds.map(id => postsByUserId.get(id) || [])
})

// リゾルバーで使用
const resolvers = {
  User: {
    posts: (user, _, context) => {
      return context.loaders.postsByUserLoader.load(user.id)
    }
  }
}

2. Apollo Serverでのセットアップ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { ApolloServer } from '@apollo/server'
import DataLoader from 'dataloader'

// リクエストごとにDataLoaderを作成
const server = new ApolloServer({
  typeDefs,
  resolvers,
})

// コンテキストでDataLoaderを渡す
const { url } = await startStandaloneServer(server, {
  context: async () => ({
    loaders: {
      postsByUserLoader: new DataLoader(batchPosts),
      commentsByPostLoader: new DataLoader(batchComments),
    }
  }),
})

3. Prismaでの対処

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Prisma FluentAPIを使用
const resolvers = {
  Query: {
    users: () => prisma.user.findMany({
      include: {
        posts: true  // JOINで取得
      }
    })
  }
}

// または findUnique + DataLoader
const userLoader = new DataLoader(async (ids) => {
  const users = await prisma.user.findMany({
    where: { id: { in: ids } },
    include: { posts: true }
  })
  return ids.map(id => users.find(u => u.id === id))
})

4. GraphQL Shieldと組み合わせ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { shield, allow } from 'graphql-shield'

const permissions = shield({
  Query: {
    users: allow,
  },
  User: {
    // DataLoaderを使うリゾルバーにも適用可能
    posts: allow,
  },
})

5. クエリの複雑度制限

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { createComplexityLimitRule } from 'graphql-validation-complexity'

const ComplexityLimitRule = createComplexityLimitRule(1000, {
  onCost: (cost) => {
    console.log('Query cost:', cost)
  },
})

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [ComplexityLimitRule],
})

デバッグ方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// クエリログを有効化(Prisma)
const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
})

// Apollo Server プラグイン
const loggingPlugin = {
  requestDidStart() {
    return {
      willSendResponse({ response }) {
        console.log('Extensions:', response.extensions)
      }
    }
  }
}

関連エラー

関連エラー

GraphQL の他のエラー

最終更新: 2025-12-22