우리 API를 느리게 만든 N+1 Insert Loop

발행: (2026년 1월 3일 오전 05:06 GMT+9)
3 min read
원문: Dev.to

Source: Dev.to

Cover image for The N+1 Insert Loop That Slowed Our API to a Crawl

문제

// ❌ 우리의 성능을 죽인 패턴
async function importUsers(users) {
  for (const user of users) {
    await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2)',
      [user.name, user.email]
    );
  }
}

1 000명의 사용자에 대해:

  • 데이터베이스에 1 000번 라운드 트립
  • 쿼리당 약 50 ms
  • ≈ 50 초 총 소요

왜 중요한가

행 수N+1 시간일괄 시간속도 향상
1005 s50 ms100×
1 00050 s100 ms500×
10 000500 s500 ms1 000×

올바른 패턴: 일괄 삽입

// ✅ 단일 쿼리, 행 수에 제한 없음
async function importUsers(users) {
  const values = users
    .map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`)
    .join(', ');

  const params = users.flatMap(u => [u.name, u.email]);

  await pool.query(`INSERT INTO users (name, email) VALUES ${values}`, params);
}

unnest() 로 더 나은 방법

// ✅ PostgreSQL unnest 패턴
async function importUsers(users) {
  await pool.query(
    `INSERT INTO users (name, email)
     SELECT * FROM unnest($1::text[], $2::text[])`,
    [users.map(u => u.name), users.map(u => u.email)]
  );
}

규칙: pg/no-batch-insert-loop

이 패턴은 eslint-plugin-pgpg/no-batch-insert-loop 규칙에 의해 감지됩니다.

ESLint 로 잡아내기

npm install --save-dev eslint-plugin-pg

권장 설정 사용 (모든 규칙)

import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];

이 규칙만 활성화하기

import pg from 'eslint-plugin-pg';

export default [
  {
    plugins: { pg },
    rules: {
      'pg/no-batch-insert-loop': 'error',
    },
  },
];

기대되는 결과

N+1 루프가 감지되면 ESLint가 다음과 같이 보고합니다:

src/import.ts
  5:3  error  ⚡ CWE-1049 | Database query loop detected. | HIGH
                 Fix: Batch queries using arrays and "UNNEST" or a single batched INSERT. |
                 https://use-the-index-luke.com/sql/joins/nested-loops-join-n1-problem

감지 패턴

pg/no-batch-insert-loop 규칙은 다음을 포착합니다:

  • for, for…of, for…in 루프 내부의 query('INSERT...')
  • whiledo…while 루프 내부의 query('INSERT...')
  • forEach, map, reduce, filter 콜백 내부의 query('INSERT...')
  • 모든 루프 구조 내부의 query('UPDATE...')
  • 모든 루프 구조 내부의 query('DELETE...')

기타 일괄 패턴

일괄 업데이트

// ✅ unnest 사용 업데이트
await pool.query(
  `
  UPDATE users SET status = data.status
  FROM unnest($1::int[], $2::text[]) AS data(id, status)
  WHERE users.id = data.id
  `,
  [ids, statuses]
);

일괄 삭제

// ✅ ANY 사용 삭제
await pool.query('DELETE FROM users WHERE id = ANY($1)', [userIds]);

빠른 설치

npm install --save-dev eslint-plugin-pg
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];

50초 걸리던 가져오기를 ~100 ms 작업으로 단축합니다.

📦 npm: eslint-plugin-pg
📖 규칙 문서: pg/no-batch-insert-loop
GitHub: https://github.com/ofri-peretz/eslint

Back to Blog

관련 글

더 보기 »