某HR吐槽:我们公司已经臭名昭著了,网上一搜全是负面评价,候选人放鸽子的概率快百分百了雹感管这可苦了我们做招聘的人了。
岗位刚挂出去,简历投得稀稀拉拉。好不容易约到面试,候选人前一晚还说“好的,明天见”,第二天人直接蒸发。你去问,中介含糊两句,候选人大概率早就去搜过公司了,一屏负面,谁看了不打退堂鼓。
公司能把自己经营到这种程度,也算有点本事。别的公司花钱做雇主品牌,拍视频、写故事、搞文化墙,生怕别人不了解自己。你们倒好,靠前员工、社交平台、投诉贴,硬是把“避雷”两个字做成了搜索联想。
最难受的是,外面的人骂完就走,里面做招聘的还得继续打电话、继续解释、继续硬着头皮约人。你嘴上说发展空间、平台机会,候选人心里想的是:这地方网上怎么全是坑。
说到底,招聘解决的是信息触达,不是洗白。名声臭了,还指望HR一张嘴把人劝进来,这活本身就有点离谱。前面把路挖烂了,后面还怪别人车开不过来。
算法题:超过 5 名学生的课
选课表里一条 SQL,跑了 3 秒。数据量不大,几万行。我第一眼就不太信是“数据多”的锅。
表结构很简单:
student(id, name)
course(id, name)
sc(id, student_id, course_id)
题目是:找出“选课人数超过 5 名学生的课”。
很多人上来就是三表 join,然后再 group by。写是能写,但现场我一般不会这么干。先盯中间表 sc,它才是数据量最大的。
最直白的 SQL 是这样:
select course_id
from sc
groupby course_id
havingcount(student_id) > 5;
这句其实已经够用了。问题在于,有人会把它写成:
select c.id, c.name
from course c
leftjoin sc s on c.id = s.course_id
groupby c.id, c.name
havingcount(s.student_id) > 5;
功能没问题,但如果 sc.course_id 没索引,执行计划就难看了。生产里这种“看起来没问题”的 SQL,我一般先 explain 一下,别等报警。
算法题本质是什么?统计 + 过滤。别绕远路。
如果用 Java 做内存版处理,其实也一样。比如你从数据库查出了所有选课记录:
classSC{
Long studentId;
Long courseId;
}
第一反应不是两层 for。直接上 Map 计数。
public List<Long> findHotCourse(List<SC> list){
Map<Long, Integer> counter = new HashMap<>();
for (SC sc : list) {
counter.merge(sc.courseId, 1, Integer::sum);
}
List<Long> result = new ArrayList<>();
for (Map.Entry<Long, Integer> entry : counter.entrySet()) {
if (entry.getValue() > 5) {
result.add(entry.getKey());
}
}
return result;
}
这里我用 merge,不是 getOrDefault + put。代码少一行,但更重要的是表达清晰:累加。
有人喜欢 Stream,一行搞定:
public List<Long> findHotCourse(List<SC> list){
return list.stream()
.collect(Collectors.groupingBy(
sc -> sc.courseId,
Collectors.counting()
))
.entrySet()
.stream()
.filter(e -> e.getValue() > 5)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
写是能写,但我线上排问题的时候不太爱这么写。出 bug 不好断点。尤其数据量一大,Stream 里再嵌套操作,栈不好看。
再往前一步,如果题目升级成“选课人数超过 5 的课程名称”,那就别在 Java 里二次查数据库。一次 SQL 搞定:
select c.id, c.name
from course c
where c.id in (
select course_id
from sc
groupby course_id
havingcount(student_id) > 5
);
注意我用的是子查询,不是 join。为什么?有些数据库在这种场景下,子查询反而更容易走半连接优化。具体看执行计划,不是拍脑袋。
如果数据特别大,比如 sc 表几千万行,这类统计我会优先考虑:
course_id上必须有索引。看是否可以加覆盖索引 (course_id, student_id)。如果是高频查询,直接做一张课程选课人数的冗余统计表。
冗余不是罪,盲目实时才是。
比如插入选课记录时顺手更新计数:
@Transactional
publicvoidselectCourse(Long studentId, Long courseId){
scMapper.insert(studentId, courseId);
courseMapper.incrCount(courseId);
}
然后查询就变成:
selectid, name
from course
where selected_count > 5;
这才是线上系统的写法。算法题是练基本功,线上是看吞吐和锁。
我做排查时有个习惯:只要看到 group by + having,就会条件反射去看数据分布。很多人以为“超过 5”是小数目,其实如果一门公共课几千人选,那聚合就是重活。
算法题到这里其实已经结束了。但真正的区别在于,你是把它当作一道练习,还是当作一条会被压测打爆的查询。
写代码不难,难的是看到 count 的时候,脑子里自动浮现执行计划。这个感觉,是慢慢练出来的。