【问题标题】:Rails expanding fields with scope, PG does not like itRails 用范围扩展字段,PG 不喜欢
【发布时间】:2011-04-25 18:27:16
【问题描述】:

我有一个小部件模型。 Widgets属于Store模型,属于Area模型,属于Company。在公司模型中,我需要找到所有相关的小部件。简单:

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 子句或用于 聚合函数第 1 行:SELECT "小部件"."id" AS t0_r0, "小部件"."user_id...

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

【问题讨论】:

  • Rails 3 大概?我在 Rails3/Heroku 中有一个更简单的情况,我没有选择特定的列 - 所以它正在执行 select * 并得到了这个错误 - 通过添加特定的列选择来修复它 - 但你正在这样做.... :(跨度>

标签: ruby-on-rails postgresql activerecord


【解决方案1】:

正如 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],但不是像@987654329这样的东西@,它们是聚合函数调用。

作为题外话,我不确定您的 ORM/模型是如何工作的,但它生成的 SQL 并不是最佳的。许多左外连接看起来应该是内连接。这将允许规划者在适用的情况下选择合适的加入顺序。

【讨论】:

    【解决方案2】:

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

    来自发行说明:

    允许在 查询目标列表时的主键 在 GROUP BY 子句中指定 (彼得·艾森特劳特)

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

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

    【讨论】:

    • 虽然这确实说明了一些问题,但对于许多 Rails 开发人员来说,升级到更高版本的 PostgreSQL 并不是一个可行的选择,因为 Heroku 在 PostgreSQL 8.3 上运行。我将把我的赏金奖励给能够提出一个简单示例的人,该示例给出相同的错误以及另一种编写查询的方式以获得相同的预期信息。如果不止一个人完成此任务,赏金将授予谁能对错误提供更清晰的解释以及将来如何避免它。
    【解决方案3】:

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

    如果您已经在生产环境中,请编写迁移以在任何有用的地方创建 normalised_date 列。

    nr我建议这里的另一个问题是使用原始 SQL,rails 不会为您解决问题。为了避免这种情况,请尝试使用名为 Squeel 的 gem(又名 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)}
    

    【讨论】:

      【解决方案4】:

      很抱歉回答我自己的问题,但我想通了。

      首先,让我向那些认为我可能遇到 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 的知识,所以非常感谢。

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-09-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-11-26
          • 2018-09-24
          • 2012-09-09
          相关资源
          最近更新 更多