SQL报表日报生成慢是很多业务系统都会遇到的性能问题,尤其是当业务运行时间变长、历史数据不断累积后,全量扫描全表统计的方式会让报表生成耗时从几分钟变成几十分钟甚至几小时。增量统计机制通过只处理新增和变更的数据,复用历史统计结果,能从根本上解决这个问题。

增量统计机制的核心原理
增量统计的核心思路是避免重复计算已经处理过的历史数据,每次生成报表时只处理上次统计之后新增、修改、删除的数据,再结合之前已经保存的累计统计结果得到最终的报表数据。
和全量统计对比,增量统计的优势非常明显:
| 统计方式 | 数据处理范围 | 耗时随数据量变化 | 资源占用 |
|---|---|---|---|
| 全量统计 | 全部历史数据 | 线性增长 | 高 |
| 增量统计 | 新增/变更数据 | 基本稳定 | 低 |
实现增量统计的前置条件
要使用增量统计优化日报生成,需要先满足几个基础条件:
- 业务表需要有明确的增量标识,比如自增主键
id、数据创建时间create_time、最后更新时间update_time,或者数据变更日志表 - 需要有存储历史累计统计结果的汇总表,每次增量计算后更新汇总表的数据
- 需要记录上次统计的截止位点,比如上次处理到的最大
id或者最大update_time,避免重复处理数据
增量统计的实现步骤
1. 设计汇总表结构
首先创建一个汇总表,用来存储日报的累计统计结果,以订单日报为例,汇总表结构如下:
-- 订单日报汇总表,存储每天的累计统计数据
CREATE TABLE order_daily_summary (
summary_date DATE NOT NULL COMMENT '统计日期',
total_order_count INT NOT NULL DEFAULT 0 COMMENT '累计订单总数',
total_order_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '累计订单总金额',
last_handled_id INT NOT NULL DEFAULT 0 COMMENT '上次处理到的订单最大id',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (summary_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单日报汇总表';
2. 记录增量位点
创建一个位点表,记录每次统计的截止位置,避免重复处理:
-- 增量统计位点表
CREATE TABLE increment_offset (
biz_type VARCHAR(50) NOT NULL COMMENT '业务类型,比如order_daily',
last_offset INT NOT NULL DEFAULT 0 COMMENT '上次处理的截止位点,对应订单表的id',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (biz_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='增量统计位点表';
3. 编写增量统计SQL逻辑
每次生成日报时,先获取上次处理的截止位点,然后查询该位点之后的新增和修改的订单数据,计算增量部分的统计值,再更新汇总表:
-- 第一步:获取上次处理的截止位点
SELECT last_offset INTO @last_id FROM increment_offset WHERE biz_type = 'order_daily';
-- 第二步:查询增量数据,计算增量统计值
SELECT
COUNT(*) AS inc_order_count,
SUM(order_amount) AS inc_order_amount,
MAX(id) AS max_handled_id
INTO
@inc_count,
@inc_amount,
@current_max_id
FROM order_info
WHERE id > @last_id
AND create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
AND create_time < CURDATE();
-- 第三步:更新汇总表,合并增量数据
INSERT INTO order_daily_summary (summary_date, total_order_count, total_order_amount, last_handled_id)
VALUES (CURDATE(), @inc_count, @inc_amount, @current_max_id)
ON DUPLICATE KEY UPDATE
total_order_count = total_order_count + VALUES(total_order_count),
total_order_amount = total_order_amount + VALUES(total_order_amount),
last_handled_id = VALUES(last_handled_id);
-- 第四步:更新位点表
UPDATE increment_offset
SET last_offset = @current_max_id
WHERE biz_type = 'order_daily';
4. 处理数据删除场景
如果业务中存在订单删除的情况,还需要额外处理删除的数据,避免汇总结果包含已删除的数据:
-- 假设有订单删除日志表order_delete_log,记录被删除的订单id和删除时间
SELECT COUNT(*) AS del_count, SUM(order_amount) AS del_amount
INTO @del_count, @del_amount
FROM order_delete_log
WHERE delete_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
AND delete_time < CURDATE();
-- 更新汇总表,减去删除的订单数据
UPDATE order_daily_summary
SET total_order_count = total_order_count - @del_count,
total_order_amount = total_order_amount - @del_amount
WHERE summary_date = CURDATE();
增量统计的注意事项
- 位点记录要可靠,避免位点丢失导致数据重复统计或者漏统计,位点表可以和业务操作放在同一个事务里
- 如果业务表没有自增主键或者更新时间字段,可以通过解析数据库的binlog日志获取增量数据,这种方式对业务表的侵入性更小
- 定期校验汇总表的数据和全量统计的结果是否一致,避免增量计算过程中出现数据偏差
- 如果日报需要统计维度较多,比如按地区、按商品分类统计,可以针对每个维度创建对应的汇总表,分别做增量更新
增量统计机制并不是所有场景都适用,如果报表需要频繁回溯很早期的历史数据,或者数据变更非常频繁,需要结合实际情况调整增量策略,避免增量计算的逻辑过于复杂反而影响性能。
实际效果对比
某电商系统之前生成订单日报需要全量扫描3000万条订单数据,耗时45分钟,采用增量统计机制后,每次只处理新增的10万条左右订单数据,报表生成耗时稳定在2分钟以内,CPU和IO资源占用也下降了70%以上,效果非常明显。