🇯🇵 日本語 | 🇺🇸 English | 🇪🇸 Español | 🇵🇹 Português | 🇹🇭 ไทย | 🇨🇳 中文

【SQLのHAVING句】GROUP BYの集計結果を絞り込む方法を徹底解説!

SQLのGROUP BY句を使いこなせるようになると、「カテゴリ別の商品数」や「顧客ごとの注文件数」といった便利な集計ができるようになりますね。しかし、データ分析はそこからが本番です。「たくさん注文してくれている優良顧客だけを見たい」「平均単価が高い人気カテゴリだけをリストアップしたい」――そんな、集計した後のグループに対して、さらに条件を絞り込みたい場面が必ずやってきます。

この「グループに対する絞り込み」を実現するのが、今回主役のHAVING句です。HAVING句は、GROUP BYとセットで使うことで、データ分析の精度と深さを格段に向上させる、まさに「かゆいところに手が届く」機能です。

この記事では、多くのSQL学習者が混乱しがちなWHERE句との違いに焦点を当てながら、HAVING句の役割と使い方をゼロから徹底的に解説します。たくさんの「コピペで動くSQL実行例(SQL online環境付き!)」を通して、「なるほど、そういうことか!」という「動く」体験をしながら、データ集計の次のステージへ進みましょう!


準備:分析用の注文データ(orders)を用意しよう

理論の前に、まずは実践の土台を固めましょう。今回は、ECサイトの「注文履歴」を模したordersテーブルを使用します。顧客ID、商品カテゴリ、価格、注文日といった、実践的なデータが含まれています。以下のSQLをまるごとコピーして、あなた自身の環境で実行するか、後述の「体験コーナー」で利用してください。

-- テーブルが存在すれば一度削除(繰り返し試せるように)
DROP TABLE IF EXISTS orders;

-- ordersテーブルを作成
CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL,
  product_category TEXT NOT NULL,
  price INTEGER NOT NULL,
  order_date DATE NOT NULL
);

-- データを挿入
INSERT INTO orders (order_id, customer_id, product_category, price, order_date) VALUES
(1, 101, 'PC周辺機器', 15000, '2025-05-10'),
(2, 102, '書籍', 3200, '2025-05-11'),
(3, 101, 'PC周辺機器', 250000, '2025-05-12'),
(4, 103, '家電', 88000, '2025-05-15'),
(5, 102, '書籍', 4500, '2025-05-20'),
(6, 101, '家電', 120000, '2025-06-01'),
(7, 104, 'PC周辺機器', 8000, '2025-06-05'),
(8, 102, 'アパレル', 7800, '2025-06-08'),
(9, 103, 'PC周辺機器', 320000, '2025-06-10'),
(10, 105, '書籍', 2900, '2025-06-15'),
(11, 101, '書籍', 3500, '2025-06-20'),
(12, 103, '家電', 35000, '2025-06-22');

このデータには、複数回購入している顧客(例: 顧客ID 101, 102, 103)や、複数の顧客に購入されているカテゴリ(例: PC周辺機器, 書籍)が含まれており、GROUP BYHAVINGの挙動を確認するのに最適です。


最重要ポイント:`WHERE`句と`HAVING`句の決定的な違い

HAVING句を理解する上で最大の鍵は、WHERE句との役割分担を明確に把握することです。どちらも「絞り込み」を行う機能ですが、絞り込みを行うタイミングと対象が全く異なります。

この違いを、レストランの調理過程に例えてみましょう。

レストランの調理に例えるSQLの実行順序

  1. テーブル結合(FROM, JOIN): まず、厨房にすべての食材(データ)を集めます。
  2. WHERE: シェフ(調理人)が登場。調理を始める前に、個々の食材を吟味し、「今日のカルパッチョにはこの魚は使えないな」と、条件に合わない食材をここで除外します。
  3. GROUP BY: シェフが、使えると判断した食材を使って、グループごとに料理(例: サラダ、パスタ、肉料理)を作ります。
  4. HAVING: フード評論家が登場。シェフが作り終えた料理(グループ)をテーブルに並べ、「売上が1万円を超えている料理だけを評価しよう」と、評価対象となる料理自体を絞り込みます。

この例えから分かるように、重要なのは以下の2点です。

