【问题标题】:How would you do this Rails sub-query using Squeel?您将如何使用 Squeel 执行此 Rails 子查询?
【发布时间】:2013-08-01 12:38:42
【问题描述】:

我想使用 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_aboutage_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 中完成的,那么这不能是一个混合的答案。这很重要,因为我希望能够使用计算列对数百万条记录进行排序和切片。

【问题讨论】:

  • 只是用我的方式扔垒球......解决这个问题有多难......呃!争夺赏金的竞赛正在进行中!
  • 我不确定在不违反您对可接受答案的第三个限制的情况下这实际上是否可行。如果您还没有看过,我已经采取了类似herehere 的步骤。
  • 我不明白。您所描述的可以简单地解决,无需任何花哨的 squeel 链接。无需使用子查询。 Compound_computed_metric 是针对表中的每一行计算的,没有聚合。
  • @wanghq - 如果是这样的话,那就太好了。你能写出实现这一目标的查询吗?错误 - 我会看看,你
  • @erroric - 避免重复的 SQL(和 DB 工作)很难。我认为它不适合您发布的其他两个示例,尽管您显然与我的思路相同

标签: ruby-on-rails activerecord arel squeel


【解决方案1】:

在您的情况下,我有 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

希望它符合您的要求:)

【讨论】:

  • 谢谢你,你的回答很有趣。不幸的是,虽然我希望在 Sqeel 而不是 Sequel 中解决这个问题,但发现 Sequel 的存在很有趣,因为我以前从未听说过它!
  • 非常抱歉,我从“squeel”读到“sequel”....,我已经阅读了squeel的文档,会更新答案。
  • 我对此印象深刻。我还没有设法使 Squeel 版本工作,但我仍在玩它,看看我是否可以。我真的很喜欢你的纯 AR 版本,但是我有一个问题。我已将它生成的 SQL 放入您的答案中(可以随意删除它,但 cmets 会被处理掉)。如果您注意到 SQL 中的计算实际上进行了两次 - 一次在子查询中,然后一次在超级查询中。是否有可能将它们减少到只发生一次以节省计算量?如果您可以在没有计算重复的情况下干净利落地做到这一点,那么积分就是您的了。
  • 我不敢相信这是多么巧妙。大翔你是我的英雄!此解决方案的关键之一是在from 子句中为子查询使用“用户”的别名。再次,太棒了!
  • 还有一件事 - 你解决了extract(epoch from ...) 的问题,这对我来说造成了各种各样的悲痛。 :)
【解决方案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

【讨论】:

  • 嗨,王,我很抱歉。在原始 SQL 中,除了复合计算指标之外,我忘记请求复合指标和年龄(以秒为单位)。你说得对,我把它简化了。我刚刚更新了问题,以便在原始 SQL 中包含对它们的请求。谢谢
  • 我相应地更新了我的答案。还是觉得子查询没必要。
  • 您是否设法完成这项工作,因为我收到以下错误:ERROR: column "computed_metric" does not exist LINE 5: computed_metric/age_in_seconds as compound_computed_metric - 我认为您不能在查询中再次使用计算列(我在你的答案顺便说一句)。也感谢您抽出宝贵时间
  • 这个问题显示了重用列的问题:stackoverflow.com/questions/8840228/…。因此子查询!
  • 您使用哪个 SQL 数据库?我使用默认的 sqlite 进行实验,执行上面显示的操作没有问题(参见控制台输出)。
【解决方案3】:

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

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

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 字符串。这让我们可以使用 Squeel 的 sifters 功能 - 在这种情况下,不过滤结果,而是包含来自 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{} 方法传递给 Arel from() 方法,而没有做任何事情(除了 Squeel 的其余魔法之外)。

那么这会给我们带来什么影响呢?一个有效的解决方案,但不像我希望的那样漂亮和优雅 - 但也许其他人可以利用它来获得更好的解决方案。

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

【讨论】:

  • 您好,Erroric,很抱歉花了这么长时间才回复您的出色回答。我相信我之前在 from 方法上遇到过与您相同的错误,我认为这与 gem 不兼容有关。您的回答真的很有趣,因为它让我试图发现筛子是什么(我以前从未知道过),并且还给了我一种以不同方式看待事物的好方法。我已经投了赞成票,也很高兴为任何 cmets 投了赞成票。谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-04
相关资源
最近更新 更多