题目:
请你筛选表中的数据,当有任意一个0级用户未完成试卷数大于2时,输出每个0级用户的试卷未完成数和未完成率(保留3位小数);若不存在这样的用户,则输出所有有作答记录的用户的这两个指标。结果按未完成率升序排序。
难点:
- 这道题最大的难点就在,它看起来是“如果A,则B;如果不是A,则C”的一个条件分支,在处理这个条件分支的实际过程中,会发现“是A”和“不是A”之间的界限划分是很需要对关系逻辑的良好运用
- 我会给出两个比较好理解的解决方法,一个是比较常规的,一个是在WHERE子句中使用CASE WHEN的 —— 后者我会直接放置@ZHMM 的答案,写得非常好,大家可以多参考
解题方法
方法 1:需要对CASE WHEN、开窗函数、WHERE的良好理解
步骤 1:先找出每一位用户的试卷作答记录,并计算出他们的作答次数、未完成次数、未完成率等
- 这一步很简单了,就是两个表联结,再使用CASE WHEN来计算作答次数、未完成次数、未完成率等,直接上代码:
SELECT ui.uid, ui.level, /* 当exam_id不为空时,才代表该用户有作答记录,SUM函数计算总作答次数 */ SUM(CASE WHEN er.exam_id IS NOT NULL THEN 1 ELSE 0 END) AS start_cnt, /* 由于是user_info左联结exam_record,因此需要score为空且start_time不为空时,才认定该用户有作答但未提交; 否则,会导致没有过作答记录、分数为NULL的用户也会被认为作答过但未提交;SUM函数计算未完成次数 */ SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) AS incomplete_cnt, /* 同样地,先看最底层的两个SUM函数,其实就是未完成次数与作答次数相除,显得出未完成率; 由于存在作答次数为0,即分母为0导致结果为NULL的情况,所以还需要使用IFNULL来将NULL转化为0; 最后则是ROUND函数来四舍五入,取3个小数位 */ ROUND(IFNULL(SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) / SUM(CASE WHEN ui.uid IS NOT NULL AND er.exam_id IS NOT NULL THEN 1 ELSE 0 END), 0), 3) AS incomplete_rate FROM user_info AS ui LEFT OUTER JOIN exam_record AS er ON ui.uid = er.uid GROUP BY ui.uid, ui.level
步骤 2:使用开窗函数,来判断是否存在“0级用户未完成试卷数大于2”的情况
- 如果存在该情况,则为对应记录行打上“1”这个标签,否则,打上“0”标签
- 我使用的是SUM函数与开窗函数的结合,所得出的新字段,实际上代表:“未完成试卷数大于2的0级用户有多少个”
- 最终,由于使用的是是OVER (),因此,所有记录行都会被打上同一个标签:“0个”或者“1及1以上”这两种情况
SELECT t1.*, SUM(CASE WHEN incomplete_cnt > 2 AND level = 0 THEN 1 ELSE 0 END) OVER () AS judge FROM (SELECT ui.uid, ui.level, SUM(CASE WHEN er.exam_id IS NOT NULL THEN 1 ELSE 0 END) AS start_cnt, SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) AS incomplete_cnt, ROUND(IFNULL(SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) / SUM(CASE WHEN ui.uid IS NOT NULL AND er.exam_id IS NOT NULL THEN 1 ELSE 0 END), 0), 3) AS incomplete_rate FROM user_info AS ui LEFT OUTER JOIN exam_record AS er ON ui.uid = er.uid GROUP BY ui.uid, ui.level) AS t1
步骤 3:关键一步,亦是最终一步,根据“是否存在未完成试卷数大于2的0级用户”来选择输出哪些用户的记录
- 这里有一个非常关键又简单的数学逻辑需要意识到:“未完成试卷数大于2的0级用户”只有“存在”和“不存在”两种情况,即,对应地,judge字段只会有“0”或者“大于等于1”两个情况
- 因此,该两个情况是互斥的,其中一个存在,则另外一个一定不存在
- 那么, 在选择SELECT哪几个用户的结果,实质上就可以用过WHERE来进行筛选了:当judge为0(即不存在该种用户时),则再选择有过作答记录(即start_cnt不为0)的用户;当judge大于等于1时,即选择等级为0(即level为0)的用户;这两个筛选条件,可以直接使用OR来连接,一并在WHERE子句中使用 —— 还是那句话,只要judge为0,则一定不会再select出judge为1的情况,反之亦然,所以这个OR实际上也只会返还一个情况下的结果
SELECT uid, incomplete_cnt, incomplete_rate FROM ( SELECT t1.*, SUM(CASE WHEN incomplete_cnt > 2 AND level = 0 THEN 1 ELSE 0 END) OVER () AS judge FROM (SELECT ui.uid, ui.level, SUM(CASE WHEN er.exam_id IS NOT NULL THEN 1 ELSE 0 END) AS start_cnt, SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) AS incomplete_cnt, ROUND(IFNULL(SUM(CASE WHEN er.score IS NULL AND start_time IS NOT NULL THEN 1 ELSE 0 END) / SUM(CASE WHEN ui.uid IS NOT NULL AND er.exam_id IS NOT NULL THEN 1 ELSE 0 END), 0), 3) AS incomplete_rate FROM user_info AS ui LEFT OUTER JOIN exam_record AS er ON ui.uid = er.uid GROUP BY ui.uid, ui.level) AS t1 ) AS t2 WHERE (judge >= 1 AND level = 0) OR (judge = 0 AND start_cnt > 0) ORDER BY incomplete_rate ASC;
方法 2:更加快速,需要对在WHERE中使用CASE WHEN有理解
- 这部分我直接上答案,后续有时间给各位解析一下
SELECT ui.uid, SUM(IF(start_time IS NOT NULL AND score IS NULL,1,0)) AS incomplete_cnt,#3.试卷未完成数 ROUND(AVG(IF(start_time IS NOT NULL AND score IS NULL,1,0)),3) AS incomplete_rate#4.未完成率 FROM user_info ui LEFT JOIN exam_record USING(uid) WHERE CASE WHEN (#1.当有任意一个0级用户未完成试卷数大于2时 SELECT MAX(lv0_incom_cnt) FROM( SELECT SUM(IF(score IS NULL,1,0)) AS lv0_incom_cnt FROM user_info JOIN exam_record USING(uid) WHERE level=0 GROUP BY uid )table1 )>2 THEN uid IN(#1.1找出每个0级用户 SELECT uid FROM user_info WHERE level=0 ) ELSE uid IN(#2.若不存在这样的用户,找出有作答记录的用户 SELECT DISTINCT uid FROM exam_record ) END GROUP BY ui.uid ORDER BY incomplete_rate#5.结果按未完成率升序排序