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。解决方案:用 relations 或 join 一次性查出。
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:
typescriptawait 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 日志:
typescriptTypeOrmModule.forRoot({ logging: true, // 打印所有 SQL })
6. 连接池配置
默认连接池大小可能不够用:
typescriptTypeOrmModule.forRoot({ type: 'postgres', extra: { max: 20, // 最大连接数 idleTimeoutMillis: 30000, }, })
max 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。