一道面試題:寫出SQL語句實現下述功能


在這裏,感謝@瑾同學,給我發過來一道特別有趣的SQL題目,題目如下:

題目: 根據表結構寫出SQL語句實現下述功能

創建四張表如下:

-- 學生表
CREATE TABLE `Student`
(
    `s_id`   VARCHAR(20), -- 學生學號
    `s_name` VARCHAR(20), -- 學生姓名
    PRIMARY KEY (`s_id`)
);

-- 課程表
CREATE TABLE `Course`
(
    `c_id`   VARCHAR(20), -- 課程編號
    `c_name` VARCHAR(20), -- 課程名稱
    `t_id`   VARCHAR(20), -- 教師編號
    PRIMARY KEY (`c_id`)
);

-- 教師表
CREATE TABLE `Teacher`
(
    `t_id`   VARCHAR(20), -- 教師編號
    `t_name` VARCHAR(20), -- 教師姓名
    PRIMARY KEY (`t_id`)
);

-- 成績表
CREATE TABLE `Score`
(
    `s_id`    VARCHAR(20), -- 學生學號
    `c_id`    VARCHAR(20), -- 課程編號
    `s_score` INT(3),      -- 成績
    PRIMARY KEY (`s_id`, `c_id`)
);

分別寫出SQL語句完成如下功能:

  1. 查詢姓張的老師的個數(難度:★☆☆☆)
  2. 查詢平均成績大於60的所有學生的學號和平均成績(難度:★★☆☆)
  3. 查詢沒學過張三老師的課的學生的學號,姓名(難度:★★★☆)
  4. 查詢課程編號爲01的課程比課程編號爲02的課程成績高的所有學生的學號(難度:★★★☆)
  5. 查詢和02號同學學習的課程完全相同的其他同學的學號與姓名(難度:★★★★)

解析

題目一: 模糊查詢(難度:★☆☆☆)

題目一直接模糊查詢即可,沒有難度

select count(*) from `Teacher` where `t_name` like '張%';

題目二: 聚合查詢(難度:★★☆☆)

題目二是一個聚合查詢,也比較簡單

select `s_id`, avg(`s_score`) from Score 	-- 查詢項爲 學生id, 學生平均分
group by `s_id` 							-- 以學生id分組進行聚合查詢
having avg(`s_score`) > 60 					-- 定義篩選條件: 只顯示平均分爲60以上的

在這裏需要注意的是,在這裏對聚合條件進行過濾,不能使用where從句,而應該使用having從句.where從句和having從句的區別有以下三點:

  1. 從作用時機來看:

    • where從句是一個約束聲明,在查詢數據庫的結果返回之前對數據庫中的查詢條件進行約束,即在結果返回之前起作用.
    • having從句是一個過濾聲明,在查詢數據庫的結果返回之後對查詢結果進行過濾進行過濾,即在結果返回之後起作用.
  2. 從作用對象來看:

    • where從句約束的是數據庫表中原有的字段,因此where從句中不能使用聚合函數.
    • having從句約束是select從句中選出的字段,因此having從句中可以使用聚合函數.

    下面舉例說明這一點

    • 下面這種場景,既可以使用where從句,也可以使用having從句,因爲所約束的字段s_score既是數據庫表中原有字段,也是select從句選中的字段.

      -- 下面兩個SQL語句都是有效的
      select `s_id`, `c_id`, `s_score` from `Score` where `s_score` > 50;
      select `s_id`, `c_id`, `s_score` from `Score` having `s_score` > 50;
      
    • 下面這個場景,不能使用having從句,因爲所約束的字段s_score沒有在select從句被選中.

      -- 使用where語句有效
      select `s_id`, `c_id` from `Score` where `s_score` > 50;
      
      -- 使用having語句無效: Unknown column 's_score' in 'having clause'
      select `s_id`, `c_id` from `Score` having `s_score` > 50;
      
    • 下面這個場景,不能使用where從句,因爲所約束的字段avg_score不是數據庫表中原有的字段.

      -- 使用having從句有效
      select `s_id`, avg(`s_score`) as avg_score from Score group by `s_id` having avg_score > 60;
      
      -- 使用where從句無效: Unknown column 'avg_score' in 'having clause'
      select `s_id`, avg(`s_score`) as avg_score from Score where avg_score > 60 group by `s_id`;
      

    在不使用聚合查詢(group by或聚合函數如count(),min())時,having從句中的條件會被合併到where從句中1.

  3. 從書寫位置上來看: where從句要寫在聚合(grouping by)從句之前,而having從句要寫在聚合(grouping by)從句之後.

