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

【SQL】GROUP BY句の使い方を徹底解説!データを自在に集計しよう

Webサイトのアクセス解析、ECサイトの売上分析、アプリのユーザー動向調査など、データを扱う上で「集計」は欠かせない作業です。「カテゴリ別の商品数」「都道府県別のユーザー数」「月別の売上合計」といった情報をパッと取り出せたら、便利だと思いませんか? そんな願いを叶えるのが、SQLのGROUP BY句です。

GROUP BYは、テーブル内のデータを特定のカラム(列)の値にもとづいてグループに分け、それぞれのグループに対して集計関数(COUNT, SUM, AVGなど)を適用するための強力な武器です。一見すると少し難しそうに感じるかもしれませんが、一度使い方をマスターすれば、データ分析の幅が劇的に広がります。

この記事では、Webクリエーターを目指す初心者の方でも大丈夫なように、GROUP BYの基本的な使い方から、HAVING句や複数列でのグループ化といった応用テクニックまで、豊富な「コピペで動くコード」を使って丁寧に解説していきます。さあ、一緒に「データをグループ化して集計する」面白さを体験してみましょう!


準備:分析用のサンプルデータを用意しよう

SQLを学ぶ一番の近道は、実際に手を動かしてみることです。今回は、架空の小さなECサイトの商品リストを想定したproductsテーブルを使います。以下のSQL文は、テーブルの作成とサンプルデータの投入を行います。この記事で紹介するSQLは、すべてこのテーブルが対象です。

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

-- productsテーブルを作成
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  prefecture TEXT NOT NULL,
  price INTEGER NOT NULL,
  stock_quantity INTEGER NOT NULL
);

-- データを挿入
INSERT INTO products (id, name, category, prefecture, price, stock_quantity) VALUES
(1, 'すごいTシャツ', 'アパレル', '東京', 3000, 50),
(2, '最高のジーンズ', 'アパレル', '岡山', 12000, 30),
(3, '魔法のキーボード', 'PC周辺機器', '東京', 8500, 20),
(4, '光るマウス', 'PC周辺機器', '神奈川', 4500, 45),
(5, '匠の包丁', 'キッチン用品', '新潟', 9800, 15),
(6, '究極のフライパン', 'キッチン用品', '新潟', 6200, 25),
(7, 'はじめてのSQL', '書籍', '東京', 2800, 100),
(8, 'Webデザイン入門', '書籍', '東京', 3200, 80),
(9, '快適スニーカー', 'アパレル', '東京', 7800, 60);

このテーブルには、商品ID、商品名、カテゴリ、生産地(都道府県)、価格、在庫数のデータが入っています。「アパレル」カテゴリが3つ、「東京」産が4つなど、意図的に重複するデータを含ませているのがポイントです。これがGROUP BYで集計する際に活きてきます。


【基本】`GROUP BY`でカテゴリ別の商品数を数える

まずはGROUP BYの最も基本的な使い方です。「商品カテゴリごとに、それぞれ何種類の商品があるか?」を調べてみましょう。これには、GROUP BYcategory列をグループ化し、おなじみのCOUNT()関数で各グループの行数を数えます。

SELECT
  category,
  COUNT(*) AS product_count
FROM
  products
GROUP BY
  category;

実行結果:

category   | product_count
-----------|---------------
PC周辺機器   | 2
アパレル     | 3
キッチン用品 | 2
書籍         | 2

見事にカテゴリごとの商品数を取得できました! SQLエンジンは内部で、まずcategory列の値('アパレル', 'PC周辺機器'など)が同じ行をまとめ、次にそのまとまり(グループ)ごとにCOUNT(*)を実行してくれています。AS product_countで結果の列に別名をつけているのもポイントです。


【応用1】`SUM`や`AVG`など他の集計関数と組み合わせる

GROUP BYのすごいところは、COUNT()だけでなく、SUM()(合計)、AVG()(平均)、MAX()(最大)、MIN()(最小)といった他の集計関数とも自由に組み合わせられる点です。

次は「カテゴリごとの平均価格」を計算してみましょう。

SELECT
  category,
  AVG(price) AS average_price
FROM
  products
GROUP BY
  category;

実行結果:

category   | average_price
-----------|---------------
PC周辺機器   | 6500
アパレル     | 7600
キッチン用品 | 8000
書籍         | 3000

このように、集計関数を変えるだけで、グループごとの様々な側面を分析できます。例えば「カテゴリごとの総在庫数」を知りたければ、SUM(stock_quantity)を使います。

SELECT
  category,
  SUM(stock_quantity) AS total_stock
FROM
  products
GROUP BY
  category;

実行結果:

category   | total_stock
-----------|-------------
PC周辺機器   | 65
アパレル     | 140
キッチン用品 | 40
書籍         | 180

これにより、「書籍」カテゴリの在庫が最も多く、「キッチン用品」が少ない、といったことが一目でわかりますね。


【応用2】集計結果を絞り込む `HAVING`句

GROUP BYで集計した結果に対して、「さらに条件で絞り込みたい」というケースは頻繁にあります。例えば、「商品が3つ以上あるカテゴリだけ表示したい」といった場合です。

ここで注意したいのが、通常のWHERE句は集計前のデータ(各行)を絞り込むために使われるため、COUNT(*)のような集計結果に対しては使えない、というルールです。

集計後のグループに対して条件を指定するのが、`HAVING`句の役割です。

SELECT
  category,
  COUNT(*) AS product_count
FROM
  products
GROUP BY
  category
HAVING
  COUNT(*) >= 3;

実行結果:

category | product_count
---------|---------------
アパレル   | 3

