TL;DR
# 基本スタイル
User.joins(:orders).group('users.id').having('COUNT(orders.id) > ?', 2)
=> User Load (0.9ms) SELECT "users".* FROM "users" GROUP BY users.id HAVING (COUNT(orders.id) > 2)
# COUNT DISTINCT(いらないかも...)
User.joins(:orders).group('users.id').having('COUNT(DISTINCT orders.id) > ?', 2)
=> User Load (1.8ms) SELECT "users".* FROM "users" INNER JOIN "orders" ON "orders"."user_id" = "users"."id" GROUP BY users.id HAVING (COUNT(DISTINCT orders.id) > 2)
# 数値カラムと比較(購入数と年齢を比較する謎仕様(笑))
User.joins(:orders).group('users.id').having('COUNT(orders.id) > users.age')
=> User Load (3.9ms) SELECT "users".* FROM "users" INNER JOIN "orders" ON "orders"."user_id" = "users"."id" GROUP BY users.id HAVING (COUNT(orders.id) > users.age)
# 別の関連モデルのレコード数と比較(購入数とレビュー数を比較)
User.joins(:orders, :reviews).group('users.id').having('COUNT(orders.id) > COUNT(reviews.id)')
=> User Load (13.5ms) SELECT "users".* FROM "users" INNER JOIN "orders" ON "orders"."user_id" = "users"."id" INNER JOIN "reviews" ON "reviews"."user_id" = "users"."id" GROUP BY users.id HAVING (COUNT(orders.id) > COUNT(reviews.id))
注意
GROUP BY ... HAVING ...
の構文は or
系の構文とたまに干渉し、エラーが生じるので、Ransackなどで複雑な条件で検索したい時は要注意です。
余談
上記構文は自動的に関連レコードのないものを検索対象外にしたが、関連レコードがないことを検索したい場合は下記の構文でできます。
# おすすめ
User.left_outer_joins(:orders).where(orders: {id: nil})
=> User Load (1.1ms) SELECT "users".* FROM "users" LEFT OUTER JOIN "orders" ON "orders"."user_id" = "users"."id" WHERE "orders"."id" IS NULL
# 一応こちらもできるが、無駄なSQLが発行される
User.includes(:orders).where(orders: {id: nil})
=> SQL (1.3ms) SELECT "users"."id" AS t0_r0, "users"."created_at" AS t0_r1, "users"."updated_at" AS t0_r2 ...