TypeORM 时序统计分表方案:解决跨境代购系统长期统计数据膨胀问题
长期运营跨境代购、国际集运平台会面临一个难以规避的底层问题:订单、运单、用户行为的每日时序统计数据持续累积,单张时序数据表数据量突破百万后,查询、聚合速度持续下滑,单纯增加索引无法根治性能瓶颈。我在二次开发 Taocarts 代购系统时,初期仅使用单张order_stat_daily存储所有日期统计数据,运营筛选半年以上历史数据时接口超时频发。经过多轮方案对比,最终落地 TypeORM 时序数据按月分表存储方案,自动按月创建独立统计表,读写时动态匹配对应月份数据表,彻底解决统计数据表膨胀导致的报表卡顿问题,本文完整讲解分表建表逻辑、动态仓储封装、定时任务自动分表写入、查询路由匹配代码,适配所有反向海淘、淘宝 1688 代购系统的数据统计模块底层优化。
传统单表存储时序统计数据存在三大硬伤:第一,数据表行数无限增长,索引体积持续变大,内存无法完全缓存索引,聚合查询频繁触发磁盘 IO;第二,删除历史归档数据风险高,直接 DELETE 会产生大量表碎片,影响全表查询性能;第三,无法按业务周期冷热分离,近 30 天活跃统计数据和一年前归档数据混存,资源无法差异化调度。按月分表后,每个月份独立一张统计表,冷数据月份可直接归档备份甚至离线迁移,热数据月份仅保留近 3 个月表,查询时仅扫描目标月份对应数据表,数据扫描量直接缩减 90% 以上。
整套分表架构核心分为四大模块:自动分表命名规则生成、动态 Repository 仓储封装、定时任务写入自动路由分表、前端日期筛选自动匹配多表联合查询。分表命名规则固定为{业务类型}stat,例如订单 2026 年 5 月统计表为order_stat_202605,系统自动根据日期解析年月匹配对应数据表,不存在对应月份表则自动执行建表语句。
通用分表动态仓储封装核心代码
`// src/common/database/shard-stat.repository.ts
import { EntityManager, Repository } from 'typeorm';
import { OrderStatDaily } from 'src/modules/statistics/entities/order-stat-daily.entity';
export class ShardStatRepository {
private entityManager: EntityManager;
private baseEntity = OrderStatDaily;
constructor(entityManager: EntityManager) {
this.entityManager = entityManager;
}
// 根据年月获取对应分表Repository,不存在则自动建表
async getMonthRepo(yearMonth: string): Promise
const tableName = order_stat_${yearMonth};
// 判断数据表是否存在
const exist = await this.entityManager.query(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?, [tableName]);
// 无表则复制基础表结构创建分表
if(exist.length === 0) {
await this.entityManager.query(CREATE TABLE ${tableName} LIKE order_stat_daily);
}
// 绑定动态表名生成仓储实例
return this.entityManager.getRepository(this.baseEntity).extend({
tableName
})
}
// 多月份区间联合查询,合并多张分表数据
async rangeQuery(startDate: Date, endDate: Date) {
const monthList = this.getMonthRange(startDate, endDate);
const allData = [];
for(const ym of monthList) {
const repo = await this.getMonthRepo(ym);
const monthData = await repo
.createQueryBuilder('stat')
.where('stat.statDate BETWEEN 😒 AND :e', { s: startDate, e: endDate })
.getMany();
allData.push(...monthData);
}
// 按日期升序统一排序
return allData.sort((a,b) => new Date(a.statDate).getTime() - new Date(b.statDate).getTime());
}
// 解析起止日期覆盖的所有年月数组
private getMonthRange(start: Date, end: Date): string[] {
const res = new Set();
const curr = new Date(start);
while(curr <= end) {
const year = curr.getFullYear();
const month = String(curr.getMonth() + 1).padStart(2, '0');
res.add(${year}${month});
curr.setMonth(curr.getMonth() + 1);
}
return Array.from(res);
}
}改造原有定时预聚合任务,写入数据时自动解析当日年月,路由至对应分表执行 upsert 操作,不再写入单张总表;前端传入自定义起止日期筛选报表时,后端自动拆分覆盖的所有月份分表,分别查询后合并排序,返回完整时序数据供给 ECharts 图表渲染。同时配套归档脚本,自动将超过 12 个月的分表离线备份至云端 OSS,线上数据库仅保留近 12 个月分表,持续控制数据库存储体积。 定时任务分表写入改造片段 // 原订单统计定时任务写入逻辑替换为分表仓储 const year = yesterday.getFullYear(); const month = String(yesterday.getMonth() + 1).padStart(2, '0'); const ym =${year}${month}`;
const shardRepo = new ShardStatRepository(this.orderRepo.manager);
const targetRepo = await shardRepo.getMonthRepo(ym);
await targetRepo.upsert([statItem], ['statDate']);
这套按月分表方案完美适配代购系统、跨境平台长期运营的数据膨胀痛点,不管是淘宝 1688 反向代购独立站,还是集运转运跨境平台,时序统计数据都可以通过分表实现冷热数据隔离,从底层数据库层面保障后台统计看板长期查询稳定,不会随着运营时间增长出现报表加载缓慢、接口超时等问题,是商用级代购源码必须具备的底层存储优化方案。