MySQL -- INNER JOIN 業務實戰

引言

  非常常見的面試題,也是平時練手的經典題,把知識點串起來的同時也很好的聯繫了業務實際。直接將代碼或思路背誦記憶同樣可在相似場景中發揮作用。

業務背景

企業/單位 績效檢查類需求

  • 檢索各項工作都達標的員工的信息(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),也可以自己添加一些括號,別名。這樣看到大段代碼也不會慌了,加油!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章