SQL HAVINGの使い方:集計結果を思い通りに絞り込む方法

はい、承知いたしました。SQLのHAVING句について、集計結果を思い通りに絞り込む方法に焦点を当て、約5000語の詳細な記事を作成します。


SQL HAVINGの使い方:集計結果を思い通りに絞り込む方法

はじめに

リレーショナルデータベースを操作する上で、SQLは最も基本的な言語です。データの抽出、挿入、更新、削除など、様々な操作を行うことができます。中でも、データの分析において非常に強力な機能を提供するのが、集計関数とそれらを組み合わせたクエリです。そして、集計結果に対して特定の条件を適用し、さらにデータを絞り込む際に不可欠となるのが HAVING 句です。

この記事では、SQLの HAVING 句に焦点を当て、その基本的な使い方から応用、さらには WHERE 句との違い、よくある落とし穴と解決策まで、詳細かつ網羅的に解説します。この記事を読み終える頃には、HAVING 句を自在に操り、集計されたデータから真に必要な情報だけを効率的に抽出できるようになっているでしょう。

HAVING句とは何か?なぜ必要なのか?

データベースに蓄積された生データは、そのままでは分析に不向きな場合が多くあります。例えば、「全商品の売上合計はいくらか?」「各顧客の購入回数は?」「部署ごとの平均給与は?」といった問いに答えるためには、データを集計する必要があります。SQLでは、COUNT, SUM, AVG, MIN, MAX といった集計関数(または集約関数)と GROUP BY 句を組み合わせてこれらの集計を行います。

しかし、集計結果に対してさらに条件を適用したい場合があります。例えば、

  • 「売上合計が100万円以上の商品カテゴリを知りたい」
  • 「購入回数が5回以上の顧客リストが欲しい」
  • 「平均給与が50万円以下の部署を特定したい」

といったケースです。このような「集計された値」に対する条件指定を行うのが HAVING 句の役割です。

WHERE 句も条件指定に使われますが、WHERE 句は集計が行われる前に、個々の行に対して条件を適用します。一方、HAVING 句は集計が行われた後に、GROUP BY 句によって作成されたグループ(集計結果)に対して条件を適用します。この違いが、HAVING 句の存在意義であり、WHERE 句だけでは実現できない高度な絞り込みを可能にします。

HAVING句の基本

構文

HAVING 句は、通常 GROUP BY 句の後に記述します。基本的な構文は以下のようになります。

sql
SELECT
カラム名,
集計関数(カラム名)
FROM
テーブル名
WHERE
個別行に対する条件 -- オプション
GROUP BY
グループ化するカラム名
HAVING
集計結果に対する条件 -- ここでHAVINGを使う
ORDER BY
カラム名または集計結果 -- オプション
LIMIT
件数 -- オプション
;

重要なのは、HAVING 句が GROUP BY 句の後に位置することです。これは、処理の順序として、まず GROUP BY でデータをグループ化し、集計関数で各グループの集計値を計算した後でなければ、その集計値に対する条件を評価できないためです。

簡単な例

仮に、以下のような orders テーブルがあるとします。

order_id customer_id order_date amount
1 101 2023-01-05 1500
2 102 2023-01-05 2500
3 101 2023-01-06 3000
4 103 2023-01-06 1000
5 102 2023-01-07 4000
6 101 2023-01-07 2000

このテーブルを使って、各顧客の注文回数を数え、その回数が2回以上の顧客だけを抽出したいとします。

まず、顧客ごとの注文回数を数えるには、GROUP BY customer_idCOUNT(*) を使用します。

sql
SELECT
customer_id,
COUNT(*) AS total_orders
FROM
orders
GROUP BY
customer_id;

このクエリの実行結果は以下のようになります。

customer_id total_orders
101 3
102 2
103 1

次に、「注文回数が2回以上」という条件を適用したいわけですが、この条件は total_orders という集計された値(COUNT(*) の結果)に対するものです。したがって、WHERE 句ではなく HAVING 句を使います。

sql
SELECT
customer_id,
COUNT(*) AS total_orders
FROM
orders
GROUP BY
customer_id
HAVING
COUNT(*) >= 2; -- または HAVING total_orders >= 2 (多くのRDBMSで可)

このクエリの実行結果は以下のようになります。

customer_id total_orders
101 3
102 2

このように、HAVING 句を使うことで、集計結果に対して簡単に条件を適用し、必要なデータを絞り込むことができます。

