当前位置: 首页 > article >正文

从 N+1 到 joinedload:理解 SQLAlchemy 关系加载的核心机制

从 N1 到 joinedload理解 SQLAlchemy 关系加载的核心机制文章摘要本文围绕 SQLAlchemy 一对多关系加载展开重点解释relationship、lazyloading、selectinload、joinedload、N1问题以及joinedload加载集合关系时为什么需要unique()。前言这篇文章解决什么问题在使用 FastAPI 和 SQLAlchemy 开发后端项目时经常会遇到一类问题两个表之间有一对多关系比如一个用户对应多条学习记录那么在代码中应该如何查询用户及其关联记录SQLAlchemy 提供了relationship来描述 ORM 对象之间的关系也提供了多种关系加载方式例如lazy loading selectinload joinedload这些方式不仅影响 SQL 查询语句的写法也会影响 ORM 如何把数据库返回的 SQL 行还原成 Python 对象。这篇文章重点整理以下几个问题relationship到底是什么lazy loading 为什么容易产生 N1 问题selectinload和joinedload有什么区别为什么joinedload加载一对多集合时通常需要unique()unique()是不是 SQL 里的DISTINCT问题背景一对多关系中的对象导航假设系统中有两个 ORM 模型User用户LearningRecord学习记录一个用户可以有多条学习记录所以这是一个典型的一对多关系。在 SQLAlchemy ORM 中通常会用类似下面的方式进行对象导航user.records record.user其中user.records表示从一个用户对象访问它的所有学习记录record.user表示从一条学习记录访问它所属的用户。需要注意的是relationship不是数据库中的真实字段。真实存在于数据库表中的通常是外键列例如learning_records.user_id而User.records这种属性是 ORM 层提供的对象关系导航能力。它方便我们在 Python 代码里用对象的方式访问关联数据但它本身不是数据库表里的字段。核心概念解释1. relationshipORM 对象之间的关系描述relationship的作用是告诉 SQLAlchemy两个 ORM 模型之间存在什么关系以及如何在 Python 对象之间进行导航。例如user.records它表达的是通过一个User对象可以访问它关联的多条LearningRecord对象。但是数据库真正识别关系依赖的是外键列比如learning_records.user_id - users.id所以可以这样理解数据库层靠外键列建立表关系 ORM 层靠 relationship 实现对象导航这两个概念有关联但不能混为一谈。2. lazy loading用到关系属性时再查lazy loading可以理解为“懒加载”。它的特点是查询父对象时不会立刻把关联对象也查出来。只有当代码真正访问关系属性时SQLAlchemy 才会再发送 SQL 查询。例如下面这句代码userssession.scalars(select(User)).all()执行完以后SQLAlchemy 只查询了User表中的数据。此时每个user对象中的普通字段已经有值比如user.iduser.name但是records这个关系属性还没有真正加载。这里容易产生一个误解既然还没有加载那user.records是不是None答案是不是。在 lazy loading 模式下未访问前的user.records不是None也不是一个已经查好的普通 Python 列表。更准确地说它是一个由 SQLAlchemy ORM 管理的“未加载关系属性”。可以这样理解user 对象已经创建 user.id、user.name 等普通字段已经加载 user.records 这个 relationship 还处于未加载状态也就是说此时 SQLAlchemy 知道User和LearningRecord之间存在关系但还没有真正去数据库中查询这个用户对应的记录。只有当代码第一次访问print(user.records)SQLAlchemy 才会发现当前访问的是一个尚未加载的 relationship。于是它会额外发送一条 SQL 查询大致相当于SELECT...FROMlearning_recordsWHERElearning_records.user_id当前用户的 id;查询完成后SQLAlchemy 会把结果填充到user.records中。如果这个用户有学习记录user.records会表现为一个包含多个LearningRecord对象的集合[LearningRecord(id10),LearningRecord(id11),]如果这个用户没有任何学习记录访问之后通常会得到空集合[]而不是None。所以 lazy loading 的关键点不是“关系属性不存在”而是关系属性存在但关联数据暂时没有加载 第一次访问关系属性时SQLAlchemy 才去数据库查询。再看下面这段代码userssession.scalars(select(User)).all()foruserinusers:print(user.records)表面上看这段代码只是查询用户然后打印每个用户的学习记录。但如果有 10 个用户实际 SQL 查询次数可能是1 次查询 users 10 次查询每个 user 对应的 records也就是总查询次数 1 N这就是常说的 N1 问题。N 表示父对象数量。用户越多额外查询次数越多性能问题越明显。因此lazy loading 的执行过程可以概括为查询 User ↓ 只加载 User 的普通字段 ↓ records 关系属性处于未加载状态 ↓ 第一次访问 user.records ↓ SQLAlchemy 额外查询该用户对应的 records ↓ 把查询结果填充到 user.records3. selectinload先查父对象再批量查子对象selectinload是一种预加载方式。它的核心思想是先查 users 再用 WHERE IN (...) 批量查询这些 users 对应的 records示例fromsqlalchemyimportselectfromsqlalchemy.ormimportselectinload stmtselect(User).options(selectinload(User.records))userssession.scalars(stmt).all()如果查到了 10 个用户SQL 查询次数通常是1 次查询 users 1 次批量查询 records也就是总查询次数 2相比 lazy loading 的1 Nselectinload可以有效避免 N1 问题。它适合一对多、多对多这类集合关系因为它不会把父表和子表强行 JOIN 成一张很宽的结果集而是用第二次查询批量拿到子对象。4. joinedload用 JOIN 一次性加载关联对象joinedload也是一种预加载方式。它的特点是使用 JOIN 风格把父对象和关联对象放在同一次 SQL 查询中加载。示例fromsqlalchemyimportselectfromsqlalchemy.ormimportjoinedload stmtselect(User).options(joinedload(User.records))userssession.scalars(stmt).unique().all()joinedload(User.records)会把users和learning_records放到同一次查询中。假设一个用户有两条学习记录底层 SQL 结果可能类似users.id | users.name | records.id | records.title 1 | Alice | 10 | SQLAlchemy CRUD 1 | Alice | 11 | Relationship Loading从 SQL 的角度看这两行不是重复行因为records.id不一样。但是从select(User)的角度看这两行都对应同一个父对象User primary_key(1,)也就是说父对象在 SQL 结果中被展开成了多行。代码示例joinedload 与 unique() 的底层过程重点来看这段代码stmtselect(User).options(joinedload(User.records))userssession.scalars(stmt).unique().all()这段代码的目标是查询用户并且同时加载每个用户的records集合。底层过程可以用下面的模型理解SQL 行 - identity map 识别对象身份 - relationship loader 填充关系集合 - Result 返回对象第一步SQL 返回多行如果User(id1)有两条学习记录SQL 结果会出现两行users.id | users.name | records.id | records.title 1 | Alice | 10 | SQLAlchemy CRUD 1 | Alice | 11 | Relationship Loading这不是 SQL 层面的重复数据而是一对多 JOIN 后的正常展开结果。第二步identity map 复用同一个 User 对象SQLAlchemy 不会简单地为这两行分别创建两个独立的User(id1)对象。更准确的过程是第 1 行 identity map 发现或创建 User(id1) relationship loader 把 Record(id10) 加入 User(id1).records 第 2 行 identity map 发现这还是 User(id1)复用同一个 Python 对象 relationship loader 把 Record(id11) 加入同一个 User(id1).records这里的identity map可以理解为 Session 内部维护的对象缓存。它会根据实体类型 主键值来判断一个 ORM 对象是否已经存在。例如(User, 1) (User, 2)如果发现当前行对应的还是(User, 1)SQLAlchemy 就会复用之前那个 Python 对象而不是重新创建一个新的用户对象。第三步relationship loader 填充集合虽然两行 SQL 都对应同一个User(id1)但是每一行里的Record不同。所以 relationship loader 会把不同的记录对象加入同一个集合中same_user.records[LearningRecord(id10),LearningRecord(id11),]这一步说明一个关键点unique()不会导致 records 丢失因为 records 的装配已经在 ORM 处理 SQL 行时完成了。第四步Result 结果流中仍然可能出现重复 User 引用在调用unique()之前逻辑上的结果流可能类似[same_user,same_user,]这两个位置指向的是同一个 Python 对象而不是两个各自只带一条 record 的对象。所以unique()的作用是把最终结果流中的重复父对象引用过滤掉[same_user,same_user,]变成[same_user,]因此unique()处理的是 ORM Result 结果层面的重复对象引用不是数据库 SQL 层面的重复行。scalars、execute 与 unique() 的关系在 SQLAlchemy 2.x 风格中下面两种写法有对应关系session.scalars(stmt)可以大致理解为session.execute(stmt).scalars()对于下面这条语句stmtselect(User).options(joinedload(User.records))execute()得到的逻辑结果是 row(User(id1),)(User(id1),)scalars()会取每个 row 的第一个元素于是得到User(id1)User(id1)所以这段代码userssession.scalars(stmt).unique().all()表达的是查询 User并对最终的 User 结果流去重也可以写成userssession.execute(stmt).unique().scalars().all()在select(User)这个场景里session.scalars(stmt).unique().all()更直接表达了意图我要的是去重后的User列表。常见误区误区一relationship 是数据库字段relationship不是数据库里的真实字段。例如user.records这是 ORM 层的对象关系属性。数据库中真正保存关系的通常是外键列例如learning_records.user_id因此不能把relationship理解成数据库表中的一列。误区二joinedload 会创建多个独立的父对象当一个用户有多条记录时JOIN 后确实会出现多行User 1 Record 10 User 1 Record 11但 SQLAlchemy 的 identity map 会根据实体类型和主键复用同一个User对象。所以它不是创建两个独立的User(id1)而是复用同一个对象并把多条记录装配到同一个records集合中。误区三unique() 会丢掉子对象unique()只过滤最终结果流里的重复父对象引用。它不会删除已经装配到user.records中的子对象。也就是说userssession.scalars(stmt).unique().all()最终users里只会出现一个User(id1)但这个用户的records仍然可以包含多条记录。误区四unique() 等于 SQL DISTINCTunique()不是 SQL 里的DISTINCT。DISTINCT是数据库层面的去重比较的是 SQL 查询结果。而unique()是 SQLAlchemy ORM 结果层面的去重主要依据实体类型 主键值对 ORM 对象身份进行过滤。对于joinedload(User.records)这种一对多集合预加载来说SQL 行必须展开成多行ORM 才能看到每一条子记录。unique()只是避免最终结果列表中重复返回同一个父对象。对比总结加载方式查询方式查询次数适用场景注意点lazy loading用到关系属性时再查可能是1 N少量对象、关系属性不一定会用到容易产生 N1selectinload先查父对象再批量查子对象通常是 2 次一对多、多对多集合关系常用于避免 N1joinedloadJOIN 一次性加载通常是 1 次一对一、多对一或需要 JOIN 预加载的场景加载集合关系时通常需要unique()对于一对多集合关系selectinload往往更容易理解也更适合作为默认选择。joinedload的优势是可以通过一次 JOIN 查询把数据带出来但它会造成父对象在 SQL 结果中被多行展开所以在 SQLAlchemy 2.x 中加载集合关系时通常需要配合unique()使用。实战建议在 FastAPI 和 SQLAlchemy 项目中可以按下面的思路选择加载方式。如果只是查询用户信息不一定需要学习记录可以保持默认的 lazy loadingstmtselect(User)userssession.scalars(stmt).all()如果接口需要返回用户及其学习记录并且用户数量可能不止一个优先考虑selectinloadstmtselect(User).options(selectinload(User.records))userssession.scalars(stmt).all()如果明确希望通过 JOIN 一次性加载关系并且加载的是一对多集合关系要记得使用unique()stmtselect(User).options(joinedload(User.records))userssession.scalars(stmt).unique().all()如果忘记unique()在 SQLAlchemy 2.x 中针对 joined eager loading 加载集合关系的场景通常会出现要求调用unique()的错误提示。这里需要注意具体 SQL 形态可能会受到数据库类型、SQLAlchemy 版本、查询条件和项目配置影响。如果要分析性能应结合实际打印出来的 SQL 和接口查询次数确认。总结SQLAlchemy 的关系加载方式本质上是在解决一个问题如何把关系型数据库中的多行数据正确还原成 Python 中的对象和对象关系。relationship负责 ORM 对象之间的导航但它不是数据库字段。数据库层面的表关系通常依赖外键列。lazy loading是用到关系属性时再查询简单但容易产生 N1 问题。selectinload会先查询父对象再批量查询子对象通常可以把1 N次查询压缩为 2 次查询适合一对多集合关系。joinedload使用 JOIN 一次性加载关联数据但在一对多场景中一个父对象会因为多条子记录在 SQL 结果中展开成多行。SQLAlchemy 会通过 identity map 复用同一个父对象再由 relationship loader 把子对象装配进集合属性。unique()的作用是过滤最终 ORM 结果流中重复出现的父对象引用。它不是 SQLDISTINCT也不会丢掉已经装配好的子对象。理解这条链路很重要SQL 行 - identity map 识别对象身份 - relationship loader 填充关系集合 - Result 返回对象掌握这条链路后再看lazy loading、selectinload、joinedload和unique()就不会只停留在“写法不同”的层面而是能理解 ORM 背后如何组织查询结果。

