数据可视化的中间表方案
1、数据统计
数据统计是所有软件应用的必备模块,因为需要通过数据去了解整个项目的运营情况,并且随着大屏的兴起变的越来越重要。先看一个实例:网站访问数据统计
统计类的SQL本身其实不难,大家都知道无外乎就是联表,以及使用SUM、Count等聚合函数。
然而既然是数据统计,往往就伴随着海量的数据。这时联表、聚合函数等操作就是致命的操作,动辄几十秒的慢查询,很容易拖垮生产应用,当然也可能拖垮你的职业生涯~
大家很容易想到,慢SQL肯定是索引没用好。然而现实是在千万级别的数据面前一切的索引优化、命中分析都是枉然~
2、心路历程
去年下半年,公司临时接到一个商品销售类项目。工时很紧,技术部全员投入,首版要求2周内上线。
我之前做的前端开发,现在已不在技术部任职,做软件售前/产品类的工作。但技术部实在忙不过来,把数据库账号密码给我,数据大屏这块就交给我开发了。大屏展示中样式和Echarts我都能搞,但SQL我只在大学时学过一点基础,从未在项目中实际开发过。但受命于公司危难之际,我只能现学现卖。
大屏第一版我直接查交易记录表,SQL写的朴实无华,毫无技巧与效率可言!但项目刚上线,每天1万多订单,全量20万数据。虽然Sum、Count、Count (distinct)一顿操作,但一切都还兜得住,大屏还获得甲方的大力赞赏!
然后遭遇国庆节,存量数据来到140万,SQL执行时间已经2秒了。此时开始优化SQL,优化索引,效果不是很明显。
当存量数据来到500万时,所有sql语句和索引层面的优化都做到了极限,查询时间依旧突破4秒。再后来因为我的慢SQL拉爆了数据库服务,导致生产业务中断了一次,后来我这个小透明因此出名了一次,我知道是时候调整方案了。
3、中间表方案
我之前干前端开发的,其实不懂Java也没中间表的概念,还是技术部同事教我中间表的用法。
中间表方案:根据大屏统计维度,设计一张中间表。先处理存量数据,每次只统计一天的数据,将上线后每一天的数据都汇集进中间表。然后,定时任务每天凌晨统计前一天的数据,插入中间表。统计类SQL全都改为从中间表查询,整体方案如下图所示:
整体架构
此方案新增了后线库和中间表,将生产业务与统计类需求隔离开。生产库只处理业务中的增删改查,然后同步给后线库。统计类SQL全部查询后线库的各种中间表,杜绝此前因统计类SQL影响生产业务的情况。
在凌晨汇总后线库前一天的数据到中间表,数据库资源占用极小,统计类SQL查询效率也是毫秒级别的。这才是能保障生产数据最稳定和安全的方案。
4、绝知此事要躬行
当时我只负责SQL编写与大屏展示,Java开发与中间表的设计、定时任务、数据汇集等都是技术部的同事帮忙干的。今年我自学了Java,写了个人网站,在数据统计功能也实践了相关的各个操作环节,非常有趣。具体如下:
我个人网站的数据统计页面
4.1、数据来源
我的网站做了前端埋点,大部分操作都有操作记录,存在log表中。比如点击菜单、翻页、留言、评论等操作,都记录一条数据,将这些步骤汇总起来就是用户的访问轨迹了。
我还创建了一个log_daily_ip_summary表,定时任务每10分钟将log表当天的数据以IP分组汇总后,插入到log_daily_ip_summary表中。然后如上图所有的图表都可以从log_daily_ip_summary表中轻松高效获取。
两个表的字段基本一样,只不过中间表(log_daily_ip_summary)是以IP分组的汇总数据。这种操作很基础,但足够承受当前的访问量,且对统计类SQL的编写提供了极大的便利性。
log表
中间表
4.2、汇集操作
学了Java后发现,以前觉得高端大气的定时任务原来被SpringBoot封装的如此简单易用,方便快捷。使用中间表这种出发方法,其实是把原有直接查业务表SQL的工作量,前置到了中间表的设计及定时任务的编写上了。
如下分享下我蹩脚的定时任务代码,编写一个查询并插入一天数据的方法,然后定时任务和存量数据回填都用这个方法。
@Slf4j
@Service
public class LogSummaryJob {@Autowiredprivate LogSummaryMapper logSummaryMapper;@Autowiredprivate LogService logService;private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");/*** 应用启动后自动回填历史数据*/public String backfillHistoryData() {// 先清空数据logSummaryMapper.cleanAll();// 定义需要回填的日期范围// 网站从4月1日上线,批量回填数据到当日// 配合10分钟定时任务,该任务不停的汇总当日数据,即可覆盖所有时间日志数据LocalDate startDate = LocalDate.of(2025, 4, 1);LocalDate endDate = LocalDate.now();List<LocalDate> datesToProcess = new ArrayList<>();LocalDate currentDate = startDate;// 填充日期数组while (!currentDate.isAfter(endDate)) {datesToProcess.add(currentDate);currentDate = currentDate.plusDays(1);}// 并行处理以提高速度(如果数据量大)datesToProcess.parallelStream().forEach(this::processDate);System.out.println(DateUtils.getCurrentDateTime() + ": 所有数据回填完成!");return DateUtils.getCurrentDateTime() + ": 所有数据回填完成!";}/*** 每10分钟汇总log表中的数据到中间表*/@Scheduled(cron = "0 */10 * * * ?") // 每10分钟执行一次(秒 分 时 日 月 周)public void periodicSummaryTask() {// 每次汇总数据,先清空中间表中当天的汇总数据logSummaryMapper.deleteToday();LocalDate today = LocalDate.now();processDate(today);System.out.println("当日日志数据汇总完成,处理日期:" + DateUtils.getCurrentDateTime());}// 汇总某一条函数private void processDate(LocalDate date){String dateStr = date.format(dateFormatter);String nextDateStr = date.plusDays(1).format(dateFormatter);// 获取需要排除IP的SQL字符串(sunq的访问ip要剔除掉)String excludeSunqSql = logService.excludeSunqSql(dateStr + " 00:00:00",nextDateStr + " 00:00:00");logSummaryMapper.insertDailyIpSummary(dateStr, dateStr + " 00:00:00", nextDateStr + " 00:00:00", excludeSunqSql);}/*** 手动触发某天的数据处理(用于测试或补数据)*/public void manualProcessDate(String dateStr) {LocalDate date = LocalDate.parse(dateStr, dateFormatter);processDate(date);}
}
4.3、源码开源
我的个人网站地址:码语人生,完全开源:Github地址
适合从前端转全栈,或者初学者实践练手,欢迎大家fork代码,指导评论。