程序员老鬼

某HR吐槽:我们公司已经臭名昭著了,网上一搜全是负面评价,候选人放鸽子的概率快百分百了雹感管这可苦了我们做招聘的人了。

岗位刚挂出去,简历投得稀稀拉拉。好不容易约到面试,候选人前一晚还说“好的,明天见”,第二天人直接蒸发。你去问,中介含糊两句,候选人大概率早就去搜过公司了,一屏负面,谁看了不打退堂鼓。

Image

公司能把自己经营到这种程度,也算有点本事。别的公司花钱做雇主品牌,拍视频、写故事、搞文化墙,生怕别人不了解自己。你们倒好,靠前员工、社交平台、投诉贴,硬是把“避雷”两个字做成了搜索联想。

最难受的是,外面的人骂完就走,里面做招聘的还得继续打电话、继续解释、继续硬着头皮约人。你嘴上说发展空间、平台机会,候选人心里想的是:这地方网上怎么全是坑。

说到底,招聘解决的是信息触达,不是洗白。名声臭了,还指望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 表几千万行,这类统计我会优先考虑:

  1. course_id 上必须有索引。
  2. 看是否可以加覆盖索引 (course_id, student_id)。
  3. 如果是高频查询,直接做一张课程选课人数的冗余统计表。

冗余不是罪,盲目实时才是。

比如插入选课记录时顺手更新计数:

@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 的时候,脑子里自动浮现执行计划。这个感觉,是慢慢练出来的。