🙏 感谢您的支持! 我们在七月份已经筹集了 $65 — 这足够我们工作到下个月。请帮助我们保持进度,进一步支持这个项目。 支持这个项目 →
SQL 代码已复制到剪贴板

课程 6.4: 公共表表达式 (CTEs)

公共表表达式,或称CTEs,是SQL中最强大且未被充分利用的特性之一。它们允许您定义可以在更大查询中引用的临时命名结果集。在本课程中,我们将探讨CTEs如何使您的SQL代码更具可读性、可维护性和更易于调试。

什么是CTEs?

公共表表达式 (CTE) 是在查询开始时使用 WITH 子句定义的临时结果集。可以将其视为一个命名的子查询,可以在同一查询中多次使用。

CTEs的主要优点:

  • 可读性:命名结果集使查询更易于理解
  • 可重用性:多次引用同一CTE而无需重新定义
  • 模块化:将复杂查询分解为逻辑上可管理的部分
  • 可维护性:逻辑的更改只需在一个地方进行
  • 调试:在组合之前独立测试每个CTE

基本CTE语法

CTE的一般语法为:

WITH cte_name AS (
    SELECT ...
)
SELECT * FROM cte_name;

组成部分:

  • WITH:引入CTE的关键字
  • cte_name:您为临时结果集指定的名称
  • AS:引入查询定义的关键字
  • (SELECT ...):定义CTE的查询
  • 主查询可以通过名称引用CTE

您的第一个CTE

让我们从一个简单的示例开始,计算客户支出:

WITH customer_spending AS (
    SELECT
        customer_id,
        SUM(amount) AS total_spent,
        COUNT(*) AS payment_count,
        AVG(amount) AS avg_payment
    FROM
        payment
    GROUP BY
        customer_id
)
SELECT
    customer_id,
    total_spent,
    payment_count,
    avg_payment
FROM
    customer_spending
WHERE
    total_spent > 100
ORDER BY
    total_spent DESC;

这个CTE:

  1. 定义了一个名为 customer_spending 的结果集
  2. 计算每个客户的支出指标
  3. 在主查询中引用此CTE以筛选高支出客户

这里的好处是清晰——意图显而易见:我们正在处理客户支出数据。

CTEs与子查询

让我们比较一下使用传统子查询方法的相同逻辑:

使用子查询:

SELECT
    customer_id,
    total_spent,
    payment_count,
    avg_payment
FROM (
    SELECT
        customer_id,
        SUM(amount) AS total_spent,
        COUNT(*) AS payment_count,
        AVG(amount) AS avg_payment
    FROM
        payment
    GROUP BY
        customer_id
) AS spending_data
WHERE
    total_spent > 100
ORDER BY
    total_spent DESC;

使用CTE:

WITH customer_spending AS (
    SELECT
        customer_id,
        SUM(amount) AS total_spent,
        COUNT(*) AS payment_count,
        AVG(amount) AS avg_payment
    FROM
        payment
    GROUP BY
        customer_id
)
SELECT
    customer_id,
    total_spent,
    payment_count,
    avg_payment
FROM
    customer_spending
WHERE
    total_spent > 100
ORDER BY
    total_spent DESC;

关键区别:

  • CTE在顶部定义,使查询结构立即清晰
  • CTE有一个有意义的名称(customer_spending),而不仅仅是一个匿名子查询
  • 主查询的意图在深入数据转换之前是可见的
  • 如果您需要多次引用此结果集,只需使用CTE定义一次

在一个查询中使用多个CTEs

您可以在单个查询中定义多个CTEs,每个CTE引用前一个CTE:

WITH customer_spending AS (
    SELECT
        customer_id,
        SUM(amount) AS total_spent
    FROM
        payment
    GROUP BY
        customer_id
),
high_spenders AS (
    SELECT
        customer_id,
        total_spent
    FROM
        customer_spending
    WHERE
        total_spent > 150
),
customer_details AS (
    SELECT
        hs.customer_id,
        hs.total_spent,
        c.first_name,
        c.last_name,
        c.email
    FROM
        high_spenders hs
    JOIN
        customer c ON hs.customer_id = c.customer_id
)
SELECT
    customer_id,
    CONCAT(first_name, ' ', last_name) AS customer_name,
    email,
    total_spent
FROM
    customer_details
ORDER BY
    total_spent DESC;

在这个查询中:

  1. customer_spending 计算每个客户的总支出
  2. high_spenders 筛选出总支出 > 150 的客户
  3. customer_details 将高支出客户与客户信息连接
  4. 主查询选择并格式化最终结果

这种结构使逻辑流程清晰且易于跟随。

CTE的可重用性

