如何在NestJS-TypeORM环境中实现PostgreSQL表分区?
在NestJS-TypeORM环境下实现PostgreSQL分区方案
TypeORM本身没有内置PostgreSQL分区支持,但不需要完全依赖原生SQL——最稳妥且易维护的方式是结合TypeORM的迁移脚本执行分区相关SQL,同时可以通过NestJS定时任务自动化分区管理。针对你那1亿行的annotation表,以下是具体实现方案:
1. 先确定分区策略
首先要根据你的查询模式选对分区键:
- 若多按时间范围查询(比如按月份拉取数据):选
created_at做范围分区(最适合大数据量时序表) - 若多按用户/业务维度查询:选
user_id或business_type做列表分区
下文以「按created_at月度范围分区」为例展开。
2. 推荐方案:TypeORM迁移脚本+原生SQL
这是生产环境最可控的方式,既利用TypeORM的迁移版本管理,又通过原生SQL实现分区逻辑,不会破坏现有实体映射。
2.1 编写迁移脚本创建分区表
假设你的annotation实体结构如下:
// src/entities/annotation.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Annotation { @PrimaryGeneratedColumn() id: number; @Column() content: string; @Column({ type: 'timestamp' }) created_at: Date; // 其他业务字段... }
创建迁移脚本(替换原有非分区表,同时迁移存量数据):
// src/migrations/1699999999999-create-partitioned-annotation.ts import { MigrationInterface, QueryRunner } from 'typeorm'; export class CreatePartitionedAnnotation1699999999999 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // 1. 备份原有表(必须做,防止数据丢失) await queryRunner.query(`CREATE TABLE annotation_backup AS TABLE annotation;`); // 2. 删除原有非分区表 await queryRunner.query(`DROP TABLE IF EXISTS annotation;`); // 3. 创建分区父表,指定分区键和分区类型 await queryRunner.query(` CREATE TABLE annotation ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, created_at TIMESTAMP NOT NULL -- 其他字段与原有表完全一致 ) PARTITION BY RANGE (created_at); `); // 4. 创建最近6个月的初始分区(按需调整) const months = 6; for (let i = 0; i < months; i++) { const date = new Date(); date.setMonth(date.getMonth() - i); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const monthEnd = new Date(year, date.getMonth() + 1, 0, 23, 59, 59) .toISOString() .slice(0, 19) .replace('T', ' '); await queryRunner.query(` CREATE TABLE annotation_${year}_${month} PARTITION OF annotation FOR VALUES FROM ('${year}-${month}-01 00:00:00') TO ('${monthEnd}'); `); } // 5. 将备份数据导入分区表(PostgreSQL会自动路由到对应分区) await queryRunner.query(`INSERT INTO annotation SELECT * FROM annotation_backup;`); // 确认数据无误后再删除备份表 // await queryRunner.query(`DROP TABLE annotation_backup;`); } public async down(queryRunner: QueryRunner): Promise<void> { // 回滚逻辑:删除分区表,恢复备份表 await queryRunner.query(`DROP TABLE IF EXISTS annotation CASCADE;`); await queryRunner.query(`ALTER TABLE annotation_backup RENAME TO annotation;`); } }
2.2 自动化维护新分区
时间分区需要定期创建未来的分区(比如每月1号创建下一个月的分区),用NestJS定时任务实现:
// src/tasks/partition-maintenance.task.ts import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { InjectConnection } from '@nestjs/typeorm'; import { Connection } from 'typeorm'; @Injectable() export class PartitionMaintenanceTask { private readonly logger = new Logger(PartitionMaintenanceTask.name); constructor(@InjectConnection() private readonly connection: Connection) {} // 每月1号凌晨1点创建下一个月的分区 @Cron('0 0 1 1 * *') async createNextMonthPartition(): Promise<void> { const nextMonth = new Date(); nextMonth.setMonth(nextMonth.getMonth() + 1); const year = nextMonth.getFullYear(); const month = String(nextMonth.getMonth() + 1).padStart(2, '0'); const monthStart = `${year}-${month}-01 00:00:00`; const monthEnd = new Date(year, nextMonth.getMonth() + 1, 0, 23, 59, 59) .toISOString() .slice(0, 19) .replace('T', ' '); try { await this.connection.query(` CREATE TABLE IF NOT EXISTS annotation_${year}_${month} PARTITION OF annotation FOR VALUES FROM ('${monthStart}') TO ('${monthEnd}'); `); this.logger.log(`Created partition annotation_${year}_${month} successfully`); } catch (error) { this.logger.error(`Failed to create partition annotation_${year}_${month}`, error.stack); } } }
在AppModule中注册定时任务并关闭TypeORM自动同步(必须关闭,否则会破坏分区结构):
// src/app.module.ts import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Annotation } from './entities/annotation.entity'; import { PartitionMaintenanceTask } from './tasks/partition-maintenance.task'; @Module({ imports: [ ScheduleModule.forRoot(), TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'your_password', database: 'your_db', entities: [Annotation], migrations: ['src/migrations/**/*.ts'], synchronize: false, // 强制关闭自动同步! }), TypeOrmModule.forFeature([Annotation]), ], providers: [PartitionMaintenanceTask], }) export class AppModule {}
3. 可选:非原生SQL的轻量方案(不推荐存量大表)
如果是新项目,你可以自定义装饰器扩展TypeORM,但兼容性和可控性不如迁移脚本:
// src/decorators/partition.decorator.ts import { Entity } from 'typeorm'; export function PartitionedEntity(tableName: string, partitionBy: string, type: 'RANGE' | 'LIST') { return (constructor: Function) => { Entity(tableName)(constructor); // 需自行扩展TypeORM的SchemaBuilder生成分区SQL,复杂度较高 }; }
使用时替换@Entity():
@PartitionedEntity('annotation', 'created_at', 'RANGE') export class Annotation { // 实体字段... }
4. 关键注意事项
- 关闭synchronize:TypeORM自动同步会忽略分区结构,必须设为
false,完全用迁移管理表结构 - 存量数据迁移:1亿行数据建议分批导入,避免长时间锁表,可使用
pg_dump分分区导出导入 - 查询优化:所有查询必须带上分区键过滤(比如
WHERE created_at BETWEEN ...),否则会扫描所有分区 - 索引管理:在父表创建的索引会自动同步到所有分区,无需单独给分区建索引
内容的提问来源于stack exchange,提问作者Jinhyuck Cha
相关产品推荐
相关产品推荐

