Open7
【達人に学ぶSQL徹底指南書メモ】6. HAVING句の力

6.1 データの歯抜けを探す
SeqTbl
seq | name |
---|---|
1 | ディック |
2 | アン |
3 | ライル |
5 | カー |
6 | マリー |
8 | ベン |
6.1.1 歯抜けあり or 歯抜けなし
SELECT '歯抜けあり' AS gap
FROM SeqTbl
HAVING COUNT(*) <> MAX(seq)
UNION ALL
SELECT '歯抜けなし' AS gap
FROM SeqTbl
HAVING COUNT(*) = MAX(seq);
これは二度のテーブルスキャンとソートが発生してパフォーマンス上の無駄が多い。
最適解は以下。
SELECT
CASE
WHEN COUNT(*) <> MAX(seq) THEN '歯抜けあり'
ELSE '歯抜けなし'
END AS gap
FROM
SeqTbl;
6.1.2 歯抜けの最小値を探す
SELECT
CASE
WHEN COUNT(*) = 0 OR MIN(seq) > 1 THEN 1
ELSE (
SELECT
MIN(seq + 1)
FROM
SeqTbl S1
WHERE
NOT EXISTS (
SELECT *
FROM SeqTbl S2
WHERE S2.seq = S1.seq + 1
)
)
END
FROM
SeqTbl;

6.2 HAVING 句でサブクエリ(最頻値を求める)
Graduates
name | income |
---|---|
サンプソン | 400,000 |
マイク | 30,000 |
ホワイト | 20,000 |
アーノルド | 20,000 |
スミス | 20,000 |
ロレンス | 15,000 |
ハドソン | 15,000 |
ケント | 10,000 |
ベッカー | 10,000 |
スコット | 10,000 |
結果
income | cnt |
---|---|
10,000 | 3 |
20,000 | 3 |
ALL 述語の利用
SELECT
income,
COUNT(*) AS cnt
FROM
Graduates
GROUP BY
income
HAVING
COUNT(*) >= ALL (
SELECT COUNT(*)
FROM Graduates
GROUP BY income
);
極値関数の利用
SELECT
income,
COUNT(*) AS cnt
FROM
Graduates
GROUP BY
income
HAVING
COUNT(*) >= (
SELECT MAX(cnt)
FROM (
SELECT COUNT(*) AS cnt
FROM Graduates
GROUP BY income
) TMP
);

6.3 NULL を含まない集合を探す
COUNT(*)
と COUNT(列名)
の違い
6.3.1 NullTbl
col_1 |
---|
NULL |
NULL |
NULL |
SELECT COUNT(*), COUNT(col_1)
FROM NullTbl;
結果
COUNT(*) |
COUNT(col_1) |
---|---|
3 | 0 |
6.3.2 すべての学生がレポート提出済みの学部を探す
Students
student_id | dpt | sbmt_date |
---|---|---|
100 | 理学部 | 2018-10-10 |
101 | 理学部 | 2018-9-22 |
102 | 文学部 | |
103 | 文学部 | 2018-9-10 |
200 | 文学部 | 2018-9-22 |
201 | 工学部 | |
202 | 経済学部 | 2018-9-25 |
結果
dpt |
---|
理学部 |
経済学部 |
COUNT 関数の利用
SELECT dpt
FROM Students
GROUP BY dpt
HAVING COUNT(*) = COUNT(sbmt_date);
CASE 式の利用
SELECT dpt
FROM Students
GROUP BY dpt
HAVING COUNT(*) = SUM(CASE WHEN sbmt_date IS NOT NULL THEN 1 ELSE 0 END);
CASE 式は各行が特定の条件を満たす集合に含まれるか決める関数を表している。
特製関数、定義関数と呼ぶ。
6.3.3 全員が 9 月中に提出済みの学部を探す
SELECT dpt
FROM Students
GROUP BY dpt
HAVING
COUNT(*) = SUM(
CASE
WHEN sbmt_date BETWEEN '2018-09-01' AND '2018-09-30' THEN 1
ELSE 0
END
);
結果
dpt |
---|
経済学部 |

