SQLのHAVING句を使いこなす（サンプルクエリあり）

## この記事の内容
- HAVING句とは
- HAVING句の例を2つ紹介

### 補足
- mysql8.0で動作確認をしています

## HAVING句ってなに？

HAVING句とはGROUP BYで集約した結果に対し条件を指定できるクエリです。

```sql
SELECT
    カラム1,
    カラム2,
    ・・・
FROM
    （テーブル名）
GROUP BY
    （カラム1）,
    （カラム2）,
    ・・・
HAVING （グループの値に対する条件）
;
```

利用する時は以下のポイントに気をつける必要があります。

### ポイント1.　HAVING句が実行されるタイミングを理解する

HAVING句の実行の順番はGROUP BY句の**後**です。
これは、HAVINGがGROUP BYで集約したグループの結果に対して、条件をつけるためです。


![](https://storage.googleapis.com/zenn-user-upload/1b575f83c314-20231115.png)

### ポイント2.WHERE句とHAVING句で迷ったらWHERE句を使う

WHERE句とHAVING句はどちらも条件を追加するためのクエリのため、どちらを使っても同じ結果になることがあります。このような場合、**WHERE句の使用をお勧めします**。

理由は、WHERE句の方が実行速度が速いためです。
DBMS内部では、HAVING句はソート処理の後に実行されるため、処理に時間がかかることがあります。
一方、WHERE句はソートの前に実行されます。そのため、ソート処理する行数が減り、処理時間が短くなります。

目に見えない部分ではありますが、これらの内部処理の違いを理解することは、パフォーマンスの向上に繋がります。

## いろんな例

### ケース1. 販売履歴から売れ筋の高いカテゴリーを取得する

HAVING句はグループに対して条件をつけることができます。
例として、販売履歴からカテゴリーを集約したグループに対し、「含まれる行が２行より多い」という条件を指定します。
すると、図のような結果となります。
２個以下しか売れていない、「家具」「日用品」が除外されていることが確認できます。
HAVING句を省くと、すべてのカテゴリーの売上個数が表示されます。

![](https://storage.googleapis.com/zenn-user-upload/56cd7e08ef4b-20231115.png)

```sql
SELECT
    category as 売れ筋カテゴリー,
    COUNT(DISTINCT product_name) AS 売上個数
FROM
    sales_history
GROUP BY
    category
HAVING COUNT(category) > 2　--「カテゴリ」グループに対して絞り込み
;
```

### ケース2. 年収の最頻値を調べる

日本の正社員の平均年収は403万円だそうです。一方で、中央値は350万円という結果が出ています。
平均値が中央値を上回ったのは、年収が高い層が平均値を引き上げたためです。
※duda調べ

このように、平均値は外れ値の影響を受けやすいです。これを解消する指標の一つに最頻値があります。
最頻値は母集団の中で最も数の多かった値です。（最頻値と中央値は別です。）



例として、年収の最頻値を求めてみましょう。
この中で最大の要素数を持つのは年収300万と年収500万で、それぞれ３人いることがわかります。

![](https://storage.googleapis.com/zenn-user-upload/4f3e9e230aad-20231115.png)

```sql
-- 最頻値を求めるSQL
SELECT
    CONCAT(annual_income, '万円') as 最頻値の年収,
    CONCAT(COUNT(*), '人') AS 最頻値の社員の数
FROM
    income
GROUP BY
    annual_income
HAVING COUNT(*) >= ALL
(
    SELECT COUNT(*)
    FROM
        income
    GROUP BY
        annual_income
)
;
```

まずALL副問い合わせに注目します。
GROUP BYで年収`annual_income`をグループ化し、各年収グループの社員数を取得します。

![](https://storage.googleapis.com/zenn-user-upload/11ad72083687-20231115.png)

「各年収のグループの社員数」と「それ以上の年収のグループの社員数」をALL句で比較します。
ALL句は指定した値と比較するときに使うクエリです。比較演算子の条件がすべて真になるとき真、条件が一つでも偽になるとき偽になります。

「３」は1,2,3以上の値であるため最頻値の人数が３人であることがわかります。

各年収のグループの社員数が３人の年収は300万と500万のため、最頻値は300万と500万ということがわかります。

![](https://storage.googleapis.com/zenn-user-upload/ae3d010bc26b-20231116.png)


## まとめ
HAVING句のポイントと使用例をまとめました。
他に便利な使い方があれば、教えていただきたいです！

### 参考文献

達人に学ぶSQL指南書

https://www.shoeisha.co.jp/book/detail/9784798157825

SQL 第2版 ゼロからはじめるデータベース操作

https://www.amazon.co.jp/SQL-%E7%AC%AC2%E7%89%88-%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E6%93%8D%E4%BD%9C-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%AD%A6%E7%BF%92%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E3%83%9F%E3%83%83%E3%82%AF/dp/4798144452

※日本人の年収の平均と中央値

https://www.persol-career.co.jp/pressroom/news/research/2023/20230307_01/#:~:text=%E3%81%AA%E3%81%8A%E3%80%81%E6%AD%A3%E7%A4%BE%E5%93%A1%E3%81%AE%E5%B9%B3%E5%9D%87%E5%B9%B4%E5%8F%8E,%E3%81%A6%E5%85%AC%E9%96%8B%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82&text=%EF%BC%8D%E6%AD%A3%E7%A4%BE%E5%93%A1%E3%81%AE%E5%B9%B4%E5%8F%8E%E4%B8%AD%E5%A4%AE%E5%80%A4,%E5%B9%B4%E5%8F%8E%EF%BC%9A347%E4%B8%87%E5%86%86%EF%BC%89%E3%80%82


### おまけ

デモ用DDLとDML
```sql
CREATE TABLE sales_history (
    product_name VARCHAR(255),  -- 商品名
    sales DECIMAL(10, 0),       -- 売上（小数点以下2桁までの金額）
    category VARCHAR(100)       -- カテゴリ
);
INSERT INTO sales_history (product_name, sales, category) VALUES ('ハリー・ポッター', 1500, '本');
INSERT INTO sales_history (product_name, sales, category) VALUES ('ミステリー小説', 1200, '本');
INSERT INTO sales_history (product_name, sales, category) VALUES ('ノート', 250, '本');
INSERT INTO sales_history (product_name, sales, category) VALUES ('バナナ', 300, 'フルーツ');
INSERT INTO sales_history (product_name, sales, category) VALUES ('オレンジ', 350, 'フルーツ');
INSERT INTO sales_history (product_name, sales, category) VALUES ('りんご', 500, 'フルーツ');
INSERT INTO sales_history (product_name, sales, category) VALUES ('みかん', 500, 'フルーツ');
INSERT INTO sales_history (product_name, sales, category) VALUES ('シャンプー', 800, '日用品');
INSERT INTO sales_history (product_name, sales, category) VALUES ('洗剤', 650, '日用品');
INSERT INTO sales_history (product_name, sales, category) VALUES ('ダイニングテーブル', 20000, '家具');
INSERT INTO sales_history (product_name, sales, category) VALUES ('椅子', 4500, '家具');
```


```sql
CREATE TABLE `income` (
  `name` varchar(255) NOT NULL,
  `annual_income` decimal(10,0) NOT NULL
);
INSERT INTO income (name, annual_income) VALUES ('ひら1', 300);
INSERT INTO income (name, annual_income) VALUES ('ひら2', 300);
INSERT INTO income (name, annual_income) VALUES ('ひら3', 300);
INSERT INTO income (name, annual_income) VALUES ('中堅1', 500);
INSERT INTO income (name, annual_income) VALUES ('中堅2', 500);
INSERT INTO income (name, annual_income) VALUES ('中堅3', 500);
INSERT INTO income (name, annual_income) VALUES ('社員1', 800);
INSERT INTO income (name, annual_income) VALUES ('社員2', 800);
INSERT INTO income (name, annual_income) VALUES ('偉い人', 1000);
INSERT INTO income (name, annual_income) VALUES ('すごく偉い人', 700000);
```