一个WITH子句如何让亿级数据查询直接起飞?

那是一个周四的下午,我正为一个金融科技客户的性能报告而焦头烂额。他们的用户群在第一季度经历了爆炸式增长,这意味着我们那些曾经响应迅速的仪表盘,现在慢得像老掉牙的拨号上网

我的任务是优化一个为他们客户消费分析功能提供数据的核心查询。这个查询本身并不算太复杂——仅仅是拉取上个季度按客户细分群体统计的总交易金额。

坑爹的是transactions (交易) 表有超过 1.5 亿行数据!

而我的那个查询呢?它跑一次要超过 2 分钟,有时候甚至直接超时。


原始查询 (又名“这玩意儿咋就这么慢呢?”)

我一开始用的查询是这样的:

SELECT
    c.customer_id,
    c.segment,
    SUM(t.amount) AS total_spent
FROM
    customers c
JOIN
    transactions t ON c.customer_id = t.customer_id
WHERE
    t.transaction_date >= '2024-01-01' -- 只筛选特定日期之后的交易
GROUP BY
    c.customer_id, c.segment;

乍一看,这 SQL 似乎还挺干净的:基于已索引的外键进行连接,用 WHERE 子句过滤近期的交易,再用 GROUP BY 按客户统计花费。

那么问题到底出在哪儿呢?

结果发现,即使我对 transaction_date 进行了过滤,数据库执行 JOIN (连接) 操作实际上是在执行日期过滤之前
这意味着数据库引擎在缩小数据范围之前,对那 1.5 亿行交易数据进行了全表扫描!🤯

我尝试了添加更多索引、重写 JOIN 的方式、加上 LIMIT 子句等等,但都没能带来我需要的性能提升。


改变局势的“大杀器”:WITH 子句 (公用表表达式 - CTE)

在近乎绝望之下,我用 CTE (Common Table Expression) 重写了查询——主要目的是为了让调试更容易一些。
结果,这一改,一切都不同了。

-- 使用 CTE 先筛选出近期的交易数据
WITH recent_transactions AS (
    SELECT
        customer_id, -- 确保 customer_id 被选出用于后续 JOIN
        amount,
        transaction_date -- 虽然这里不直接用,但筛选条件基于它
    FROM
        transactions
    WHERE
        transaction_date >= '2024-01-01'
)
-- 然后再将筛选后的结果与 customers 表连接
SELECT
    c.customer_id,
    c.segment,
    SUM(r.amount) AS total_spent -- r 代表 recent_transactions
FROM
    customers c
JOIN
    recent_transactions r ON c.customer_id = r.customer_id
GROUP BY
    c.customer_id, c.segment;

现在,查询不再是先扫描 1.5 亿行交易数据然后再进行日期过滤,而是首先从 transactions 表中过滤出相关的(比如)2000 万行数据到 recent_transactions 这个临时的结果集中,然后再将这个小得多的结果集与 customers 表进行连接。

🚀 执行时间从 127 秒 骤降到了 11.4 秒

没有其他任何改动,同样的数据,仅仅是优化了查询的结构


让我们来分析一下为什么这样改动就有效了

1. 尽早过滤 = 更少的数据参与连接
当参与连接的数据集已经被大幅缩减后,连接操作自然就快得多了。你可以把它想象成在把一堆衣服扔进洗衣机之前,先做好了分类筛选,挑出了真正需要洗的那些。

2. 更优的执行计划 (Cleaner execution plan)
现代的查询优化器(比如 PostgreSQL 12+、Google BigQuery 和 SQL Server 中的优化器)能够将 CTE 视为内联视图(inline views)或有效地进行处理——这有助于它们在优化查询时做出更明智的决策,优先执行 CTE 部分以缩小数据集。

3. 模块化的逻辑 = 更容易调试
将你的复杂查询逻辑拆分成更小、更专注的 CTE,可以让你更容易发现低效的部分,并且可以单独测试查询的每个组成部分是否符合预期。


额外优化:在 CTE 内部直接进行聚合

想让性能更上一层楼吗?下面这个版本,我直接在 CTE 内部就完成了按 customer_id 的聚合操作:

-- 在 CTE 中就完成按客户ID的聚合
WITH recent_spend AS (
    SELECT
        customer_id,
        SUM(amount) AS total_spent -- 直接在 CTE 里计算每个客户的总花费
    FROM
        transactions
    WHERE
        transaction_date >= '2024-01-01'
    GROUP BY
        customer_id -- 按 customer_id 分组
)
-- 然后将聚合后的结果与 customers 表连接
SELECT
    c.customer_id,
    c.segment,
    r.total_spent -- r 代表 recent_spend
FROM
    customers c
JOIN
    recent_spend r ON c.customer_id = r.customer_id;
-- 注意:这里的 GROUP BY customers.segment 如果还需要,则应加在最外层查询
-- 如果 customer_id 已经是唯一的,且 segment 与 customer_id 一一对应或不需要再次聚合,则可省略

现在,我是在一个已经聚合好的结果集(recent_spend)上进行连接,这同时减少了需要连接的行数和后续的处理时间。在我的实际案例中,这个版本的查询执行时间被压缩到了 6 秒以内


最终思考

写了这么多年 SQL,我学到了一点:一个好查询和一个真正牛逼的查询之间的区别,不在于语法有多花哨,而在于其结构

使用 WITH 子句(CTE)就像给你的复杂逻辑加上了清晰的注释。它能阐明你的意图、分离不同的逻辑关注点,并且在很多情况下,能够极大地提升查询性能

但这里也有几点需要注意(The catch):

  • • 在一些老版本的 MySQL 中,CTE 可能会被“物化”(materialized),即数据库会为 CTE 创建实际的临时表,这反而可能降低性能(新版本 MySQL 对 CTE 的优化已大大改善)。

  • • 在 PostgreSQL 中,建议使用 12 或更新版本,以便充分利用其对 CTE 的内联优化(inlining)能力。

  • • 在 Google BigQuery 中,通常推荐使用 CTE,这对于成本控制和查询性能的可见性都有好处。

要点总结 (Key Points)

  • • 我那个处理 1.5 亿行数据的查询,仅仅通过使用 CTE 就获得了将近 10 倍的速度提升。

  • • 使用 WITH 子句尽早过滤数据,以减轻后续 JOIN 操作的负载。

  • • 优化前后一定要测试性能——你的 SQL 数据库引擎及其版本很重要。

  • • 整洁的 SQL = 更快的 SQL = 更快乐的数据分析师(和开发者)!

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值