HAVING句とGROUP BY句の関係

HAVING 句は、原則として GROUP BY 句とセットで使用されます。なぜなら、HAVING 句の条件は通常、集計関数によって計算された値に対して適用されるからです。集計関数は、GROUP BY で指定されたグループごとに値を計算します。もし GROUP BY 句がない場合、テーブル全体が一つのグループとして扱われ、HAVING 句はその単一のグループの集計結果に対して条件を適用します。

ただし、GROUP BY 句がない場合でも HAVING 句を使用することは文法上可能ですが、その場合はテーブル全体が単一のグループとして扱われるため、あまり一般的ではありません。例えば、

sql
SELECT
COUNT(*) AS total_count
FROM
orders
HAVING
COUNT(*) > 0; -- テーブル全体の件数が0より大きい場合のみ表示

このクエリは、orders テーブルの総件数が0より大きい場合にのみ、その総件数を表示します。これは WHERE COUNT(*) > 0 とは異なり、WHERE は集計前の行に適用するため、WHERE COUNT(*) > 0 という構文自体がエラーになります(集計関数は WHERE 句で直接使えない)。

HAVING句とWHERE句の決定的な違い

HAVING 句と WHERE 句はどちらも条件を指定してデータを絞り込むために使われますが、適用されるタイミングと対象が全く異なります。この違いを理解することは、SQLクエリを正しく記述し、意図した結果を得るために非常に重要です。

適用されるタイミングと対象

  • WHERE 句:

    • タイミング: FROM 句で指定されたテーブルからデータを読み込み、GROUP BY によるグループ化や集計が行われる
    • 対象: テーブルの個々の行
    • 使える条件: 個々の行が持つカラムの値に対する条件。集計関数は使用できません。
  • HAVING 句:

    • タイミング: GROUP BY 句によってデータがグループ化され、SELECT 句や HAVING 句で使用される集計関数によって集計値が計算された
    • 対象: GROUP BY 句によって作成された各グループ(つまり、集計結果)。
    • 使える条件: 集計関数によって計算された値や、GROUP BY 句で指定されたカラムの値に対する条件。集計関数を条件に含めるのが一般的です。

処理順序

SQLクエリの一般的な論理的な処理順序は以下のようになります(実際の実行計画はオプティマイザによって最適化されますが、概念的な順序はこれです)。

  1. FROM: データを取得するテーブルを指定。
  2. JOIN: 複数のテーブルを結合。
  3. WHERE: 個々の行に対して条件を適用し、フィルタリング。ここで不要な行が先に除外されるため、後続の処理(GROUP BY, 集計)の対象データ量を減らすことができる
  4. GROUP BY: WHERE 句で絞り込まれた行を、指定されたカラムの値に基づいてグループ化。
  5. 集計関数の計算: 各グループに対して、SELECT 句や HAVING 句で使用されている集計関数が計算される。
  6. HAVING: グループ化され、集計関数が計算された結果(各グループ)に対して条件を適用し、フィルタリング。
  7. SELECT: HAVING 句で絞り込まれたグループから、表示するカラムや集計結果を選択。
  8. DISTINCT: SELECT 句で選択された行から重複を排除。
  9. ORDER BY: 最終的な結果を指定されたカラムで並べ替え。
  10. LIMIT / OFFSET: 表示する行数を制限。

この順序からわかるように、WHERE 句は集計の前にデータを減らす役割を果たし、HAVING 句は集計の後にグループを減らす役割を果たします。

具体的な例で違いを理解する

再び orders テーブルを使用します。

order_id customer_id order_date amount
1 101 2023-01-05 1500
2 102 2023-01-05 2500
3 101 2023-01-06 3000
4 103 2023-01-06 1000
5 102 2023-01-07 4000
6 101 2023-01-07 2000

例1: WHERE句のみを使用

例えば、「2023年1月6日以降の注文」に絞って、その注文の合計金額を知りたい場合。これは個々の注文日に対する条件なので、WHERE 句を使います。

sql
SELECT
SUM(amount) AS total_amount
FROM
orders
WHERE
order_date >= '2023-01-06';

処理の流れ:
1. orders テーブルから全行を取得。
2. WHERE order_date >= '2023-01-06' で、以下の行に絞り込む。
* order_id: 3, order_date: 2023-01-06, amount: 3000
* order_id: 4, order_date: 2023-01-06, amount: 1000
* order_id: 5, order_date: 2023-01-07, amount: 4000
* order_id: 6, order_date: 2023-01-07, amount: 2000
3. これらの行に対して SUM(amount) を計算する。

