引言
非常常見的面試題,也是平時練手的經典題,把知識點串起來的同時也很好的聯繫了業務實際。直接將代碼或思路背誦記憶同樣可在相似場景中發揮作用。
業務背景
企業/單位 績效檢查類需求
- 檢索各項工作都達標的員工的信息(n 項工作都達到了基本指標,指標可以是分數,等級等)
- 檢索工作項項目 A 完成得比項目 B 好的員工的個人信息,即找到員工們各自的優勢,若景常發現這一部分員工更擅長項目 A,而另一部分員工更擅長 B(完成 B 的質量比 A
高),則可適當調整未來的工作計劃與安排。 - 檢索每個項目都做得不錯的員工,即每個項目如 A,B,C 都達到了目標分數 n 或目標等級x,可更好的準備年終獎。
準備 & 用到的知識
本次實戰用到的數據雖然看起來比較 low,老生常談的學生成績查詢,但只要將列名稍作更改,併發揮一定的想象力,就不難發現與業務背景所說的看似高大上的需求有異曲同工之妙。
- 三張表
- SQL 編輯器(筆者 dbForge Studio)
- SQL 基本知識(SELECT, GROUP BY, AS, 多表聯結 join)
本次實戰將會瘋狂的使用 join,所以整潔的代碼格式對實現業務需求功不可沒。
題目要求 & 效果前後
- 用一條SQL語句查詢每門課程都是大於80分的學生基本信息(id & name)
- 查詢“語文”課程比“數學”課程成績高的學生的信息以及課程分數
- 查詢平均成績大於65分的學生的id和姓名以及平均成績
流程分析 & 實現步驟
需求1:用一條SQL語句查詢每門課程都是大於80分的學生基本信息(id & name)
id,學生名,課程名,分別在不同的表格,首先連接起來是必須的,連接的方式有多種,這裏選擇 INNER JOIN,因爲需要自帶去重效果,沒有說保留哪邊捨棄哪邊一說(LEFT/RIGHT JOIN),畢竟有可能出現髒數據(某學生只錄入了 sid,而沒有錄入學生姓名),總的來說,INNER JOIN 是“嚴格 join”
-- 查詢每門課程都是大於 80 分的同學
SELECT
s.Sid
/* 這種格式寫代碼比較整潔
1. 可以直接註釋掉一行而不影響其他行
2. 可在一行末尾添加註釋而不影響其他行
3. 可提醒自己不要漏掉括號*/
, s.Sname
, sc.score
FROM
sc
INNER JOIN s
ON s.Sid = sc.Sid
INNER JOIN c
ON c.Cid = sc.Cid
;
問題拆解:每門課都高於 80 分,關鍵在於轉化表述方式,腦海翻譯成 SQL 代碼,如下展示多個 “翻譯” 版本。
- 每門課高於 80即平均分高於 80?嘗試求這幾門課的平均分 – 否決:可能有高分科目將 79 分的科目拉高
- 每門課都高於 80,最差的一科也要高於 80?最差 – MIN – 可以值得一試
下面將代碼排版縮減(佔空間少,但當然不夠上一段代碼那麼清晰簡潔)
SELECT s.Sid, s.Sname
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
GROUP BY s.Sname HAVING MIN(sc.score) > 80;
這裏需要 group by 一下,因爲學生名字會有重複,最後我們顯示的是不重複的學生名字,代碼 MIN(sc.score) > 80 不難理解,但爲什麼是放在 group by 後面,接在 having 後呢?having 放在 group by 後,where 在 group by 前已經是不成文的規定了。解釋:若代碼 having 及後面改成在 group by 前,則無法運行
SELECT s.Sid, s.Sname
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
WHERE MIN(sc.score) > 80
-- 如果上一行是最原始的 WHERE sc.score > 80,
-- 那當然不會報錯,添加了聚合函數,就意味着你希望的篩選動作是在分組後執行
GROUP BY s.Sname; -- 哪怕這一行去掉,也還是會報錯
則含義爲篩選最低分科目是大於 80 分的(首先中文語義就已經不通順了)。再者,SQL 語法中 group by 存在時,若想使用帶聚合函數的篩選條件,應在 group by 後使用 HAVING ,而不是在 group by 前就着急的使用 WHERE + 聚合函數。原因:WHERE 是對 初始表 的行進行篩選,而我們現在是需要對 分組之後 的數據進行篩選(使用了聚合函數),就已經不是最原始的行了,所以再使用 WHERE 就自然會報錯。如果對這句話不理解的朋友,可以在腦海中想一下 這段 SQL 代碼的閱讀順序,從(from) 某表中讀入數據 --> 拼接(join)其他表後 --> 按照某個要求分組(group by) --> 最後以 select 的形式呈現出來。我們希望的 MIN(sc.score) > 80 這個篩選條件是對每一組的同學都使用一下。
SELECT s.Sid, s.Sname
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
GROUP BY s.Sname HAVING MIN(sc.score) > 80;
效果圖
如果覺得上面的一段話理解起來有難度,則可以參考一下下面三種不同的解釋方式
- 聚合函數不能放在 where 後面!where sc.score > 80 表示普通的過濾,添加了聚合函數變成 where MIN(sc.score > 80 後,便表示篩選,“ 篩選 ” 與 “ 過濾 ” 這兩個詞的意思並不一樣)
- 以下段代碼爲例(從代碼閱讀順序不同來切入),where 後不添加聚合函數時
SELECT s.Sid, s.Sname
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
WHERE sc.score > 80
GROUP BY s.Sname;
閱讀順序爲 從(from) sc 表中讀入數據,根據要求 on 內連接(inner join) s,c 兩表,對連接後的表(相對於 group by 前來說還是 “ 原來的表 ” 進行 where 條件篩選,對篩選完後的表進行分組 group by) – 可以運行
- 以下段代碼爲例,where 後添加聚合函數時
SELECT s.Sid, s.Sname
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
WHERE MIN(sc.score) > 80
GROUP BY s.Sname;
聚集函數的別稱爲列函數,是基於整列數據進行計算的,而 where 子句則是對數據行進行過濾(過濾 和 篩選 兩個詞的意義不同)的,在篩選過程中依賴 “ 基於已經篩選完畢的數據得出的計算結果 ” 是一種悖論,這是行不通的。更簡單地說,因爲聚集函數要對全列數據時行計算,因而使用它的前提是:結果集已經確定!而where子句還處於“確定”結果集的過程中,因而不能使用聚集函數。
需求2:-- 查詢“語文”課程比“數學”課程成績高的學生的信息以及課程分數
對於該需求,我們不必催毛求疵一步完美實現,畢竟需求的實現並不是一蹴而就的。
問題分析
- 會不會有的同學並沒有參加語文和數學這兩門考試,或者缺考了其中一門
- 打印數據,這裏使用 inner join 去掉了那些可能存在名字或者學號錄入失誤的學生信息
SELECT
s.Sid
, s.Sname
, c.Cname
, sc.score
FROM sc
INNER JOIN s
ON s.Sid = sc.Sid
INNER JOIN c
ON c.Cid = sc.Cid
;
發現的確有同學存在缺考這兩門的情況。那接下來就好辦了,先簡單的分批打印一下數據,語文成績表一個,數學成績表一個,先看看概覽。
驚喜的發現似乎只有 吳蘭 和 鄭竹 這兩位同學比較突出,如果能將他們去掉就好了,當我們將兩個表的信息都打印出來以後,自然就能想到如果能在任意一個表上多添加一列另一科目的成績列,再去掉一下突出者(inner join 的天然作用),最後在過濾一下數據(where),就完美了。這時我們不妨在思路中這樣構思
關鍵是這兩個表的 “ 建表代碼 ” 又已經確定,所以只需要中間來個 inner join 就行了,如果按照整潔的老司機寫法,代碼長度看起來會比較大,如下:
-- 查詢“語文”課程比“數學”課程成績高的學生的信息以及課程分數
SELECT
chinese_table.Sid
, chinese_table.Sname
, chinese_table.chinese
, math_table.math
FROM
-- 僅含語文成績列的表
(SELECT
s.Sid
, s.Sname
, c.Cname
, sc.score AS chinese
FROM
sc
INNER JOIN s
ON s.Sid = sc.Sid
INNER JOIN c
ON c.Cid = sc.Cid
WHERE c.Cname = '語文') AS chinese_table
INNER JOIN -- 大“INNER JOIN”
-- 僅含數學成績列的表
(SELECT
s.Sid
, s.Sname -- select 的內容可以適當減小,這裏都是按照最詳細的來列
, c.Cname
, sc.score AS math
FROM
sc
INNER JOIN s
ON s.Sid = sc.Sid
INNER JOIN c
ON c.Cid = sc.Cid
WHERE c.Cname = '數學') AS math_table
-- 兩個小表內連接,以學生名爲連接標準
ON chinese_table.Sname = math_table.Sname
-- 呼應需求,語文成績 比 數學成績高
WHERE chinese_table.chinese > math_table.math
;
大段代碼的分框閱讀技巧跟思路圖一樣
瘦版代碼
-- 查詢“語文”課程比“數學”課程成績高的學生的信息以及課程分數
SELECT chinese_table.Sid, chinese_table.Sname
, chinese_table.chinese, math_table.math
FROM
(SELECT s.Sid, s.Sname, c.Cname, sc.score AS chinese
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
WHERE c.Cname = '語文') AS chinese_table
INNER JOIN
(SELECT
s.Sid, s.Sname, c.Cname, sc.score AS math
FROM sc
INNER JOIN s ON s.Sid = sc.Sid
INNER JOIN c ON c.Cid = sc.Cid
WHERE c.Cname = '數學') AS math_table
ON chinese_table.Sname = math_table.Sname
WHERE chinese_table.chinese > math_table.math
;
需求3:查詢平均成績大於65分的學生的id和姓名以及平均成績
懂得了 having 與 where 的區別,這個需求就是分分鐘的事情了。
SELECT
s.Sid
, s.Sname
, AVG(sc.score) AS avg_score
FROM
sc
INNER JOIN s
ON s.Sid = sc.Sid
INNER JOIN c
ON c.Cid = sc.Cid
GROUP BY s.Sid, s.Sname
HAVING AVG(sc.score) > 65
;
難易結合的三個需求
模擬面試
- 現場寫代碼(重點)
- group by 與 having 的區別,使用 where 時有什麼注意點(由淺入深的講解,注意細節)
後記
SQL 作爲數據分析師的必備技能,有許多非常精彩的技巧和用法等着我們去掌握(後續會出),另外,拿到高手的代碼時,按照簡潔版或文中的老司機寫法將代碼拆解,雖然看起來會增加代碼長度,但整潔性會給你帶來意想不到的守護哦。然後在其源代碼上添加一些框框(截圖工具 Snipaste),也可以自己添加一些括號,別名。這樣看到大段代碼也不會慌了,加油!