当前位置:首页 > 文章列表 > 文章 > 常见问题 > 接口列表页越查越慢怎么办:N+1 查询从 120 次降到 3 次

接口列表页越查越慢怎么办:N+1 查询从 120 次降到 3 次

来源:17golang原创 2026-06-29 19:49:08 0浏览 收藏

列表接口刚上线时很快,数据量一涨就开始变慢:第一页还能接受,翻到后面或者筛选条件复杂一点,接口耗时就从几十毫秒升到几百毫秒。很多人第一反应是“数据库慢了”,但真实问题常常是 N+1 查询。

所谓 N+1,不是某条 SQL 特别慢,而是一次列表请求先查出 N 条主数据,再为每条数据单独查用户、商品、标签或统计信息。单次看每条查询都很快,合起来就把接口拖慢。下面用一个订单列表接口做案例,按指标驱动的方式把问题拆开。

目录
  • 基线数据:先看一次请求到底查了多少次
  • 假设验证:为什么不是单条 SQL 慢
  • 改动点:把逐条查询改成批量加载
  • 压测方法:固定条件再比较结果
  • 结果对比:查询次数和 P95 都降下来
  • 边界条件:什么时候还要继续治理

基线数据:先看一次请求到底查了多少次

问题接口是一个订单列表,返回 40 条订单,每条订单还要带用户昵称、商品标题和最近一次支付记录。业务代码一开始写得很直观:

