PostgreSQL 教程

PostgreSQL HAVING 子句

HAVING 子句用于过滤由 GROUP BY 子句汇总后的数据行分组。

虽然 WHERE 子句可以在数据被分组之前过滤单个数据行,但 HAVING 是在执行了 GROUP BY 操作并且聚合函数计算出结果之后,再去过滤这些分组的。这种区别对于构建需要基于“聚合数据”(如总和、平均值等)进行条件判断的查询至关重要。

1. 理解 WHERE 与 HAVING 的区别

WHERE 子句在发生任何分组或聚合操作之前作用于单个数据行。它负责筛选出哪些行能够参与后续的 GROUP BY 操作。

相反,HAVING 子句作用于 GROUP BY 子句的结果,它通常基于聚合函数(如 COUNT、SUM 等)的条件来过滤这些分组本身。

考虑一个名为 orders(订单)的表,包含 customer_id(客户 ID)、order_amount(订单金额)和 order_date(订单日期)等列。

1.1 WHERE 子句示例

如果在按客户分组之前,你想找到所有金额大于 100 的订单,你会使用 WHERE

SELECT
    customer_id,
    order_amount
FROM
    orders
WHERE
    order_amount > 100; -- 过滤 order_amount 大于 100 的独立数据行

此查询返回金额超过 100 的单个订单的详细信息。

1.2 HAVING 子句示例

如果你想找出订单金额大于 500 的客户,你需要首先按 customer_id 分组,然后对 SUM(order_amount) 应用条件:

SELECT
    customer_id,
    SUM(order_amount) AS total_spent
FROM
    orders
GROUP BY
    customer_id
HAVING
    SUM(order_amount) > 500; -- 过滤订单总和 (total_spent) 大于 500 的分组

此查询可以识别出跨越所有订单、总消费金额超过 500 的客户。

2. 结合使用 WHERE 和 HAVING

在同一个查询中同时使用 WHEREHAVING 是非常常见的做法。

WHERE 子句首先过滤单个数据行,在分组前缩小数据集的范围。然后,GROUP BY 子句对过滤后的行进行分组。最后,HAVING 子句根据聚合条件过滤这些分组。这种操作顺序对于优化查询性能和实现精确过滤必不可少。

再次考虑 orders 表。如果你想找出在特定日期之后下单、且这些订单的金额大于 500 的客户,你需要结合使用 WHEREHAVING

SELECT
    customer_id,
    SUM(order_amount) AS total_spent
FROM
    orders
WHERE
    order_date >= '2023-01-01' -- 首先过滤出 2023-01-01 及之后下单的独立订单
GROUP BY
    customer_id
HAVING
    SUM(order_amount) > 500; -- 然后过滤出(这些过滤后的订单)总消费金额大于 500 的客户分组

这个查询首先将订单缩小到 2023 年或之后下达的订单,然后按客户对这些剩余订单进行分组,最后仅筛选出那些从 2023 年及以后的订单中总消费超过 500 的客户。

3. HAVING 子句的实际案例

让我们使用一个假设的 sales(销售)表来演示 HAVING 子句的更多实际案例:

product_id (产品ID)region (地区)sales_amount (销售额)sales_date (销售日期)
101North (北部)150.002023-01-15
102South (南部)200.002023-01-18
101North (北部)100.002023-02-01
103East (东部)50.002023-02-05
102West (西部)300.002023-02-10
101North (北部)75.002023-02-12
103East (东部)120.002023-03-01
104South (南部)500.002023-03-05

3.1 示例 1:按平均销售额过滤

查找平均 sales_amount(销售额)大于 100 的地区:

SELECT
    region,
    AVG(sales_amount) AS average_sales
FROM
    sales
GROUP BY
    region
HAVING
    AVG(sales_amount) > 100;

解析:

  • GROUP BY region: 按各自的地区对所有销售记录进行分组。
  • AVG(sales_amount): 计算每个地区的平均销售额。
  • HAVING AVG(sales_amount) > 100: 过滤这些分组,只保留计算出的平均销售额超过 100 的地区。

