You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 01:01:11