首页
/ TypeORM Query Builder 性能优化实战:消除 N+1 查询、按需取数与字段裁剪

TypeORM Query Builder 性能优化实战:消除 N+1 查询、按需取数与字段裁剪

2026-09-08 11:29:09作者:裴麒琰

在 TypeORM 应用中,ORM 带来的便利背后往往隐藏着性能陷阱:最常见的是 N+1 查询问题——对每一行结果都额外发起若干条子查询;其次是"取太多"——加载了并不需要的完整实体与整表字段,白白增加网络传输与内存开销。本篇技术指南以仓库 performance-optimization/2-efficient-use-of-query-builder.md 为核心,围绕 Query Builder 这一工具讲解三类可立即落地的优化手段:用 leftJoinAndSelect/innerJoinAndSelect 单条 SQL 合并关联、用 getRawMany() 跳过实体映射取裸数据、用 select() 裁剪字段。读完你将掌握如何在保留 TypeORM 类型能力的同时,把查询数量与数据体积压到最低,并理解其底层 SelectQueryBuilder 的实现原理。

背景:ORM 性能问题的三大来源

在进入 Query Builder 技巧之前,先回顾 性能优化系列导言 中归纳的常见 ORM 挑战:

  • 不必要的数据检索:默认加载实体全部列,甚至触发关联加载;
  • N+1 查询问题:对每一行数据反复执行子查询,查询总数随数据量线性膨胀;
  • 未利用索引、缓存等优化工具:详见本系列 索引缓存 章节。

因此,优化的总体目标可以概括为:减少发往数据库的 SQL 条数、让复杂查询跑得更快、按需取数避免浪费。本篇文章聚焦"Query Builder 三件套"来达成前两条。

消除 N+1 查询问题:用 Join + Select 一次取全

N+1 查询问题发生在系统对检索到的每一行数据都额外执行多条子查询的场景。假设有 UserPost 两个实体(一对一/一对多关系),若先取 100 个用户、再逐个取每个用户的文章,数据库就要承受 1 条主查询加 100 条关联查询的压力,这就是典型 N+1。

leftJoinAndSelect 合并为单条 SQL

避免 N+1 的标准做法是使用 leftJoinAndSelect(或 innerJoinAndSelect)把关联表"拼"进同一条查询:

const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.posts", "post")
    .getMany()

这里 leftJoinAndSelect 会在一条 SQL 里把 user 与 posts 做 LEFT JOIN,并把 post 的列也选出来,一次往返即可取回所有用户及其文章,而不是很多条小查询。命名上 AndSelect 的含义是"既做连接、又把被连接实体的属性加入 SELECT 列表"——从源码可见其实现就是两步叠加:先 this.addSelect(alias) 将别名注册进选择列,再执行对应的 leftJoin(参见 SelectQueryBuilder.tsleftJoinAndSelectinnerJoinAndSelect 的实现)。

leftJoinAndSelect vs innerJoinAndSelect:按是否需要保留主行选用

两者签名完全一致,区别仅在于 JOIN 语义:

方法 JOIN 语义 适用场景
leftJoinAndSelect LEFT JOIN,保留左侧主表所有行 主行必须返回、关联数据可有可无(如"用户及其可选资料")
innerJoinAndSelect INNER JOIN,仅返回两侧都匹配的行 只需要存在关联数据的记录(如"发过文章的用户")

innerJoinAndSelect 还会在内部先执行 this.addSelect(alias)innerJoin,即同样把关联实体字段带入 SELECT。除了把实体关系作为连接目标外,这两个方法还支持三种扩展形态(源码以函数重载暴露,见 SelectQueryBuilder.ts):

  1. 连接实体innerJoinAndSelect(User, "u", "u.team = team.id"),按条件直接连接任意实体表;
  2. 连接子查询:传入 (qb) => ... 子查询工厂,把一段子查询当作被连接的数据源;
  3. 连接裸表名:传入字符串表名。

何时不该用 Join + Select:当一对多关系的两侧都存在大量数据时,JOIN 会产生行数相乘的宽结果集(笛卡尔式膨胀),网络与内存开销反而更大。此时应回到"主查询 + 按需加载关联",或参考本系列 懒加载与预加载对比 决定加载策略。

从源码看 Join 的真正收益