输出结果(基于示例数据):

分组后的初始数据:

| region | average_sales |
| :----- | :------------ |
| North  | 108.333333333 |
| South  | 350.000000000 |
| East   | 85.000000000  |
| West   | 300.000000000 |

经过 HAVING 过滤后:

| region | average_sales |
| :----- | :------------ |
| North  | 108.333333333 |
| South  | 350.000000000 |
| West   | 300.000000000 |

3.2 示例 2:按产品种类数量过滤

查找销售了超过 2 种不同产品的地区:

SELECT
    region,
    COUNT(DISTINCT product_id) AS distinct_products_sold
FROM
    sales
GROUP BY
    region
HAVING
    COUNT(DISTINCT product_id) > 2;

解析:

  • GROUP BY region: 按地区分组。
  • COUNT(DISTINCT product_id): 计算每个地区内有多少个唯一的 product_id 值。
  • HAVING COUNT(DISTINCT product_id) > 2: 过滤分组,仅保留销售了两种以上不同产品的地区。

输出结果(基于示例数据):

分组后的初始数据:

| region | distinct_products_sold |
| :----- | :--------------------- |
| North  | 1                      |
| South  | 2                      |
| East   | 1                      |
| West   | 1                      |

经过 HAVING 过滤后:(由于没有地区满足条件,结果集将为)。

3.3 示例 3:结合 WHERE 按最低销售额过滤

查找在 2023 年 2 月 1 日之后发生的销售中,最低 sales_amount 大于 100 的地区:

SELECT
    region,
    MIN(sales_amount) AS min_sales_after_feb
FROM
    sales
WHERE
    sales_date > '2023-02-01' -- 在分组前过滤独立的数据行
GROUP BY
    region
HAVING
    MIN(sales_amount) > 100; -- 根据过滤后数据行计算出的最低销售额来过滤分组

输出结果(基于示例数据):

| region | min_sales_after_feb |
| :----- | :------------------ |
| West   | 300.00              |
| South  | 500.00              |

4. 真实业务应用场景

设想一家全球电商公司需要追踪客户订单。他们可能会使用 HAVING 子句来识别高价值客户群体或表现不佳的产品类别。

4.1 识别高价值客户

营销团队想要为满足以下条件的客户开展特别促销活动:至少下了 5 个订单,并且平均订单价值大于 $200。

SELECT
    c.customer_id,
    c.customer_name,
    COUNT(o.order_id) AS total_orders,
    AVG(o.order_total) AS average_order_value
FROM
    customers c
JOIN
    orders o ON c.customer_id = o.customer_id
GROUP BY
    c.customer_id, c.customer_name
HAVING
    COUNT(o.order_id) >= 5 AND AVG(o.order_total) > 200;

该查询首先将所有订单按客户进行分组,计算每位客户的总订单数和平均订单价值,然后使用 HAVING 筛选出同时满足这两个条件的客户:订单数至少为 5,且平均订单价值超过 $200。

4.2 检测表现不佳的产品

产品经理想要识别在上个季度总收入低于 $1000 的产品,前提是这些产品至少被售出了 10 次。这可能意味着该产品虽然销售频繁,但单件利润极低。

SELECT
    p.product_id,
    p.product_name,
    SUM(li.quantity * li.unit_price) AS total_revenue,
    COUNT(li.order_id) AS total_sales_transactions
FROM
    products p
JOIN
    order_line_items li ON p.product_id = li.product_id
JOIN
    orders o ON li.order_id = o.order_id
WHERE
    o.order_date >= '2023-10-01' AND o.order_date < '2024-01-01' -- 过滤出上个季度的数据
GROUP BY
    p.product_id, p.product_name
HAVING
    SUM(li.quantity * li.unit_price) < 1000 AND COUNT(li.order_id) >= 10;

在这里,WHERE 子句首先将分析范围限制在特定季度内的订单。然后,按产品对数据进行分组,并用聚合函数计算总收入和销售交易次数。最后,HAVING 子句过滤这些产品分组,找出符合表现不佳标准的产品:在那个季度内总收入低并且销售频率高。