結果:
| total_amount |
| :———– |
| 10000 |

例2: HAVING句のみを使用

例えば、「各顧客の注文合計金額が5000円以上の顧客」を抽出したい場合。これは集計された値(顧客ごとの注文合計金額)に対する条件なので、HAVING 句を使います。

まず、顧客ごとの注文合計金額を計算します。

sql
SELECT
customer_id,
SUM(amount) AS total_spent
FROM
orders
GROUP BY
customer_id;

結果:
| customer_id | total_spent |
| :———- | :———- |
| 101 | 6500 |
| 102 | 6500 |
| 103 | 1000 |

次に、「total_spent が5000円以上の顧客」という条件を適用します。

sql
SELECT
customer_id,
SUM(amount) AS total_spent
FROM
orders
GROUP BY
customer_id
HAVING
SUM(amount) >= 5000; -- または HAVING total_spent >= 5000

処理の流れ:
1. orders テーブルから全行を取得。
2. GROUP BY customer_id で顧客ごとにグループ化。
3. 各グループに対して SUM(amount) を計算。
* customer_id 101: 1500 + 3000 + 2000 = 6500
* customer_id 102: 2500 + 4000 = 6500
* customer_id 103: 1000 = 1000
4. HAVING SUM(amount) >= 5000 で、以下のグループに絞り込む。
* customer_id 101 (合計 6500)
* customer_id 102 (合計 6500)
5. 絞り込まれたグループから customer_idSUM(amount) を選択。

結果:
| customer_id | total_spent |
| :———- | :———- |
| 101 | 6500 |
| 102 | 6500 |

例3: WHERE句とHAVING句を組み合わせる

例えば、「2023年1月6日以降の注文」に絞り込み、さらにその中で「顧客ごとの注文合計金額が5000円以上の顧客」を抽出したい場合。

sql
SELECT
customer_id,
SUM(amount) AS total_spent
FROM
orders
WHERE
order_date >= '2023-01-06' -- 集計前の個別行に対する条件
GROUP BY
customer_id
HAVING
SUM(amount) >= 5000; -- 集計後のグループに対する条件

処理の流れ:
1. orders テーブルから全行を取得。
2. WHERE order_date >= '2023-01-06' で、以下の行に絞り込む(例1と同じ)。
* order_id: 3, order_date: 2023-01-06, amount: 3000
* order_id: 4, order_date: 2023-01-06, amount: 1000
* order_id: 5, order_date: 2023-01-07, amount: 4000
* order_id: 6, order_date: 2023-01-07, amount: 2000
3. 絞り込まれたこれらの行に対して GROUP BY customer_id でグループ化。
* customer_id 101: 行 (3000), (2000) -> グループ化
* customer_id 102: 行 (4000) -> グループ化
* customer_id 103: 行 (1000) -> グループ化
4. 各グループに対して SUM(amount) を計算。
* customer_id 101: 3000 + 2000 = 5000
* customer_id 102: 4000 = 4000
* customer_id 103: 1000 = 1000
5. HAVING SUM(amount) >= 5000 で、以下のグループに絞り込む。
* customer_id 101 (合計 5000)
6. 絞り込まれたグループから customer_idSUM(amount) を選択。

結果:
| customer_id | total_spent |
| :———- | :———- |
| 101 | 5000 |

この例からわかるように、WHERE 句はまず個々の注文を集計対象から外し、HAVING 句はその後に残った注文を顧客ごとに集計した結果をフィルタリングしています。

パフォーマンスに関する考慮:

WHERE 句は集計処理の前に実行されるため、処理対象となる行数を減らすことができます。これにより、GROUP BY や集計関数の計算にかかるコストを削減できる可能性があります。一方、HAVING 句は集計後のグループに適用されるため、WHERE 句でできるフィルタリングを HAVING 句で行うと、無駄な集計が発生しパフォーマンスが悪化する可能性があります。

したがって、可能な限り WHERE 句で絞り込みを行い、集計後の結果に対する条件は HAVING 句で指定するのが、効率の良いクエリを書くための鉄則です。

HAVING句で使える条件式

HAVING 句では、WHERE 句と同様に様々な条件式を使用できます。ただし、その条件が評価されるタイミングが集計後であるため、主に集計関数を含む条件が使われます。

