Real world example: We have models Order and Payment.
Orders have many payments, so client could try another form of payment if first
try failed for whatever reason.
problem: How to fetch only “paid” orders? In this case, orders having at least one payment in 'authorized' or 'charged' state.
First, I’ve tried to count such orders using ActiveRecord.
It works, but for some reason resulting join is quite inefficient. I don’t need payments
though, I just want to know if corresponding paid payments exist!
Let’s try plain old SQL (almost):
Works much faster! But it isn’t very DRY. I had to repeat Payment.state condition. Maybe Arel could help me?
Turns out, Arel provides exists method on relations, which returns Arel “node” for EXISTS(subquery for your scope).
You can write "payments.order_id = orders.id" portion using Arel too, but I don’t find it particularly readable:
But wait! Could we do without EXISTS(), using IN (), for example? Active record already provides us with convenient
shortcut for IN (?) predicates:
Check out Order.paid.to_sql:
It’s a subselect, not a list of values. Neat!
To my (not so much) surprise, Order.paid.explain shows the same query plan (on postgresql, which probably matters there) as variant with EXISTS. You’re welcome.
So, what’s going on there? One more useful but undocumented feature in active record is that you can put ActiveRecord::Relation
instance as argument for where(column: ...), and it will be inserted into resulting query as IN(subquery).
Resulting in one query, not two, as one could expect, and avoiding returning to your application (possibly
huge) subquery result.
One more approach for creating inverted :unpaid scope exists since Rails 6.1, using LEFT OUTER JOIN:
I’m not sure why paid scope has IS NOT NULL with INNER JOIN though, seems superfluous.