相关文章:

从 N+1 到 joinedload:理解 SQLAlchemy 关系加载的核心机制

从 N1 到 joinedload:理解 SQLAlchemy 关系加载的核心机制文章摘要 本文围绕 SQLAlchemy 一对多关系加载展开,重点解释 relationship、lazy loading、selectinload、joinedload、N1 问题以及 joinedload 加载集合关系时为什么需要unique()。前言&#xf…...

3个技巧快速掌握加密压缩包密码找回:ArchivePasswordTestTool新手指南

3个技巧快速掌握加密压缩包密码找回:ArchivePasswordTestTool新手指南 【免费下载链接】ArchivePasswordTestTool 利用7zip测试压缩包的功能 对加密压缩包进行自动化测试密码 项目地址: https://gitcode.com/gh_mirrors/ar/ArchivePasswordTestTool 你是否曾…...

GPT-4 Turbo访问权、优先响应、高级数据分析——ChatGPT Plus五大隐藏权益深度拆解,92%用户根本没用全

更多请点击: https://intelliparadigm.com 第一章:ChatGPT Plus订阅值不值得买 ChatGPT Plus 提供每月 $20 的固定订阅服务,主打 GPT-4 模型访问、优先响应队列、文件上传解析(PDF/CSV/TXT 等)及自定义 GPTs 功能。是…...

