编者注:原论文《How not to structure your database-backed web applications: a study of performance bugs in the wild》,发表于 ICSE’18。虽然已经过去了 7 年,但依然历久弥新。
芝加哥大学和华盛顿大学的研究团队通过一项全面的研究,揭示了 ORM(对象关系映射)框架应用中常见的性能问题及其解决方案。在这项研究中,他们对 12 个有代表性的真实 ORM 应用进行了全面分析,从超过 200 个性能问题中总结出 9 种 ORM 性能反模式。通过手动修复最新版本中的 64 个性能问题,我们获得了 2 倍的中位数性能提升(最高达 39 倍),而大多数修复仅需不到 5 行代码更改。
ORM API 误用(约占一半的性能问题)
1. 低效的计算
同样的操作可以通过不同的 ORM 调用实现,但性能差异巨大。例如:
# 低效方式
variants.where(track_inventory: false).any? # 全表扫描计算数量
# 高效方式
variants.where(track_inventory: false).exists? # 只需找到第一条匹配记录
简单替换 API 调用方式,性能可提升 1.7 倍。
2. 不必要的计算
重复执行相同的查询或执行结果可预知的查询:
# 低效:循环内重复相同查询
items.each do |item|
read_only_attribute_names(user).include? # 每次循环都重复查询
end
# 高效:提前查询一次
names = read_only_attribute_names(user) # 循环外查询一次
items.each do |item|
names.include?
end
3. 低效的数据访问
最著名的 “N+1 查询” 问题:加载 N 个对象后,需要 N 次单独查询来加载关联数据。通过使用 eager loading 而非 lazy loading,可将多个查询合并为一个,大幅减少网络往返时间。
4. 不必要的数据检索
检索不会被后续使用的持久化数据。修复这类问题可将页面加载时间从 3.0 秒降至 1.1 秒。
5. 低效的渲染
视图渲染时反复调用相同的渲染函数。使用简单的字符串替换可提高性能,虽然可能降低代码可读性。
数据库设计问题
6. 缺失字段
是否将派生字段物理存储在数据库中是关键决策。例如,一个地图应用花费大量时间基于经纬度生成位置名称字符串,而将其直接存储可将操作时间从 1 秒减少到 0.36 秒。
7. 缺失索引
这是 ORM 应用 bug 追踪系统中最常见的性能问题。开发者往往在设计阶段缺乏选择最佳索引的专业知识,导致查询效率低下。
应用设计权衡
8. 内容显示权衡
最常见的可扩展性问题是在一页中显示满足特定条件的所有记录。随着数据库大小增加,页面加载时间显著增长。通过分页显示固定数量的记录,性能得到显著提升。
9. 功能权衡 (FT)
有时需要权衡功能和性能,例如移除显示用户指南的昂贵检查,可将页面加载时间从 2 秒减少到不到 1 秒。
总结
这项研究深入揭示了 ORM 应用中常见的性能问题,并提供了简单有效的解决方案。通过理解这些反模式,开发者可以显著提升应用性能,提供更好的用户体验。正如研究所示,许多性能问题只需几行代码更改即可解决,但收益却是巨大的。
无论你是使用 Rails、Django 还是 Hibernate,这些发现都具有普遍适用性。在下一次 Web 应用开发中,不妨参考这些反模式,避免陷入常见的性能陷阱。