导致我们的 API 变慢至爬行的 N+1 插入循环

发布: (2026年1月3日 GMT+8 04:06)
3 min read
原文: Dev.to

Source: Dev.to

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

问题

// ❌ The pattern that killed our performance
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×

正确的模式:批量插入

// ✅ Single query, any number of rows
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 pattern
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 规则会捕获:

  • query('INSERT...') 出现在 forfor…offor…in 循环中
  • query('INSERT...') 出现在 whiledo…while 循环中
  • query('INSERT...') 出现在 forEachmapreducefilter 回调中
  • query('UPDATE...') 出现在任何循环结构中
  • query('DELETE...') 出现在任何循环结构中

其他批量模式

批量更新

// ✅ Update with 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]
);

批量删除

// ✅ Delete with 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

相关文章

阅读更多 »