返回博客

Dashboard 接口 p99 破秒,SQL 单条却只要 5ms

某次给后台管理系统的首页数据看板做性能排查,开 MySQL 慢查询日志一条都没捞到——每条 SQL 执行时间均在 5ms 以内。但 API 实测 p99 稳定破 1.2s,运营每天催"打开很慢"。

这个反差本身就是线索:慢不在 SQL,慢在网络

现象

  • 后台首页看板展示"今日订单数 / 昨日订单数 / 各状态服务单数量"等约 20 个指标卡片。
  • 每个指标卡片对应代码里一次独立的 count()/sum() 查询,getOverview() 里整齐地列了 23 行。
  • 服务部署在公网云主机,MySQL 部署在另一台云主机,实测 RTT 约 40ms。
  • 结果:23 × 40ms = 920ms 纯等待,查询本身的计算时间几乎可以忽略。

根因

每一条 SQL 语句都要经历一次完整的网络往返(TCP send → DB 处理 → TCP recv)。即便查询计划是索引扫描、执行时间亚毫秒,只要 DB 不在本机,RTT 就是硬开销。

跨公网 / VPN 隧道 / SSH 隧道的 RTT 通常在 30–100ms 之间,不受 SQL 优化影响。23 条串行查询 × 50ms RTT ≈ 1.15s,这就是"SQL 不慢但接口慢"的真正原因。

坑:用 ORM 逐条 count() 对远端 DB 是性能陷阱
// 23 次网络往返 ≈ 1.15s 纯等待
$todayOrders = $db->name('order')->whereBetweenTime(...)->count();
$ydayOrders  = $db->name('order')->whereBetweenTime(...)->count();
$todayRevenue = $db->name('order')->where(...)->sum('amount');
// ... 又 20 条
foreach ([1,2,3,4,5,6,7] as $s) {
    $statusCount[$s] = $db->name('service')->where('status', $s)->count();
}

DB CPU 接近 0,慢查询日志一条没有,锅在网络——每次 ORM 方法调用都是独立的一次 RTT。

正解

SUM(CASE WHEN condition THEN 1 ELSE 0 END) 可以在一次全表扫描里同时算出任意多个条件的计数,把 N 次网络往返压缩为 1 次。

// 1 次网络往返,搞定所有订单维度统计
$rows = $db->query(
    "SELECT
        SUM(CASE WHEN create_time >= ? AND create_time < ? THEN 1 ELSE 0 END) AS today_orders,
        SUM(CASE WHEN create_time >= ? AND create_time < ? THEN 1 ELSE 0 END) AS yday_orders,
        SUM(CASE WHEN create_time >= ? AND create_time < ?
                  AND pay_status >= 3 THEN actual_amount ELSE 0 END) AS today_revenue,
        SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS s1,
        SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS s2,
        SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS s3
     FROM order_table",
    [$todayStart, $todayEnd, $ydayStart, $ydayEnd, $todayStart, $todayEnd]
);
$agg = $rows[0] ?? [];
$todayOrders = (int)($agg['today_orders'] ?? 0);

多张表的计数用 UNION ALL 合并,PHP 拿到结果集后在内存里分桶:

SELECT 'type_a' AS kind, status, COUNT(*) AS cnt
  FROM service_table_a WHERE deleted = 0 GROUP BY status
UNION ALL
SELECT 'type_b', status, COUNT(*)
  FROM service_table_b WHERE deleted = 0 GROUP BY status
UNION ALL
SELECT 'type_c', status, COUNT(*)
  FROM service_table_c WHERE deleted = 0 GROUP BY status

实际改造结果:原来 23 条独立 SQL → 合并为 5 次查询,接口延迟从 1.2s 降至约 120ms,p99 下降 90%。

正解:一次 RTT 返回所有聚合结果
  • 同表多维度统计:SUM(CASE WHEN ...) 合并为单条 SQL
  • 跨表同类统计:UNION ALL + 内存分桶
  • 参数全部用 ? 占位符绑定,不要拼接用户输入进合并 SQL
适用判断

满足以下条件再用这个模式:DB 在网络对端(RTT 可感知);同一个接口跑 ≥4 个独立 count/sum;这些聚合能走同一索引或同一次全表扫描。若 DB 在本机、RTT < 1ms,合并收益是噪音,不必为此牺牲代码可读性。

一句话外卖

远端数据库的性能瓶颈往往是网络往返次数而非 SQL 执行时间——慢查询日志干净的接口,先数一数 ORM 调了多少次。