从源码结构看,SelectQueryBuilder 内部用 joins 数组统一记录每个 JOIN 的 type"inner" | "left")、aliasparentAliasrelationMetadata 以及 select 标记(见 SelectQueryBuilder.ts),并在 getQuery() 中按固定顺序拼装 SQL:SELECT → JOIN → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT/OFFSET → LOCK(见 SelectQueryBuilder.ts)。这解释了为什么"join 并 select 关联字段"能真正减少往返:关联表的列被纳入同一条 SELECT 语句,实体映射阶段再据此还原对象图,而非运行时逐条补查。

只需要裸数据:用 getRawMany() 跳过实体处理

在某些场景中,你并不需要 TypeORM 还原出带类型、带关联结构的实体对象,而只是要数据库返回的原始行。此时应使用 getRawMany()

const rawPosts = await dataSource
    .getRepository(Post)
    .createQueryBuilder("post")
    .select("post.title, post.createdAt")
    .getRawMany()

getMany() 的差异在于:

  • getMany() 会把查询结果映射为实体实例,并按关系元数据装配对象图(涉及大量类型转换与关联组织开销);
  • getRawMany()返回未加工的数据库行。其实现会在执行前显式把 expressionMap.queryEntity 置为 false,以此跳过实体装配环节(见 SelectQueryBuilder.ts),因此更适合报表、导出、看板等只读展示场景。

配套方法还有:

  • getRawOne():取第一条原始行,其源码实现就是 (await this.getRawMany())[0](见 SelectQueryBuilder.ts);
  • getRawAndEntities():同时返回原始行与由它们构建出的实体,供需要两套视角的高级场景使用。

需要提示的限制:raw 结果不会套用实体的列名→属性名映射,返回的键是数据库列名getRawMany() 也不支持乐观锁模式(源码在入口处对 lockMode === "optimistic" 直接抛出 OptimisticLockCanNotBeUsedError)。

select 裁剪字段,降低内存与传输开销

默认情况下,TypeORM 会把实体的所有列都纳入 SELECT。若一张宽表有几十个字段而页面只用两个,属于明显浪费。select 能精确限定返回列:

const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .select(["user.name", "user.email"])
    .getMany()

这里 .select(["user.name", "user.email"]) 会让生成的 SQL 只查询这两列,达到降低内存占用、减少不必要数据传输的目的。

使用 select 的关键语义与坑

源码中 select 的行为对正确使用至关重要(见 SelectQueryBuilder.ts):

  • select() 语义为 "替换":文档注释明确写着 "Replaces all previous selections if they exist"——每次调用都会清空此前的选择(源码中对字符串与数组都直接重置 expressionMap.selects);
  • 若想追加选择列,需改用 addSelect(),它把新列追加到已有选择列表末尾(见 SelectQueryBuilder.ts);
  • select 支持三种入参形态:单个字符串(可配别名)、字符串数组、以及子查询回调(配合 selectionAliasName 把一段子查询结果选为一列);
  • 裁剪字段与实体映射存在权衡select 只选中部分列后,getMany() 返回的实体对象中未选中的属性会是 undefined,因此更常用于与 raw 结果搭配,或与后续 where/orderBy 需要的列保持一致。

一个完整的最佳实践组合

将上述手段组合,可在单条 SQL 内同时实现"只取需要的关联、只要需要的字段":

const rows = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .innerJoinAndSelect("user.posts", "post")      // 单条 SQL 合并关联,规避 N+1
    .select(["user.id", "user.name", "post.title"]) // 只取必需字段
    .orderBy("post.createdAt", "DESC")
    .getRawMany()                                   // 纯数据输出,跳过实体装配

当结果集较大、需要切片展示时,还可叠加本系列的 分页(Pagination) 章节推荐的 take/skipgetManyAndCount(),从"数量"与"体积"两个维度共同压榨查询成本。

小结

优化手段 解决的问题 对应 API
合并关联查询 N+1 查询、查询次数过多 leftJoinAndSelect / innerJoinAndSelect
跳过实体装配 不必要的类型映射与对象图组织开销 getRawMany() / getRawOne()
裁剪返回字段 传输与内存浪费 select() / addSelect()

三个技巧统一回答"少发 SQL、少取数据、少做转换"三个问题:Join + Select 在单条语句内完成关联读取,直接规避 N+1;getRawMany() 面向只读场景剥离实体映射成本;select/addSelect 则把列集合收敛到业务真正需要的子集。这些 API 均定义于仓库 SelectQueryBuilder,你可以直接阅读其重载与实现加深理解;若想系统学习,建议顺序浏览 性能优化系列 中关于索引、懒加载与预加载、分页与缓存的其余章节,形成完整的 TypeORM 查询性能优化知识栈。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391