🧗‍♂️

SQL の集計には LIMIT が効かない

2022/12/14に公開

問題提起

いくら LIMIT しても平均値が変わらないので調べたら LIMIT が効いてなかった。

require "active_record"
ActiveRecord::VERSION::STRING  # => "7.1.3.2"
ActiveSupport::LogSubscriber.colorize_logging = false
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define do
  create_table :items do |t|
    t.integer :score
  end
end

class Item < ActiveRecord::Base
end

としてからレコードを3つ作る。

Item.create!(score: 1)    # => #<Item id: 1, score: 1>
Item.create!(score: 2)    # => #<Item id: 2, score: 2>
Item.create!(score: 100)  # => #<Item id: 3, score: 100>

ここで LIMIT 2 とした場合のスコア平均は 1.5 になるだろう。

Item.order(:id).limit(2).average(:score).to_f  # => 34.33333333333333
# >   Item Average (0.1ms)  SELECT AVG("items"."score") FROM "items" LIMIT ?  [["LIMIT", 2]]

???

対策1. 長期的になら副SQLにする

sub_sql = Item.order(:id).limit(2)
Item.where(id: sub_sql).average(:score).to_f  # => 1.5
# >   Item Average (0.1ms)  SELECT AVG("items"."score") FROM "items" WHERE "items"."id" IN (SELECT "items"."id" FROM "items" ORDER BY "items"."id" ASC LIMIT ?)  [["LIMIT", 2]]

それか、すでに [1, 2] を取得済みなら where(id: [1, 2]) となるように渡す。

ちなみに id を明示したからといって LIMIT が効いたりはしない。

Item.where(id: [1, 2]).limit(1).average(:score).to_f  # => 1.5

というか無意味な LIMIT の指定があればエラーを出してほしい(小声)

対策2. 今だけでいいならRDBを使わなくてもいい

scores = Item.order(:id).limit(2).pluck(:score)
# >   Item Pluck (0.1ms)  SELECT "items"."score" FROM "items" ORDER BY "items"."id" ASC LIMIT ?  [["LIMIT", 2]]
scores.sum.fdiv(scores.size)  # => 1.5

Discussion