集計関数を含む条件

これが HAVING 句の最も典型的な使い方です。COUNT(), SUM(), AVG(), MIN(), MAX() といった集計関数を使って計算された値と、リテラル(定数)、他の集計関数、あるいは GROUP BY 句で指定されたカラムの値を比較します。

sql
HAVING COUNT(*) > 10
HAVING SUM(amount) BETWEEN 10000 AND 50000
HAVING AVG(price) < 1000
HAVING MIN(quantity) <= 0
HAVING MAX(salary) > (SELECT AVG(salary) FROM employees) -- サブクエリとの連携も可能

集計関数を含まない条件

HAVING 句では、GROUP BY 句で指定されたカラムに対する条件を指定することも可能です。これは、GROUP BY 句によってグループ化された結果が、そのグループ化キーのカラム値を持つためです。

sql
HAVING customer_id = 101 -- GROUP BY customer_id の場合
HAVING category IN ('Electronics', 'Books') -- GROUP BY category の場合

ただし、集計関数を含まない条件は、多くの場合 WHERE 句で代替可能です。そして、前述のように WHERE 句で先に絞り込んだ方がパフォーマンス上有利になることが多いです。したがって、HAVING 句で集計関数を含まない条件を使うのは、特別な理由がある場合(例: サブクエリの結果に依存する場合で、そのサブクエリが集計結果に依存するなど)を除いて、あまり推奨されません。

複数の条件の組み合わせ

WHERE 句と同様に、HAVING 句でも AND, OR, NOT などの論理演算子を使って複数の条件を組み合わせることができます。

sql
HAVING COUNT(*) >= 5 AND SUM(amount) > 10000
HAVING AVG(score) < 60 OR MAX(score) < 80
HAVING NOT (MIN(price) = MAX(price)) -- 価格が全て同じではないグループ

様々な集計関数とHAVING句の組み合わせ

ここでは、主要な集計関数と HAVING 句を組み合わせた具体的な例をさらに見ていきます。例として、以下のような sales テーブルを使用します。

sale_id product_id category price quantity sale_date
1 1 Electronics 50000 1 2023-03-01
2 2 Books 2000 3 2023-03-01
3 1 Electronics 50000 1 2023-03-02
4 3 Electronics 10000 2 2023-03-02
5 4 Clothing 8000 1 2023-03-03
6 2 Books 2000 2 2023-03-03
7 1 Electronics 50000 1 2023-03-04
8 5 Clothing 12000 1 2023-03-04
9 3 Electronics 10000 3 2023-03-05
10 2 Books 2000 1 2023-03-05

COUNT() と HAVING

COUNT() はグループ内の行数を数えます。HAVING COUNT(...) は、グループの要素数が特定の条件を満たすものだけを抽出したい場合に使います。

例1: 3回以上販売された商品

sql
SELECT
product_id,
COUNT(*) AS sales_count
FROM
sales
GROUP BY
product_id
HAVING
COUNT(*) >= 3; -- 販売回数が3回以上の商品を絞り込む

結果:
| product_id | sales_count |
| :——— | :———- |
| 1 | 3 |
| 2 | 3 |

例2: 複数回販売されたカテゴリ

sql
SELECT
category,
COUNT(*) AS category_sales_count
FROM
sales
GROUP BY
category
HAVING
COUNT(*) > 1; -- そのカテゴリに属する販売記録が複数あるカテゴリを絞り込む

結果:
| category | category_sales_count |
| :———- | :——————- |
| Electronics | 4 |
| Books | 3 |
| Clothing | 2 |

SUM() と HAVING

SUM() はグループ内の数値カラムの合計を計算します。HAVING SUM(...) は、グループの合計値が特定の条件を満たすものだけを抽出したい場合に使います。

例3: 合計売上金額が10万円以上のカテゴリ

各販売の金額は price * quantity で計算できます。SUM(price * quantity) でカテゴリごとの合計売上金額を計算し、その合計が10万円以上であるカテゴリを抽出します。

sql
SELECT
category,
SUM(price * quantity) AS total_revenue
FROM
sales
GROUP BY
category
HAVING
SUM(price * quantity) >= 100000; -- 合計売上金額が10万円以上のカテゴリを絞り込む

