《sql進階教程》之用 SQL 處理數列
本文是《sql進階教程》閱讀筆記,感興趣可以閱讀該書對應章節,這本適合有一定sql基礎的同學閱讀。另外作者《sql基礎教程》也值得一看
生成連續編號
在思考這道例題之前,請先思考下面一個問題:
00 ~ 99 的 100 個數中,0, 1, 2,…, 9 這 10 個數字分別出現了多少次?
如果把數看成字串,其實它就是由各個數位上的數字組成的集合
Digits
digit( 數字 ) |
---|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
通過對兩個 Digits 集合求笛卡兒積而得出 0 ~ 99的數字。
create table Digits (
digit INTEGER
);
INSERT INTO Digits(digit)VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
-- 求連續編號(1):求0~99 的數
SELECT D1.digit + (D2.digit * 10) AS seq
FROM Digits D1 CROSS JOIN Digits D2
ORDER BY seq;
通過追加 D3、D4 等集合,不論多少位的數都可以生成。而且,如果只想生成從 1 開始,或者到 542 結束的數,只需在 WHERE子句中加入過濾條件就可以了。
-- 求連續編號(2):求1~542 的數
SELECT D1.digit + (D2.digit * 10) + (D3.digit * 100) AS seq
FROM Digits D1 CROSS JOIN Digits D2
CROSS JOIN Digits D3
WHERE D1.digit + (D2.digit * 10)
+ (D3.digit * 100) BETWEEN 1 AND 542
ORDER BY seq;
通過將這個查詢的結果儲存在視圖裡,就可以在需要連續編號時通過簡單的 SELECT 來獲取需要的編號
-- 生成序列檢視(包含0~999)
CREATE VIEW Sequence (seq)
AS SELECT D1.digit + (D2.digit * 10) + (D3.digit * 100)
FROM Digits D1 CROSS JOIN Digits D2
CROSS JOIN Digits D3;
-- 從序列檢視中獲取1~100
SELECT seq
FROM Sequence
WHERE seq BETWEEN 1 AND 100
ORDER BY seq;
求全部的缺失編號
查詢連續編號中的缺失編號的方法。作為示例,假設存在下面這樣一張編號有缺失的表。
Seqtbl
seq(連續編號) |
---|
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
缺失的編號 3、9、10
--EXCEPT 版
SELECT seq
FROM Sequence
WHERE seq BETWEEN 1 AND 12
EXCEPT
SELECT seq FROM SeqTbl;
--NOT IN 版
SELECT seq
FROM Sequence
WHERE seq BETWEEN 1 AND 12
AND seq NOT IN (SELECT seq FROM SeqTbl);
-- 動態地指定連續編號範圍的SQL 語句
--像下面這麼做效能會有所下降,但是通過擴
--展 BETWEEN 謂詞的引數,可以動態地指定目標表的最大值和最
SELECT seq
FROM Sequence
WHERE seq BETWEEN (SELECT MIN(seq) FROM SeqTbl)
AND (SELECT MAX(seq) FROM SeqTbl)
EXCEPT
SELECT seq FROM SeqTbl;
這種寫法在查詢上限和下限未必固定的表時非常方便。兩個自查詢沒有相關性,
而且只會執行一次。如果在“seq”列上建立索引,那麼極值函式的執行可以變得更快速。
三個人能坐得下嗎
預約火車票或機票時考慮連坐問題
Seats
seat ( 座位 ) | status ( 狀態 ) |
---|---|
1 | 已預訂 |
2 | 已預訂 |
3 | 未預訂 |
4 | 未預訂 |
5 | 未預訂 |
6 | 已預訂 |
7 | 未預訂 |
8 | 未預訂 |
9 | 未預訂 |
10 | 未預訂 |
11 | 未預訂 |
12 | 已預訂 |
13 | 已預訂 |
14 | 未預訂 |
15 | 未預訂 |
要求:找出連續 3 個空位的全部組合
把由連續的整數構成的集合,也就是連續編號的集合稱為“序列”。這樣序列中就不能出現缺失編號。
- 3~5
- 7 ~9
- 8 ~ 10
- 9 ~ 11
(7, 8, 9, 10, 11) 這個序列中,包含 3 個子序列 (7, 8, 9)、(8, 9, 10)、(9,10, 11),我們也把它們當成不同的序列。還有,通常火車的一排只有幾個座位,所以可能我們表裡的座位會分佈在幾排裡,但我們暫時忽略掉這個問題,假設所有的座位排成了一條直線
藉助上面的圖表我們可以知道,需要滿足的條件是,以 n 為起點、 n+(3-1) 為終點的座位全部都是未預訂狀態(請注意如果不減 1,會多取一個座位)
-- 找出需要的空位(1):不考慮座位的換排
SELECT S1.seat AS start_seat, '~' , S2.seat AS end_seat
FROM Seats S1, Seats S2
WHERE S2.seat = S1.seat + (:head_cnt -1) -- 決定起點和終點
AND NOT EXISTS
(SELECT * FROM Seats S3
WHERE S3.seat BETWEEN S1.seat AND S2.seat
AND S3.status <> '未預訂'
);
注:“:head_cnt ”
是表示需要的空位個數的引數
接下來看一下這道例題的升級版,即發生換排的情況。假設這列火車每一排有 5 個座位。我們在表中加上表示行編號
row_id
列。
Seats2
seat ( 座位 ) | row_id( 行編號 ID) | status ( 狀態 ) |
---|---|---|
1 | A | 已預訂 |
2 | A | 已預訂 |
3 | A | 未預訂 |
4 | A | 未預訂 |
5 | A | 未預訂 |
6 | B | 已預訂 |
7 | B | 未預訂 |
8 | B | 未預訂 |
9 | B | 未預訂 |
10 | B | 未預訂 |
11 | C | 未預訂 |
12 | C | 已預訂 |
13 | C | 已預訂 |
14 | C | 未預訂 |
15 | C | 未預訂 |
-- 找出需要的空位(2):考慮座位的換排
SELECT S1.seat AS start_seat, '~' , S2.seat AS end_seat
FROM Seats2 S1, Seats2 S2
WHERE S2.seat = S1.seat + (:head_cnt -1) -- 決定起點和終點
AND NOT EXISTS
(SELECT *
FROM Seats2 S3
WHERE S3.seat BETWEEN S1.seat AND S2.seat
AND ( S3.status <> '未預訂'
OR S3.row_id <> S1.row_id));
最多能坐下多少人
按現在的空位狀況,最多能坐下多少人”。換句話說,要求的是最長的序列
Seats3
seat ( 座位 ) | status ( 狀態 ) |
---|---|
1 | 已預訂 |
2 | 未預訂 |
3 | 未預訂 |
4 | 未預訂 |
5 | 未預訂 |
6 | 已預訂 |
7 | 未預訂 |
8 | 已預訂 |
9 | 未預訂 |
10 | 未預訂 |
長度為 4 的序列“2 ~ 5”就是我們的答案
條件 1:起點到終點之間的所有座位狀態都是“未預訂”。
條件 2:起點之前的座位狀態不是“未預訂”。
條件 3:終點之後的座位狀態不是“未預訂”
-- 第一階段:生成儲存了所有序列的檢視
CREATE VIEW Sequences (start_seat, end_seat, seat_cnt) AS
SELECT S1.seat AS start_seat,
S2.seat AS end_seat,
S2.seat - S1.seat + 1 AS seat_cnt
FROM Seats3 S1, Seats3 S2
WHERE S1.seat <= S2.seat -- 第一步:生成起點和終點的組合
AND NOT EXISTS -- 第二步:描述序列內所有點需要滿足的條件
(SELECT *
FROM Seats3 S3
WHERE ( S3.seat BETWEEN S1.seat AND S2.seat
AND S3.status <> '未預訂') -- 條件1 的否定
OR (S3.seat = S2.seat + 1 AND S3.status = '未預訂' )
-- 條件2 的否定
OR (S3.seat = S1.seat - 1 AND S3.status = '未預訂' ));
-- 條件3 的否定
單調遞增和單調遞減
假設存在下面這樣一張反映了某公司股價動態的表
MyStock
deal_date( 交易日期 ) | price( 股價 ) |
---|---|
2007-01-06 | 1000 |
2007-01-08 | 1050 |
2007-01-09 | 1050 |
2007-01-12 | 900 |
2007-01-13 | 880 |
2007-01-14 | 870 |
2007-01-16 | 920 |
2007-01-17 | 1000 |
-- 生成起點和終點的組合的SQL 語句
SELECT S1.deal_date AS start_date,
S2.deal_date AS end_date
FROM MyStock S1, MyStock S2
WHERE S1.deal_date < S2.deal_date;
-- 求單調遞增的區間的SQL 語句:子集也輸出
SELECT S1.deal_date AS start_date,
S2.deal_date AS end_date
FROM MyStock S1, MyStock S2
WHERE S1.deal_date < S2.deal_date -- 第一步:生成起點和終點的組合
AND NOT EXISTS
( SELECT * -- 第二步:描述區間內所有日期需要滿足的條件
FROM MyStock S3, MyStock S4
WHERE S3.deal_date BETWEEN S1.deal_date AND S2.deal_date
AND S4.deal_date BETWEEN S1.deal_date AND S2.deal_date
AND S3.deal_date < S4.deal_date
AND S3.price >= S4.price
);
-- 排除掉子集,只取最長的時間區間
SELECT MIN(start_date) AS start_date, -- 最大限度地向前延伸起點
end_date
FROM (SELECT S1.deal_date AS start_date,
MAX(S2.deal_date) AS end_date -- 最大限度地向後延伸終點
FROM MyStock S1, MyStock S2
WHERE S1.deal_date < S2.deal_date
AND NOT EXISTS
(SELECT *
FROM MyStock S3, MyStock S4
WHERE S3.deal_date BETWEEN S1.deal_date AND S2.deal_date
AND S4.deal_date BETWEEN S1.deal_date AND S2.deal_date
AND S3.deal_date < S4.deal_date
AND S3.price >= S4.price)
GROUP BY S1.deal_date
) TMP
GROUP BY end_date;