6月2日 01:46

TypeORM 查询太慢怎么优化?N+1、索引和批量插入实战

TypeORM 性能问题主要来自三个地方:N+1 查询、缺少索引、大量数据操作用了逐条插入。逐一解决后,大部分应用的数据库性能就够了。

1. 解决 N+1 查询

最常见也最致命的性能问题。查询用户列表后逐个查用户的文章:

typescript
// N+1:1 次查用户 + N 次查文章 const users = await userRepo.find(); for (const user of users) { user.posts = await postRepo.find({ where: { authorId: user.id } }); }

100 个用户 = 101 条 SQL。解决方案:用 relationsjoin 一次性查出。

typescript
// 方案一:relations(发 2 条 SQL,但比 N+1 好很多) const users = await userRepo.find({ relations: ['posts'] }); // 方案二:QueryBuilder join(1 条 SQL) const users = await userRepo .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();

leftJoinAndSelect 同时查出关联数据。leftJoin 只 join 不 select——用于过滤条件但不返回关联数据。

2. 只查需要的字段

find() 默认 SELECT 所有列。大表里有 TEXT/BLOB 列时,查全列浪费网络带宽和内存:

typescript
// 查全列 const users = await userRepo.find(); // 只查需要的列 const users = await userRepo.find({ select: ['id', 'name', 'email'] }); // QueryBuilder 方式 const users = await userRepo .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany();

列表页只需要 id 和 name,详情页才需要所有字段。按场景选择查询范围。

3. 分页不要用 skip/take 处理大偏移

typescript
// 大偏移时很慢:MySQL 要扫描前 10000 行然后丢掉 const posts = await postRepo.find({ skip: 10000, take: 20 });

改用游标分页(cursor-based pagination):

typescript
// 基于 ID 的游标分页 const posts = await postRepo.find({ where: { id: LessThan(lastId) }, order: { id: 'DESC' }, take: 20 });

游标分页不依赖偏移量,性能与页码无关。缺点是不能跳转到指定页码。

4. 批量插入

逐条插入 1000 条数据 = 1000 条 INSERT 语句,极慢:

typescript
// 慢:逐条插入 for (const item of data) { await repo.save(item); }
typescript
// 快:批量插入 await repo.save(data); // TypeORM 自动合并成批量 INSERT

save 传入数组时,TypeORM 会合并成一条 INSERT 语句(部分数据库支持)。更大数据量用 createQueryBuilder 的 INSERT:

typescript
await createQueryBuilder() .insert() .into(User) .values(data) .execute();

极大数据量(10 万+)分批插入,每批 1000 条,避免单条 SQL 过长。

5. 索引优化

TypeORM 的 @Index 装饰器声明索引:

typescript
@Entity() @Index(['email']) // 单列索引 @Index(['firstName', 'lastName']) // 复合索引 export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() firstName: string; @Column() lastName: string; }

索引设计原则:

  • WHERE 条件里的列加索引
  • 复合索引把区分度高的列放前面(@Index(['status', 'createdAt']) 如果 status 只有 3 个值就不如 @Index(['createdAt', 'status'])
  • 不要过度索引——每个索引增加写入开销

查看慢查询:EXPLAIN ANALYZE + SQL 日志。TypeORM 开启 SQL 日志:

typescript
TypeOrmModule.forRoot({ logging: true, // 打印所有 SQL })

6. 连接池配置

默认连接池大小可能不够用:

typescript
TypeOrmModule.forRoot({ type: 'postgres', extra: { max: 20, // 最大连接数 idleTimeoutMillis: 30000, }, })

max 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。

标签:TypeORM