カテゴリごとの合計売上:
* Electronics: (500001) + (500001) + (100002) + (500001) + (100003) = 50000 + 50000 + 20000 + 50000 + 30000 = 200000
* Books: (2000
3) + (20002) + (20001) = 6000 + 4000 + 2000 = 12000
* Clothing: (80001) + (120001) = 8000 + 12000 = 20000

結果:
| category | total_revenue |
| :———- | :———— |
| Electronics | 200000 |

例4: 1日の合計販売数量が5個以上の特定日付

日付ごとに販売記録をグループ化し、その日の合計販売数量 (SUM(quantity)) が5個以上の日付を抽出します。

sql
SELECT
sale_date,
SUM(quantity) AS daily_total_quantity
FROM
sales
GROUP BY
sale_date
HAVING
SUM(quantity) >= 5; -- 1日の合計販売数量が5個以上の日付を絞り込む

日付ごとの合計販売数量:
* 2023-03-01: 1 + 3 = 4
* 2023-03-02: 1 + 2 = 3
* 2023-03-03: 1 + 2 = 3
* 2023-03-04: 1 + 1 = 2
* 2023-03-05: 3 + 1 = 4

(注:このデータ例では >= 5 の条件を満たす日付はありません。もし >= 3 であれば2023-03-01, 2023-03-02, 2023-03-03, 2023-03-05 が表示されます)

もし >= 3 に条件を変えると:
sql
SELECT
sale_date,
SUM(quantity) AS daily_total_quantity
FROM
sales
GROUP BY
sale_date
HAVING
SUM(quantity) >= 3;

結果:
| sale_date | daily_total_quantity |
| :——— | :——————- |
| 2023-03-01 | 4 |
| 2023-03-02 | 3 |
| 2023-03-03 | 3 |
| 2023-03-05 | 4 |

AVG() と HAVING

AVG() はグループ内の数値カラムの平均値を計算します。HAVING AVG(...) は、グループの平均値が特定の条件を満たすものだけを抽出したい場合に使います。

例5: 平均価格が1万円以上のカテゴリ

sql
SELECT
category,
AVG(price) AS average_price
FROM
sales
GROUP BY
category
HAVING
AVG(price) >= 10000; -- 平均価格が1万円以上のカテゴリを絞り込む

カテゴリごとの平均価格:
* Electronics: (50000 + 50000 + 10000 + 50000 + 10000) / 5 = 170000 / 5 = 34000
* Books: (2000 + 2000 + 2000) / 3 = 6000 / 3 = 2000
* Clothing: (8000 + 12000) / 2 = 20000 / 2 = 10000

結果:
| category | average_price |
| :———- | :———— |
| Electronics | 34000 |
| Clothing | 10000 |

MIN() / MAX() と HAVING

MIN()MAX() は、それぞれグループ内の最小値と最大値を計算します。HAVING MIN(...)HAVING MAX(...) は、グループの最小値または最大値が特定の条件を満たすものだけを抽出したい場合に使います。

例6: 最小販売価格が5000円以上のカテゴリ

sql
SELECT
category,
MIN(price) AS min_price
FROM
sales
GROUP BY
category
HAVING
MIN(price) >= 5000; -- 最小販売価格が5000円以上のカテゴリを絞り込む

カテゴリごとの最小価格:
* Electronics: MIN(50000, 50000, 10000, 50000, 10000) = 10000
* Books: MIN(2000, 2000, 2000) = 2000
* Clothing: MIN(8000, 12000) = 8000

結果:
| category | min_price |
| :———- | :——– |
| Electronics | 10000 |
| Clothing | 8000 |

例7: 最大販売数量が3個以上の商品

sql
SELECT
product_id,
MAX(quantity) AS max_quantity_per_sale
FROM
sales
GROUP BY
product_id
HAVING
MAX(quantity) >= 3; -- 1回の販売で最大3個以上売れたことがある商品を絞り込む

商品ごとの最大販売数量:
* product_id 1: MAX(1, 1, 1) = 1
* product_id 2: MAX(3, 2, 1) = 3
* product_id 3: MAX(2, 3) = 3
* product_id 4: MAX(1) = 1
* product_id 5: MAX(1) = 1

結果:
| product_id | max_quantity_per_sale |
| :——— | :——————– |
| 2 | 3 |
| 3 | 3 |

HAVING句の応用例

複数の集計関数をHAVING句で使用する

HAVING 句では、複数の集計関数を組み合わせて条件を指定できます。

