如何修复使用 Python ORM 工具 SQLAlchemy 时的常见陷阱


Posted in Python onNovember 19, 2019

在使用 SQLAlchemy 时,那些看似很小的选择可能对这种对象关系映射工具包的性能产生重要影响。

对象关系映射Object-relational mapping(ORM)使应用程序开发人员的工作更轻松,在很大程度是因为它允许你使用你可能知道的语言(例如 Python)与数据库交互,而不是使用原始 SQL 语句查询。

SQLAlchemy 是一个 Python ORM 工具包,它提供使用 Python 访问 SQL 数据库的功能。它是一个成熟的 ORM 工具,增加了模型关系、强大的查询构造范式、简单的序列化等优点。然而,它的易用性使得人们很容易忘记其背后发生了什么。使用 SQLAlchemy 时做出的看似很小的选择可能产生非常大的性能影响。

本文解释了开发人员在使用 SQLAlchemy 时遇到的一些最重要的性能问题,以及如何解决这些问题。

只需要计数但检索整个结果集

有时开发人员只需要一个结果计数,但是没有使用数据库计数功能,而是获取了所有结果,然后使用 Python 中的 len 完成计数。

count = len(User.query.filter_by(acct_active=True).all())

相反,使用 SQLAlchemy 的 count 方法将在服务器端执行计数,从而减少发送到客户端的数据。在前面的例子中调用 all() 也会导致模型对象的实例化,如果有很多数据,那么时间代价可能会非常昂贵。

除非还需要做其他的事情,否则只需使用 count 方法:

count = User.query.filter_by(acct_active=True).count()

只需要几列时检索整个模型

在许多情况下,发出查询时只需要几列数据。SQLAlchemy 可以只获取你想要的列,而不是返回整个模型实例。这不仅减少了发送的数据量,还避免了实例化整个对象。使用列数据的元组而不是模型可以快得多。

result = User.query.all()
for user in result:
  print(user.name, user.email)

反之,使用 with_entities 方法只选择所需要的内容:

result = User.query.with_entities(User.name, User.email).all()
for (username, email) in result:
  print(username, email)

每次循环都更新一个对象

避免使用循环来单独更新集合。虽然数据库可以非常快地执行单个更新,但应用程序和数据库服务器之间的往返时间将快速累加。通常,在合理的情况下争取更少的查询。

for user in users_to_update:
 user.acct_active = True
 db.session.add(user)

改用批量更新方法:

query = User.query.filter(user.id.in_([user.id for user in users_to_update]))
query.update({"acct_active": True}, synchronize_session=False)

触发级联删除

ORM 允许在模型关系上进行简单的配置,但是有一些微妙的行为可能会令人吃惊。大多数数据库通过外键和各种级联选项维护关系完整性。SQLAlchemy 允许你使用外键和级联选项定义模型,但是 ORM 具有自己的级联逻辑,可以取代数据库。

考虑以下模型:

class Artist(Base):
  __tablename__ = "artist"
  id = Column(Integer, primary_key=True)
  songs = relationship("Song", cascade="all, delete")
class Song(Base):
  __tablename__ = "song"
  id = Column(Integer, primary_key=True)
  artist_id = Column(Integer, ForeignKey("artist.id", ondelete="CASCADE"))

删除歌手将导致 ORM 在 song 表上发出 delete 查询,从而防止由于外键导致的删除操作。这种行为可能会成为复杂关系和大量记录的瓶颈。

请包含 passive_deletes 选项,以确保让数据库来管理关系。但是,请确保你的数据库具有此功能。例如,SQLite 默认情况下不管理外键。

