達人に学ぶSQL徹底指南書 第2版 Chapter10 SQLで数列を扱う

SQL勉強メモ

出典: 

の輪読会資料

まとめ

  • SQLのデータの扱い方は2通り

    • 順序のない集合

      • 集合と述語
    • 順序集合

      • ウィンドウ関数
  • 全称文の表し方は2通り

    • 存在文と2重否定に同値変形(5章)

      • ∀xPx = ¬∃x¬Px
    • 【補】集約(6章)

      • 「ぜんぶおなじ」: MIN = MAX

        • 比較可能なデータ型であること(全順序)
      • もっと複雑なやつ: SUM(特性関数) とか

        • 複雑な部分を特性関数に押し込める

はじめに

  • 関係モデルのデータ構造には「順序」という概念がない
  • RDBのテーブルやビューにも行列の順序がない

    • ORDER BYはカーソル定義の一部
  • SQLも、もともと順序集合を扱う目的ではできていない

    • ので、もともと順序集合を扱う文化とは異なるアプローチをとった

      • 述語論理の量子化

        • 後で出てくる「単調増加と単調減少」のような例
      • 順序数を定義する再帰集合

        • 【補】p.57

          • 0 := φ
          • 1 := {0}
          • 2 := {0, 1}
  • 近年、ウィンドウ関数で順序集合を直接的に扱えるようになった
  • 新旧の解法を比較し、本質をとらえるのが趣旨

連番を作ろう

00から99までの100個の数の中には、0,1,2,…9の各数字はそれぞれ何個含まれるか

00 01 02 03 04 05 06 07 08 09
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
  • 計20個

    • 10の位: 10個
    • 1の位: 10個

ある数を「文字列」として捉えると、各位の数字の組み合わせとして表現できる

0-99の連番を作る

1-542の連番を作る

シーケンスビューを作る

シーケンスビューから1-100まで取得

sqlite online

【補】0-255の連番をつくる

べつに10進数にこだわる必要もない

sqlite online

この連番生成方法について

![20181226054549](../../../imgs/20181226054549.png)
勉強に飽きて唐突にお絵描きしたくなった
  • 数の「順序」という性質を使っていない

    • 各位の数字の組み合わせ = 集合 = 順序がない

      • 集合の元は<10の位, 1の位>という順序対なので注意

欠番を全部求める

欠番のない連番との差分を求めればよい

差集合演算

sqlite online

【演習】NOT EXIST

-- 欠番を求める -- 外部結合/中間テーブル
SELECT Sequence.seq AS seq_dense,
       SeqTbl.seq   AS seq_sparse
  FROM Sequence
  LEFT OUTER JOIN SeqTbl ON seq_dense = seq_sparse
 WHERE seq_dense BETWEEN (SELECT MIN(SeqTbl.seq) FROM SeqTbl) 
                     AND (SELECT MAX(SeqTbl.seq) FROM SeqTbl);

【演習】外部結合

-- 欠番を求める -- 外部結合/中間テーブル
SELECT Sequence.seq AS seq_dense,
       SeqTbl.seq AS seq_sparse
  FROM Sequence
  LEFT OUTER JOIN SeqTbl ON seq_dense = seq_sparse
 WHERE seq_dense BETWEEN (SELECT MIN(SeqTbl.seq) FROM SeqTbl) 
                     AND (SELECT MAX(SeqTbl.seq) FROM SeqTbl);
seq_dense seq_sparse
1 1
2 2
3 null
4 4
5 5
6 6
7 7
8 8
9 null
10 null
11 11
12 12
-- 欠番を求める -- 外部結合
SELECT Sequence.seq AS seq
  FROM Sequence
  LEFT OUTER JOIN SeqTbl ON Sequence.seq = SeqTbl.seq
 WHERE Sequence.seq BETWEEN (SELECT MIN(SeqTbl.seq) FROM SeqTbl) 
                        AND (SELECT MAX(SeqTbl.seq) FROM SeqTbl)
   AND SeqTbl.seq IS NULL;
seq
3
9
10

3人なんですけど、座れますか?

sqlite online

NOT EXISTS

  • 全座席を対象に、連続3座席をピックアップ
  • 間がすべて空の席ならOK

OK

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

NG

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

ウィンドウ関数

  • 空の席だけを対象に、連続3座席をピックアップ(seat ~ end_seat)
  • 全座席でみたときに、seat ~ end_seat が連続3座席になっていればOK

OK

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

NG

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

【補】前者の考え方で、ウィンドウ関数で書けないか?

  • 全座席を対象に、連続3座席をピックアップ
  • 間がすべて空の席ならOK

OK

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

NG

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  • こっちの方が断然直感的じゃないですか

    • 「3」という知識が1回しか出てこないのが良い
  • ウィンドウ関数でこれを書けないか? => あまり綺麗になりませんでした

クソクエリ

