우리 API를 느리게 만든 N+1 Insert Loop
Source: Dev.to

문제
// ❌ 우리의 성능을 죽인 패턴
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 시간 | 일괄 시간 | 속도 향상 |
|---|---|---|---|
| 100 | 5 s | 50 ms | 100× |
| 1 000 | 50 s | 100 ms | 500× |
| 10 000 | 500 s | 500 ms | 1 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-pg 의 pg/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...')while및do…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