このため、COUNT()SUM()といった集計関数(aggregate function)を使った条件は、グループに対してしか意味をなさないため、HAVING句でしか使えません。WHERE COUNT(*) > 10 のような書き方はできないのです。


実践!`HAVING`句で集計結果を絞り込む

それでは、実際のコードでHAVING句の威力を見ていきましょう。まずはGROUP BYで「顧客ごとの注文件数」を集計してみます。これがHAVING句を適用する前のベースとなります。

-- まずはHAVINGなしで、顧客ごとの注文件数を集計
SELECT
  customer_id,
  COUNT(order_id) AS order_count
FROM
  orders
GROUP BY
  customer_id;

実行結果:

customer_id | order_count
------------|-------------
101         | 4
102         | 3
103         | 3
104         | 1
105         | 1

この結果から、「注文件数が3件以上の優良顧客だけを抽出したい」と考えたとします。ここでHAVING句の出番です。集計結果であるorder_countCOUNT(order_id))に対して条件を指定します。

-- 【HAVING + COUNT】注文件数が3件以上の顧客を抽出
SELECT
  customer_id,
  COUNT(order_id) AS order_count
FROM
  orders
GROUP BY
  customer_id
HAVING
  COUNT(order_id) >= 3;

実行結果:

customer_id | order_count
------------|-------------
101         | 4
102         | 3
103         | 3

見事に、注文件数が3件以上(HAVING COUNT(order_id) >= 3)の顧客だけが絞り込まれました。これがHAVING句の基本的な使い方です。


応用例:`SUM`, `AVG` を使った条件指定

HAVING句はCOUNTだけでなく、もちろんSUM(合計)やAVG(平均)といった他の集計関数とも組み合わせられます。これにより、より高度なデータ分析が可能になります。

【HAVING + SUM】合計購入金額が10万円以上のカテゴリを抽出

「どのカテゴリが売上に貢献しているのか?」を知るために、カテゴリごとの合計売上を算出し、その額が10万円を超えているカテゴリだけを見てみましょう。

-- 【HAVING + SUM】合計売上が10万円を超えるカテゴリを抽出
SELECT
  product_category,
  SUM(price) AS total_sales
FROM
  orders
GROUP BY
  product_category
HAVING
  SUM(price) > 100000;

実行結果:

product_category | total_sales
------------------|-------------
PC周辺機器         | 593000
家電               | 243000

この結果から、「PC周辺機器」と「家電」が主要な収益源であることが一目瞭然です。


【HAVING + AVG】平均単価が5万円以上のカテゴリを抽出

次に、「高単価な商品が売れているカテゴリはどれか?」を調べるために、カテゴリごとの平均単価を算出し、それが5万円を超えているカテゴリを抽出します。

-- 【HAVING + AVG】平均単価が5万円を超えるカテゴリを抽出
SELECT
  product_category,
  AVG(price) AS average_price
FROM
  orders
GROUP BY
  product_category
HAVING
  AVG(price) > 50000;

実行結果:

product_category | average_price
------------------|---------------
PC周辺機器         | 148250
家電               | 81000

合計売上だけでなく、平均単価という別の視点からもカテゴリを評価できることがわかりますね。


究極の連携:`WHERE`句と`HAVING`句を同時に使う

WHEREHAVINGは敵対関係ではなく、協力してより複雑な絞り込みを実現するパートナーです。この両者を一つのクエリで使うことで、非常に強力な分析ができます。

分析したいこと:「2025年6月以降の注文に限定して、顧客ごとの合計購入金額を算出し、その金額が10万円を超えている顧客を知りたい」

この要求を分解すると、2つの絞り込みがあることがわかります。

  1. グループ化前の絞り込み: 注文日が「2025年6月1日以降」のレコードに限定する。 → これは個々のレコードに対する条件なのでWHEREの出番です。
  2. グループ化後の絞り込み: 顧客ごとに集計した合計金額が「10万円より大きい」グループに限定する。 → これはグループに対する条件なのでHAVINGの出番です。

これをSQLにすると以下のようになります。

-- 【WHERE + HAVING】6月以降の注文で、合計購入額が10万円超の顧客
SELECT
  customer_id,
  SUM(price) AS total_spent_in_june_onwards
FROM
  orders
WHERE
  order_date >= '2025-06-01'
GROUP BY
  customer_id
HAVING
  SUM(price) > 100000;

実行結果:

