1. 程式人生 > 其它 >SQL視窗函式踩坑筆記

SQL視窗函式踩坑筆記

技術標籤:mysqlsqlsqlite

誰也不能否認,編寫SQL語句是一個數據分析師的重要(甚至是最重要)的技能之一,而Python/R之類,有時候承擔的只是膠水語言的功能(比方說最近有個出Excel日報的工作,Python就很好地替代了大量人工勞動)。SQL是資料分析的生產工具,因為企業級的生產資料幾乎都存放在資料庫或資料倉庫中(企業中一般是線上儲存的),而對它們進行操作的幾乎都屬於SQL的某個分支。但SQL這個東西,初學容易、入門簡單、精通困難,實在是事業成功的攔路虎。

我這麼說的理由有如下幾點:

  • 各種資料庫支援的SQL語句有區別,標準不統一

  • 個人難以建立複雜的應用場景, 導致SQL技能在實習和工作中才能提升

  • 一些SQL技巧的使用方法難以使用小型可重複性例子進行展示

  • SQL非程式語言,一些程式語言幾行程式碼能解決的任務SQL往往需要更長的程式碼

首先,SQL的分支實在太多了,而同一種SQL又因為不同的版本而不同。比如MySQL 8.0之前的版本是沒有視窗函式的。第二點的意思是,如果你沒有在公司的資料分析崗位寫過SQL的話,接觸到工業級應用場景的機會幾乎為0。因為自己本地能搭建的資料庫幾乎都是玩具資料,關聯邏輯簡單、資料量小。而LeeCode等平臺的SQL面試題類的程式碼往往更強調準確性,真實的應用場景則往往會非常多地考慮效能要求。第三點,不同於Python/R,如果需要復現某種SQL技巧,往往需要自己造資料。這個過程就非常複雜而且不方便。不像Python或R可以利用自帶的資料集復現幫助文件中的程式碼。最後一點,程式語言的邏輯幾乎都是 輸出=函式(輸入),而SQL中沒有這個等號,這種思維方式和程式語言差別還是挺大的。

我的SQL在工作前一直很爛。直到工作後我才慢慢上手,甚至能寫一些複雜的邏輯了。但現在我仍不敢說自己在寫SQL上都多大能耐,因為我目前只大量使用了Apache Doris(支援MySQL協議但和純MySQL還是有區別)這一種資料庫。此外我SQL差勁的原因大概是覺得寫SQL真的很無聊吧。

視窗函式

SQL對於分組的情況預設會聚合結果並且減少行數,那麼如何實現不聚合卻分組的計算呢?比方說分組排序這種任務?這裡就會用到視窗函數了。視窗函式被視作SQL的高階功能,甚至和OLAP(Online Analytical Processing)這個名稱關聯上了哈哈,這裡稍微總結下視窗函式吧。在參加工作之前之前我完全沒了解過這塊內容,現在對之前踩過的坑做個總結。

比方說如下的題目:

表結構如下:
Student(Sid,Sname,Sage,Ssex) 學生表 sid為key
Course(Cid,Cname,Tid) 課程表 cid 為key
SC(Sid,Cid,score) 成績表 (sid, cid) 為key
Teacher(Tid,Tname) 教師表 tid為key

查詢選修“王五”老師所授課程的學生中,成績最高的學生學號、姓名及其成績

我最初給出的答案是這樣:

select Sid, Sname,score
from (
  select sc.Sid as Sid
     , Sname
     , score 
     , row_number() over (order by score desc) as ranking
  from SC sc
  join Course on c.Cid = sc.Cid
  join Teacher t on t.Tid = c.Tid
  join Student s on s.Sid = sc.Sid
  where Tname = '王五'
) a where rank = 1

乍一看答案沒有錯,但是這裡沒有考慮到相同分數的情況。如果前2名分數一致,那麼應該都算第1名。所以這裡的row_number()函式應該改為rank()。為了更好地展示這些視窗函式的區別,我用資料庫的真實資料來示範一下。注意這裡沒有進行分組操作(也就是沒有partition by),不帶partition by的視窗函式也可以使用,即單純地對所有查詢結果排序。

select cost
     , vendor_adunit_id
     , row_number() over (order by vendor_adunit_id desc) as ranking
from vendor_creative_report
where date = date_sub(curdate(), 1) and vendor_account_id = 189
limit 10
vendor_adunit_idranking
984750211
974521572
972289763
970583504
950575865
616034106
616034107
597304248
597291029
5972910210

首先使用row_number(),可以看到就是無腦排了個序。

select vendor_adunit_id
     , rank() over (order by vendor_adunit_id desc) as ranking
from vendor_creative_report
where date = date_sub(curdate(), 1) and vendor_account_id = 189
limit 10
vendor_adunit_idranking
984750211
974521572
972289763
970583504
950575865
616034106
616034106
597304248
597291029
597291029

接著我們試下rank(),我們發現出現了2個第6名,它們都被算作第6名,而之後的被算作第8名,這種排序方式更加合理。

select vendor_adunit_id
     , dense_rank() over (order by vendor_adunit_id desc) as ranking
from vendor_creative_report
where date = date_sub(curdate(), 1) and vendor_account_id = 189
limit 10
vendor_adunit_idranking
984750211
974521572
972289763
970583504
950575865
616034106
616034106
597304247
597291028
597291028

稍有區別的是dense_rank(),我們可以看到即便有兩個第6名,接下來的仍然被視為第7名。這種排序方法在特殊的業務場景下可能會用到。

總結

這篇文章用了一個demo總結了下SQL分組排序的視窗函式。當然美中不足的是大家沒辦法復現上面的程式碼了,這也是SQL難上手的一個方面吧。今後我儘量多總結一些工作中遇到的資料庫內容,誰讓我的工作大部分是在和SQL打交道呢?

在這裡插入圖片描述
掃碼關注公眾號ApocalypseNow,分享好玩的資料分析