Open7

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

churuchuruchuruchuru

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;
churuchuruchuruchuru

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
  );
churuchuruchuruchuru

6.3 NULL を含まない集合を探す

6.3.1 COUNT(*)COUNT(列名) の違い

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
経済学部
churuchuruchuruchuru

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
churuchuruchuruchuru

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 ×
churuchuruchuruchuru

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
東京 亜鉛
東京 亜鉛
名古屋 チタン
名古屋 炭素鋼
名古屋 炭素鋼
名古屋 チタン
churuchuruchuruchuru

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