はい、承知いたしました。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_id
と COUNT(*)
を使用します。
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クエリの一般的な論理的な処理順序は以下のようになります(実際の実行計画はオプティマイザによって最適化されますが、概念的な順序はこれです)。
FROM
: データを取得するテーブルを指定。JOIN
: 複数のテーブルを結合。WHERE
: 個々の行に対して条件を適用し、フィルタリング。ここで不要な行が先に除外されるため、後続の処理(GROUP BY, 集計)の対象データ量を減らすことができる。GROUP BY
:WHERE
句で絞り込まれた行を、指定されたカラムの値に基づいてグループ化。- 集計関数の計算: 各グループに対して、
SELECT
句やHAVING
句で使用されている集計関数が計算される。 HAVING
: グループ化され、集計関数が計算された結果(各グループ)に対して条件を適用し、フィルタリング。SELECT
:HAVING
句で絞り込まれたグループから、表示するカラムや集計結果を選択。DISTINCT
:SELECT
句で選択された行から重複を排除。ORDER BY
: 最終的な結果を指定されたカラムで並べ替え。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_id
と SUM(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_id
と SUM(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: (20003) + (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 NULL
や IS 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 BY
と HAVING
で顧客ごとの回数を集計・フィルタリングしています。これにより、集計処理の負荷を軽減しています。
シナリオ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 BY
と HAVING
で同じメッセージ内容の出現回数を集計・フィルタリングしています。
まとめ
この記事では、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
といった句の役割と実行順序を正確に理解することは、効率的かつ意図通りのクエリを書くための基礎となります。特に WHERE
と HAVING
の違いは混同しやすいため、今回の解説と具体的な例を通じてしっかりと区別できるようになりましょう。
この記事が、あなたのSQLスキル向上の一助となれば幸いです。ぜひ、ご自身のデータを使って様々な集計や HAVING
句による絞り込みを試してみてください。実践を通じて理解が深まるはずです。
約5000語でSQLのHAVING句について詳細に解説した記事を作成しました。基本的な使い方から応用、WHERE句との違い、注意点、実践シナリオまで網羅しています。