の輪読会資料
まとめ
-
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まで取得
【補】0-255の連番をつくる
べつに10進数にこだわる必要もない
この連番生成方法について
-
数の「順序」という性質を使っていない
-
各位の数字の組み合わせ = 集合 = 順序がない
- 集合の元は
<10の位, 1の位>
という順序対なので注意
- 集合の元は
-
欠番を全部求める
欠番のない連番との差分を求めればよい
差集合演算
【演習】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人なんですけど、座れますか?
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
句として直接サポート
- ウィンドウ関数は、
(狭義の)単調増加と単調減少
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;