PostgreSQL 入門

PostgreSQL HAVING 句

HAVING句は、GROUP BY句によってサマリー(集計)された後のデータ行のグループをフィルタリングするために使用されます。

WHERE句がデータをグループ化する前に個別の行をフィルタリングするのに対し、HAVING句GROUP BY 操作が実行され、集約関数(Aggregate functions)の結果が算出された後に、それらのグループに対してフィルタリングを行います。この違いを理解することは、合計値や平均値などの「集約データ」に基づいた条件判断が必要なクエリを構築する上で極めて重要です。

1. WHERE と HAVING の違いを理解する

WHERE句は、グループ化や集計操作が行われる前に、個別のデータ行に対して作用します。これは、後の GROUP BY 操作に参加できる行をあらかじめ絞り込む役割を果たします。

対照的に、HAVING句GROUP BY句 の結果に対して作用します。通常、COUNTSUM などの集約関数に基づいた条件を用いて、グループそのものをフィルタリングします。

customer_id(顧客ID)、order_amount(注文金額)、order_date(注文日)などのカラムを含む orders(注文)テーブルを例に考えてみましょう。

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;

解説:

  1. GROUP BY region: 各地域ごとにすべての販売記録をグループ化します。
  2. AVG(sales_amount): 地域ごとの平均売上額を計算します。
  3. HAVING AVG(sales_amount) > 100: 計算された平均売上額が100を超えるグループのみを保持します。

出力結果(サンプルデータに基づく):
グループ化後の初期データ(中間状態):

regionaverage_sales
North108.333333333
South350.000000000
East85.000000000
West300.000000000

HAVING フィルタリング後:

regionaverage_sales
North108.333333333
South350.000000000
West300.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;

解説:

  1. GROUP BY region: 地域ごとにグループ化します。
  2. COUNT(DISTINCT product_id): 各地域内でユニークな product_id の数をカウントします。
  3. HAVING COUNT(...) > 2: 2種類より多い製品を販売した地域のみを抽出します。

(※今回のサンプルデータでは条件を満たす地域がないため、結果セットは空になります。)

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; -- フィルタリング後のデータから算出された最小値でグループをフィルタリング

出力結果:

regionmin_sales_after_feb
West300.00
South500.00

4. 実際のビジネス活用シーン

グローバルなECサイトが顧客の注文を追跡する場合を想定しましょう。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 を使用して両方の条件を満たす顧客のみを絞り込みます。

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句 で、「売上は低いが販売頻度は高い」という基準に合致する製品グループを抽出します。