【问题标题】:Rails 5 Activerecord: How to show 'type of' record per 'user' in a dashboard viewRails 5 Activerecord:如何在仪表板视图中显示每个“用户”的“类型”记录
【发布时间】:2019-02-22 10:34:06
【问题描述】:

我目前正在努力重构一些向经理显示仪表板的旧代码。

用户发送了许多与订餐相关的电子邮件 - 这些电子邮件主要用于审核目的。我们跟踪每个用户发送的电子邮件数量和该预订的价值,按 email_type 分组

在 ActiveRecord 获取每封电子邮件的那一刻,使用 where 子句在 created_at 上进行过滤,rails 然后将其添加到一个数组中,然后将其输出到一个表中。这看起来效率很低,而且 Nginx 正在超时,所以我们看不到结果。

我觉得对某些组使用主要是 ActiveRecord 会使这一切变得简单得多。

我刚刚向 user.rb 添加了一个关联,如下所示 - 因为没有一个关联 (!),而且它目前没有被使用:

  has_many :emails, :foreign_key => "triggered_by_id"

目前 MVC 看起来是这样的:

模型 - email.rb:

  scope :sent_between, -> ( start_date, end_date ) { where("emails.created_at >= ? AND emails.created_at <= ?", start_date, end_date) }

  def self.sent_today
    sent_between(DateTime.now.beginning_of_day, DateTime.now.end_of_day)
  end

  def self.metric_hash
    {
      "venue_confirmation"                    => [0, 0],
      "enquiry_confirmation"                  => [0, 0],
      "menu_verification"                     => [0, 0],
      "amendment_information"                 => [0, 0],
      "amendment_confirmation"                => [0, 0],
      "booking_confirmation_to_venue"         => [0, 0],
      "released_confirmation_to_venue"        => [0, 0],
      "cancellation_confirmation_to_venue"    => [0, 0],
      "transfer"                              => [0, 0],
      "total"                                 => [0, 0]
    }
  end

  def self.type_for_metrics(email)
    if %w(released_confirmation_to_venue cancellation_confirmation_to_venue).include?(email.email_type)
      return "transfer" if email.booking.transferred_at
  end

    email.email_type
  end

  def self.metrics(start_date, end_date)
    metrics = {}
    totals = metric_hash
    observed_bookings = Set.new

     Email.includes(:booking).select(:id, :email_type, :triggered_by_id, :booking_id).sent_between(start_date, end_date).references(:booking).select(:booking_total).find_each do |email|

      email_type = type_for_metrics(email)

      if totals.has_key?(email_type)
        metrics[email.triggered_by_id] ||= metric_hash
        metrics[email.triggered_by_id][email_type][0] += 1
        metrics[email.triggered_by_id][email_type][1] += email.booking.booking_total
        metrics[email.triggered_by_id]["total"][0] += 1
        metrics[email.triggered_by_id]["total"][1] += email.booking.booking_total
        totals[email_type][0] += 1
        totals[email_type][1] += email.booking.booking_total
        totals["total"][0] += 1

        # Only count the each booking once for the total value
        unless observed_bookings.include?(email.booking_id)
          totals["total"][1] += email.booking.booking_total
          observed_bookings << email.booking_id
        end
      end
    end

    results = metrics.inject({}) do |memo, row|
      if row[1]["total"][0] > 0
        if row[0]
          user = User.find(row[0])
          memo[user.name] = row[1]
        else
          memo["Sent Before Tracking"] = row[1]
        end
        memo
      end
    end
    results["Total"] = totals

    results
  end

index_controller.rb:

  def metrics
      @start = ( params[:start] && Time.zone.parse(params[:start]) ) || DateTime.now.start_of_period
      @end   = ( params[:end] && Time.zone.parse(params[:end]) ) || DateTime.now.end_of_period
      @email_metrics = Email.metrics(@start, @end)
  end

_metrics.html.erb