6.4 特製関数の応用
TestResults
student_id | class | sex | score |
---|---|---|---|
001 | A | 男 | 100 |
002 | A | 女 | 100 |
003 | A | 女 | 49 |
004 | A | 男 | 30 |
005 | B | 女 | 100 |
006 | B | 男 | 92 |
007 | B | 男 | 80 |
008 | B | 男 | 80 |
009 | B | 女 | 10 |
010 | C | 男 | 80 |
011 | C | 男 | 21 |
012 | C | 女 | 100 |
013 | D | 女 | 100 |
014 | D | 女 | 0 |
015 | D | 女 | 0 |
クラスの 75%以上の生徒が 80 点以上のクラスを選択する
SELECT class
FROM TestResults
GROUP BY class
HAVING
COUNT(*) * 0.75 <= SUM(
CASE
WHEN score >= 80 THEN 1
ELSE 0
END
);
結果
class |
---|
B |
50 点以上を取った生徒のうち、男子の数が女子の数より多いクラスを選択する
SELECT class
FROM TestResults
GROUP BY class
HAVING
SUM(
CASE
WHEN score >= 50 AND sex = '男' THEN 1
ELSE 0
END
) > SUM(
CASE
WHEN score >= 50 AND sex = '女' THEN 1
ELSE 0
END
);
score >= 50
を WHERE 句で絞り込んでも良い。
WHERE 句は HAVING 句より先に実行される。
集計時のソート前に行数を減らせるため、パフォーマンスが良い。
結果
class |
---|
B |
C |
女子の平均点が、男子の平均点より高いクラスを選択する
SELECT class
FROM TestResults
GROUP BY class
HAVING
AVG(
CASE
WHEN sex = '男' THEN score
ELSE NULL
END
) < AVG(
CASE
WHEN sex = '女' THEN score
ELSE NULL
END
);
標準 SQL では、空集合に AVG 関数を適用した場合は、NULL を返すので、女子しかいないクラス D の男子の平均は未定義(NULL
)になる。
結果
class |
---|
A |

6.5 HAVING 句で全称量化
Teams
member | team_id | status |
---|---|---|
ジョー | 1 | 待機 |
ケン | 1 | 出動中 |
ミック | 1 | 待機 |
カレン | 2 | 出動中 |
キース | 2 | 休暇 |
ジャン | 3 | 待機 |
ハート | 3 | 待機 |
ディック | 3 | 待機 |
ベス | 4 | 待機 |
アレン | 5 | 出動中 |
ロバート | 5 | 休暇 |
ケーガン | 5 | 待機 |
すべてのメンバーが待機中であるチームを選択する
全称文を述語で表現する
SELECT
team_id,
member
FROM
Teams T1
WHERE
NOT EXISTS (
SELECT *
FROM Teams T2
WHERE T1.team_id = T2.team_id
AND status <> '待機'
);
結果
team_id | member |
---|---|
3 | ジャン |
3 | ハート |
3 | ディック |
4 | ベス |
全称文を集合で表現する
SELECT
team_id
FROM
Teams
GROUP BY
team_id
HAVING
COUNT(*) = SUM(
CASE
WHEN status = '待機' THEN 1
ELSE 0
END
);
SELECT
team_id
FROM
Teams
GROUP BY
team_id
HAVING
MAX(status) = '待機'
AND MIN(status) = '待機';
結果
team_id |
---|
3 |
4 |
集合に対する条件を SELECT
句に移せば一覧表示できる。
SELECT
team_id,
CASE
WHEN MAX(status) = '待機'
AND MIN(status) = '待機' THEN '⚪︎'
ELSE '×'
END AS status
FROM
Teams
GROUP BY
team_id;
結果
team_id | status |
---|---|
1 | × |
2 | × |
3 | ⚪︎ |
4 | ⚪︎ |
5 | × |

