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")

クエリはすべて DB で動作する必要があり、何百万ものレコードを並べ替えてスライスする必要があるため、ハイブリッド Ruby ソリューションであってはなりません。

私は問題を設定して、通常のuserテーブルに対して実行するようにし、その代わりに別のテーブルでプレイできるようにしました。

回答可否の制限

  • Userクエリは、すべての通常の属性を持つオブジェクトを返す必要があります
  • extra_metric_we_care_about各ユーザー オブジェクトには、age_in_secondsおよびも含める必要があります。compound_computed_metric
  • クエリは、複数の場所で文字列を出力するだけでロジックを複製するべきではありません-同じことを2回行うことを避けたいです
  • [更新] 数百万のレコードで構成される可能性のある結果セットを DB で並べ替えてスライスしてから Rails に戻すことができるように、クエリはすべて DB で実行できる必要があります。
  • [更新] ソリューションは Postgres DB で機能するはずです

希望するソリューションの例

以下の解決策は機能しませんが、私が達成したいエレガンスのタイプを示しています

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. 私が使用しているDBはPostgresです。
  2. クエリがすべて DB で実行されることを 100% 明確にしたかどうかはわかりません。ロジックの一部が基本的にRailsで行われている場合、ハイブリッドな答えになることはできません。計算列を使用して何百万ものレコードを並べ替えてスライスできるようにしたいので、これは重要です。
4

3 に答える 3

7

あなたの場合、2つの解決策があります。私のデータベースは mysql です。デモ用にコードを単純化します。拡張できると思います。

1つ目はSqueelのやり方で、Squeelの「sift」とActiveRecord Queryの「from」を混ぜてみました。「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"

2 つ目は 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 に答える