3大实战场景:PX4无人机视觉定位从零部署到工业级应用

3大实战场景:PX4无人机视觉定位从零部署到工业级应用 【免费下载链接】PX4-Autopilot PX4 Autopilot Software 项目地址: https://gitcode.com/gh_mirrors/px/PX4-Autopilot PX4-Autopilot作为开源无人机飞控的标杆,其视觉定位能力正在重塑自主飞…...

终极AMD锐龙处理器调试指南:深度掌握硬件性能调优的完整解决方案

终极AMD锐龙处理器调试指南:深度掌握硬件性能调优的完整解决方案 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: …...

设计系统文本化:用YAML/JSON统一管理设计令牌,实现多端一致与自动化

1. 项目概述:当设计系统遇上纯文本 最近在跟一个跨职能团队协作时,我们遇到了一个典型的老大难问题:设计师在Figma里更新了一个按钮的主色调,前端工程师在代码库里改了对应的CSS变量,但负责撰写产品文档和营销材料的同…...

别盲目学AI!先搞懂人工智能的3大核心分支,选对方向少走3年弯路

文章目录前言第一大分支:大模型与生成式AI——AI行业的"水电煤"什么是大模型与生成式AI?大模型技术在2026年的发展现状大模型方向的主要岗位和薪资大模型方向的学习路线第二大分支:智能体与多智能体系统——2026年AI行业最大的风口…...

