Rails用范围扩展字段,PG不喜欢它

时间:2011-04-25 18:27:16

标签: ruby-on-rails postgresql activerecord

我有一个Widgets模型。窗口小部件属于商店模型,属于区域模型,属于公司。在公司模型中,我需要找到所有相关的小部件。易:

class Widget < ActiveRecord::Base
  def self.in_company(company)
    includes(:store => {:area => :company}).where(:companies => {:id => company.id})
  end
end

这会生成这个漂亮的查询:

> Widget.in_company(Company.first).count

SQL (50.5ms)  SELECT COUNT(DISTINCT "widgets"."id") FROM "widgets" LEFT OUTER JOIN "stores" ON "stores"."id" = "widgets"."store_id" LEFT OUTER JOIN "areas" ON "areas"."id" = "stores"."area_id" LEFT OUTER JOIN "companies" ON "companies"."id" = "areas"."company_id" WHERE "companies"."id" = 1
 => 15088 

但是,我后来需要在更复杂的范围内使用此范围。问题是AR通过选择单个字段来扩展查询,这些字段在PG中失败,因为所选字段必须在GROUP BY子句或聚合函数中。

这是更复杂的范围。

def self.sum_amount_chart_series(company, start_time)
  orders_by_day = Widget.in_company(company).archived.not_void.
                  where(:print_datetime => start_time.beginning_of_day..Time.zone.now.end_of_day).
                  group(pg_print_date_group).
                  select("#{pg_print_date_group} as print_date, sum(amount) as total_amount")

end

def self.pg_print_date_group
  "CAST((print_datetime + interval '#{tz_offset_hours} hours') AS date)"
end

这是PG投掷的选择:

> Widget.sum_amount_chart_series(Company.first, 1.day.ago)

SELECT "widgets"."id" AS t0_r0, "widgets"."user_id" AS t0_r1,<...BIG SNIP, YOU GET THE IDEA...> FROM "widgets" LEFT OUTER JOIN "stores" ON "stores"."id" = "widgets"."store_id" LEFT OUTER JOIN "areas" ON "areas"."id" = "stores"."area_id" LEFT OUTER JOIN "companies" ON "companies"."id" = "areas"."company_id" WHERE "companies"."id" = 1 AND "widgets"."archived" = 't' AND "widgets"."voided" = 'f' AND ("widgets"."print_datetime" BETWEEN '2011-04-24 00:00:00.000000' AND '2011-04-25 23:59:59.999999') GROUP BY CAST((print_datetime + interval '-7 hours') AS date)

会产生此错误:

  

PGError:错误:列   “widgets.id”必须出现在   GROUP BY子句或用于   聚合函数LINE 1:SELECT   “小部件”。“id”AS t0_r0,   “窗口小部件”,“。USER_ID ...

如何重写Widget.in_company范围,以便AR不会扩展select查询以包含每个Widget模型字段?

5 个答案:

答案 0 :(得分:10)

正如弗兰克解释的那样,PostgreSQL将拒绝任何不返回可重现行集的查询。

假设你有一个类似的查询:

select a, b, agg(c)
from tbl
group by a

PostgreSQL会拒绝它,因为在b语句中未指定group by。相比之下,在MySQL中运行它,它将被接受。但是,在后一种情况下,启动一些插入,更新和删除,磁盘页面上的行顺序不同。

如果内存服务,实现细节是这样的,MySQL实际上将按a,b排序并返回集合中的第一个b。但就SQL标准而言,行为是未指定的 - 果然,PostgreSQL在运行聚合函数之前 总是排序。

这可能会导致PostgreSQL结果集中b的值不同。因此,除非你更具体,否则PostgreSQL会产生错误:

select a, b, agg(c)
from tbl
group by a, b

Frank强调的是,在PostgreSQL 9.1中,如果a是主键,那么你可以不指定b - 已经教会规划者在适用的主要时忽略后续的分组字段键意味着一个独特的行。

特别针对您的问题,您需要按照目前的方式指定您的论坛, plus 您基于聚合的每个字段,即"widgets"."id", "widgets"."user_id", [snip]但不是像sum(amount),它们是聚合函数调用。

作为一个偏离主题的旁注,我不确定你的ORM /模型是如何工作的,但它生成的SQL并不是最佳的。许多左外连接似乎应该是内连接。这将导致计划者在适用的情况下选择适当的连接顺序。

答案 1 :(得分:3)

PostgreSQL版本9.1(beta at this moment)可能会解决您的问题,但前提是主键上存在功能依赖。

从发行说明:

  

允许非GROUP BY列   主键时查询目标列表   在GROUP BY子句中指定   (Peter Eisentraut)

     

其他一些数据库系统已经存在   允许这种行为,因为   主键,结果是   毫不含糊的。

您可以运行测试,看看它是否能解决您的问题。如果您可以等待生产版本,则可以在不更改代码的情况下解决问题。

答案 2 :(得分:2)

首先通过将所有日期存储在标准时区来简化您的生活。为了方便用户,应该在视图中更改带有时区的日期。仅这一点就可以为你节省很多痛苦。

如果您已投入生产,请编写迁移内容以创建normalised_date列,只要它有用。

nrI建议这里的另一个问题是使用原始SQL,哪些rails不会为你找到。为了避免这种情况,请尝试使用名为Squeel(aka Metawhere 2)http://metautonomo.us/projects/squeel/

的宝石

如果你使用它,你应该能够删除硬编码的SQL,并让rails回到它的魔力。

例如:

.select("#{pg_print_date_group} as print_date, sum(amount) as total_amount")

成为(一旦你删除了规范化日期的需要):

.select{sum(amount).as(total_amount)}

答案 3 :(得分:0)

很抱歉回答我自己的问题,但我明白了。

首先,让我向那些认为我可能有SQL或Postgres问题的人道歉,事实并非如此。问题出在ActiveRecord和它正在生成的SQL上。

答案是......使用 .joins 而不是 .includes 。所以我只是更改了顶部代码中的行,它按预期工作。

class Widget < ActiveRecord::Base
  def self.in_company(company)
    joins(:store => {:area => :company}).where(:companies => {:id => company.id})
  end
end

我猜测在使用.includes时,ActiveRecord试图变得聪明并在SQL中使用JOINS,但是对于这种特殊情况它并不够智能,并且正在生成那个丑陋的SQL来选择所有相关的列。

然而,所有回复都让我了解了很多关于Postgres我不知道的事情,所以非常感谢你。

答案 4 :(得分:0)

在mysql中排序:

> ids = [11,31,29]
=> [11, 31, 29]
> Page.where(id: ids).order("field(id, #{ids.join(',')})")

在postgres中:

def self.order_by_ids(ids)
  order_by = ["case"]
  ids.each_with_index.map do |id, index|
    order_by << "WHEN id='#{id}' THEN #{index}"
  end
  order_by << "end"
  order(order_by.join(" "))
end

User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#=> [3,2,1]