題目三: 多層嵌套子查詢(難度:★★★☆)

題目要求查詢沒學過張三老師的課的學生的學號和姓名,我們可以進行三次子查詢並將他們嵌套起來,內層的查詢結果作爲外層查詢的條件.

  1. 先查詢張三老師上過的所有課程的c_id集合:

    select `c_id` from `Course` join `Teacher` using (`t_id`) where `Teacher`.`t_name` = '張三';
    
  2. 再查詢所有上過’張三’老師課的學生的s_id集合:

    select distinct `s_id` from `Score` where `c_id` in ${張三老師課程的c_id集合};
    
  3. 最後對上述學生集合取反集:

    select `s_id`, `s_name` from `Student` where `s_id` not in ${上過張三老師課的學生的s_id集合};
    

將上面三個子查詢嵌套起來,得到這道題的結果:

select `s_id`, `s_name` from `Student` where `s_id` not in (
	select distinct `s_id` from `Score` where `c_id` in (
		select `c_id` from `Course` join `Teacher` using (`t_id`) where `Teacher`.`t_name` = '張三'));

查看其運行計劃如下:

id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY Student ALL 6 100 Using where
2 SUBQUERY Score ALL 12 100 Using where
2 SUBQUERY Course eq_ref PRIMARY PRIMARY 62 sql_test.Score.c_id 1 100 Distinct
2 SUBQUERY Teacher ALL PRIMARY 4 75 Using where; Distinct; Using join buffer (Block Nested Loop)

題目四: 連接子查詢(難度:★★★☆)

題目要求查找課程編號爲01的課程比課程編號爲02的課程成績高的所有學生的學號,因此我們可以通過兩次子查詢分別得到兩門課的成績表,再進行連接查詢.

  1. 分別查詢兩門課的成績表:

    (select * from `Score` where `c_id` = '01') as score_of_course01
    (select * from `Score` where `c_id` = '02') as score_of_course02
    
  2. 將這兩張成績表進行連接查詢:

    select `Score_of_course02`.`s_id`
    from `Score_of_course01` join `Score_of_course02` using (`s_id`)
    where `Score_of_course01`.`s_score` > `Score_of_course02`.`s_score`
    

將上面兩步合成在一起,得到這道題的結果:

select `Score_of_course02`.`s_id`
from (select * from `Score` where `c_id` = '01') as `Score_of_course01`
join (select * from `Score` where `c_id` = '02') as `Score_of_course02` using (`s_id`)
where `Score_of_course01`.`s_score` > `Score_of_course02`.`s_score`

查看其運行計劃如下:

id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY <derived2> ALL 12 100
1 PRIMARY <derived3> ref <auto_key0> <auto_key0> 62 score_of_course01.s_id 2 100 Using where
3 DERIVED score ALL 12 100 Using where
2 DERIVED score ALL 12 100 Using where

題目五: 子查詢+聚合查詢(難度:★★★★)

題目要求查詢和02號同學學習的課程完全相同的其他同學,我們反其道而行之,考慮和02號同學學習的課程有所不同的同學,其中包含兩種情況:

  1. 情況1: 有一部分同學上過了’02’號同學沒有上過的某一門課(上多了),要找到這些同學,需要下面兩步:

    1. 先找到02號同學上過的所有課程c_id集合:

      select `c_id` from `Score` where `s_id`='02';
      
    2. 再找到上過上述集合外的課程的同學:

      select distinct `s_id` from `Score`
      where `c_id` not in ${'02'號同學上過的所有課程的`c_id`集合};
      
  2. 情況2: 還有一部分同學沒有上過’02’號同學沒上過的課,但沒上過某門’02’號同學上過的課(上少了).我們可以對篩掉情況1後剩下的同學進行聚合查詢,統計每個同學上過的課的門數,來篩掉這部分同學.

    1. 查詢’02’號同學上過的課程門數

      select count(*) from `Score` where `s_id` = '02';
      
    2. 聚合查詢統計每個學生上過的課程門數,過濾留下與’02’號同學上過的課程門數相同的人

      select `s_id` from `Score` group by `s_id` having count(*) = ${'02'號同學上過的課程門數}
      