如何永久免费使用AI编程助手:Cursor Free VIP完整指南

如何永久免费使用AI编程助手:Cursor Free VIP完整指南 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached your tri…...

Java统一AI SDK实战:集成OpenAI、Claude、Gemini多模型API

1. 项目概述与核心价值 最近在折腾一个需要集成多个大模型API的Java项目,从OpenAI到Claude再到Google Gemini,每个厂商的SDK调用方式、请求体结构、错误处理都不太一样,光是写适配代码就够喝一壶的。更别提还要处理流式响应、文件上传、Func…...

实在Agent实测:解决采购合同审核流程冗长与原材料交付周期拉长的架构之道

大家好,我是企业架构师老王。站在2026年5月这个时间节点回看,全球供应链的复杂程度已远超三年前的预判。近期我在为几家制造型企业做数字化诊断时发现,一个幽灵般的困境正在吞噬企业的利润:采购合同审核流程冗长,直接导…...

AI小白必看:收藏这份从零入门大模型的核心概念指南

本文通过一个生动的故事,用通俗易懂的方式讲解了AI领域最核心的7个概念:LLM(大语言模型)、Agent(智能体)、Skill(技能包)、MCP(模型上下文协议)、IDE&#xf…...

解决每次打开JFlash就提示:Device: TLE9863QXW20: Flash bank 0x11000000: No loader specified的问题