例8: 複数回販売され、かつ合計売上金額が一定額以上の商品

sql
SELECT
product_id,
COUNT(*) AS sales_count,
SUM(price * quantity) AS total_revenue
FROM
sales
GROUP BY
product_id
HAVING
COUNT(*) >= 2 AND SUM(price * quantity) >= 50000; -- 販売回数が2回以上 かつ 合計売上金額が5万円以上の商品を絞り込む

商品ごとの集計:
* product_id 1: COUNT=3, SUM=200000
* product_id 2: COUNT=3, SUM=12000
* product_id 3: COUNT=2, SUM=50000
* product_id 4: COUNT=1, SUM=8000
* product_id 5: COUNT=1, SUM=12000

条件 COUNT(*) >= 2 AND SUM(price * quantity) >= 50000 を満たすのは product_id 1 と 3 です。

結果:
| product_id | sales_count | total_revenue |
| :——— | :———- | :———— |
| 1 | 3 | 200000 |
| 3 | 2 | 50000 |

サブクエリとHAVING句の連携

HAVING 句の条件として、サブクエリの結果を使用することも可能です。特に、全体の平均値や最小値などを基準にして各グループをフィルタリングしたい場合に便利です。

例9: カテゴリごとの平均価格が、全商品の平均価格より高いカテゴリ

まず、全商品の平均価格を計算するサブクエリを用意します。

sql
SELECT AVG(price) FROM sales; -- 全商品の平均価格

結果: 34000 / 5 = 6800 (ユニークなprice値で割るのではなく、全行のpriceの平均: (500004 + 20003 + 100002 + 80001 + 12000*1) / 10 は間違い。単純にprice列の全行の平均: (50000+2000+50000+10000+8000+2000+50000+12000+10000+2000)/10 = 194000/10 = 19400 です。)

正しい全商品の平均価格は (50000+2000+50000+10000+8000+2000+50000+12000+10000+2000) / 10 = 19400 です。

次に、このサブクエリの結果を HAVING 句で使用します。

sql
SELECT
category,
AVG(price) AS category_average_price
FROM
sales
GROUP BY
category
HAVING
AVG(price) > (SELECT AVG(price) FROM sales); -- カテゴリの平均価格が全商品の平均価格より高い

カテゴリごとの平均価格(再掲):
* Electronics: 34000
* Books: 2000
* Clothing: 10000

全商品の平均価格: 19400

条件 AVG(price) > 19400 を満たすのは Electronics だけです。

結果:
| category | category_average_price |
| :———- | :——————— |
| Electronics | 34000 |

WHERE句とHAVING句の組み合わせの再確認と使い分け

前述の通り、WHERE 句と HAVING 句は適用タイミングが異なります。この違いを利用して、クエリを効率的に記述できます。

例10: 2023年3月に入ってから、合計売上金額が3万円以上のカテゴリ

この要件は二つの条件を含みます。
1. 「2023年3月の販売」: 個々の販売記録に対する条件 (sale_date が ‘2023-03-01’ から ‘2023-03-31’ の間)。これは集計前の条件なので WHERE 句を使います。
2. 「合計売上金額が3万円以上」: カテゴリごとの集計値 (SUM(price * quantity)) に対する条件。これは集計後の条件なので HAVING 句を使います。

sql
SELECT
category,
SUM(price * quantity) AS total_revenue
FROM
sales
WHERE
sale_date >= '2023-03-01' AND sale_date <= '2023-03-31' -- または sale_date BETWEEN '2023-03-01' AND '2023-03-31'
GROUP BY
category
HAVING
SUM(price * quantity) >= 30000; -- 3月の合計売上金額が3万円以上のカテゴリを絞り込む

処理の流れ:
1. FROM sales
2. WHERE sale_date BETWEEN '2023-03-01' AND '2023-03-31': 3月の販売記録のみに絞り込む。この例のデータは全て3月なので、全行が残ります。
3. GROUP BY category: カテゴリごとにグループ化。
4. 集計関数 SUM(price * quantity) を計算。
* Electronics: 200000
* Books: 12000
* Clothing: 20000
5. HAVING SUM(price * quantity) >= 30000: 合計金額が3万円以上のグループに絞り込む。Electronics (200000) が該当します。
6. 結果を選択。

結果:
| category | total_revenue |
| :———- | :———— |
| Electronics | 200000 |