結合上述兩種情況,得到這道題目的結果:

select distinct `s_id` from `Score`	where `s_id` != '02'	-- 先排除掉2號同學自身
-- 排除掉情況1中的同學
and s_id not in (select distinct `s_id` from `Score` where `c_id` not in (select `c_id` from `Score` where `s_id` = '02'))
-- 排除掉情況2中的同學
group by s_id having count(*) = (select count(*) from `Score` where `s_id` = '02');

查看其運行計劃如下:

id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY score index PRIMARY PRIMARY 124 10 100 Using where; Using index
4 SUBQUERY Score ref PRIMARY PRIMARY 62 const 2 100 Using where; Using index
2 SUBQUERY Score index PRIMARY PRIMARY 124 10 100 Using where; Using index
3 SUBQUERY Score ref PRIMARY PRIMARY 62 const 2 100 Using where; Using index

若大家有更好的解法,歡迎留言討論

附錄: 測試數據

建表語句:

-- 學生表
CREATE TABLE `Student`
(
    `s_id`   VARCHAR(20), -- 學生學號
    `s_name` VARCHAR(20), -- 學生姓名
    PRIMARY KEY (`s_id`)
);

-- 課程表
CREATE TABLE `Course`
(
    `c_id`   VARCHAR(20), -- 課程編號
    `c_name` VARCHAR(20), -- 課程名稱
    `t_id`   VARCHAR(20), -- 教師編號
    PRIMARY KEY (`c_id`)
);

-- 教師表
CREATE TABLE `Teacher`
(
    `t_id`   VARCHAR(20), -- 教師編號
    `t_name` VARCHAR(20), -- 教師姓名
    PRIMARY KEY (`t_id`)
);

-- 成績表
CREATE TABLE `Score`
(
    `s_id`    VARCHAR(20), -- 學生學號
    `c_id`    VARCHAR(20), -- 課程編號
    `s_score` INT(3),      -- 成績
    PRIMARY KEY (`s_id`, `c_id`)
);

插入數據

-- 學生表數據
INSERT INTO Student (s_id, s_name) VALUES ('01', '學生1');
INSERT INTO Student (s_id, s_name) VALUES ('02', '學生2');
INSERT INTO Student (s_id, s_name) VALUES ('03', '學生3');
INSERT INTO Student (s_id, s_name) VALUES ('04', '學生4');
INSERT INTO Student (s_id, s_name) VALUES ('05', '學生5');
INSERT INTO Student (s_id, s_name) VALUES ('06', '學生6');

-- 課程表數據
INSERT INTO Course (c_id, c_name, t_id) VALUES ('01', '課程1', '01');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('02', '課程2', '01');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('03', '課程3', '02');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('04', '課程4', '02');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('05', '課程5', '03');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('06', '課程6', '04');
INSERT INTO Course (c_id, c_name, t_id) VALUES ('07', '課程7', '04');

-- 教師表數據
INSERT INTO Teacher (t_id, t_name) VALUES ('01', '張三');
INSERT INTO Teacher (t_id, t_name) VALUES ('02', '李四');
INSERT INTO Teacher (t_id, t_name) VALUES ('03', '王五');
INSERT INTO Teacher (t_id, t_name) VALUES ('04', '趙六');

-- 成績表
INSERT INTO Score (s_id, c_id, s_score) VALUES ('01', '01', 30);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('01', '02', 50);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('02', '01', 60);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('02', '02', 40);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('03', '01', 80);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('03', '02', 70);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('04', '01', 44);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('05', '04', 54);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('06', '04', 46);
INSERT INTO Score (s_id, c_id, s_score) VALUES ('06', '05', 23);

  1. MySQL官方文檔 ↩︎

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