customer_id | total_spent_in_june_onwards
------------|-----------------------------
101         | 123500
103         | 355000

このクエリの処理の流れは、まさにレストランの例え通りです。

  1. まずWHERE order_date >= '2025-06-01'によって、6月以降の注文レコードだけがテーブルから選ばれます。
  2. 次に、その選ばれたレコードがGROUP BY customer_idで顧客ごとにグループ化されます。
  3. 最後に、HAVING SUM(price) > 100000によって、グループごとの合計金額が10万円を超えているものだけが最終結果として残ります。

このWHEREHAVINGの連携こそ、データ分析におけるSQLの真骨頂と言えるでしょう。


【体験コーナー】SQL Fiddle: ブラウザでHAVING句を動かそう!

さあ、知識を確かなスキルに変える時間です!下の「SQLオンライン実行環境」を使えば、ブラウザだけで自由にSQLを試すことができます。この記事で紹介したordersテーブルはすでに読み込まれています。

色々な条件を試してみてください。「合計購入額が...」「平均単価が...」「注文件数が...」など、条件の数字を変えたり、>=<に変えてみたりするだけでも、結果がどう変わるかが見えてきて、理解がぐっと深まります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SQL HAVING句 オンライン実行環境</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: #333; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
    h1 { color: #2c3e50; }
    textarea { width: 100%; height: 200px; font-family: "SF Mono", "Consolas", monospace; font-size: 16px; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; margin-bottom: 1rem; }
    button { background-color: #8e44ad; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
    button:hover { background-color: #70368b; }
    button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
    #result-container { margin-top: 2rem; border: 1px solid #ddd; padding: 1rem; border-radius: 6px; background: #fdfdfd; min-height: 50px; overflow-x: auto;}
    #error-message { color: #e74c3c; font-weight: bold; }
    table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
    th, td { border: 1px solid #ddd; padding: 10px; text-align: left; white-space: nowrap; }
    th { background-color: #f2f2f2; }
    tr:nth-child(even) { background-color: #f9f9f9; }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
</head>
<body>

  <h1>SQLを試してみよう!</h1>
  <p>下のテキストエリアにSQL文を入力して「実行」ボタンを押してください。記事で紹介した色々なSQLを試してみましょう!</p>

  <textarea id="sql-input">-- 色々試してみよう!
-- 例:PC周辺機器カテゴリに絞って(WHERE)、
--      顧客ごとの合計購入額を出し(GROUP BY)、
--      その合計額が20万円以上の顧客だけを表示(HAVING)
SELECT
  customer_id,
  SUM(price) AS total_spent_on_pc
FROM
  orders
WHERE
  product_category = 'PC周辺機器'
GROUP BY
  customer_id
HAVING
  SUM(price) > 200000;</textarea>
  
  <button id="execute-btn">実行</button>
  
  <div id="result-container">
    <p id="error-message"></p>
    <div id="result-output"></div>
  </div>

  <script>
    const sqlInput = document.getElementById('sql-input');
    const executeBtn = document.getElementById('execute-btn');
    const errorMsg = document.getElementById('error-message');
    const resultOutput = document.getElementById('result-output');

    let db;

    async function initDb() {
      executeBtn.disabled = true;
      executeBtn.textContent = 'DB準備中...';
      try {
        const SQL = await initSqlJs({
          locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`
        });
        db = new SQL.Database();
        
        const setupSql = `
          DROP TABLE IF EXISTS orders;
          CREATE TABLE orders (
            order_id INTEGER PRIMARY KEY,
            customer_id INTEGER NOT NULL,
            product_category TEXT NOT NULL,
            price INTEGER NOT NULL,
            order_date DATE NOT NULL
          );
          INSERT INTO orders (order_id, customer_id, product_category, price, order_date) VALUES
          (1, 101, 'PC周辺機器', 15000, '2025-05-10'), (2, 102, '書籍', 3200, '2025-05-11'),
          (3, 101, 'PC周辺機器', 250000, '2025-05-12'), (4, 103, '家電', 88000, '2025-05-15'),
          (5, 102, '書籍', 4500, '2025-05-20'), (6, 101, '家電', 120000, '2025-06-01'),
          (7, 104, 'PC周辺機器', 8000, '2025-06-05'), (8, 102, 'アパレル', 7800, '2025-06-08'),
          (9, 103, 'PC周辺機器', 320000, '2025-06-10'), (10, 105, '書籍', 2900, '2025-06-15'),
          (11, 101, '書籍', 3500, '2025-06-20'), (12, 103, '家電', 35000, '2025-06-22');
        `;
        db.run(setupSql);
        
        executeBtn.disabled = false;
        executeBtn.textContent = '実行';
        resultOutput.innerHTML = '<p>準備完了!自由にSQLを試してみてください。</p>';

      } catch (err) {
        errorMsg.textContent = 'データベースの初期化に失敗しました: ' + err.message;
        console.error(err);
      }
    }

    function executeSql() {
      if (!db) return;
      const sql = sqlInput.value;
      errorMsg.textContent = '';
      resultOutput.innerHTML = '';
      try {
        const results = db.exec(sql);
        if (results.length === 0) {
          resultOutput.innerHTML = '<p>クエリは成功しましたが、結果セットは返されませんでした。(例: INSERT, UPDATEなど)</p>';
          return;
        }
        results.forEach(result => {
          const table = document.createElement('table');
          const thead = document.createElement('thead');
          const tbody = document.createElement('tbody');
          const headerRow = document.createElement('tr');
          result.columns.forEach(colName => {
            const th = document.createElement('th');
            th.textContent = colName;
            headerRow.appendChild(th);
          });
          thead.appendChild(headerRow);
          result.values.forEach(row => {
            const bodyRow = document.createElement('tr');
            row.forEach(cellValue => {
              const td = document.createElement('td');
              td.textContent = cellValue === null ? 'NULL' : (typeof cellValue === 'number' ? cellValue.toLocaleString() : cellValue);
              bodyRow.appendChild(td);
            });
            tbody.appendChild(bodyRow);
          });
          table.appendChild(thead);
          table.appendChild(tbody);
          resultOutput.appendChild(table);
        });
      } catch (err) {
        errorMsg.textContent = 'SQLエラー: ' + err.message;
        console.error(err);
      }
    }
    executeBtn.addEventListener('click', executeSql);
    initDb();
  </script>
</body>
</html>

SQLの頭の中:クエリの実行(評価)順序

WHEREHAVINGの違いをさらに深く理解するために、SQLが内部でクエリをどのように処理しているか、その「論理的な実行順序」を知っておくと非常に役立ちます。私たちが書くコードの順序(SELECT, FROM, WHERE...)と、SQLが解釈・実行する順序は異なります。

SQLの論理的な実行順序:

  1. FROM: まず、どのテーブルからデータを取得するかを決定します。
  2. WHERE: 次に、個々の行を条件でフィルタリングします。
  3. GROUP BY: フィルタリングされた行をグループにまとめます。
  4. HAVING: グループ化された結果を条件でフィルタリングします。
  5. SELECT: 最終的にどの列を表示するかを決定します。
  6. ORDER BY: 結果セットを指定された順序で並べ替えます。
  7. LIMIT: 表示する行数を制限します。

この順序を見ると、WHEREGROUP BYより前に、HAVINGGROUP BYの直後にあることが明確にわかりますね。これが、WHEREでは集計関数が使えず、HAVINGで使える理由の核心です。

また、SELECT句で付けた別名(例: SUM(price) AS total_sales)をWHERE句やHAVING句で使えない(一部のデータベースを除く)のも、SELECT句が評価されるのが後だから、という理由がこの順序から理解できます。(ORDER BY句では使えることが多いです。なぜならORDER BYSELECTの後に評価されるからです。)


まとめ:`HAVING`句をマスターして、データ分析を次のレベルへ

今回は、GROUP BYで集計した結果をさらに絞り込むための強力なツール、HAVING句を深掘りしました。

最後に、重要なポイントをもう一度おさらいしましょう。

HAVING句は、一見すると地味な機能かもしれません。しかし、これを使いこなせるかどうかで、あなたがデータから引き出せる情報の質と深さは天と地ほど変わってきます。「売れ筋カテゴリの中でも、特に利益率の高いものは?」「アクティブユーザーの中でも、課金額が上位のユーザー層は?」といった、ビジネスの意思決定に直結するような、より鋭い問いにSQLで答えられるようになるのです。

ぜひ、この記事のオンライン実行環境で遊び倒して、HAVING句と友達になってください。あなたのWebクリエイターとしてのデータ活用能力が、飛躍的に向上することを保証します。