HAVING COUNT(*) >= 3という条件を付け加えたことで、商品数が3つである「アパレル」カテゴリだけが結果として返ってきました。

`WHERE`はグループ化の前、`HAVING`はグループ化の後。この違いをしっかり覚えておきましょう。


【応用3】複数の列でグループ化する

GROUP BYは、複数の列を組み合わせて、より細かなグループを作ることもできます。例えば、「カテゴリと生産地の組み合わせごとに、商品数が知りたい」という、少し複雑な集計も簡単です。

GROUP BY句に、カンマ区切りで列名を複数指定するだけです。

SELECT
  category,
  prefecture,
  COUNT(*) AS product_count
FROM
  products
GROUP BY
  category, prefecture;

実行結果:

category   | prefecture | product_count
-----------|------------|---------------
PC周辺機器   | 神奈川       | 1
PC周辺機器   | 東京         | 1
アパレル     | 岡山         | 1
アパレル     | 東京         | 2
キッチン用品 | 新潟         | 2
書籍         | 東京         | 2

「アパレル in 岡山」と「アパレル in 東京」が別のグループとして扱われているのがわかりますね。これにより、「東京のアパレル製品は2種類ある」といった、より詳細な分析が可能になります。


【体験コーナー】ブラウザでSQLを自由に実行しよう!

理論はもう十分! ここからは、あなたが実際にSQLを書いて、動かして、結果を確かめる時間です。

以下のHTMLコードを丸ごとコピーして、sql_groupby_test.htmlのようなファイル名でPCに保存してください。そして、そのファイルをWebブラウザで開いてみてください。この記事で使っているproductsテーブルが内蔵された、あなただけのSQL実行環境が立ち上がります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SQL GROUP BY 実行環境</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: 180px; 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: #3498db; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
    button:hover { background-color: #2980b9; }
    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; }
    #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; }
    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">-- 都道府県ごとの平均価格と最高価格を表示してみよう
SELECT
  prefecture,
  AVG(price) AS "平均価格",
  MAX(price) AS "最高価格"
FROM
  products
GROUP BY
  prefecture;</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 products;
          CREATE TABLE products (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            category TEXT NOT NULL,
            prefecture TEXT NOT NULL,
            price INTEGER NOT NULL,
            stock_quantity INTEGER NOT NULL
          );
          INSERT INTO products (id, name, category, prefecture, price, stock_quantity) VALUES
          (1, 'すごいTシャツ', 'アパレル', '東京', 3000, 50),
          (2, '最高のジーンズ', 'アパレル', '岡山', 12000, 30),
          (3, '魔法のキーボード', 'PC周辺機器', '東京', 8500, 20),
          (4, '光るマウス', 'PC周辺機器', '神奈川', 4500, 45),
          (5, '匠の包丁', 'キッチン用品', '新潟', 9800, 15),
          (6, '究極のフライパン', 'キッチン用品', '新潟', 6200, 25),
          (7, 'はじめてのSQL', '書籍', '東京', 2800, 100),
          (8, 'Webデザイン入門', '書籍', '東京', 3200, 80),
          (9, '快適スニーカー', 'アパレル', '東京', 7800, 60);
        `;
        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>

【挑戦課題】


初心者がつまずきやすい注意点:`SELECT`句のルール

GROUP BYを使うときに、初心者が最も遭遇しやすいエラーがあります。それは、「SELECT句に書けるのは、GROUP BYで指定した列か、集計関数で囲んだ列だけ」というルールです。

例えば、以下のSQLはエラーになります。

-- これはエラーになる例
SELECT
  category,
  name -- ← GROUP BY にも集計関数にも指定されていない列
FROM
  products
GROUP BY
  category;

なぜエラーになるのでしょう? 考えてみれば当然です。GROUP BY categoryで「アパレル」という1行にまとめたとき、name列には「すごいTシャツ」「最高のジーンズ」「快適スニーカー」の3つの候補があります。SQLエンジンは、そのうちのどれを表示すればいいか判断できないため、エラーを返します。

GROUP BYで集計した結果の行は、元の複数の行を代表する1行です。そのため、SELECT句にはそのグループを特定する列(この場合はcategory)か、グループ全体の値(COUNT(*)SUM(price)など)しか書けない、と覚えておきましょう。


関連テクニック:`ORDER BY`で結果を並び替える

GROUP BYで得られた結果を、特定の順序で並び替えたいことも多いでしょう。その場合は、おなじみのORDER BY句を使います。ORDER BYGROUP BYHAVINGの後に記述します。

例えば、先ほどの「カテゴリごとの商品数」を、商品数が多い順(降順)に並び替えてみましょう。

SELECT
  category,
  COUNT(*) AS product_count
FROM
  products
GROUP BY
  category
ORDER BY
  product_count DESC;

実行結果:

category   | product_count
-----------|---------------
アパレル     | 3
PC周辺機器   | 2
キッチン用品 | 2
書籍         | 2

ORDER BY product_count DESCとすることで、集計結果のproduct_count列を使って降順(DESC)にソートできました。これで、どのカテゴリの商品が最も多いかが一目瞭然です。


まとめ

今回は、データを自在に集計するための強力な武器、GROUP BY句について学びました。

GROUP BYを使いこなせるようになると、単にデータを取得するだけでなく、データに隠された傾向や意味を読み解く「データ分析」の世界への扉が開かれます。ぜひ、今回の記事の体験コーナーで色々な集計を試して、そのパワフルさを実感してください。あなたのWebクリエイターとしてのスキルセットに、強力な「データ集計能力」が加わるはずです。