Rails 性能:来自生产环境的经验教训 — #3
前两篇文章讨论的是如何降低查询成本——减少 N+1 查询问题、使用索引、避免将数据加载回 Ruby 中。本文将这一理念转化为一个更实用的习惯:让数据库去做它擅长做的事。 如果在错误的位置使用了
.count,或者误用了.present?,你可能会为了获取一个简单的数字而将整个表的数据加载到内存中。全文将沿用同一个示例(一个shipments[运单] 表)。
💥 一个吃光服务器内存的报表页面
我们有一个管理后台报表,用于显示每位快递员“已送达”的运单数量。起初运行良好——直到有人调整了过滤逻辑。此后,打开该页面会导致内存激增,在流量压力下甚至导致服务器因内存溢出(OOM)而被杀死。
罪魁祸首是这行代码:
courier.shipments.select { |s| s.status == "delivered" }.size
看起来似乎合理——“这只是个 select 操作。”但是,这个 .select { ... }(带代码块)是 Ruby 数组的方法,而不是 Active Record 的 select(:col)。它会先将该快递员的所有运单加载到内存中,然后在 Ruby 中逐一过滤。在小表中没问题;但在大表中,它会耗尽所有随机存取存储器(RAM)。
仅仅一个词 select,却有两种完全不同的含义——一种构建结构化查询语言(SQL),另一种则加载所有数据并在 Ruby 中进行过滤。这是最容易陷入且最难发现的陷阱之一。解决方法是让数据库进行过滤和计数:
courier.shipments.where(status: "delivered").count
# → SELECT COUNT(*) FROM shipments WHERE courier_id = ? AND status = 'delivered'
数据库完成过滤、计数,并返回单个数字。内存问题随之消失。
这背后反映了一个反复出现的模式:为了获取单个数字、布尔值或总和,你将整批行数据加载回了 Ruby 中。 本文将探讨其常见的变体情况。
延续系列文章 #1 中的四个层级概念:将数据从 ③ 数据库移回 ④ 内存是有成本的。核心主题始终是——除非必要,否则不要移动数据;如果必须移动,则尽量少移动。
🔢 计数:count 与 size 与 length
这三者都用于“计数”,但它们的工作方式不同——选错就会陷入上述陷阱。
count — 始终向数据库发送 SELECT COUNT(*) 并返回一个数字。
Shipment.where(status: "delivered").count
# → SELECT COUNT(*) FROM
免责声明:本文内容来自互联网,该文观点不代表本站观点。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请到页面底部单击反馈,一经查实,本站将立刻删除。