9

我想使用 Squeel 重构下面的查询。我想这样做,以便我可以将其中的运算符链接起来,并在查询的不同部分重新使用逻辑。

User.find_by_sql("SELECT 
    users.*,
    users.computed_metric,
    users.age_in_seconds,
    ( users.computed_metric / age_in_seconds) as compound_computed_metric
    from
    (
      select
        users.*,
        (users.id *2 ) as computed_metric,
        (extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds
        from users
    ) as users")

查询必须全部在数据库中运行,并且不应该是混合 Ruby 解决方案,因为它必须对数百万条记录进行排序和切片。

我已经设置了问题,以便它应该针对普通user桌子运行,这样您就可以使用它的替代方案。

可接受答案的限制

  • 查询应该返回一个User具有所有正常属性的对象
  • 每个用户对象还应包括extra_metric_we_care_about,age_in_secondscompound_computed_metric
  • 查询不应该通过在多个地方打印出一个字符串来重复任何逻辑 - 我想避免两次做同样的事情
  • [更新]查询应该在数据库中都可以执行,以便在返回 Rails 之前可以在数据库中对可能包含数百万条记录的结果集进行排序和切片
  • [更新] 该解决方案应该适用于 Postgres 数据库

我想要的解决方案类型示例

下面的解决方案不起作用,但它显示了我希望达到的优雅类型

class User < ActiveRecord::Base
# this doesn't work - it just illustrates what I want to achieve

  def self.w_all_additional_metrics
    select{ ['*', 
              computed_metric, 
              age_in_seconds, 
              (computed_metric / age_in_seconds).as(compound_computed_metric)] 
      }.from{ User.w.compound_computed_metric.w_age_in_seconds }
  end

  def self.w_computed_metric
    where{ '(id *2 ) as computed_metric' }
  end

  def self.w_age_in_seconds
    where{ '(extract(epoch from now()) - extract(epoch from created_at) ) as age_in_seconds' }
  end
end

您应该能够针对您现有的数据库运行它

请注意,我有些人为地设计了这个问题,以便您可以使用现有的User课程并在控制台中使用它。

编辑

  1. 我使用的数据库是 Postgres。
  2. 我不确定我是否 100% 清楚地表明查询应该全部在数据库中执行。如果某些逻辑本质上是在 Rails 中完成的,那么这不能是一个混合的答案。这很重要,因为我希望能够使用计算列对数百万条记录进行排序和切片。
4

3 回答 3

7

在您的情况下,我有 2 个解决方案。我的数据库是mysql,我为demo简化了你的代码,我想你可以扩展它。

第一种是 Squeel 方式,我将 Squeel 中的“sift”和 ActiveRecord Query 中的“from”混合在一起。我刚刚安装了postgresql并测试了我的解决方案,似乎很难同时使用“squeel”和“epoch from”,但我在postgresql中找到了另一种方法,它称为“date_part”。我还修改了sql,减少了重复计算:

class User < ActiveRecord::Base           
  sifter :w_computed_metric do
    (id * 2).as(computed_metric)
  end

  sifter :w_age_in_seconds do
    (date_part('epoch' , now.func) - date_part('epoch', created_at)).as(age_in_seconds)
  end

  sifter :w_compound_computed_metric do
    (computed_metric / age_in_seconds).as(compound_computed_metric)
  end

  def self.subquery
    select{['*', sift(w_computed_metric) , sift(w_age_in_seconds)]}
  end

  def self.w_all_additional_metrics
    select{['*', sift(w_compound_computed_metric)]}.from("(#{subquery.to_sql}) users")
  end      
end

它产生了sql:

SELECT *, "users"."computed_metric" / "users"."age_in_seconds" AS compound_computed_metric 
FROM (SELECT *, 
             "users"."id" * 2 AS computed_metric, 
             date_part('epoch', now()) - date_part('epoch', "users"."created_at") AS age_in_seconds FROM "users" 
     ) users

您可以使用控制台对其进行测试:

> User.w_all_additional_metrics.first.computed_metric
=> "2"
> User.w_all_additional_metrics.first.age_in_seconds
=> "633.136693954468"
> User.w_all_additional_metrics.first.compound_computed_metric
=> "0.00315887551471441"

第二种是 ActiveRecord 方式,因为你的 sql 不是很复杂,所以你可以在 ActiveRecord Query 中链接它,有一些范围就足够了:

class User < ActiveRecord::Base
  scope :w_computed_metric, proc { select('id*2 as computed_metric') }
  scope :w_age_in_seconds, proc { select('extract (epoch from (now()-created_at)) as age_in_seconds') }
  scope :w_compound_computed_metric, proc { select('computed_metric/age_in_seconds as compound_computed_metric') }

  def self.subquery
    select('*').w_computed_metric.w_age_in_seconds
  end

  def self.w_all_additional_metrics
    subquery.w_compound_computed_metric.from("(#{subquery.to_sql}) users")
  end
end

这将产生以下 SQL:

SELECT 
  *, id*2 as computed_metric, 
  extract (epoch from (now()-created_at)) as age_in_seconds, 
  computed_metric / age_in_seconds as compound_computed_metric
FROM (
    SELECT 
      *, 
      id*2 as computed_metric, 
      extract (epoch from (now()-created_at)) as age_in_seconds 
    FROM 
      "users" 
    ) users 
ORDER BY compound_computed_metric DESC 
LIMIT 1

希望能满足你的要求:)

于 2013-08-15T07:40:55.690 回答
2

我很可能完全错了。我觉得你为了让别人理解它而过于简化你的问题。由于我无法在评论中给出这个格式正确的代码,所以我在这里输入了答案。

SELECT 
    users.*,
    users.computed_metric,
    users.age_in_seconds,
    ( users.computed_metric / age_in_seconds) as compound_computed_metric
    from
    (
      select
        users.*,
        (users.id *2 ) as computed_metric,
        (extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds
        from users
    ) as users

下面的 SQL 相当于你上面的 SQL。这就是为什么我说子查询没有必要。

select
  users.*,
  (users.id *2 ) as computed_metric,
  (extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds,
  computed_metric/age_in_seconds as compound_computed_metric
  from users

如果这是正确的,那么可以通过以下方式计算 Compound_computed_metric。不需要自定义查询。

class User < ActiveRecord::Base

  def compound_computed_metric
    computed_metric/age_in_seconds
  end
  def computed_metric
    self.id * 2
  end
  def age_in_seconds
    Time.now - self.created_at
  end
end

1.9.3p327 :001 > u = User.first
  User Load (0.1ms)  SELECT "users".* FROM "users" LIMIT 1
 => #<User id: 1, name: "spider", created_at: "2013-08-10 04:29:35", updated_at: "2013-08-10 04:29:35">
1.9.3p327 :002 > u.compound_computed_metric
 => 1.5815278998954843e-05
1.9.3p327 :003 > u.age_in_seconds
 => 126471.981447
1.9.3p327 :004 > u.computed_metric
 => 2
于 2013-08-11T01:47:51.717 回答
1

让我们以此作为开头,这不是您要寻找的答案...

现在,除此之外,这就是我尝试的方法以及它与我在问题评论中发布的两个链接的关系。

class User < ActiveRecord::Base
  # self-referential association - more on this later
  belongs_to :myself, class_name: "User", foreign_key: :id

  scope :w_computed_metric, ->() { select{[id, (id *2).as(computed_metric)]} }
  scope :w_age_in_seconds,  ->() { select{[id, (extract('epoch from now()') - extract('epoch from users.created_at')).as(age_in_seconds)]} }
  scope :w_default_attributes, ->() { select{`*`} }

  def self.compound_metric
    scope = User.w_default_attributes.select{(b.age_in_seconds / a.computed_metric).as(compound_metric)}
    scope = scope.joins{"inner join (" + User.w_computed_metric.to_sql + ") as a on a.id = users.id"}
    scope = scope.joins{"inner join (" + User.w_age_in_seconds.to_sql + ") as b on b.id = users.id"}
  end

  sifter :sift_computed_metric do
    (id * 2).as(computed_metric)
  end

  sifter :sift_age_in_seconds do
    (extract(`epoch from now()`) - extract(`epoch from users.created_at`)).as(age_in_seconds)
  end

  def self.using_sifters_in_select
    User.w_default_attributes.joins{myself}.select{[(myself.sift :sift_computed_metric), (myself.sift :sift_age_in_seconds)]}
  end

  def self.using_from
    scope = User.w_default_attributes
    scope = scope.select{[(age_in_seconds / computed_metric).as(compound_metric)]}
    scope = scope.from{User.w_computed_metric.w_age_in_seconds}
  end
end

因此,User.compound_metric在控制台中运行将产生您正在寻找的结果 -User具有附加属性的对象:computed_metricage_in_secondscompound_metric. 不幸的是,这违反了您对可接受答案的第三个约束。那好吧...

我还尝试了其他一些事情(如您从上面看到的):

首先要注意的是自我参照关联,我对此感到非常自豪——尽管它并没有让我们到达我们想去的地方。

belongs_to :myself, class_name: "User", foreign_key: :id

这段漂亮的代码让您可以通过连接访问同一个对象。为什么这很重要?好吧,Squeel 只允许您通过该joins{}方法访问关联,除非您向它传递一个 SQL 字符串。这让我们可以使用sifterSqueel 的 s 特性——在这种情况下,不过滤结果,而是包含来自 db 的聚合列,让 Squeel 完成别名和连接语句的繁重工作。你可以用

def self.using_sifters_in_select
  User.w_default_attributes.joins{myself}.select{[(myself.sift :sift_computed_metric), (myself.sift :sift_age_in_seconds)]}
end

实现这一点的筛选器的美妙之处在于可链接性和合成糖 - 它非常扁平且可读。

我尝试玩的最后一点是.from{}. 在这个问题之前,我什至不知道它的存在。我很高兴我错过了一些简单的事情,比如包含查询的源(在这种情况下是子选择)。使用 using_from 进行测试

def self.using_from
    scope = User.w_default_attributes
    scope = scope.select{[(age_in_seconds / computed_metric).as(compound_metric)]}
    scope = scope.from{User.w_computed_metric.w_age_in_seconds}
end

导致类型错误:

TypeError: Cannot visit Arel::SelectManager
  from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:28:in `rescue in visit'
  from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:19:in `visit'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:348:in `visit_Arel_Nodes_JoinSource'
  from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:21:in `visit'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:139:in `visit_Arel_Nodes_SelectCore'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `block in visit_Arel_Nodes_SelectStatement'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `map'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `visit_Arel_Nodes_SelectStatement'
  from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:21:in `visit'
  from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:5:in `accept'
  from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:19:in `accept'

(是的,我正在测试 Arel 和 Squeel 的本地副本)。我对 Arel 的内部运作不够熟悉,无法在没有进一步努力的情况下解决问题(很可能是 Arel 的一个分支)。看起来 Squeel 只是将from{}方法传递给 Arelfrom()方法而没有做任何事情(除了 Squeel 的其余魔法之外)。

那么,我们将何去何从?一个有效的解决方案,但不像我希望的那样漂亮和优雅 - 但也许其他人可以利用它来获得更好的解决方案。

PS - 这是 Rails v3.2.13 和 Arel 的相应版本。Rails v4 和 Arel 的源代码完全不同,因此未经测试。

于 2013-08-12T13:36:53.797 回答