CTEs的一个强大方面是可以多次引用自己:

WITH monthly_sales AS (
    SELECT
        DATE_TRUNC('month', payment_date) AS month,
        SUM(amount) AS monthly_total
    FROM
        payment
    GROUP BY
        DATE_TRUNC('month', payment_date)
)
SELECT
    m1.month AS current_month,
    m1.monthly_total AS current_sales,
    m2.monthly_total AS previous_month_sales,
    ROUND(((m1.monthly_total - m2.monthly_total) / m2.monthly_total * 100), 2) AS percent_change
FROM
    monthly_sales m1
LEFT JOIN
    monthly_sales m2 ON m1.month = m2.month + INTERVAL '1 month'
WHERE
    m1.month IS NOT NULL
ORDER BY
    m1.month;

在这里,我们引用了 monthly_sales 两次——一次作为 m1,一次作为 m2。如果我们不使用CTE,这将需要两个单独的子查询。

CTE与窗口函数

CTEs与窗口函数完美结合:

WITH ranked_rentals AS (
    SELECT
        customer_id,
        rental_date,
        return_date,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id 
            ORDER BY rental_date DESC
        ) AS rental_rank
    FROM
        rental
),
most_recent_rental AS (
    SELECT
        customer_id,
        rental_date,
        return_date
    FROM
        ranked_rentals
    WHERE
        rental_rank = 1
)
SELECT
    c.customer_id,
    CONCAT(c.first_name, ' ', c.last_name) AS customer_name,
    mrr.rental_date AS last_rental_date,
    DATEDIFF(CURDATE(), mrr.rental_date) AS days_since_rental
FROM
    customer c
LEFT JOIN
    most_recent_rental mrr ON c.customer_id = mrr.customer_id
ORDER BY
    days_since_rental DESC
LIMIT 20;

这个查询:

  1. 使用 ROW_NUMBER() 确定每个客户最近的租赁
  2. 筛选出每个客户的最近租赁
  3. 与客户表连接以显示客户名称并计算自租赁以来的天数

模块化结构使其易于理解和修改。

实际示例:队列分析

CTEs非常适合复杂的分析查询,如队列分析:

WITH customer_first_rental AS (
    SELECT
        customer_id,
        MIN(rental_date) AS first_rental_date,
        DATE_TRUNC('month', MIN(rental_date)) AS cohort_month
    FROM
        rental
    GROUP BY
        customer_id
),
customer_rental_history AS (
    SELECT
        cfr.customer_id,
        cfr.cohort_month,
        DATE_TRUNC('month', r.rental_date) AS rental_month,
        COUNT(*) AS rentals_in_month
    FROM
        customer_first_rental cfr
    JOIN
        rental r ON cfr.customer_id = r.customer_id
    GROUP BY
        cfr.customer_id,
        cfr.cohort_month,
        DATE_TRUNC('month', r.rental_date)
)
SELECT
    cohort_month,
    rental_month,
    COUNT(DISTINCT customer_id) AS customers,
    SUM(rentals_in_month) AS total_rentals
FROM
    customer_rental_history
GROUP BY
    cohort_month,
    rental_month
ORDER BY
    cohort_month,
    rental_month;

这个复杂的分析通过CTEs变得可管理:

  1. 第一个CTE识别每个客户的队列(第一次租赁月份)
  2. 第二个CTE构建所有租赁的历史记录,并包含队列信息
  3. 最终查询汇总以显示队列随时间的表现

优势总结

方面CTE子查询
可读性通过命名结果集高度可读可能变得难以阅读(嵌套结构)
可重用性易于多次引用每次使用必须重新定义
调试可以独立测试每个CTE难以隔离特定逻辑
组织逻辑的自上而下结构线性但有时杂乱
性能相同或更好(依赖优化器)深度嵌套时可能效率较低

关键要点

  • CTEs 是使用 WITH 子句定义的临时命名结果集
  • 可读性:命名CTE使查询自我文档化
  • 多个CTE:将CTE串联在一起,每个CTE基于前一个构建
  • 可重用性:多次引用同一CTE而无需重新定义
  • 无性能惩罚:CTEs不会创建中间存储;它们是查询优化工具
  • 适用于所有情况:CTEs可以包括连接、聚合、窗口函数等
  • 模块化:将复杂查询分解为更易于理解和维护的逻辑部分

CTEs将复杂查询从难以理解的嵌套结构转变为清晰、可读、易于维护的代码。它们是任何数据分析师工具包中的重要工具。

在下一课中,我们将探讨递归CTEs——处理层次数据的强大特性。

尝试解决以下任务,以巩固您在本课中学到的内容。

  1. 平均租赁数量
  2. 最活跃客户