问题现象:每次打开JFlash就提示: Device: TLE9863QXW20: Flash bank 0x11000000: No loader specified解决方法:1.WinR 输入以下 打开目录:%APPDATA%\SEGGER\JLinkDevices2.文本打开xml文件:我是打开华大(HDCS)芯片的时…...

2026年三款最值得在线预约小程序,解决您的预约难题

本文围绕在线预约小程序这一核心主题展开,系统梳理了2026年主流平台的特性与差异。内容涵盖微信、支付宝、抖音三大平台的功能对比、适用场景及操作流程解析,并结合实际案例深度剖析技术实现原理。同时提供选型指南与实操建议,帮助用户根据业…...

手机资料恢复

在数字化生活高度渗透的今天,手机早已不仅是通讯工具,更是承载个人记忆、工作文档与社交关系的核心载体。一张重要的照片、一份未备份的合同、一段珍贵的聊天记录,一旦因误操作或意外损坏而丢失,带来的往往是情感上的失落与业务上…...

D3KeyHelper:暗黑3游戏宏助手终极指南,五分钟轻松搞定技能连点

D3KeyHelper:暗黑3游戏宏助手终极指南,五分钟轻松搞定技能连点 【免费下载链接】D3keyHelper D3KeyHelper是一个有图形界面,可自定义配置的暗黑3鼠标宏工具。 项目地址: https://gitcode.com/gh_mirrors/d3/D3keyHelper 想要在《暗黑破…...

BK3633深度睡眠功耗实测:如何配置到1uA并保持定时器工作(避坑指南)

BK3633深度睡眠功耗优化实战:从理论到1uA的完整实现路径 在电池供电的物联网设备设计中,低功耗性能往往直接决定产品的市场竞争力。BK3633作为一款集成蓝牙5.2和专有2.4GHz协议的双模芯片,其规格书中标榜的"深度睡眠约1uA"参数尤其…...

14602开源|黄大年茶思屋第146期第二题:支持采集内容运动的静态3DGS重建

AI无偏差版脱敏题目标准化解题详解(第二题:支持采集内容运动的静态3DGS重建) 模块一:脱敏题目原文复刻 【脱敏题目原文】 支持采集内容运动的静态3DGS重建 基于3DGS及其扩展形式,支持动态建模和静态渲染,输…...

基于MCP协议构建AI驱动的Upwork自动化工作流:从工具化接口到安全实践

1. 项目概述:一个连接AI与自由职业平台的智能桥梁如果你是一名自由职业者,或者正在探索如何用AI来优化你的工作流程,那么你肯定对在Upwork、Fiverr这类平台上反复刷新、筛选项目、撰写提案的繁琐过程深有体会。每天花上几个小时,只…...

ChatGPT Plus值不值得买?——从服务器响应延迟、上下文长度、并发请求上限到插件可用性,11维硬指标逐项打分