もしこのクエリで WHERE sale_date >= '2023-03-01' AND sale_date <= '2023-03-31'HAVING 句に移動させた場合(多くの場合エラーになるか、意図しない結果になります)、または WHERE 句だけで SUM を使おうとした場合(エラー)、正しい結果は得られません。

よくある落とし穴と解決策

1. HAVING句で集計関数を使わない条件を指定してしまう

HAVING 句の条件が集計関数を含まない場合、多くはその条件を WHERE 句に移動させるべきです。

間違いやすい例:

sql
SELECT
customer_id,
COUNT(*) AS total_orders
FROM
orders
GROUP BY
customer_id
HAVING
customer_id = 101; -- この条件はWHERE句に移すべき

このクエリは多くのRDBMSで実行可能ですが、非効率的です。まず全ての顧客の注文回数を数え、その後で customer_id = 101 のグループだけを選び出しています。

正しい書き方:

sql
SELECT
customer_id,
COUNT(*) AS total_orders
FROM
orders
WHERE
customer_id = 101 -- 集計前に顧客101の注文だけを抽出
GROUP BY
customer_id;

こちらの方が効率的です。まず顧客101の注文のみを抽出し、その後にグループ化と集計を行います。処理対象のデータ量が減るため、特にテーブルが大きい場合にパフォーマンスの差が出ます。

例外: サブクエリの結果に依存するなど、どうしても集計後のグループ属性でフィルタリングしたい場合は HAVING 句に記述することもありますが、基本的には WHERE 優先で考えましょう。

2. SELECT句にない集計関数をHAVING句で使う

HAVING 句では、SELECT 句に記述していない集計関数やカラムも条件に使用できます。しかし、可読性のために、HAVING 句で使用する集計関数は SELECT 句にも含めて表示することが多いです。

sql
-- HAVING句で使っている集計関数をSELECT句にも表示する例
SELECT
product_id,
COUNT(*) AS sales_count,
SUM(price * quantity) AS total_revenue -- HAVING句で使っている
FROM
sales
GROUP BY
product_id
HAVING
SUM(price * quantity) >= 50000;

このように、HAVING 句の条件に含まれる集計値を SELECT 句で確認できるようにしておくと、クエリの結果と条件の関係が分かりやすくなります。

3. NULL値の扱い

集計関数は通常、NULL値を無視して計算します。しかし、HAVING 句で NULL 値を含むカラムを集計したり、NULL 値との比較を行ったりする際には注意が必要です。= NULL!= NULL は常に false になるため、IS NULLIS NOT NULL を使用します。

例えば、「注文合計金額がNULLではない顧客」を抽出したい場合(全ての顧客に注文があるテーブルでは意味がありませんが、外部結合などで発生しうるケース)、HAVING SUM(amount) IS NOT NULL のように記述します。

実践的なシナリオ

いくつかのより複雑で実践的なシナリオで HAVING 句の使い方を見てみましょう。

シナリオ1: eコマース – 特定の商品カテゴリで、個別の商品の販売回数が一定回数以上のもの

例えば、「Electronics」カテゴリに絞り込み、その中で個別の商品 (product_id) が2回以上販売されているものだけを抽出したい場合。

sql
SELECT
category,
product_id,
COUNT(*) AS sales_count
FROM
sales
WHERE
category = 'Electronics' -- まずElectronicsカテゴリの販売だけに絞る
GROUP BY
category, product_id -- カテゴリと商品IDでグループ化
HAVING
COUNT(*) >= 2; -- 各商品の販売回数が2回以上であることを条件とする

処理の流れ:
1. sales テーブルからデータを取得。
2. WHERE category = 'Electronics' で、Electronicsカテゴリの行に絞り込む。
* sale_id: 1, product_id: 1, category: Electronics, price: 50000, quantity: 1, sale_date: 2023-03-01
* sale_id: 3, product_id: 1, category: Electronics, price: 50000, quantity: 1, sale_date: 2023-03-02
* sale_id: 4, product_id: 3, category: Electronics, price: 10000, quantity: 2, sale_date: 2023-03-02
* sale_id: 7, product_id: 1, category: Electronics, price: 50000, quantity: 1, sale_date: 2023-03-04
* sale_id: 9, product_id: 3, category: Electronics, price: 10000, quantity: 3, sale_date: 2023-03-05
3. これらの行を GROUP BY category, product_id でグループ化。
* (Electronics, product_id 1): 3回販売
* (Electronics, product_id 3): 2回販売
4. HAVING COUNT(*) >= 2 で、販売回数が2回以上のグループ(商品)に絞り込む。product_id 1 (3回) と product_id 3 (2回) が該当。
5. 結果を選択。

