LoginSignup
0
1

More than 5 years have passed since last update.

ORDER key ASC しつつ Last N 件のレコードを取得する

Posted at

動かしたのは Rails5.0.1 です

:thinking:やりたいこと 

例えば、

id
1
2
3
4
5

のデータが有るhogesテーブルがあったときに

Hoge.where.not(id: 3).order(:id).endmost(3)
#=> [#<Hoge:0x007f83f2b312a8 id: 2>, #<Hoge:0x007f83f2b310c8 id: 4>, #<Hoge:0x007f83f2b30f10 id: 5>]

となるようなendmostメソッドをどう実装すればよいか、という話です。
(メソッド名は↓で紹介する記事から取りました)

  • orderで昇順で取得できる
  • 下からN件取れる
  • ArrayではなくActiveRecord_Relationである

という条件です。

方法 :one:

RETRIEVING THE LAST N ORDERED RECORDS WITH ACTIVERECORD
に書いてある方法です。

class Hoge
  class << self
    def endmost(n)
      all.only(:order).from(all.reverse_order.limit(n), table_name)
    end
  end
end

onlyやfromなどあまり見慣れないメソッドを使っていますが、発行されるクエリを見るととてもシンプルです
Hoge.where.not(id: 3).order(:id).endmost(3)を実行した時発行されるクエリは↓です。

SELECT "hoges".*
FROM
  (SELECT "hoges".*
   FROM "hoges"
   WHERE ("hoges"."id" != 3)
   ORDER BY "hoges"."id" DESC LIMIT 3) hoges
ORDER BY "hoges"."id" ASC

from句のサブクエリとしてwhere.not(id: 3)の部分と下からN件が処理され、
外側のクエリでorder(:id)で並び替えられています。

方法 :two:

def endmost(n)
  all.only(:order).where(id: all.reverse_order.limit(n).select(:id))
end

sqlは↓。

SELECT "hoges".*
FROM "hoges"
WHERE "hoges"."id" IN
    (SELECT "hoges"."id"
     FROM "hoges"
     WHERE ("hoges"."id" != 3)
     ORDER BY "hoges"."id" DESC LIMIT 3)
ORDER BY "hoges"."id" ASC

先程はselect句のサブクエリでしたが、今回はwhere句のサブクエリとして下からN件を取得しています。

:warning:注意点

Hoge.where.not(id: 3).order(:id).endmost(3)

Hoge.order(:id).endmost(3).where.not(id: 3)

は結果が異なります。
後者のSQLは下になります。(方法1の場合)

SELECT "hoges".*
FROM
  (SELECT "hoges".*
   FROM "hoges"
   ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC

where.not(3)の部分のsqlがサブクエリの中か外か、の違いです。
サブクエリ内で下から3件を取得した後、WHERE ("hoges"."id" != 3)が処理されることになるので、
結果は、もともと期待していたのは
[#<Hoge:0x007f83f2b312a8 id: 2>, #<Hoge:0x007f83f2b310c8 id: 4>, #<Hoge:0x007f83f2b30f10 id: 5>]
の3件だったのに
[#<Hoge:0x007f83f1c9fc08 id: 4>, #<Hoge:0x007f83f1c8eb10 id: 5>]
の2件になってしまいます。

重要なのは、

  • endmost以前のorder以外は全てサブクエリとして処理される
  • endmost以降は何を書いても外側のメインのクエリとして処理される

ということです。

Hoge.where.not(id: 3).joins(:user).order(:id).endmost(3).where(id: 4).select(:id) # このコードに意味は無いです。すごく適当に作りました

としたときに発行されるクエリは↓になります。

SELECT "id"
FROM
  (SELECT "hoges".*
   FROM "hoges"
   INNER JOIN "users" ON "users"."id" = "hoges"."user_id"
   WHERE ("hoges"."id" != 3)
   ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE "hoges"."id" = 4
ORDER BY "hoges"."id" ASC

:stars:最後に

方法1方法2のどちらが良いのかは判断できてないです。
from句書いたことがないのでちょっと怖い気持ちがあるけど、
勘ではクエリ的にはwhere句よりfrom句のサブクエリのほうが早そう、(SQLの気持ちわかってないので実際どっちが効率的なのかは知らないです。。)
というところです。

こっちの方法が良い、とか、この方法は間違っている、とか、もっと良い方法がある、などありましたらコメントをいただけると幸いです:bow:


ここまででこの記事のメインは終わります。
これ以降は調べてる途中にわかった補足です。


onlyについて

onlyは
Hoge.where(id: 1).only(:order) # whereが無視される
Hoge.order(:id).where(id: 1).only(:order) whereが無視され、orderだけ有効
というようなメソッドです。
方法1では
all.only(:order).from(all.reverse_order.limit(n), table_name)
としてましたが、
このonlyの入れる位置はもとても重要です。
これ以降も方法1を例にして説明していきます。

そもそもonlyがない場合

def endmost(n)
  all.from(all.reverse_order.limit(n), table_name)
end
Hoge.where.not(id: 3).order(:id).endmost(3)

の場合、sqlは↓になります。

SELECT "hoges".*
FROM
  (SELECT "hoges".*
   FROM "hoges"
   WHERE ("hoges"."id" != 3)
   ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC

WHERE ("hoges"."id" != 3)が2回現れています。
結果は変わらないはずですが、無駄に処理が行われてしまいます。

onlyをfromの中に入れた場合

def endmost(n)
  all.from(all.only(:order).reverse_order.limit(n), table_name)
end
Hoge.where.not(id: 3).order(:id).endmost(3)

のケースです。
sqlは↓になります。

SELECT "hoges".*
FROM
  (SELECT "hoges".*
   FROM "hoges"
   ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC

注意点のところで書いたSQLと同じです。
where.not(id: 3)のwhere句がサブクエリ内ではなく外にいます。
その為結果は、
[#<Hoge:0x007f83f1c9fc08 id: 4>, #<Hoge:0x007f83f1c8eb10 id: 5>]
となってしまいます。

そもそもなぜこのendmostが必要になったか

Query Objectを作ろうとしていて必要になりました。

class AwesomeHugaQuery
  def initialize(relation: Huga.all, limit:)
    @relation = relation
    @limit = limit
  end

  def all
    @relation.awesome_scope.order(:id).endmost(@limit) # こんな感じの複雑なクエリ
  end
end

query = AwesomeHugaQuery.new(relation: Huga.active, limit: LIMIT)
query.all.includes(:foo)
query.all.includes(:bar)

ということがしたかったのでした。

@relation.awesome_scope.order(:id).last(@limit)
@relation.awesome_scope.order(id: :desc).limit(@limit).reverse

だとArrayになってしまうのでquery.all.includes(:foo)でエラーになってしまうし、
だからといって

@relation.awesome_scope.order(id: :desc).limit(@limit)
query.all.includes(:foo).reverse
query.all.includes(:bar).reverse

ともしたくなかったし、

def all(association = nil)
  scope = @relation.awesome_scope
  if association
    scope.includes(association)
  end
  scope.order(:id).last(@limit)
end
query.all(:foo)
query.all(:bar)

というようなこともしたくなかったので、endmostが必要になりました

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1