目錄
- 1、聚合函數
- 1.1、SQL 類別高難度試卷得分的截斷平均值(較難)
- 1.2、統計作答次數
- 1.3、得分不小于平均分的最低分
- 2、分組查詢
- 2.1、平均活躍天數和月活人數
- 2.2、月總刷題數和日均刷題數
- 2.3、未完成試卷數大于 1 的有效用戶(較難)
- 3、嵌套子查詢
- 3.1、月均完成試卷數不小于 3 的用戶愛作答的類別(較難)
- 3.2、試卷發布當天作答人數和平均分
- 3.3、作答試卷得分大于過 80 的人的用戶等級分布
- 4、合并查詢
- 4.1、每個題目和每份試卷被作答的人數和次數
- 4.2、分別滿足兩個活動的人
- 5、連接查詢
- 5.1、滿足條件的用戶的試卷完成數和題目練習數(困難)
- 5.2、每個 6/7 級用戶活躍情況(困難)
1、聚合函數
1.1、SQL 類別高難度試卷得分的截斷平均值(較難)
描述:要查看大家在 SQL 類別中高難度試卷的得分情況。
請從exam_record
數據表中計算所有用戶完成 SQL 類別高難度試卷得分的截斷平均值(去掉一個最大值和一個最小值后的平均值)。
示例數據:examination_info
(exam_id
試卷 ID, tag
試卷類別, difficulty
試卷難度, duration
考試時長, release_time
發布時間)
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2020-01-01 |
2 | 9002 | 算法 | medium | 80 | 2020-08-02 |
示例數據:exam_record
(uid
用戶 ID, exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分)
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 |
2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 |
3 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:31:01 | 84 |
4 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 |
5 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) |
6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
7 | 1002 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 |
8 | 1002 | 9001 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 |
9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 |
10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) |
根據輸入你的查詢結果如下:
tag | difficulty | clip_avg_score |
---|---|---|
SQL | hard | 81.7 |
從examination_info
表可知,試卷 9001 為高難度 SQL 試卷,該試卷被作答的得分有[80,81,84,90,50],去除最高分和最低分后為[80,81,84],平均分為 81.6666667,保留一位小數后為 81.7
輸入描述:
輸入數據中至少有 3 個有效分數
思路一: 要找出高難度 sql 試卷,肯定需要聯 examination_info 這張表,然后找出高難度的課程,由 examination_info 得知,高難度 sql 的 exam_id
為 9001,那么等下就以 exam_id = 9001 作為條件去查詢;
先找出 9001 號考試 select * from exam_record where exam_id = 9001
然后,找出最高分 select max(score) 最高分 from exam_record where exam_id = 9001
接著,找出最低分 select min(score) 最低分 from exam_record where exam_id = 9001
在查詢出來的分數結果集當中,去掉最高分和最低分,最直觀能想到的就是 NOT IN 或者 用 NOT EXISTS 也行,這里以 NOT IN 來做
首先將主體寫出來select tag, difficulty, round(avg(score), 1) clip_avg_score from examination_info info INNER JOIN exam_record record
小 tips : MYSQL 的 ROUND() 函數 ,ROUND(X)
返回參數 X 最近似的整數 ROUND(X,D)
返回 X ,其值保留到小數點后 D 位,第 D 位的保留方式為四舍五入。
再將上面的 “碎片” 語句拼湊起來即可, 注意在 NOT IN 中兩個子查詢用 UNION ALL 來關聯,用 union 把 max 和 min 的結果集中在一行當中,這樣形成一列多行的效果。
答案一:
SELECT tag,difficulty,ROUND(AVG(score),1) clip_avg_score
FROM examination_info info INNER JOIN exam_record record
WHERE info.exam_id = record.exam_id AND record.exam_id = 9001AND record.score NOT IN (SELECT MAX(score)FROM exam_recordWHERE exam_id = 9001UNION ALLSELECT MIN(score)FROM exam_recordWHERE exam_id = 9001)
這是最直觀,也是最容易想到的解法,但是還有待改進,這算是投機取巧過關,其實嚴格按照題目要求應該這么寫:
SELECT info.tag, info.difficulty,ROUND(AVG(score),1) clip_avg_score
FROM examination_info info
INNER JOIN exam_record record
WHERE info.exam_id = record.exam_id AND record.exam_id = (SELECT examination_info.exam_idFROM examination_infoWHERE tag = 'SQL'AND difficulty = 'hard') AND record.score NOT IN (SELECT MAX(score)FROM exam_recordWHERE exam_id = (SELECT examination_info.exam_idFROM examination_infoWHERE tag = 'SQL'AND difficulty = 'hard')UNION ALL SELECT MIN(score)FROM exam_recordWHERE exam_id = (SELECT examination_info.exam_idFROM examination_infoWHERE tag = 'SQL'AND difficulty = 'hard'))
GROUP BY info.tag, info.difficulty;
然而你會發現,重復的語句非常多,所以可以利用WITH
來抽取公共部分
WITH 子句介紹:
WITH
子句,也稱為公共表表達式(Common Table Expression,CTE),是在 SQL 查詢中定義臨時表的方式。它可以讓我們在查詢中創建一個臨時命名的結果集,并且可以在同一查詢中引用該結果集。
基本用法:
WITH cte_name (column1, column2, ..., columnN) AS (-- 查詢體SELECT ...FROM ...WHERE ...
)
-- 主查詢
SELECT ...
FROM cte_name
WHERE ...
WITH
子句由以下幾個部分組成:
cte_name
: 給臨時表起一個名稱,可以在主查詢中引用。(column1, column2, ..., columnN)
: 可選,指定臨時表的列名。AS
: 必需,表示開始定義臨時表。CTE
查詢體: 實際的查詢語句,用于定義臨時表中的數據。
WITH
子句的主要用途之一是增強查詢的可讀性和可維護性,尤其在涉及多個嵌套子查詢或需要重復使用相同的查詢邏輯時。通過將這些邏輯放在一個命名的臨時表中,我們可以更清晰地組織查詢,并消除重復代碼。
此外,WITH
子句還可以在復雜的查詢中實現遞歸查詢。遞歸查詢允許我們在單個查詢中執行對同一表的多次迭代,逐步構建結果集。這在處理層次結構數據、組織結構和樹狀結構等場景中非常有用。
小細節:MySQL 5.7 版本以及之前的版本不支持在 WITH 子句中直接使用別名。
下面是改進后的答案:
WITH t1 AS (SELECT record.*,info.tag,info.difficultyFROM exam_record recordINNER JOIN examination_info info ON record.exam_id = info.exam_idWHERE info.tag = 'SQL'AND info.difficulty = 'hard'
)
SELECT tag,difficulty,ROUND(AVG(score), 1) AS clip_avg_score
FROM t1
WHERE score NOT IN (SELECT MAX(score)FROM t1UNION ALLSELECT MIN(score)FROM t1
)
GROUP BY tag, difficulty;
思路二:
- 篩選 SQL 高難度試卷:
where tag="SQL" and difficulty="hard"
- 計算截斷平均值:(和-最大值-最小值) / (總個數-2):
(sum(score) - max(score) - min(score)) / (count(score) - 2)
- 有一個缺點就是,如果最大值和最小值有多個,這個方法就很難篩選出來, 但是題目中說了----->去掉一個最大值和一個最小值后的平均值, 所以這里可以用這個公式。
答案二:
SELECT info.tag,info.difficulty,ROUND((SUM(record.score)-MIN(record.score)-MAX(record.score))/(COUNT(record.score)-2),1) AS clip_avg_score
FROM examination_info info,exam_record record
WHERE info.exam_id = record.exam_idAND info.tag = 'SQL'AND info.difficulty = 'hard';
1.2、統計作答次數
有一個試卷作答記錄表 exam_record
,請從中統計出總作答次數 total_pv
、試卷已完成作答數 complete_pv
、已完成的試卷數 complete_exam_cnt
。
示例數據 exam_record
表(uid
用戶 ID, exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分):
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 |
2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 |
3 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:31:01 | 84 |
4 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 |
5 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) |
6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
7 | 1002 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 |
8 | 1002 | 9001 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 |
9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 |
10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) |
示例輸出:
total_pv | complete_pv | complete_exam_cnt |
---|---|---|
10 | 7 | 2 |
解釋:表示截止當前,有 11 次試卷作答記錄,已完成的作答次數為 7 次(中途退出的為未完成狀態,其交卷時間和份數為 NULL),已完成的試卷有 9001 和 9002 兩份。
思路: 這題一看到統計次數,肯定第一時間就要想到用COUNT
這個函數來解決,問題是要統計不同的記錄,該怎么來寫?使用子查詢就能解決這個題目(這題用 case when 也能寫出來,解法類似,邏輯不同而已);首先在做這個題之前,讓我們先來了解一下COUNT
的基本用法;
COUNT()
函數的基本語法如下所示:
COUNT(expression)
其中,expression
可以是列名、表達式、常量或通配符。下面是一些常見的用法示例:
- 計算表中所有行的數量:
SELECT COUNT(*) FROM table_name;
- 計算特定列非空(不為 NULL)值的數量:
SELECT COUNT(column_name) FROM table_name;
- 計算滿足條件的行數:
SELECT COUNT(*) FROM table_name WHERE condition;
- 結合
GROUP BY
使用,計算分組后每個組的行數:
SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name;
- 計算不同列組合的唯一組合數:
SELECT COUNT(DISTINCT column_name1, column_name2) FROM table_name;
在使用COUNT()
函數時,如果不指定任何參數或者使用 COUNT(*)
,將會計算所有行的數量。而如果使用列名,則只會計算該列非空值的數量。
另外,COUNT()
函數的結果是一個整數值。即使結果是零,也不會返回 NULL,這點需要謹記。
答案:
SELECT COUNT(*) total_pv,(SELECT COUNT(*) FROM exam_record WHERE submit_time IS NOT NULL) complete_pv,(SELECT COUNT(DISTINCT exam_id, score IS NOT NULL OR NULL) FROM exam_record) complete_exam_cnt
FROM exam_record
這里著重說一下COUNT( DISTINCT exam_id, score IS NOT NULL OR NULL )
這一句,判斷 score 是否為 null ,如果是即為真,如果不是返回 null;注意這里如果不加 or null
在不是 null 的情況下只會返回 false 也就是返回 0;
COUNT
本身是不可以對多列求行數的,distinct
的加入是的多列成為一個整體,可以求出現的行數了;count distinct
在計算時只返回非 null 的行, 這個也要注意;
另外通過本題 get 到了------>count 加條件常用句式count( 列判斷 or null)
1.3、得分不小于平均分的最低分
描述: 請從試卷作答記錄表中找到 SQL 試卷得分不小于該類試卷平均得分的用戶最低得分。
示例數據 exam_record
表(uid 用戶 ID, exam_id 試卷 ID, start_time 開始作答時間, submit_time 交卷時間, score 得分):
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 |
2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) |
4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
5 | 1002 | 9001 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 |
6 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 |
7 | 1003 | 9002 | 2021-02-06 12:01:01 | (NULL) | (NULL) |
8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) |
examination_info
表(exam_id
試卷 ID, tag
試卷類別, difficulty
試卷難度, duration
考試時長, release_time
發布時間)
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2020-01-01 |
2 | 9002 | SQL | easy | 60 | 2020-02-01 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 |
示例輸出數據:
min_score_over_avg |
---|
87 |
解釋:試卷 9001 和 9002 為 SQL 類別,作答這兩份試卷的得分有[80,89,87,90],平均分為 86.5,不小于平均分的最小分數為 87
思路:這類題目第一眼看確實很復雜, 因為不知道從哪入手,但是當我們仔細讀題審題后,要學會抓住題干中的關鍵信息。以本題為例:請從試卷作答記錄表中找到SQL試卷得分不小于該類試卷平均得分的用戶最低得分
。你能一眼從中提取哪些有效信息來作為解題思路?
第一條:找到SQL試卷得分
第二條:該類試卷平均得分
第三條:該類試卷的用戶最低得分
然后中間的 “橋梁” 就是不小于
將條件拆分后,先逐步完成
-- 找出tag為‘SQL’的得分 【80, 89,87,90】
SELECT record.score
FROM exam_record record,examination_info info
WHERE record.exam_id = info.exam_id AND info.tag = 'SQL' AND record.score IS NOT NULL-- 再算出這一組的平均得分
SELECT ROUND(AVG(score),1)
FROM examination_info info
INNER JOIN exam_record record
WHERE info.exam_id = record.exam_id
AND tag= 'SQL'
然后再找出該類試卷的最低得分,接著將結果集【80, 89,87,90】 去和平均分數作比較,方可得出最終答案。
答案:
SELECT MIN(score) AS min_score_over_avg
FROM examination_info info
INNER JOIN exam_record record
WHERE info.exam_id = record.exam_id
AND info.tag = 'SQL'
AND score > (SELECT ROUND(AVG(score),1) from examination_info info INNER JOIN exam_record recordwhere info.exam_id = record.exam_idand tag= 'SQL'
)
其實這類題目給出的要求看似很 “繞”,但其實仔細梳理一遍,將大條件拆分成小條件,逐個拆分完以后,最后將所有條件拼湊起來。反正只要記住:抓主干,理分支,問題便迎刃而解。
2、分組查詢
2.1、平均活躍天數和月活人數
描述:用戶在試卷作答區作答記錄存儲在表exam_record
中,內容如下:
exam_record
表(uid
用戶 ID, exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間,score
得分)
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 |
2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) |
4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 |
6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 |
7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) |
8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) |
10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 |
11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 |
13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 |
請計算 2021 年每個月里試卷作答區用戶平均月活躍天數 avg_active_days
和月度活躍人數 mau
,上面數據的示例輸出如下:
month | avg_active_days | mau |
---|---|---|
202107 | 1.50 | 2 |
202109 | 1.25 | 4 |
解釋:2021 年 7 月有 2 人活躍,共活躍了 3 天(1001 活躍 1 天,1002 活躍 2 天),平均活躍天數 1.5;2021 年 9 月有 4 人活躍,共活躍了 5 天,平均活躍天數 1.25,結果保留 2 位小數。
注:此處活躍指有交卷行為。
思路:讀完題先注意高亮部分;一般求天數和月活躍人數馬上就要想到相關的日期函數;這一題我們同樣來進行拆分,把問題細化再解決;首先求活躍人數,肯定要用到COUNT()
,那這里首先就有一個坑,不知道大家注意了沒有?用戶 1002 在 9 月份做了兩種不同的試卷,所以這里要注意去重,不然在統計的時候,活躍人數是錯的;第二個就是要知道日期的格式化,如上表,題目要求以202107
這種日期格式展現,要用到DATE_FORMAT
來進行格式化。
基本用法:
DATE_FORMAT(date_value, format)
date_value
參數是待格式化的日期或時間值。format
參數是指定的日期或時間格式(這個和 Java 里面的日期格式一樣)。
答案:
SELECT DATE_FORMAT(submit_time,'%Y%m') month,COUNT(DISTINCT uid,DATE_FORMAT(submit_time, '%Y%m%d'))/COUNT(DISTINCT UID) avg_active_days,COUNT(DISTINCT uid) mau
FROM exam_record
WHERE YEAR(submit_time) = '2021'
GROUP BY month
這里多說一句, 使用COUNT(DISTINCT uid, DATE_FORMAT(submit_time, '%Y%m%d')
可以統計在 uid
列和 submit_time
列按照年份、月份和日期進行格式化后的組合值的數量。
2.2、月總刷題數和日均刷題數
描述:現有一張題目練習記錄表 practice_record
,示例內容如下:
id | uid | question_id | submit_time | score |
---|---|---|---|---|
1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 |
2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 |
3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 |
4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 |
5 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 |
請從中統計出 2021 年每個月里用戶的月總刷題數 month_q_cnt
和日均刷題數 avg_day_q_cnt
(按月份升序排序)以及該年的總體情況,示例數據輸出如下:
submit_month | month_q_cnt | avg_day_q_cnt |
---|---|---|
202108 | 2 | 0.065 |
202109 | 3 | 0.100 |
2021 匯總 | 5 | 0.161 |
解釋:2021 年 8 月共有 2 次刷題記錄,日均刷題數為 2/31=0.065(保留 3 位小數);2021 年 9 月共有 3 次刷題記錄,日均刷題數為 3/30=0.100;2021 年共有 5 次刷題記錄(年度匯總平均無實際意義,這里我們按照 31 天來算 5/31=0.161)
如果您采用最新的 Mysql 版本,如果您運行結果出現錯誤:ONLY_FULL_GROUP_BY,意思是:對于 GROUP BY
聚合操作,如果在 SELECT 中的列,沒有在 GROUP BY 中出現,那么這個 SQL 是不合法的,因為列不在 GROUP BY
從句中,也就是說查出來的列必須在 group by 后面出現否則就會報錯,或者這個字段出現在聚合函數里面。
思路:
看到實例數據就要馬上聯想到相關的函數,比如submit_month
就要用到DATE_FORMAT
來格式化日期。然后查出每月的刷題數量。
每月的刷題數量
SELECT MONTH ( submit_time ), COUNT( question_id )
FROMpractice_record
GROUP BYMONTH (submit_time)
接著第三列這里要用到DAY(LAST_DAY(date_value))
函數來查找給定日期的月份中的天數。
示例代碼如下:
SELECT DAY(LAST_DAY('2023-07-08')) AS days_in_month;
-- 輸出:31SELECT DAY(LAST_DAY('2023-02-01')) AS days_in_month;
-- 輸出:28 (閏年中的二月份)SELECT DAY(LAST_DAY(NOW())) AS days_in_current_month;
-- 輸出:31 (當前月份的天數)
使用 LAST_DAY()
函數獲取給定日期的當月最后一天,然后使用 DAY()
函數提取該日期的天數。這樣就能獲得指定月份的天數。
需要注意的是,LAST_DAY()
函數返回的是日期值,而 DAY()
函數用于提取日期值中的天數部分。
有了上述的分析之后,即可馬上寫出答案,這題復雜就復雜在處理日期上,其中的邏輯并不難。
答案:
SELECT DATE_FORMAT(submit_time, '%Y%m') submit_month,count(question_id) month_q_cnt,ROUND(COUNT(question_id) / DAY (LAST_DAY(submit_time)), 3) avg_day_q_cnt
FROM practice_record
WHERE DATE_FORMAT(submit_time, '%Y') = '2021'
GROUP BY submit_month
UNION ALL
SELECT '2021匯總' AS submit_month,count(question_id) month_q_cnt,ROUND(COUNT(question_id) / 31, 3) avg_day_q_cnt
FROM practice_record
WHERE DATE_FORMAT(submit_time, '%Y') = '2021'
ORDER BY submit_month
在實例數據輸出中因為最后一行需要得出匯總數據,所以這里要 UNION ALL加到結果集中;別忘了最后要排序!
2.3、未完成試卷數大于 1 的有效用戶(較難)
描述:現有試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID,start_time
開始作答時間, submit_time
交卷時間, score
得分),示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 |
2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) |
4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 |
6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 |
7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) |
8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) |
10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 |
11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 |
13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 |
還有一張試卷信息表examination_info
表(exam_id
試卷 ID, tag
試卷類別, difficulty
試卷難度, duration
考試時長, release_time
發布時間)
示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2020-01-01 |
2 | 9002 | SQL | easy | 60 | 2020-02-01 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 |
請統計 2021 年每個未完成試卷作答數大于 1 的有效用戶的數據(有效用戶指完成試卷作答數至少為 1 且未完成數小于 5),輸出用戶 ID、未完成試卷作答數、完成試卷作答數、作答過的試卷 tag 集合,按未完成試卷數量由多到少排序。示例數據的輸出結果如下:
uid | incomplete_cnt | complete_cnt | detail |
---|---|---|---|
1002 | 2 | 4 | 2021-09-01:算法;2021-07-02:SQL;2021-09-02:SQL;2021-09-05:SQL;2021-07-05:SQL |
解釋:2021 年的作答記錄中,除了 1004,其他用戶均滿足有效用戶定義,但只有 1002 未完成試卷數大于 1,因此只輸出 1002,detail 中是 1002 作答過的試卷{日期:tag}集合,日期和 tag 間用 : 連接,多元素間用 ; 連接。
思路:
仔細讀題后,分析出:首先要聯表,因為后面要輸出tag
;
篩選出 2021 年的數據
SELECT *
FROM exam_record er
LEFT JOIN examination_info ei ON er.exam_id = ei.exam_id
WHERE YEAR(er.start_time) = '2021'
根據 uid 進行分組,然后對每個用戶進行條件進行判斷,題目中要求完成試卷數至少為1,未完成試卷數要大于1,小于5
那么等會兒寫 sql 的時候條件應該是:未完成 > 1 and 已完成 >=1 and 未完成 < 5
因為最后要用到字符串的拼接,而且還要組合拼接,這個可以用GROUP_CONCAT
函數,下面簡單介紹一下該函數的用法:
基本格式:
GROUP_CONCAT([DISTINCT] expr [ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [, ...]] [SEPARATOR sep])
expr
:要連接的列或表達式。DISTINCT
:可選參數,用于去重。當指定了 DISTINCT,相同的值只會出現一次。ORDER BY
:可選參數,用于排序連接后的值。可以選擇升序 (ASC) 或降序 (DESC) 排序。SEPARATOR sep
:可選參數,用于設置連接后的值的分隔符。(本題要用這個參數設置 ; 號 )
GROUP_CONCAT()
函數常用于 GROUP BY
子句中,將一組行的值連接為一個字符串,并在結果集中以聚合的形式返回。
答案:
SELECT a.uid,SUM(CASE WHEN a.submit_time IS NULL THEN 1END) AS incomplete_cnt,SUM(CASE WHEN a.submit_time IS NOT NULL THEN 1END) AS complete_cnt,GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time,'%Y-%m-%d'),':',b.tag)ORDER BY start_time SEPARATOR ';') AS detail
FROM exam_record a
LEFT JOIN examination_info b ON a.exam_id = b.exam_id
WHERE YEAR (a.start_time)= 2021
GROUP BY a.uid
HAVING incomplete_cnt > 1
AND complete_cnt >=1
AND incomplete_cnt < 5
ORDER BY incomplete_cnt DESC
SUM(CASE WHEN a.submit_time IS NULL THEN 1 END)
統計了每個用戶未完成的記錄數量。SUM(CASE WHEN a.submit_time IS NOT NULL THEN 1 END)
統計了每個用戶已完成的記錄數量。GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time, '%Y-%m-%d'), ':', b.tag) ORDER BY a.start_time SEPARATOR ';')
將每個用戶的考試日期和標簽以逗號分隔的形式連接成一個字符串,并按考試開始時間進行排序。
3、嵌套子查詢
3.1、月均完成試卷數不小于 3 的用戶愛作答的類別(較難)
描述:現有試卷作答記錄表 exam_record
(uid
:用戶 ID, exam_id
:試卷 ID, start_time
:開始作答時間, submit_time
:交卷時間,沒提交的話為 NULL, score
:得分),示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-07-02 09:01:01 | (NULL) | (NULL) |
2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 |
4 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 |
5 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) |
6 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
7 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
8 | 1003 | 9001 | 2021-09-08 13:01:01 | (NULL) | (NULL) |
9 | 1003 | 9002 | 2021-09-08 14:01:01 | (NULL) | (NULL) |
10 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) |
11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
12 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
13 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 |
試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 |
2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 |
請從表中統計出 “當月均完成試卷數”不小于 3 的用戶們愛作答的類別及作答次數,按次數降序輸出,示例輸出如下:
tag | tag_cnt |
---|---|
C++ | 4 |
SQL | 2 |
算法 | 1 |
解釋:用戶 1002 和 1005 在 2021 年 09 月的完成試卷數目均為 3,其他用戶均小于 3;然后用戶 1002 和 1005 作答過的試卷 tag 分布結果按作答次數降序排序依次為 C++、SQL、算法。
思路:這題考察聯合子查詢,重點在于月均回答>=3
, 但是個人認為這里沒有表述清楚,應該直接說查 9 月的就容易理解多了;這里不是每個月都要>=3 或者是所有答題次數/答題月份。不要理解錯誤了。
先查詢出哪些用戶月均答題大于三次
SELECT uid
FROM exam_record
GROUP BY uid,MONTH(start_time)
HAVING COUNT(submit_time) >= 3
有了這一步之后再進行深入,只要能理解上一步(我的意思是不被題目中的月均所困擾),然后再套一個子查詢,查哪些用戶包含其中,然后查出題目中所需的列即可。記得排序!!
SELECT tag,COUNT(start_time) AS tag_cnt
FROM exam_record record
INNER JOIN examination_info info ON record.exam_id = info.exam_id
WHERE uid IN (SELECT uidFROM exam_recordGROUP BY uid,MONTH(start_time)HAVING COUNT(submit_time) >= 3)
GROUP BY tag
ORDER BY tag_cnt DESC
3.2、試卷發布當天作答人數和平均分
描述:現有用戶信息表 user_info
(uid
用戶 ID,nick_name
昵稱, achievement
成就值,level
等級, job
職業方向, register_time
注冊時間),示例數據如下:
id | uid | nick_name | achievement | level | job | register_time |
---|---|---|---|---|---|---|
1 | 1001 | 機器人 1 號 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 |
2 | 1002 | 機器人 2 號 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 |
3 | 1003 | 機器人 3 號 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 |
4 | 1004 | 機器人 4 號 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 |
5 | 1005 | 機器人 5 號 | 1600 | 6 | C++ | 2020-01-01 10:00:00 |
6 | 1006 | 機器人 6 號 | 3000 | 6 | C++ | 2020-01-01 10:00:00 |
釋義:用戶 1001 昵稱為機器人 1 號,成就值為 3100,用戶等級是 7 級,職業方向為算法,注冊時間 2020-01-01 10:00:00
試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 |
試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 70 |
2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 |
4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 |
5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 |
6 | 1002 | 9002 | 2021-08-02 12:01:01 | 2021-08-02 12:31:01 | 70 |
7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 |
8 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) |
9 | 1003 | 9002 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 70 |
12 | 1003 | 9001 | 2021-09-08 14:01:01 | (NULL) | (NULL) |
13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) |
14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 |
15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 |
請計算每張 SQL 類別試卷發布后,當天 5 級以上的用戶作答的人數 uv
和平均分 avg_score
,按人數降序,相同人數的按平均分升序,示例數據結果輸出如下:
exam_id | uv | avg_score |
---|---|---|
9001 | 3 | 81.3 |
解釋:只有一張 SQL 類別的試卷,試卷 ID 為 9001,發布當天(2021-09-01)有 1001、1002、1003、1005 作答過,但是 1003 是 5 級用戶,其他 3 位為 5 級以上,他們三的得分有[70,80,85,90],平均分為 81.3(保留 1 位小數)。
思路:這題看似很復雜,但是先逐步將“外邊”條件拆分,然后合攏到一起,答案就出來,多表查詢反正記住:由外向里,抽絲剝繭。
先把三種表連起來,同時給定一些條件,比如題目中要求等級> 5
的用戶,那么可以先查出來
SELECT DISTINCT ui.uid
FROM examination_info ei
INNER JOIN exam_record er
INNER JOIN user_info ui
WHERE ei.exam_id = er.exam_idAND er.uid = ui.uidAND ui.`level` > 5
接著注意題目中要求:每張sql類別試卷發布后,當天作答用戶
,注意其中的當天,那我們馬上就要想到要用到時間的比較。
對試卷發布日期和開始考試日期進行比較:DATE(ei.release_time) = DATE(er.start_time);不用擔心submit_time
為 null 的問題,后續在 where 中會給過濾掉。
答案:
SELECT er.exam_id AS exam_id,COUNT(DISTINCT ui.uid) AS uv,ROUND(SUM(er.score)/COUNT(ui.uid),1) AS avg_score
FROM examination_info ei
INNER JOIN exam_record er
INNER JOIN user_info ui
WHERE ei.exam_id = er.exam_idAND er.uid = ui.uidAND DATE(ei.release_time) = DATE(er.start_time)AND submit_time IS NOT NULLAND ei.tag = 'SQL'AND ui.`level` > 5
GROUP BY er.exam_id
ORDER BY uv DESC,avg_score ASC
注意最后的分組排序!先按人數排,若一致,按平均分排。
3.3、作答試卷得分大于過 80 的人的用戶等級分布
描述:
現有用戶信息表 user_info
(uid
用戶 ID,nick_name
昵稱, achievement
成就值,level
等級, job
職業方向, register_time
注冊時間),示例數據如下:
id | uid | nick_name | achievement | level | job | register_time |
---|---|---|---|---|---|---|
1 | 1001 | 機器人 1 號 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 |
2 | 1002 | 機器人 2 號 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 |
3 | 1003 | 機器人 3 號 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 |
4 | 1004 | 機器人 4 號 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 |
5 | 1005 | 機器人 5 號 | 1600 | 6 | C++ | 2020-01-01 10:00:00 |
6 | 1006 | 機器人 6 號 | 3000 | 6 | C++ | 2020-01-01 10:00:00 |
試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 |
試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 79 |
2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 |
3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 |
4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 |
5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 |
6 | 1002 | 9002 | 2021-08-02 12:01:01 | 2021-08-02 12:31:01 | 70 |
7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 |
8 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) |
9 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 |
10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 81 |
12 | 1003 | 9001 | 2021-09-08 14:01:01 | (NULL) | (NULL) |
13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) |
14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 |
15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 |
16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 |
統計作答 SQL 類別的試卷得分大于過 80 的人的用戶等級分布,按數量降序排序(保證數量都不同)。示例數據結果輸出如下:
level | level_cnt |
---|---|
6 | 2 |
5 | 1 |
解釋:9001 為 SQL 類試卷,作答該試卷大于 80 分的人有 1002、1003、1005 共 3 人,6 級兩人,5 級一人。
思路:這題和上一題都是一樣的數據,只是查詢條件改變了而已,上一題理解了,這題分分鐘做出來。
答案:
SELECT ui.`level` AS level,COUNT(DISTINCT ui.uid) AS level_cnt
FROM examination_info ei
INNER JOIN exam_record er
INNER JOIN user_info ui
WHERE ei.exam_id = er.exam_idAND er.uid = ui.uidAND ei.tag = 'SQL'AND er.score > 80
GROUP BY level
ORDER BY level_cnt DESC
4、合并查詢
4.1、每個題目和每份試卷被作答的人數和次數
描述:
現有試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 81 |
2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 |
3 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 |
4 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 |
5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 |
6 | 1002 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) |
題目練習表 practice_record
(uid
用戶 ID, question_id
題目 ID, submit_time 提交時間, score
得分):
id | uid | question_id | submit_time | score |
---|---|---|---|---|
1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 |
2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 |
3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 |
4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 |
5 | 1003 | 8001 | 2021-08-02 19:38:01 | 70 |
6 | 1003 | 8001 | 2021-08-02 19:48:01 | 90 |
7 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 |
請統計每個題目和每份試卷被作答的人數和次數,分別按照"試卷"和"題目"的 uv & pv 降序顯示,示例數據結果輸出如下:
tid | uv | pv |
---|---|---|
9001 | 3 | 3 |
9002 | 1 | 3 |
8001 | 3 | 5 |
8002 | 2 | 2 |
解釋:“試卷”有 3 人共練習 3 次試卷 9001,1 人作答 3 次 9002;“刷題”有 3 人刷 5 次 8001,有 2 人刷 2 次 8002
思路:這題的難點和易錯點在于UNION
和ORDER BY
同時使用的問題
有以下幾種情況:使用union
和多個order by
不加括號,報錯!
order by
在union
連接的子句中不起作用;
比如不加括號:
SELECT exam_id AS tid,COUNT(DISTINCT UID) AS uv,COUNT(UID) AS pv
FROM exam_record
GROUP BY exam_id
ORDER BY uv DESC,pv DESC
UNION
SELECT question_id AS tid,COUNT(DISTINCT UID) AS uv,COUNT(UID) AS pv
FROM practice_record
GROUP BY question_id
ORDER BY uv DESC,pv DESC
直接報語法錯誤,如果沒有括號,只能有一個order by
還有一種order by
不起作用的情況,但是能在子句的子句中起作用,這里的解決方案就是在外面再套一層查詢。
答案:
SELECT *
FROM(SELECT exam_id AS tid,COUNT(DISTINCT exam_record.uid) uv,COUNT(*) pvFROM exam_recordGROUP BY exam_idORDER BY uv DESC, pv DESC) t1
UNION
SELECT *
FROM(SELECT question_id AS tid,COUNT(DISTINCT practice_record.uid) uv,COUNT(*) pvFROM practice_recordGROUP BY question_idORDER BY uv DESC, pv DESC) t2;
4.2、分別滿足兩個活動的人
描述: 為了促進更多用戶在平臺學習和刷題進步,我們會經常給一些既活躍又表現不錯的用戶發放福利。假使以前我們有兩撥運營活動,分別給每次試卷得分都能到 85 分的人(activity1)、至少有一次用了一半時間就完成高難度試卷且分數大于 80 的人(activity2)發了福利券。
現在,需要你一次性將這兩個活動滿足的人篩選出來,交給運營同學。請寫出一個 SQL 實現:輸出 2021 年里,所有每次試卷得分都能到 85 分的人以及至少有一次用了一半時間就完成高難度試卷且分數大于 80 的人的 id 和活動號,按用戶 ID 排序輸出。
現有試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 |
3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 |
現有試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 |
2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 |
3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 |
4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 89 |
5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
示例數據輸出結果:
uid | activity |
---|---|
1001 | activity2 |
1003 | activity1 |
1004 | activity1 |
1004 | activity2 |
解釋:用戶 1001 最小分數 81 不滿足活動 1,但 29 分 59 秒完成了 60 分鐘長的試卷得分 81,滿足活動 2;1003 最小分數 86 滿足活動 1,完成時長都大于試卷時長的一半,不滿足活動 2;用戶 1004 剛好用了一半時間(30 分鐘整)完成了試卷得分 85,滿足活動 1 和活動 2。
思路: 這一題需要涉及到時間的減法,需要用到 TIMESTAMPDIFF()
函數計算兩個時間戳之間的分鐘差值。
下面我們來看一下基本用法
示例:
TIMESTAMPDIFF(MINUTE, start_time, end_time)
TIMESTAMPDIFF()
函數的第一個參數是時間單位,這里我們選擇 MINUTE
表示返回分鐘差值。第二個參數是較早的時間戳,第三個參數是較晚的時間戳。函數會返回它們之間的分鐘差值
了解了這個函數的用法之后,我們再回過頭來看activity1
的要求,求分數大于 85 即可,那我們還是先把這個寫出來,后續思路就會清晰很多
SELECT DISTINCT uid
FROM exam_record
WHERE score >= 85AND YEAR(start_time) = '2021'
根據條件 2,接著寫出在一半時間內完成高難度試卷且分數大于80的人
SELECT uid
FROM exam_record er
INNER JOIN examination_info ei
WHERE er. exam_id = ei.exam_id
AND score >= 80
AND (TIMESTAMPDIFF(MINUTE,start_time,submit_time)) < (ei.duration/2)
AND ei.difficulty = 'hard'
然后再把兩者UNION
起來即可。(這里特別要注意括號問題和order by
位置,具體用法在上一篇中已提及)
SELECT DISTINCT uid,'activity1' AS activity
FROM exam_record
WHERE score >= 85AND YEAR(start_time) = '2021'
UNION
SELECT uid,'activity2' AS activity
FROM exam_record er
INNER JOIN examination_info ei
WHERE er. exam_id = ei.exam_id
AND score >= 80
AND (TIMESTAMPDIFF(MINUTE,start_time,submit_time)) < (ei.duration/2)
AND ei.difficulty = 'hard'
ORDER BY uid
5、連接查詢
5.1、滿足條件的用戶的試卷完成數和題目練習數(困難)
描述:
現有用戶信息表 user_info
(uid
用戶 ID,nick_name
昵稱, achievement
成就值,level
等級, job
職業方向, register_time
注冊時間),示例數據如下:
id | uid | nick_name | achievement | level | job | register_time |
---|---|---|---|---|---|---|
1 | 1001 | 機器人 1 號 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 |
2 | 1002 | 機器人 2 號 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 |
3 | 1003 | 機器人 3 號 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 |
4 | 1004 | 機器人 4 號 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 |
5 | 1005 | 機器人 5 號 | 1600 | 6 | C++ | 2020-01-01 10:00:00 |
6 | 1006 | 機器人 6 號 | 2000 | 6 | C++ | 2020-01-01 10:00:00 |
現有試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 |
3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 |
現有試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
id | uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|---|
1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 |
2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 |
3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 |
4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 |
5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
6 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 |
7 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 |
8 | 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 80 |
題目練習表 practice_record
(uid
用戶 ID, question_id
題目 ID, submit_time 提交時間, score
得分):
id | uid | question_id | submit_time | score |
---|---|---|---|---|
1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 |
2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 |
3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 |
4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 |
5 | 1004 | 8001 | 2021-08-02 19:38:01 | 70 |
6 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 |
7 | 1001 | 8002 | 2021-08-02 19:38:01 | 70 |
8 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 |
9 | 1004 | 8002 | 2021-08-02 19:58:01 | 94 |
10 | 1004 | 8003 | 2021-08-02 19:38:01 | 70 |
11 | 1004 | 8003 | 2021-08-02 19:48:01 | 90 |
12 | 1004 | 8003 | 2021-08-01 19:38:01 | 80 |
請你找到高難度 SQL 試卷得分平均值大于 80 并且是 7 級的紅名大佬,統計他們的 2021 年試卷總完成次數和題目總練習次數,只保留 2021 年有試卷完成記錄的用戶。結果按試卷完成數升序,按題目練習數降序。
示例數據輸出如下:
uid | exam_cnt | question_cnt |
---|---|---|
1001 | 1 | 2 |
1003 | 2 | 0 |
解釋:用戶 1001、1003、1004、1006 滿足高難度 SQL 試卷得分平均值大于 80,但只有 1001、1003 是 7 級紅名大佬;1001 完成了 1 次試卷 1001,練習了 2 次題目;1003 完成了 2 次試卷 9001、9002,未練習題目(因此計數為 0)
思路:
先將條件進行初步篩選,比如先查出做過高難度 sql 試卷的用戶
SELECT er.uid
FROM exam_record er
INNER JOIN examination_info ei
WHERE er.exam_id = ei.exam_id
AND ei.tag = 'SQL'
AND ei.difficulty = 'hard'
然后根據題目要求,接著再往里疊條件即可;
但是這里又要注意:
第一:不能YEAR(submit_time)= 2021
這個條件放到最后,要在ON
條件里,因為左連接存在返回左表全部行,右表為 null 的情形,放在 JOIN
條件的 ON
子句中的目的是為了確保在連接兩個表時,只有滿足年份條件的記錄會進行連接。這樣可以避免其他年份的記錄被包含在結果中。即 1001 做過 2021 年的試卷,但沒有練習過,如果把條件放到最后,就會排除掉這種情況。
第二,必須是COUNT(distinct er.exam_id) exam_cnt, COUNT(distinct pr.id) question_cnt
,要加 distinct,因為有左連接產生很多重復值。
答案:
SELECT er.uid,COUNT(DISTINCT er.exam_id) AS exam_cnt,COUNT(DISTINCT pr.id) AS question_cnt
FROM exam_record er
LEFT JOIN practice_record pr ON er.uid = pr.uid
AND YEAR(er.submit_time) = 2021
AND YEAR(pr.submit_time) = 2021
WHERE er.uid IN (SELECT er.uidFROM exam_record erINNER JOIN examination_info ei ON er.exam_id = ei.exam_idINNER JOIN user_info ui ON ui.uid = er.uidWHERE ei.tag = 'SQL'AND ei.difficulty = 'hard'AND LEVEL = 7GROUP BY er.uidHAVING AVG(score) > 80
)
GROUP BY er.uid
ORDER BY exam_cnt,question_cnt DESC
能細心的小伙伴會發現,為什么明明將條件限制了tag = 'SQL' AND difficulty = 'hard'
,但是用戶 1003 仍然能查出兩條考試記錄,其中一條的考試tag
為 C++
; 這是由于LEFT JOIN
的特性,即使沒有與右表匹配的行,左表的所有記錄仍然會被保留。
5.2、每個 6/7 級用戶活躍情況(困難)
描述:
現有用戶信息表 user_info
(uid
用戶 ID,nick_name
昵稱, achievement
成就值,level
等級, job
職業方向, register_time
注冊時間),示例數據如下:
id | uid | nick_name | achievement | level | job | register_time |
---|---|---|---|---|---|---|
1 | 1001 | 機器人 1 號 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 |
2 | 1002 | 機器人 2 號 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 |
3 | 1003 | 機器人 3 號 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 |
4 | 1004 | 機器人 4 號 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 |
5 | 1005 | 機器人 5 號 | 1600 | 6 | C++ | 2020-01-01 10:00:00 |
6 | 1006 | 機器人 6 號 | 2000 | 7 | C++ | 2020-01-01 10:00:00 |
現有試卷信息表 examination_info
(exam_id
:試卷 ID, tag
:試卷類別, difficulty
:試卷難度, duration
:考試時長, release_time
:發布時間),示例數據如下:
id | exam_id | tag | difficulty | duration | release_time |
---|---|---|---|---|---|
1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 |
2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 |
3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 |
現有試卷作答記錄表 exam_record
(uid
用戶 ID,exam_id
試卷 ID, start_time
開始作答時間, submit_time
交卷時間, score
得分) 示例數據如下:
uid | exam_id | start_time | submit_time | score |
---|---|---|---|---|
1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 |
1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 |
1005 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 |
1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:59 | 84 |
1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 81 |
1002 | 9001 | 2020-09-01 13:01:01 | 2020-09-01 13:41:01 | 81 |
1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
題目練習表 practice_record
(uid
用戶 ID, question_id
題目 ID, submit_time 提交時間, score
得分):
uid | question_id | submit_time | score |
---|---|---|---|
1001 | 8001 | 2021-08-02 11:41:01 | 60 |
1004 | 8001 | 2021-08-02 19:38:01 | 70 |
1004 | 8002 | 2021-08-02 19:48:01 | 90 |
1001 | 8002 | 2021-08-02 19:38:01 | 70 |
1004 | 8002 | 2021-08-02 19:48:01 | 90 |
1006 | 8002 | 2021-08-04 19:58:01 | 94 |
1006 | 8003 | 2021-08-03 19:38:01 | 70 |
1006 | 8003 | 2021-08-02 19:48:01 | 90 |
1006 | 8003 | 2020-08-01 19:38:01 | 80 |
請統計每個 6/7 級用戶總活躍月份數、2021 年活躍天數、2021 年試卷作答活躍天數、2021 年答題活躍天數,按照總活躍月份數、2021 年活躍天數降序排序。由示例數據結果輸出如下:
uid | act_month_total | act_days_2021 | act_days_2021_exam |
---|---|---|---|
1006 | 3 | 4 | 1 |
1001 | 2 | 2 | 1 |
1005 | 1 | 1 | 1 |
1002 | 1 | 0 | 0 |
1003 | 0 | 0 | 0 |
解釋:6/7 級用戶共有 5 個,其中 1006 在 202109、202108、202008 共 3 個月活躍過,2021 年活躍的日期有 20210907、20210804、20210803、20210802 共 4 天,2021 年在試卷作答區 20210907 活躍 1 天,在題目練習區活躍了 3 天。
思路:
這題的關鍵在于CASE WHEN THEN
的使用,不然要寫很多的left join
因為會產生很多的結果集。
CASE WHEN THEN
語句是一種條件表達式,用于在 SQL 中根據條件執行不同的操作或返回不同的結果。
語法結構如下:
CASEWHEN condition1 THEN result1WHEN condition2 THEN result2...ELSE result
END
在這個結構中,可以根據需要添加多個WHEN
子句,每個WHEN
子句后面跟著一個條件(condition)和一個結果(result)。條件可以是任何邏輯表達式,如果滿足條件,將返回對應的結果。
最后的ELSE
子句是可選的,用于指定當所有前面的條件都不滿足時的默認返回結果。如果沒有提供ELSE
子句,則默認返回NULL
。
例如:
SELECT score,CASEWHEN score >= 90 THEN '優秀'WHEN score >= 80 THEN '良好'WHEN score >= 60 THEN '及格'ELSE '不及格'END AS grade
FROM student_scores;
在上述示例中,根據學生成績(score)的不同范圍,使用 CASE WHEN THEN 語句返回相應的等級(grade)。如果成績大于等于 90,則返回"優秀";如果成績大于等于 80,則返回"良好";如果成績大于等于 60,則返回"及格";否則返回"不及格"。
那了解到了上述的用法之后,回過頭看看該題,要求列出不同的活躍天數。
count(distinct act_month) as act_month_total,
count(distinct case when year(act_time)='2021'then act_day end) as act_days_2021,
count(distinct case when year(act_time)='2021' and tag='exam' then act_day end) as act_days_2021_exam,
count(distinct case when year(act_time)='2021' and tag='question'then act_day end) as act_days_2021_question
這里的 tag 是先給標記,方便對查詢進行區分,將考試和答題分開。
找出試卷作答區的用戶
SELECT uid,exam_id AS ans_id,start_time AS act_time,DATE_FORMAT(start_time,'%Y%m') AS act_month,DATE_FORMAT(start_time,'%Y%m%d') AS act_day,'exam' AS tag
FROM exam_record
緊接著就是答題作答區的用戶
SELECT uid,question_id AS ans_id,submit_time AS act_time,DATE_FORMAT(submit_time,'%Y%m') AS act_month,DATE_FORMAT(submit_time,'%Y%m%d') AS act_day,'question' AS tag
FROM practice_record
最后將兩個結果進行UNION
最后別忘了將結果進行排序 (這題有點類似于分治法的思想)
答案:
SELECT user_info.uid,count(distinct act_month) as act_month_total,count(distinct case when year(act_time)='2021'then act_day end) as act_days_2021,count(distinct case when year(act_time)='2021' and tag='exam' then act_day end) as act_days_2021_exam,count(distinct case when year(act_time)='2021' and tag='question'then act_day end) as act_days_2021_question
FROM (SELECT uid,exam_id AS ans_id,start_time AS act_time,DATE_FORMAT(start_time,'%Y%m') AS act_month,DATE_FORMAT(start_time,'%Y%m%d') AS act_day,'exam' AS tagFROM exam_recordUNION ALLSELECT uid,question_id AS ans_id,submit_time AS act_time,DATE_FORMAT(submit_time,'%Y%m') AS act_month,DATE_FORMAT(submit_time,'%Y%m%d') AS act_day,'question' AS tagFROM practice_record
) total
RIGHT JOIN user_info ON total.uid = user_info.uid
WHERE user_info.LEVEL IN (6,7)
GROUP BY user_info.uid
ORDER BY act_month_total DESC,act_days_2021 DESC
好文推薦:
《【SQL】SQL常見面試題總結(1)》
《【SQL】SQL常見面試題總結(2)》