1. 程式人生 > >《sql進階教程》之用 SQL 處理數列

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