6.6 一意集合と多重集合
Materials
center | receive_date | material |
---|---|---|
東京 | 2018-04-01 | 錫 |
東京 | 2018-04-12 | 亜鉛 |
東京 | 2018-05-17 | アルミニウム |
東京 | 2018-05-20 | 亜鉛 |
大阪 | 2018-04-20 | 銅 |
大阪 | 2018-04-22 | ニッケル |
大阪 | 2018-04-29 | 鉛 |
名古屋 | 2018-03-15 | チタン |
名古屋 | 2018-04-01 | 炭素鋼 |
名古屋 | 2018-04-24 | 炭素鋼 |
名古屋 | 2018-05-02 | マグネシウム |
名古屋 | 2018-05-10 | チタン |
福岡 | 2018-05-10 | 亜鉛 |
福岡 | 2018-05-28 | 錫 |
資材のダブっている拠点を選択する。
集合で表現する
SELECT
center
FROM
Materials
GROUP BY
center
HAVING COUNT(*) <> COUNT(DISTINCT material);
結果
center |
---|
東京 |
名古屋 |
SELECT
center,
CASE
WHEN COUNT(*) <> COUNT(DISTINCT material) THEN 'ダブりあり'
ELSE 'ダブりなし'
END AS status
FROM
Materials
GROUP BY
center;
結果
center | status |
---|---|
大阪 | ダブりなし |
東京 | ダブりあり |
福岡 | ダブりなし |
名古屋 | ダブりあり |
述語で表現する
SELECT
center,
material
FROM
Materials M1
WHERE
EXISTS(
SELECT *
FROM Materials M2
WHERE
M1.center = M2.center
AND M1.receive_date <> M2.receive_date
AND M1.material = M2.material
);
結果
center | material |
---|---|
東京 | 亜鉛 |
東京 | 亜鉛 |
名古屋 | チタン |
名古屋 | 炭素鋼 |
名古屋 | 炭素鋼 |
名古屋 | チタン |

6.7 関係除算でバスケット分析
Items
item |
---|
ビール |
紙オムツ |
自転車 |
ShopItems
shop | item |
---|---|
仙台 | ビール |
仙台 | 紙オムツ |
仙台 | 自転車 |
仙台 | カーテン |
東京 | ビール |
東京 | 紙オムツ |
東京 | 自転車 |
大阪 | テレビ |
大阪 | 紙オムツ |
大阪 | 自転車 |
Items テーブルのすべての商品そろえている店舗を選択する。
剰余を持った除算
ビールと紙オムツと自転車をすべて置いている仙台店と東京店を選択する。
SELECT
SI.shop
FROM
ShopItems SI
INNER JOIN Items I
ON SI.item = I.item
GROUP BY
SI.shop
HAVING
COUNT(SI.item) = (
SELECT COUNT(item)
FROM Items
);
結果
shop |
---|
仙台 |
東京 |
厳密な関係除算
カーテンを置いている仙台店も除外して、東京店のみ選択する。
SELECT
SI.shop
FROM
ShopItems SI
LEFT OUTER JOIN Items I
ON SI.item = I.item
GROUP BY
SI.shop
HAVING
COUNT(SI.item) = (SELECT COUNT(item) FROM Items) -- 条件1
AND COUNT(I.item) = (SELECT COUNT(item) FROM Items); -- 条件2
結果
shop |
---|
東京 |
ShopItems テーブルと Items テーブルの外部結合の結果
shop | SI.item | I.item |
---|---|---|
仙台 | ビール | ビール |
仙台 | 紙オムツ | 紙オムツ |
仙台 | 自転車 | 自転車 |
仙台 | カーテン | NULL |
東京 | ビール | ビール |
東京 | 紙オムツ | 紙オムツ |
東京 | 自転車 | 自転車 |
大阪 | テレビ | NULL |
大阪 | 紙オムツ | 紙オムツ |
大阪 | 自転車 | 自転車 |
条件 1 によって COUNT(SI.item) = 4
の仙台店が除外され、条件 2 によって COUNT(I.item) = 2
の大阪点が除外される。
要件を満たさなかった店舗についても表示する
SELECT
SI.shop,
COUNT(SI.item) AS my_item_cnt,
(SELECT COUNT(item) FROM Items) - COUNT(SI.item) AS diff_cnt
FROM
ShopItems SI
INNER JOIN Items I
ON SI.item = I.item
GROUP BY
SI.shop;
結果
shop | my_item_cnt | diff_cnt |
---|---|---|
仙台 | 3 | 0 |
大阪 | 2 | 1 |
東京 | 3 | 0 |