如何使用多个类别的总和来优化 ActiveRecord 查询

发布于 2024-10-31 08:42:28 字数 310 浏览 6 评论 0原文

模型:

  • 用户 has_many 类别、has_many 条目
  • 类别 has_many 条目
  • 属于类别和用户

让我们假设,条目上有名称和金额。 如果我有一个视图需要为特定用户显示特定月份(现在让我们使用created_at),并且想要显示一个表,其中包含该特定月份中按类别分组的所有条目(不显示空类别)和总和以及类别内的条目数。

我的问题是: 查询数据库(并充分利用缓存)最有效的方法是什么?该视图将经常呈现,并且每次用户创建新条目时,一个类别的总和自然会发生变化,但其他类别的总和不会发生变化。

Models:

  • User has_many Categories, has_many Entries
  • Category has_many Entries
  • Entry belongs_to both Category and User

Let's assume, there is a name and an amount on entries.
If I have a view that needs to display a particular month (let's take created_at for now) for a specific user and want to display a table with all entries in that particular month grouped by categories (empty categories are not displayed) and with a sum and count of the entries within a category.

My question is:
What is the most efficient way to query the database (and make best use of caching)? This view will be rendered quite often and each time a new entry is created by the user, naturally the sum of one category changes but not those of the others.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

就是爱搞怪 2024-11-07 08:42:28

查询部分可以通过范围非常简单地完成:

class Category
  scope :grouped_entries, lambda { |user, date|
    select(['categories.*', 'COUNT(entries.id) as count_entries'])
      .joins(:entries)
      .where('entries.user_id = ?', user.id)
      .where('MONTH(entries.created_at) = ?', date.month)
      .group('categories.id')
  }
end

然后可以循环:

<% Category.grouped_entries(current_user, Date.today).each do |category| %>
  <%= category.name %> with <%= category.count_entries %> entries this month.
<% end %>

当然,缓存这要求您在本月创建条目时刷新缓存。例如,您可以像这样缓存查询:

@categories = Rails.cache.fetch("/grouped_entries/#{current_user.id}/#{Date.today.month}") do
  Category.grouped_entries(current_user, Date.today).all
end

然后,当使用 user_id 和条目创建的月份创建新条目时,只需将其过期即可。我想说,在尝试单独缓存每个类别的条目之前,您应该首先使用这种方法。该查询应该执行得相当快,因此您不必深入研究单独缓存每一行。它还将执行单个查询,而不是针对每个类别执行一个查询。

这就是为什么我不会单独缓存每一行:

  • 您仍然需要查询数据库来获取用户的类别或类别 ID 列表,因此无论如何您都必须执行一个查询。
  • 缓存过期更加复杂,因为在更多情况下,您必须使两个缓存过期,例如,当条目的类别发生更改时,您必须使旧类别缓存和新类别缓存过期。
  • 您最终可能会对数据库运行更多查询以获取过期的缓存信息,并且数据库的延迟可能最终会比实际查询花费更长的时间。
  • 您不需要缓存每一行,因为查询很简单并且使用索引。您应该在 user_id 和 Category_id 上有一个条目索引。

The query part can be accomplished quite simply with a scope:

class Category
  scope :grouped_entries, lambda { |user, date|
    select(['categories.*', 'COUNT(entries.id) as count_entries'])
      .joins(:entries)
      .where('entries.user_id = ?', user.id)
      .where('MONTH(entries.created_at) = ?', date.month)
      .group('categories.id')
  }
end

Which can then be looped over:

<% Category.grouped_entries(current_user, Date.today).each do |category| %>
  <%= category.name %> with <%= category.count_entries %> entries this month.
<% end %>

Of course, caching this requires that you refresh the cache anytime an entry is created this month. For example, you could cache the query like this:

@categories = Rails.cache.fetch("/grouped_entries/#{current_user.id}/#{Date.today.month}") do
  Category.grouped_entries(current_user, Date.today).all
end

And then simply expire it when a new entry is created by using the user_id and the entry created_at month. I would say that you should use this approach first, before trying to cache every single category's entries individually. This query should perform quite quickly, so you shouldn't have to delve into caching each row individually. It will also perform a single query rather than one for each category.

Here is why I wouldn't cache each row individually:

  • You still have to query the database to get a list of categories or category ids for a user, so you have to perform one query anyways.
  • Cachine expiration is more complicated because there are more cases where you have to expire two caches, for example when a category for an entry changes you have to expire the old category cache and the new category cache.
  • You may end up running more queries against your database to grab expired cache information and the latency to the database will probably end up taking longer than the actual query.
  • You don't need to cache every row because the query is simple and uses indexes. You should have an index on user_id and category_id for entries.
旧情勿念 2024-11-07 08:42:28

潘老师的回答很完美,很有帮助。对于档案:这是我在应用程序中使用的查询:

scope :grouped_entries, lambda { |user, date|
  select(['categories.*', 'COUNT(entries.id) as count_entries']).
    joins(:entries).
    where('entries.user_id = ?', user.id).
    where(':first_day <= entries.created_at AND entries.created_at <= :last_day', { 
       :first_day => date.at_beginning_of_month,
       :last_day => date.at_end_of_month
    } ).
    group('categories.id')
}

Pan's answer was perfect and of great help. For the archives: this is the query I used in my app:

scope :grouped_entries, lambda { |user, date|
  select(['categories.*', 'COUNT(entries.id) as count_entries']).
    joins(:entries).
    where('entries.user_id = ?', user.id).
    where(':first_day <= entries.created_at AND entries.created_at <= :last_day', { 
       :first_day => date.at_beginning_of_month,
       :last_day => date.at_end_of_month
    } ).
    group('categories.id')
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文