-- 人数分の空席を探す:補1 ウィンドウ関数
SELECT seat,
       '~',
       seat + seat_count - 1 AS end_seat
  FROM (SELECT seat,
               MIN(status) OVER W AS lower_status,
               MAX(status) OVER W AS upper_status,
               -- 3シート集計してくれる保証はない
               -- 14, 15 で終わりとか
               COUNT(seat) OVER W AS seat_count
          FROM Seats
        -- 3シートずつ集計する
        WINDOW W AS (ORDER BY seat
                         ROWS BETWEEN CURRENT ROW
                                  AND (3 - 1) FOLLOWING))

 WHERE seat_count = 3      -- ちゃんと3シート集計してくれていて
   AND upper_status = '空' -- ウィンドウ関数の集計範囲のシートが全部同status
   AND lower_status = '空' -- かつ、そのstatusというのが空
;
  • 「連続3座席が全部空」というのをMAX(status) = MIN(status) = 空 で表現

    • cf. 6節 HAVING句の力 p.123
  • 「3座席」である保証がないのでCOUNTも必要なのが残念

    • (1,2,3), (2,3,4), … ,(13,14,15), (14,15), (15)
    • これのせいで「3」という知識をまとめることはできない
  • 同じウィンドウ関数Wを何度も適用しているのが見るからにやばい

まあまあクエリ

-- 人数分の空席を探す:補2 ウィンドウ関数 + 特性関数
SELECT seat,
       '~',
       seat + seat_available_count - 1 AS end_seat
  FROM (SELECT seat,
               -- 空シートを1、それ以外をNULLとする特性関数を通して総和
               -- 集計対象のシートが全部空なら、集計件数になる
               SUM(CASE WHEN status = '空' THEN 1
                        ELSE NULL
                        END) OVER W AS seat_available_count
          FROM Seats
        -- 3シートずつ集計する
        WINDOW W AS (ORDER BY seat
                         ROWS BETWEEN CURRENT ROW
                                  AND (3 - 1) FOLLOWING))

 WHERE seat_available_count = 3  -- 集計範囲で3シート空いていること
 ;
  • コメントを消して冒頭のSELECT句を1行にまとめれば、p.206のクエリと同じくらいの行数
  • 「連続3座席が全部空」というのをSUM(特性関数(status)) = COUNT(status) = 3 で表現

    • 1、他をNULLとする
  • 「3座席」である保証がないのでCOUNTも必要なのはおなじ

折り返しのある数列

  • 座席を座席列line_idごとに類別すればいいだけ

    • ウィンドウ関数は、PARTITION BY句として直接サポート

(狭義の)単調増加と単調減少

sqlite online

  • a_0 < a_1 < a_2 < ... ってやつ
  • 狭義なので a_0 <= a_1 <= a_2 < ... は含めない

【補】別解

-- 自己結合でシーケンスをグループ化
SELECT MIN(deal_date) AS start_date,
       '~',
       MAX(deal_date) AS end_date
  FROM (SELECT M1.deal_date,
               COUNT(M2.row_num) - MIN(M1.row_num) AS gap
          FROM MyStockUpSeq M1 INNER JOIN MyStockUpSeq M2
            ON M2.row_num <= M1.row_num
         GROUP BY M1.deal_date) TMP
 GROUP BY gap;

-- ウィンドウ関数による解法
SELECT MIN(deal_date) AS start_date,
       '~',
       MAX(deal_date) AS end_date
  FROM (SELECT deal_date,
               ROW_NUMBER() OVER (ORDER BY row_num) - row_num AS gap
          FROM MyStockUpSeq) TMP
 GROUP BY gap;

でもいい

別解解説

教科書の解答: サブクエリ内で自己結合している

p.211の解答のサブクエリをほぐすと、以下に行きつく

-- 自己結合でシーケンスをグループ化
SELECT M1.deal_date,
       COUNT(M2.row_num) cnt,
       MIN(M1.row_num) min_row_num,
       COUNT(M2.row_num) - MIN(M1.row_num) AS gap
  FROM MyStockUpSeq M1 INNER JOIN MyStockUpSeq M2
    ON M2.row_num <= M1.row_num
 GROUP BY M1.deal_date;
deal_date cnt minrownum gap
2018-01-08 1 2 -1
2018-01-16 2 7 -5
2018-01-17 3 8 -5
2018-01-18 4 9 -5

cntを求めるために自己結合している

M1

deal_date minrownum
2018-01-08 2
2018-01-16 7
2018-01-17 8
2018-01-18 9

M2

deal_date minrownum
2018-01-08 2
2018-01-16 7
2018-01-17 8
2018-01-18 9

M1

deal_date minrownum
2018-01-08 2
2018-01-16 7
2018-01-17 8
2018-01-18 9

M2

deal_date minrownum
2018-01-08 2
2018-01-16 7
2018-01-17 8
2018-01-18 9

同じことをウィンドウ関数でやる

結局欲しいのはこれ

deal_date cnt row_num gap
2018-01-08 1 2 -1
2018-01-16 2 7 -5
2018-01-17 3 8 -5
2018-01-18 4 9 -5

ウィンドウ関数で求めるとこうなる

-- ウィンドウ関数でもいい
SELECT deal_date,
       ROW_NUMBER() OVER (ORDER BY row_num) AS cnt,
       row_num,
       ROW_NUMBER() OVER (ORDER BY row_num) - row_num AS gap
  FROM MyStockUpSeq;