5

我正在尝试在 Rails 中制作图表,例如给定日期范围内每天的平均销售额

假设我有一个 products_sold 模型,它有一个“sales_price”浮动属性。但是如果某一天没有销售(例如模型/数据库中没有销售),我想简单地返回 0。

MySQL/Rails 中完成这项工作的最佳方法是什么?我知道我可以做这样的事情:

这个 SQL 查询可能是完全错误的方式来获得我想要的东西

SELECT avg(sales_price) AS avg, DATE_FORMAT(created_at, '%m-%d-%Y') AS date
    FROM products_sold WHERE merchant_id = 1 GROUP BY date;

并得到这样的结果:

| 平均 | 日期 |
  23 01-03-2009
  50 01-05-2009
  34 01-07-2009
  ……

我想得到的是:

| 平均 | 日期 |
  23 01-03-2009
   0 01-04-2009
  50 01-05-2009
   0 01-06-2009
  34 01-07-2009
   0 01-08-2009
  ……

我可以使用 SQL 执行此操作,还是必须对结果进行后处理以查找日期范围中的哪些日期不在 SQL 结果集中?也许我需要一些子选择或 IF 语句?

感谢大家的帮助。

4

4 回答 4

7

有没有理由(除了已经提到的日期)为什么不使用 ActiveRecord 中的内置组功能功能?您似乎担心“后处理”,我认为这并不是真正值得担心的事情。

您在 Rails 中,因此您可能应该首先寻找 Rails 解决方案[1]。我的第一个想法是做类似的事情

Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])

ActiveRecord 几乎变成了您描述的 SQL。假设在 Merchant 和 Product 之间声明了has_many关联,那么您可能会更好地使用它,例如:

ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")

(我希望您将模型描述为“products_sold”是某种转录错误,顺便说一句 - 如果不是,您的类命名有点离题!)

毕竟,您回到了起点,但您以更传统的 Rails 方式到达那里(而且 Rails 真的很重视约定!)。现在我们需要填补空白。

我假设您知道您的日期范围,假设它被定义为从from_dateto的所有日期to_date

date_aves = (from_date..to_date).map{|dt| [dt, 0]}

这会将完整的日期列表构建为一个数组。我们不需要得到平均值的日期:

ave_price_dates = ave_prices.collect{|ave_price| ave_price[0]} # build an array of dates
date_aves.delete_if { |dt| ave_price.dates.index(dt[0]) } # remove zero entries for dates retrieved from DB
date_aves.concat(ave_prices)     # add the query results
date_aves.sort_by{|ave| ave[0] } # sort by date

对我来说,那部分看起来有点杂乱:我认为它可以更简洁更干净。我会研究构建一个 Hash 或 Struct 而不是留在数组中。


[1] 我并不是说不要使用 SQL - ActiveRecord 无法生成最有效的查询并且您依赖find_by_sql. 没关系,它应该是这样的,但我认为你应该尝试仅作为最后的手段使用它。

于 2009-01-08T10:16:30.100 回答
2

对于任何此类查询,您将需要找到一种机制来为要报告的每个日期生成一个包含一行的表。然后,您将对该表与您正在分析的数据表进行外部连接。您可能还必须使用 NVL 或 COALESCE 将空值转换为零。

困难的部分是如何生成(临时)表,其中包含您需要分析的范围的日期列表。那是特定于 DBMS 的。

不过,您将日期/时间值映射到单个日期的想法是正确的。如果您想分析每周销售额,您需要使用类似的技巧 - 将所有日期映射到 ISO 8601 日期格式,例如第 1 周的 2009-W01 。

此外,您最好将 DATE 格式映射到 2009-01-08 表示法,因为这样您就可以使用纯字符排序按日期顺序进行排序。

于 2009-01-08T06:18:42.000 回答
2

干一点:

ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
date_aves = (from_date..to_date).map{|dt| [dt, ave_prices[dt.strftime "%Y-%m-%d"] || 0]}
于 2009-07-18T21:47:45.623 回答
0

MySQL 有设置返回函数吗?即在查询的每一行上返回不同值的函数?作为 PostgreSQL 的示例,您可以执行以下操作:

select 'foo', generate_series(3, 5);

这将产生一个由 2 列和 3 行组成的结果集,其中左列每行包含“foo”,右列包含 3、4 和 5。

所以,假设你在 MySQL 中有一个等价的generate_series()和子查询:你需要的是LEFT OUTER JOIN从这个函数到你已经拥有的查询。这将确保您看到每个日期都出现在输出中:

SELECT
    avg(sales_price) as avg,
    DATE_FORMAT(the_date, '%m-%d-%Y') as date
FROM (select cast('2008-JAN-01' as date) + generate_series(0, 364) as the_date) date_range
LEFT OUTER JOIN products_sold on (the_date = created_at)
WHERE merchant_id = 1
GROUP BY date;

您可能需要稍微调整一下才能获得适合 MySQL 的语法。

于 2009-01-08T06:18:13.937 回答