結果:
| category | product_id | sales_count |
| :———- | :——— | :———- |
| Electronics | 1 | 3 |
| Electronics | 3 | 2 |

シナリオ2: 顧客分析 – 一定期間内に特定回数以上購入している顧客

仮に customers テーブルと orders テーブルがあり、orders テーブルには customer_id, order_date があるとします。過去1年間に3回以上購入した顧客リストを取得したい場合。

sql
-- 現在の日付を '2024-01-01' と仮定
SELECT
customer_id,
COUNT(*) AS order_count
FROM
orders
WHERE
order_date >= DATE('2024-01-01', '-1 year') -- 過去1年間の注文に絞る (SQLite記法、RDBMSにより異なる)
-- PostgreSQL/MySQL: order_date >= NOW() - INTERVAL '1 year' または DATE_SUB(NOW(), INTERVAL 1 YEAR)
-- SQL Server: order_date >= DATEADD(year, -1, GETDATE())
GROUP BY
customer_id
HAVING
COUNT(*) >= 3; -- 注文回数が3回以上の顧客を絞る

この例では、WHERE 句でまず期間内の注文に絞り込み、その後に GROUP BYHAVING で顧客ごとの回数を集計・フィルタリングしています。これにより、集計処理の負荷を軽減しています。

シナリオ3: ログ分析 – 特定のエラーが一定回数以上発生しているログエントリ

仮に logs テーブルがあり、log_time, log_level, message などのカラムがあるとします。ある期間で、特定の message が5回以上記録されている log_level='ERROR' のログエントリを抽出したい場合。

sql
SELECT
log_level,
message,
COUNT(*) AS error_count
FROM
logs
WHERE
log_level = 'ERROR' -- まずエラーログに絞る
AND log_time >= '2023-10-01' -- 期間で絞る
AND log_time < '2023-11-01'
GROUP BY
log_level, message -- エラーレベルとメッセージ内容でグループ化
HAVING
COUNT(*) >= 5; -- 同じエラーメッセージが5回以上出現しているものを絞る

ここでも、WHERE 句でまずエラーレベルと期間で絞り込み、その後に GROUP BYHAVING で同じメッセージ内容の出現回数を集計・フィルタリングしています。

まとめ

この記事では、SQLの HAVING 句について、その役割、基本的な使い方、そして WHERE 句との決定的な違いを詳細に解説しました。

重要なポイントを改めて確認しましょう。

  • HAVING 句は、GROUP BY 句による集計結果に対して条件を適用し、表示するグループを絞り込むために使用されます。
  • HAVING 句の条件には、通常 COUNT(), SUM(), AVG(), MIN(), MAX() といった集計関数が含まれます。
  • HAVING 句は 集計後 に実行されます。一方、WHERE 句は 集計前 に個々の行に適用されます。
  • WHERE 句は集計対象のデータ量を減らすために使われ、HAVING 句は集計されたグループの数を減らすために使われます。
  • パフォーマンスの観点から、集計前の個々の行に対する条件は WHERE 句で指定し、集計後のグループに対する条件は HAVING 句で指定するのが原則です。
  • HAVING 句でも AND, OR, NOT で複数の条件を組み合わせたり、サブクエリを使用したりできます。

HAVING 句をマスターすることで、より洗練されたデータ分析が可能になります。「合計値が〇〇以上のグループ」「平均値が△△以下のグループ」「要素数が□□を超えるグループ」といった、集計結果に基づいた複雑なデータ抽出要求にも柔軟に対応できるようになります。

データベース操作において、SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY といった句の役割と実行順序を正確に理解することは、効率的かつ意図通りのクエリを書くための基礎となります。特に WHEREHAVING の違いは混同しやすいため、今回の解説と具体的な例を通じてしっかりと区別できるようになりましょう。

この記事が、あなたのSQLスキル向上の一助となれば幸いです。ぜひ、ご自身のデータを使って様々な集計や HAVING 句による絞り込みを試してみてください。実践を通じて理解が深まるはずです。


約5000語でSQLのHAVING句について詳細に解説した記事を作成しました。基本的な使い方から応用、WHERE句との違い、注意点、実践シナリオまで網羅しています。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール