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

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

class Widget  {: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, 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“widgets”。“id”AS t0_r0,“widgets”。“user_id …

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

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

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

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

PostgreSQL会拒绝它,因为bgroup 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未指定 – 当适用的主键表示唯一行时,计划程序已被教导忽略后续的分组字段。

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

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

PostgreSQL版本9.1(此时为测试版 )可能会解决您的问题,但前提是主键上存在function依赖性。

从发行说明:

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

其他一些数据库系统已经允许这种行为,并且由于主键,结果是明确的。

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

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

如果您已经在生产中,请编写迁移以创建normalised_date列,只要它有用。

nrI建议这里的另一个问题是使用原始SQL,哪些rails不会为你而烦恼。 为了避免这种情况,请尝试使用名为Squeel(又名Metawhere 2)的gemhttp://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)} 

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

首先,让我向那些认为我可能有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,非常感谢你。

在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]