<h2>Emails Sent</h2>

  <table>
    <thead>
      <tr>
        <th></th>
        <th title="New Enquiry">New</th>
        <th title="Menu Confirmation">Menu</th>
        <th title="Operator Confirmation">Confirm</th>
        <th title="Released Enquiry">Released</th>
        <th title="Cancelled Booking">Cancelled</th>
        <th title="Amendment Information">Amend Info</th>
        <th title="Amendment Confirmation">Amend Confirm</th>
        <th title="Transferred">Transfer</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      <% @email_metrics.each do |key, metrics| %>
        <tr>
          <th rowspan="2"><%= key %></th>
          <td><%= metrics["venue_confirmation"][0] %></td>
          <td><%= metrics["menu_verification"][0] %></td>
          <td><%= metrics["booking_confirmation_to_venue"][0] %></td>
          <td><%= metrics["released_confirmation_to_venue"][0] %></td>
          <td><%= metrics["cancellation_confirmation_to_venue"][0] %></td>
          <td><%= metrics["amendment_information"][0] %></td>
          <td><%= metrics["amendment_confirmation"][0] %></td>
          <td><%= metrics["transfer"][0] %></td>
          <td><%= metrics["total"][0] %></td>
        </tr>
        <tr>
          <td><%= number_to_currency metrics["venue_confirmation"][1] %></td>
          <td><%= number_to_currency metrics["menu_verification"][1] %></td>
          <td><%= number_to_currency metrics["booking_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["released_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["cancellation_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["amendment_information"][1] %></td>
          <td><%= number_to_currency metrics["amendment_confirmation"][1] %></td>
          <td><%= number_to_currency metrics["transfer"][1] %></td>
          <td>N/A</td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

我的感觉是我会从 User.emails.etc 开始......但我被多个 group_by 和当前存在的大量复杂数组所困。

这是它的外观截图。开发环境目前只有一个用户。

Screenshot of how the data should look

【问题讨论】:

  • 在给定的时间范围内有多少电子邮件?
  • @zeitnot - 该表在生产时有 500K 行。在页面加载/范围搜索时一次调用多达 10,000 个的任何内容
  • 今天我将检查这段代码并尝试找出如何优化它。

标签: mysql ruby-on-rails ruby nginx activerecord


【解决方案1】:

我想出了一个解决方案。

  scope :sent_between, -> ( start_date, end_date ) { where("emails.created_at >= ? AND emails.created_at <= ?", start_date, end_date) }

  def self.sent_today
    sent_between(DateTime.now.beginning_of_day, DateTime.now.end_of_day)
  end

  def self.metric_hash
    {
        "venue_confirmation"                    => [0, 0],
        "enquiry_confirmation"                  => [0, 0],
        "menu_verification"                     => [0, 0],
        "amendment_information"                 => [0, 0],
        "amendment_confirmation"                => [0, 0],
        "booking_confirmation_to_venue"         => [0, 0],
        "released_confirmation_to_venue"        => [0, 0],
        "cancellation_confirmation_to_venue"    => [0, 0],
        "transfer"                              => [0, 0],
        "total"                                 => [0, 0]
    }
  end

  def self.type_for_metrics(email)
    if %w(released_confirmation_to_venue cancellation_confirmation_to_venue).include?(email.email_type)
      return "transfer" if email.booking.transferred_at
    end

    email.email_type
  end

  def self.metrics(start_date, end_date)
    metrics = {}
    totals = metric_hash
    observed_bookings = {}

    Email.includes(:booking).select(:id, :email_type, :triggered_by_id, :booking_id).sent_between(start_date, end_date).
        references(:booking).select(:booking_total).find_each do |email|

      email_type = type_for_metrics(email)

      if totals[email_type]
        metrics[email.triggered_by_id] ||= metric_hash
        metrics[email.triggered_by_id][email_type][0] += 1
        metrics[email.triggered_by_id][email_type][1] += email.booking.booking_total
        metrics[email.triggered_by_id]["total"][0] += 1
        metrics[email.triggered_by_id]["total"][1] += email.booking.booking_total
        totals[email_type][0] += 1
        totals[email_type][1] += email.booking.booking_total
        totals["total"][0] += 1

        # Only count the each booking once for the total value
        unless observed_bookings[email.booking_id]
          totals["total"][1] += email.booking.booking_total
          observed_bookings[email.booking_id] = true
        end
      end
    end

    users = users = User.select(:id,:name).where(id: metrics.keys).group_by(&:id).transform_values{ |value| value.first }
    results = metrics.inject({}) do |memo, row|
      if row[1]["total"][0] > 0
        if row[0]
          user = users[row[0]]
          memo[user.name] = row[1]
        else
          memo["Sent Before Tracking"] = row[1]
        end
        memo
      end
    end
    results["Total"] = totals

    results
  end

我在这里所做的是将observed_bookings 的类型更改为哈希。因为哈希具有极快的查找速度,并且在您的场景中将新项目插入哈希是 O(​​1)。 在您的场景中,您进行查找并且希望数据结构是唯一的。所以哈希是你的朋友。

我已将totals.has_key?(email_type) 更改为totals[email_type]。再次在这里,我们非常清楚地查找了哪个 O(1)。

最后,由于用户 ID 实际上是 metrics 键,因此只需查询我们就可以从数据库中获取用户并将结果结果转换为哈希。

是的,用户是哈希,用户 ID 是哈希的键。所以我们又一次得到了一个非常快速的查找,它是 O(1)。

我认为这个查询应该给出您对响应时间的期望。

【讨论】:

  • 您没有考虑过检索和迭代超过 10000 条记录会导致速度变慢的原因吗?
  • 获取和迭代 10000 条记录是线性增长。但是,如果您在此迭代中执行一些循环,则增长是二次的。所以我实际上所做的只是删除了额外的循环。这将使它更快。
  • @zeitnot 这对我有用,可以让应用程序再次运行。所以谢谢你。
  • 有没有人认为配置 Redis 服务器可能会对此有所帮助?
  • @PeterFealey 欢迎您。如果您使用postgres 作为数据库,那么您会考虑使用物化视图。
【解决方案2】:

我同意你的观点,仅仅检索所有电子邮件、迭代它们并手动构建结果集似乎完全适得其反。

您将无法通过单个查询来解决此问题,但您可以通过一些(更快的查询)来解决它,而不是获取 10000 条记录,并让数据库完成它的工作。

我将给出一些示例,希望能帮助您入门。

获取给定时间段内的所有类型:

Email.sent_between(start_date, end_date).group(:email_type).count 

获取给定时间段内每个用户的邮件数量

Email.sent_between(start_date, end_date).group(:triggered_by_id).count 

这会返回两件事:用户和他们的数量。使用这些用户获取每个用户的结果:

Email.where(trigger_by_id: user_id).sent_between(start_date, end_date).group(:email_type).count 

booking-total 似乎有点困难,但我认为这样的事情应该可行:

Email.includes(:booking).sent_between(start_date, end_date).references(:booking).group(:email_type).sum(:booking_total) 

我知道这不是一个完整的解决方案,您仍然需要编写“结果哈希”,但这应该会更快地获取您的原始数据。

【讨论】:

  • 感谢您在此输入。第一个答案对我有用,但我绝对有一些重要的价值,让您在这里的经验,看看我是否可以根据您的建议进一步改进。我会更新任何进展。谢谢!
猜你喜欢
  • 1970-01-01
  • 2021-09-10
  • 2022-11-26
  • 2021-11-15
  • 2017-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多