songs = relationship("Song", cascade  all, delete", passive_deletes=True)

当要使用贪婪加载时,应使用延迟加载

延迟加载是 SQLAlchemy 处理关系的默认方法。从上一个例子构建来看,加载一个歌手时不会同时加载他或她的歌曲。这通常是一个好主意,但是如果总是需要加载某些关系,单独的查询可能会造成浪费。

如果允许以延迟方式加载关系,像 Marshmallow 这样流行的序列化框架可以触发级联查询。

有几种方法可以控制此行为。最简单的方法是通过 relationship 函数本身。

songs = relationship("Song", lazy="joined", cascade="all, delete")
这将导致一个左连接被添加到任何歌手的查询中,因此,songs 集合将立即可用。尽管有更多数据返回给客户端,但往返次数可能会少得多。

SQLAlchemy 为无法采用这种综合方法的情况提供了更细粒度的控制,可以使用 joinedload() 函数在每个查询的基础上切换连接的加载。

from sqlalchemy.orm import joinedload
artists = Artist.query.options(joinedload(Artist.songs))
print(artists.songs) # Does not incur a roundtrip to load

使用 ORM 进行批量记录导入

导入成千上万条记录时,构建完整模型实例的开销会成为主要瓶颈。想象一下,从一个文件中加载数千首歌曲记录,其中每首歌曲都先被转换为字典。

for song in songs:
  db.session.add(Song(`song))

相反,绕过 ORM,只使用核心的 SQLAlchemy 参数绑定功能。

batch = []
insert_stmt = Song.__table__.insert()
for song in songs:
  if len(batch) > 1000:
    db.session.execute(insert_stmt, batch)
    batch.clear()
  batch.append(song)
if batch:
  db.session.execute(insert_stmt, batch)

请记住,此方法会自然而然地跳过你可能依赖的任何客户端 ORM 逻辑,例如基于 Python 的列默认值。尽管此方法比将对象加载为完整的模型实例要快,但是你的数据库可能具有更快的批量加载方法。例如,PostgreSQL 的 COPY 命令为加载大量记录提供了最佳性能。

过早调用提交或刷新

在很多情况下,你需要将子记录与其父记录相关联,反之亦然。一种显然的方法是刷新会话,以便为有问题的记录分配一个 ID。

artist = Artist(name="Bob Dylan")
song = Song(title="Mr. Tambourine Man")
db.session.add(artist)
db.session.flush()
song.artist_id = artist.id

对于每个请求,多次提交或刷新通常是不必要的,也是不可取的。数据库刷新涉及强制在数据库服务器上进行磁盘写入,在大多数情况下,客户端将阻塞,直到服务器确认已写入数据为止。

SQLAlchemy 可以在幕后跟踪关系和管理相关键。

artist = Artist(name="Bob Dylan")
song = Song(title="Mr. Tambourine Man")
artist.songs.append(song)

总结

我希望这一系列常见的陷阱可以帮助你避免这些问题,并使你的应用平稳运行。通常,在诊断性能问题时,测量是关键。大多数数据库都提供性能诊断功能,可以帮助你定位问题,例如 PostgreSQL 的 pg_stat_statements 模块。

Python 相关文章推荐
python基础教程之基本内置数据类型介绍
Feb 20 Python
Python3写入文件常用方法实例分析
May 22 Python
简单谈谈python中的语句和语法
Aug 10 Python
Python基础之getpass模块详细介绍
Aug 10 Python
Python Flask-web表单使用详解
Nov 18 Python
python的exec、eval使用分析
Dec 11 Python
在Python 2.7即将停止支持时,我们为你带来了一份python 3.x迁移指南
Jan 30 Python
用TensorFlow实现多类支持向量机的示例代码
Apr 28 Python
Python爬虫小例子——爬取51job发布的工作职位
Jul 10 Python
详解Python 中的容器 collections
Aug 17 Python
python文本处理的方案(结巴分词并去除符号)
May 26 Python
Python识别花卉种类鉴定网络热门植物并自动整理分类
Apr 08 Python
Python高级特性之闭包与装饰器实例详解
Nov 19 #Python
Python高级编程之继承问题详解(super与mro)
Nov 19 #Python
Python3 Tkinkter + SQLite实现登录和注册界面
Nov 19 #Python
Python csv文件的读写操作实例详解
Nov 19 #Python
Python高级property属性用法实例分析
Nov 19 #Python
wxPython之wx.DC绘制形状
Nov 19 #Python
python matplotlib拟合直线的实现
Nov 19 #Python
You might like
memcache命令启动参数中文解释
2014/01/13 PHP
推荐一款PHP+jQuery制作的列表分页的功能模块
2014/10/14 PHP
php创建、获取cookie及基础要点分析
2015/01/26 PHP
php根据日期或时间戳获取星座信息和生肖等信息
2015/10/20 PHP
Mootools 1.2教程 输入过滤第二部分(字符串)
2009/09/15 Javascript
JS在textarea光标处插入文本的小例子
2013/03/22 Javascript
js 利用image对象实现图片的预加载提高访问速度
2013/03/29 Javascript
提取jquery的ready()方法单独使用示例
2014/03/25 Javascript
JavaScript学习笔记之JS事件对象
2015/01/22 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
原生js封装二级城市下拉列表的实现代码
2016/06/16 Javascript
javascript的replace方法结合正则使用实例总结
2016/06/16 Javascript
js排序与重组的实例讲解
2017/08/28 Javascript
详解vantUI框架在vue项目中的应用踩坑
2018/12/06 Javascript
详解如何使用微信小程序云函数发送短信验证码
2019/03/13 Javascript
vue 2.5.1 源码学习 之Vue.extend 和 data的合并策略
2019/06/04 Javascript
python解析文件示例
2014/01/23 Python
pandas 将list切分后存入DataFrame中的实例
2018/07/03 Python
Python实现的列表排序、反转操作示例
2019/03/13 Python
解决Python3.8用pip安装turtle-0.0.2出现错误问题
2020/02/11 Python
PyTorch中model.zero_grad()和optimizer.zero_grad()用法
2020/06/24 Python
如何通过python计算圆周率PI
2020/11/11 Python
详解如何获取localStorage最大存储大小的方法
2020/05/21 HTML / CSS
英国皇室御用百货:福南梅森(Fortnum & Mason)
2017/12/03 全球购物
印度和世界各地的精美产品:Ikka Dukka
2018/02/12 全球购物
印度在线杂货店:bigbasket
2018/08/23 全球购物
馥绿德雅美国官方网站:Rene Furterer头皮护理专家
2019/05/01 全球购物
学生周末回家住宿长期请假条
2014/02/15 职场文书
解除劳动合同协议书
2014/04/14 职场文书
反对四风问题自我剖析材料
2014/09/29 职场文书
群众路线教育实践活动心得体会(教师)
2014/10/31 职场文书
大学考试作弊检讨书
2015/05/06 职场文书
建党伟业电影观后感
2015/06/01 职场文书
MySQL表字段时间设置默认值
2021/05/13 MySQL
用React Native制作一个简单的游戏引擎
2021/05/27 Javascript
浅谈Web Storage API的使用
2021/06/23 Javascript