function listOrders(PDO $db): array
{
    $orders = $db->query(
        "select id, user_id, product_id, amount, created_at
         from orders
         order by id desc
         limit 40"
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($orders as &$order) {
        $order['user'] = findUser($db, (int) $order['user_id']);
        $order['product'] = findProduct($db, (int) $order['product_id']);
        $order['pay'] = findLastPayment($db, (int) $order['id']);
    }

    return $orders;
}

40 条订单,每条补 3 类信息,就会出现:

1 次订单列表查询
40 次用户查询
40 次商品查询
40 次支付查询
合计约 121 次 SQL

在本地或测试库里,每条小 SQL 只要 1 到 3 毫秒,看起来没有问题。线上并发上来后,连接池、网络往返、数据库 CPU 和锁等待会把这些碎片成本放大。

列表接口 N+1 查询基线证据:一次请求产生大量小 SQL

假设验证:为什么不是单条 SQL 慢

判断 N+1 的关键是看“查询次数”和“相同 SQL 模板重复次数”。如果只看慢 SQL 日志,可能没有任何一条 SQL 超过阈值。建议在调试环境先加一层轻量计数,记录本次请求内 SQL 次数和模板摘要。

final class SqlCounter
{
    public int $count = 0;
    public array $samples = [];

    public function mark(string $sql): void
    {
        $this->count++;
        $key = preg_replace('/\\b\\d+\\b/', '?', $sql);
        $this->samples[$key] = ($this->samples[$key] ?? 0) + 1;
    }
}

一次请求结束后打印摘要:

SQL count: 121
select id,nickname from users where id = ?          40
select id,title from products where id = ?          40
select * from payments where order_id = ? limit 1   40
select ... from orders limit 40                      1

这组数据说明,问题不是某条 SQL 特别慢,而是同类查询重复了太多次。此时继续只加索引,收益有限;更有效的方向是把循环里的单条查询移到循环外,改成批量加载。

改动点:把逐条查询改成批量加载

改造目标很明确:主表查一次,用户批量查一次,商品批量查一次,支付记录批量查一次。先收集 ID,再用 where id in (...) 或聚合子查询拿到补充信息,最后在内存里按 ID 组装。

function listOrdersFast(PDO $db): array
{
    $orders = $db->query(
        "select id, user_id, product_id, amount, created_at
         from orders
         order by id desc
         limit 40"
    )->fetchAll(PDO::FETCH_ASSOC);

    $userIds = array_values(array_unique(array_column($orders, 'user_id')));
    $productIds = array_values(array_unique(array_column($orders, 'product_id')));
    $orderIds = array_values(array_column($orders, 'id'));

    $users = fetchUsersByIds($db, $userIds);
    $products = fetchProductsByIds($db, $productIds);
    $payments = fetchLastPaymentsByOrderIds($db, $orderIds);

    foreach ($orders as &$order) {
        $order['user'] = $users[$order['user_id']] ?? null;
        $order['product'] = $products[$order['product_id']] ?? null;
        $order['pay'] = $payments[$order['id']] ?? null;
    }

    return $orders;
}

批量查询函数可以统一把结果转成以主键为 key 的数组:

function fetchUsersByIds(PDO $db, array $ids): array
{
    if (!$ids) {
        return [];
    }

    $ids = array_values(array_unique(array_map('intval', $ids)));
    $idList = implode(',', $ids);
    $rows = $db->query(
        "select id, nickname
         from users
         where id in ($idList)"
    )->fetchAll(PDO::FETCH_ASSOC);

    $map = [];
    foreach ($rows as $row) {
        $map[(int) $row['id']] = $row;
    }
    return $map;
}

支付记录如果要取每个订单最近一条,不能简单用 where order_id in (...) 后全量返回。可以用子查询先找每个订单的最大支付记录 ID,再回表取详情:

select p.*
from payments p
join (
  select order_id, max(id) as max_id
  from payments
  where order_id in (?,?,?)
  group by order_id
) t on p.order_id = t.order_id and p.id = t.max_id;

N+1 查询改造:收集 ID 后批量加载并按映射组装响应

压测方法:固定条件再比较结果

优化前后要固定相同条件,否则结果容易误判。建议固定这几项:

  • 相同页大小,例如每页 40 条。
  • 相同筛选条件,例如同一状态、同一时间范围。
  • 相同数据库环境,不要一边连测试库一边连线上从库。
  • 相同并发和持续时间,例如并发 20、持续 2 分钟。

可以用任意压测工具,这里只展示指标表。关键是记录请求耗时和 SQL 次数,而不是只看平均值。

场景:订单列表 limit 40
并发:20
持续:2 分钟
记录:SQL 次数、平均耗时、P95、数据库 QPS

结果对比:查询次数和 P95 都降下来

下面是一次典型对比结果。数字不是为了证明某个固定收益,而是说明判断方向:

版本 单请求 SQL 次数 平均耗时 P95 数据库 QPS
逐条查询 121 380ms 720ms 2400+
批量加载 4 92ms 160ms 310
支付聚合合并后 3 76ms 130ms 260

这个结果说明两件事:第一,N+1 的核心成本是大量小查询和网络往返;第二,接口 P95 比平均值更能反映用户感受,因为并发下偶发等待会被放大。

边界条件:什么时候还要继续治理

批量加载不是终点。下面这些情况还要继续拆:

  • 页大小过大:一次返回 500 条,即使只有 3 次 SQL,也可能在传输和组装阶段变慢。
  • in 条件过长:可以限制页大小,或者按业务维度预聚合。
  • 关联数据变化频繁:缓存要谨慎,避免列表展示旧状态。
  • 补充字段很多:考虑列表接口只返回摘要,详情页再查完整信息。
  • 统计字段昂贵:把实时统计拆成异步汇总表或定时刷新字段。

总结一下:列表接口越查越慢时,先别急着给每条 SQL 加索引。把一次请求内的 SQL 次数、重复模板和 P95 指标拉出来。如果发现同一类查询重复几十次,就优先治理 N+1:收集 ID、批量查询、映射组装,再用固定压测条件确认收益。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
PHP-FPM 慢请求报警运行手册:从 slowlog 到进程池参数调整PHP-FPM 慢请求报警运行手册:从 slowlog 到进程池参数调整
上一篇
PHP-FPM 慢请求报警运行手册:从 slowlog 到进程池参数调整
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    2903次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    2691次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    2624次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    2857次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    2797次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码