PostgreSQL HAVING 句
HAVING句は、GROUP BY句によってサマリー(集計)された後のデータ行のグループをフィルタリングするために使用されます。
WHERE句がデータをグループ化する前に個別の行をフィルタリングするのに対し、HAVING句は GROUP BY 操作が実行され、集約関数(Aggregate functions)の結果が算出された後に、それらのグループに対してフィルタリングを行います。この違いを理解することは、合計値や平均値などの「集約データ」に基づいた条件判断が必要なクエリを構築する上で極めて重要です。
1. WHERE と HAVING の違いを理解する
WHERE句は、グループ化や集計操作が行われる前に、個別のデータ行に対して作用します。これは、後の GROUP BY 操作に参加できる行をあらかじめ絞り込む役割を果たします。
対照的に、HAVING句は GROUP BY句 の結果に対して作用します。通常、COUNT や SUM などの集約関数に基づいた条件を用いて、グループそのものをフィルタリングします。
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 の組み合わせ
同一のクエリ内で WHERE と HAVING を併用することは非常に一般的な手法です。
まず WHERE句 が個別の行をフィルタリングし、グループ化の前にデータセットの範囲を絞り込みます。次に GROUP BY句 がフィルタリングされた行をグループ化します。最後に HAVING句 が集計条件に基づいてそれらのグループをフィルタリングします。この実行順序は、クエリのパフォーマンス最適化と正確なフィルタリングを実現するために不可欠です。
再び orders テーブルを例にします。「特定のコンテキスト(日付)以降に注文され、かつその合計金額が500を超える顧客」を探す場合、WHERE と HAVING を組み合わせて使用します。
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 (販売日) |
|---|---|---|---|
| 101 | North (北部) | 150.00 | 2023-01-15 |
| 102 | South (南部) | 200.00 | 2023-01-18 |
| 101 | North (北部) | 100.00 | 2023-02-01 |
| 103 | East (東部) | 50.00 | 2023-02-05 |
| 102 | West (西部) | 300.00 | 2023-02-10 |
| 101 | North (北部) | 75.00 | 2023-02-12 |
| 103 | East (東部) | 120.00 | 2023-03-01 |
| 104 | South (南部) | 500.00 | 2023-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(...) > 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; -- フィルタリング後のデータから算出された最小値でグループをフィルタリング出力結果:
| region | min_sales_after_feb |
|---|---|
| West | 300.00 |
| South | 500.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句 で、「売上は低いが販売頻度は高い」という基準に合致する製品グループを抽出します。