SQL135 每个6/7级用户活跃情况
版本1
with
exam as (
select distinct
uid,
level,
start_time as submit_time,
exam_id as qq_id
from
user_info
left join exam_record using (uid)
),
question as (
select distinct
uid,
level,
submit_time,
question_id as qq_id
from
user_info
left join practice_record using (uid)
),
temp1 as (
select
uid,
count(distinct date_format(submit_time, "%Y-%m")) as act_month_total,
count(
distinct if(
year(submit_time) = 2021,
date_format(submit_time, "%Y-%m-%d"),
null
)
) as act_days_2021
from
(
select
*
from
exam
union
select
*
from
question
) as t1
where
level in (6, 7)
group by
uid
),
temp2 as (
select
uid,
count(
distinct if(
year(submit_time) = 2021,
date_format(submit_time, "%Y-%m-%d"),
null
)
) as act_days_2021_exam
from
exam
where
level in (6, 7)
group by
uid
),
temp3 as (
select
uid,
count(
distinct if(
year(submit_time) = 2021,
date_format(submit_time, "%Y-%m-%d"),
null
)
) as act_days_2021_question
from
question
where
level in (6, 7)
group by
uid
)
select
*
from
temp1
join temp2 using (uid)
join temp3 using (uid)
order by
act_month_total desc,
act_days_2021 desc;
数据准备阶段:
- 创建
exam
临时表:从用户信息和考试记录中提取用户ID、等级、提交时间和考试ID - 创建
question
临时表:从用户信息和练习记录中提取用户ID、等级、提交时间和问题ID
- 创建
汇总统计阶段:
temp1
:计算每个用户的总活跃月数和2021年的活跃天数(合并考试和练习记录)temp2
:单独计算每个用户在2021年通过考试的活跃天数temp3
:单独计算每个用户在2021年通过练习的活跃天数
最终输出:
- 将三个临时表通过用户ID连接起来
- 按总活跃月数(降序)和2021年总活跃天数(降序)排序
好啰嗦,受不了了....
版本2
WITH user_activities AS (
SELECT
u.uid,
u.level,
a.submit_time,
CASE
WHEN e.exam_id IS NOT NULL THEN 'exam' ELSE 'question'
END AS activity_type
FROM user_info u
LEFT JOIN (
SELECT uid, start_time AS submit_time, exam_id FROM exam_record
UNION ALL
SELECT uid, submit_time, question_id FROM practice_record
) a ON u.uid = a.uid
LEFT JOIN exam_record e ON a.uid = e.uid AND a.submit_time = e.start_time
WHERE u.level IN (6, 7)
)
SELECT
uid,
COUNT(DISTINCT DATE_FORMAT(submit_time, '%Y-%m')) AS act_month_total,
COUNT(DISTINCT IF(YEAR(submit_time) = 2021, DATE(submit_time), NULL)) AS act_days_2021,
COUNT(DISTINCT IF(YEAR(submit_time) = 2021 AND activity_type = 'exam', DATE(submit_time), NULL)) AS act_days_2021_exam,
COUNT(DISTINCT IF(YEAR(submit_time) = 2021 AND activity_type = 'question', DATE(submit_time), NULL)) AS act_days_2021_question
FROM user_activities
GROUP BY uid
ORDER BY act_month_total DESC, act_days_2021 DESC;
这个优化后的SQL查询在以下几个方面进行了简化:
1. 合并数据源
原查询使用了三个临时表(exam
、question
、temp1
、temp2
、temp3
),而新查询通过一个user_activities
CTE合并了所有数据源,减少了中间表的数量。
2. 简化JOIN逻辑
原查询:
- 先分别创建
exam
和question
两个临时表 - 然后在
temp1
中通过UNION合并 - 最后再JOIN两个单独统计的表
新查询:
- 一次性通过UNION ALL合并考试和练习记录
- 使用CASE WHEN直接标记活动类型
- 只需要一次GROUP BY就能完成所有统计
3. 减少重复计算
原查询:
- 对2021年的活跃天数计算了三次(总天数、考试天数、练习天数)
新查询:
- 在一次GROUP BY中同时计算所有指标
- 使用条件聚合(COUNT DISTINCT + IF)一次性得出三个指标
4. 更清晰的逻辑
- 使用
activity_type
字段明确区分活动类型 - 所有统计逻辑集中在一个SELECT中,更容易理解
- 避免了多次JOIN操作
5. 性能优势
- 减少了对基表的扫描次数(原查询扫描了三次:exam、question、合并后)
- 减少了中间结果的存储和传递
- 只需要一次分组操作就能得到所有结果
LEFT JOIN exam_record e ON a.uid = e.uid AND a.submit_time = e.start_time
是查询中最关键的部分之一,这条JOIN语句的目的是将合并后的活动记录(a
)与原始的考试记录(e
)重新关联起来。虽然子查询a
已经包含了考试记录,但我们额外做这个JOIN是为了:
- 区分考试和练习活动:通过检查
e.exam_id IS NOT NULL
来判断是否是考试- 确保数据一致性:二次验证考试记录的真实性
a.uid = e.uid
:确保是同一个用户a.submit_time = e.start_time
:确保是同一次考试(时间戳匹配)使用LEFT JOIN是为了:
- 保留所有活动记录(包括练习记录)
- 对于练习记录,
e
表的字段都会是NULL(因为时间戳不匹配)- 对于考试记录,能关联到原始的考试详情
版本3
SELECT
u.uid,
COUNT(DISTINCT DATE_FORMAT(a.submit_time, '%Y-%m')) AS act_month_total,
COUNT(DISTINCT IF(YEAR(a.submit_time) = 2021, DATE(a.submit_time), NULL)) AS act_days_2021,
COUNT(DISTINCT IF(YEAR(a.submit_time) = 2021 AND e.exam_id IS NOT NULL, DATE(a.submit_time), NULL)) AS act_days_2021_exam,
COUNT(DISTINCT IF(YEAR(a.submit_time) = 2021 AND e.exam_id IS NULL, DATE(a.submit_time), NULL)) AS act_days_2021_question
FROM user_info u
LEFT JOIN (
SELECT uid, start_time AS submit_time, exam_id FROM exam_record
UNION ALL
SELECT uid, submit_time, NULL FROM practice_record
) a ON u.uid = a.uid
LEFT JOIN exam_record e ON a.submit_time = e.start_time AND a.uid = e.uid
WHERE u.level IN (6, 7)
GROUP BY u.uid
ORDER BY act_month_total DESC, act_days_2021 DESC;
这个最新版本的SQL查询相比上一个版本又做了几项重要的优化:
1. 消除了CASE WHEN判断
上一个版本使用了CASE WHEN e.exam_id IS NOT NULL THEN 'exam' ELSE 'question'
来判断活动类型,而新版本直接在JOIN阶段通过exam_id IS NOT NULL
和exam_id IS NULL
来区分考试和练习活动,减少了条件判断的开销。
2. 优化了UNION ALL子查询
上一个版本在UNION ALL后保留了question_id,但实际上不需要这个字段。新版本在练习记录中直接使用NULL
代替question_id,减少了不必要的数据传输。
3. 简化了JOIN逻辑
上一个版本需要LEFT JOIN exam_record来确定活动类型,而新版本通过UNION ALL子查询中保留的exam_id信息就能区分活动类型,减少了JOIN操作的复杂度。
4. 更精确的JOIN条件
新版本在LEFT JOIN exam_record时同时使用了时间(a.submit_time = e.start_time
)和用户ID(a.uid = e.uid
)作为连接条件,比上一个版本更精确,避免了可能的错误匹配。
5. 减少了一次子查询嵌套
上一个版本使用了CTE (WITH子句),而新版本直接在FROM子句中使用内联视图,对于简单查询可以减少一层查询嵌套。
性能影响
- 减少了内存使用(UNION ALL中传输的数据更少)
- 降低了CPU计算量(去除了CASE WHEN判断)
- 提高了JOIN效率(更精确的JOIN条件)
- 查询计划可能更简单直接
补充知识
字段名继承:UNION ALL
会使用第一个SELECT语句中的字段名作为结果集的列名