更多请点击: https://intelliparadigm.com 第一章:ChatGPT Plus值不值得买? ChatGPT Plus 以 $20/月的订阅费提供 GPT-4 级别响应、优先访问高峰时段、更长上下文窗口(最高 32K tokens)及图像/文件解析能力。但是否值…...

H3C模拟器实战:基于时间与部门的精细化ACL策略部署

1. 企业网络访问控制的痛点与ACL解决方案 在企业网络管理中,最让人头疼的就是如何平衡安全性和便利性。我见过太多公司要么一刀切封锁所有端口导致业务受阻,要么放任自流引发数据泄露。就拿去年帮某中型企业排查的问题来说,他们的销售部员工在…...

为AI编程助手构建本地知识库:YAP项目实战指南

1. 项目概述:当AI编程助手遇上专属知识库如果你和我一样,日常重度依赖Cursor这类AI编程助手,那你一定遇到过这样的场景:面对一个复杂的内部项目,或者一个使用了大量私有库、自定义框架的代码库,Cursor的响应…...

Taotoken在数据预处理与分析脚本中调用大模型的集成案例

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 Taotoken在数据预处理与分析脚本中调用大模型的集成案例 应用场景类,设想一个数据科学家使用Python脚本进行数据分析时…...

Confluence数据迁移踩坑实录:从物理机到K8s集群,我是如何无损迁移200G知识库的?

Confluence企业级数据迁移实战:从物理架构到Kubernetes的无缝过渡 当企业知识库规模突破200GB时,迁移不再是简单的备份还原操作。去年我们团队将一个运行7年的Confluence实例从老旧物理服务器迁移到Kubernetes集群,期间经历了数据库崩溃、文件…...

Anthropic Claude Haiku 4.5 安全突破:勒索行为从96%降至0%

上一篇: Google I/O 2026前瞻:Gemini 4.0、Android XR与AI原生生态的全面突破 下一篇: Anthropic ARR突破440亿美元:Q1营收同比增长80倍深度分析 核心结论: Anthropic通过"困难建议数据集"和宪法训练方法,成功将Claude模型的勒索行…...

【Oracle数据库指南】第35篇:Oracle特殊对象——簇与索引组织表(IOT)

上一篇【第34篇】Oracle索引管理与优化详解 下一篇【第36篇】Oracle用户与权限管理详解(完整版)(明日更新,敬请期待) 摘要 除了普通堆组织表(Heap-Organized Table)之外,Oracle还提…...

【Oracle数据库指南】第32篇:Oracle归档日志管理与LogMiner日志分析

上一篇【第31篇】Oracle重做日志文件管理操作详解 下一篇【第33篇】Oracle表管理与分区表详解 摘要 归档日志(Archive Log)是Oracle数据库实现时间点恢复的核心机制,也是数据库备份恢复策略的重要组成部分。本文详细讲解归档模式的开启与配置…...

购买腾讯云时最容易被忽略的痛点:配置、成本和运维闭环

很多客户在购买腾讯云或开始使用腾讯云时,真正的痛点往往不是“不会下单”,而是下单前后缺少一套清晰的决策和运维闭环。第一个痛点是配置选择不确定。不少团队会先纠结 CPU、内存、带宽、地域、系统盘和数据盘怎么选。配置买低了担心业务跑不动&#xf…...

基于Terraform与Ansible的OpenClaw私有化AI代理自动化部署实践

1. 项目概述如果你和我一样,对AI助手的能力有更高的期待,希望它能深度融入你的工作流,甚至能帮你处理一些自动化任务,那么OpenClaw这个项目绝对值得你花时间研究。它不是一个简单的聊天机器人,而是一个可以部署在你私有…...

如何在5分钟内体验完整的Windows 12网页版:创新系统模拟器终极指南

如何在5分钟内体验完整的Windows 12网页版:创新系统模拟器终极指南 【免费下载链接】win12 Windows 12 网页版,在线体验 点击下面的链接在线体验 项目地址: https://gitcode.com/gh_mirrors/wi/win12 想要在浏览器中运行完整的Windows系统界面吗&…...

Cursor Pro破解工具:3分钟快速激活高级功能的终极方案

Cursor Pro破解工具:3分钟快速激活高级功能的终极方案 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached your tri…...