一道面試題:寫出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語句完成如下功能:
- 查詢姓張的老師的個數(難度:★☆☆☆)
- 查詢平均成績大於60的所有學生的學號和平均成績(難度:★★☆☆)
- 查詢沒學過張三老師的課的學生的學號,姓名(難度:★★★☆)
- 查詢課程編號爲01的課程比課程編號爲02的課程成績高的所有學生的學號(難度:★★★☆)
- 查詢和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
從句的區別有以下三點:
-
從作用時機來看:
where
從句是一個約束聲明,在查詢數據庫的結果返回之前對數據庫中的查詢條件進行約束,即在結果返回之前起作用.having
從句是一個過濾聲明,在查詢數據庫的結果返回之後對查詢結果進行過濾進行過濾,即在結果返回之後起作用.
-
從作用對象來看:
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. -
從書寫位置上來看:
where
從句要寫在聚合(grouping by
)從句之前,而having
從句要寫在聚合(grouping by
)從句之後.
題目三: 多層嵌套子查詢(難度:★★★☆)
題目要求查詢沒學過張三老師的課的學生的學號和姓名,我們可以進行三次子查詢並將他們嵌套起來,內層的查詢結果作爲外層查詢的條件.
-
先查詢張三老師上過的所有課程的
c_id
集合:select `c_id` from `Course` join `Teacher` using (`t_id`) where `Teacher`.`t_name` = '張三';
-
再查詢所有上過’張三’老師課的學生的
s_id
集合:select distinct `s_id` from `Score` where `c_id` in ${張三老師課程的c_id集合};
-
最後對上述學生集合取反集:
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的課程成績高的所有學生的學號,因此我們可以通過兩次子查詢分別得到兩門課的成績表,再進行連接查詢.
-
分別查詢兩門課的成績表:
(select * from `Score` where `c_id` = '01') as score_of_course01 (select * from `Score` where `c_id` = '02') as score_of_course02
-
將這兩張成績表進行連接查詢:
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
: 有一部分同學上過了’02’號同學沒有上過的某一門課(上多了),要找到這些同學,需要下面兩步:-
先找到02號同學上過的所有課程的
c_id
集合:select `c_id` from `Score` where `s_id`='02';
-
再找到上過上述集合外的課程的同學:
select distinct `s_id` from `Score` where `c_id` not in ${'02'號同學上過的所有課程的`c_id`集合};
-
-
情況2
: 還有一部分同學沒有上過’02’號同學沒上過的課,但沒上過某門’02’號同學上過的課(上少了).我們可以對篩掉情況1
後剩下的同學進行聚合查詢,統計每個同學上過的課的門數,來篩掉這部分同學.-
查詢’02’號同學上過的課程門數
select count(*) from `Score` where `s_id` = '02';
-
聚合查詢統計每個學生上過的課程門數,過濾留下與’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);