2011年7月19日 星期二

第二十回 Oracle 裏的 TOP N

剛剛又遇到同事詢問這個問題,平常我也不寫程式,都在處理資料庫的問題,一時要寫 SQL 還真寫不出來,找了一些資訊後,趕緊來筆記一下

假設一個Table S,內容如下

LOC INV
----------------- ----------
aaa 20
aaa 30
aaa 50
aaa 60
bbb 60
bbb 60
bbb 70
bbb 90
bbb 100

試著找出每個 LOC 中,INV 最多的前兩筆,用文字編輯器改一改就是要如下結果
LOC INV
---------- ----------
aaa 60
aaa 50
bbb 100
bbb 90

LOC=aaa 中 INV 最大的兩筆為 60 和 50
LOC=bbb 中 INV 最大的兩筆為 100 和 90

所以先 create 一個 Table S

create table s
( loc varchar2(20) not null,
inv number
);

然後將資料 insert
insert into s values('aaa',20);
insert into s values('aaa',30);
......後略

因為 Oracle 沒有 Top N ,所以每次在用這種找出前幾筆的 Function 時,總是很麻煩得用 ROWNUM 來做
如果是一張表格要抓前兩名,其實也不難

select * from (select * from s order by inv desc) where rownum <=2
出來的結果如下

LOC INV
---------- ----------
bbb 100
bbb 90

但是我要的不是這個東西,因此不知在 Oracle 8i 還是 9i 就有了 RANK() 和 DENSE_RANK() 這兩個函數,這邊只依我原有範例說明用法,詳細的說明 這裏 寫的更清楚。

RANK() 會產生一個 column base 在後面的 over 子句下法
所以如果要用 RANK() 產生 TOP N 的結果就如下所示的寫法

select loc,inv,rank() over (order by inv desc ) test from s
LOC INV TEST
---------- ---------- ----------
bbb 100 1
bbb 90 2
bbb 70 3
aaa 60 4
bbb 60 4
bbb 60 4
aaa 50 7

aaa 30 8
aaa 20 9

會發現多了一個 test column 上面記載了排名,特別注意這邊有三筆 60 的記錄,如果使用 row_num 這三筆會被排名為 4,5,6 ,但在 RANK() 的下法中,會統一被規類為排名4

但使用 DENSE_RANK() 時會有什麼結果呢?
select loc,inv,dense_rank() over (order by inv desc ) test from s
LOC INV TEST
---------- ---------- ----------
bbb 100 1
bbb 90 2
bbb 70 3
aaa 60 4
bbb 60 4
bbb 60 4
aaa 50 5

aaa 30 6
aaa 20 7

由此可發現使用 RANK() 時,因為第 5和第6名均被佔據了,所以下一筆會直接代入7,但使用 DENSE_RANK() ,第5名和第6名仍是存在的

但如此還是無法達成原有的需求,不過因為在 over 子句裏還有一個 partition by 的參數,可以做表分割
所以改為下列寫法
select loc,inv from (
select loc, inv,rank() over(partition by loc order by inv desc ) rank from s)
where rank <= 2;

如此就會依據 loc 去分別計算各區的排名,結果就出來了
LOC INV
---------- ----------
aaa 60
aaa 50
bbb 100
bbb 90

但是,原本我以為如果像 SQL Server 有 Top N 可用,配合 Group BY 時是可以有這種功能的,結果我錯了,SQL Server 到了 2005 後也支援 rank() Function ,才可達到類似功能,不然就得寫 SP 才做的到了。

沒有留言:

張貼留言